From 398a222dc8396351e06fe7ba1363b32f90617473 Mon Sep 17 00:00:00 2001
From: Mark Harman Store compass direction - If selected, then photos will be tagged with the compass direction.
Only supported for JPEG format. Not supported for RAW photos (DNG format) or videos. Store compass direction - If selected, then photos will be tagged with the device's yaw, pitch and roll.
+ Store yaw, pitch and roll - If selected, then photos will be tagged with the device's yaw, pitch and roll.
Note that Exif data does not have direct support for this, instead it will be written as a string in the Exif data's
User Comment for the image. Only supported for JPEG format. Not supported for RAW photos (DNG format) or videos.
+Version 1.48.2 (Work in progress)
+
+UPDATED Selecting remote device type for Bluetooth remote control no longer calls DeviceScanner
+ activity directly; DeviceScanner activity no longer exported.
+
Version 1.48.1 (2020/05/02)
FIXED Crash on devices with Camera2 API where camera reports no picture, video or preview
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 36415a575..34644990d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -72,11 +72,8 @@
Version 1.48.2 (Work in progress)
+FIXED Manual focus and focus bracketing seekbars weren't being hidden when in immersive mode.
UPDATED Selecting remote device type for Bluetooth remote control no longer calls DeviceScanner
activity directly; DeviceScanner activity no longer exported.
diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
index d2fc36ed2..c8595caaa 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -4730,6 +4730,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Artist - If text is entered in this setting, then the text will be stored in the image's Exif metadata as the - "Artist" tag. Only supported for JPEG format. Not supported for RAW photos (DNG format). (Requires Android 7.)
+ "Artist" tag. Only supported for JPEG format. Not supported for RAW photos (DNG format).Copyright - If text is entered in this setting, then the text will be stored in the image's Exif metadata as the - "Copyright" tag. Only supported for JPEG format. Not supported for RAW photos (DNG format). (Requires Android 7.)
+ "Copyright" tag. Only supported for JPEG format. Not supported for RAW photos (DNG format).Stamp photos - Option to add a date and timestamp to the resultant photos. If "Store
location data" is enabled (see "Location settings" below), then the current location latitude
diff --git a/_docs/history.html b/_docs/history.html
index c4eab43b7..f259bcbd1 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -52,6 +52,7 @@ Version 1.48.2 (Work in progress)
FIXED Manual focus and focus bracketing seekbars weren't being hidden when in immersive mode.
UPDATED Switched to AndroidX support library.
+UPDATED Artist, Copyright exif tags option now supported for devices running Android 6 or earlier.
UPDATED Selecting remote device type for Bluetooth remote control no longer calls DeviceScanner
activity directly; DeviceScanner activity no longer exported.
diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index 2ed19e848..4f1ed0b9c 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -46,7 +46,6 @@ import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.renderscript.Allocation;
-import androidx.annotation.RequiresApi;
import android.util.Log;
import android.util.Xml;
@@ -58,12 +57,6 @@ import org.xmlpull.v1.XmlSerializer;
public class ImageSaver extends Thread {
private static final String TAG = "ImageSaver";
- // note that ExifInterface now has fields for these types, but that requires Android 6 or 7
- private static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection";
- private static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef";
- private static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
- private static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
-
private final Paint p = new Paint();
private final MainActivity main_activity;
@@ -1683,10 +1676,9 @@ public class ImageSaver extends Thread {
}
// rotate the bitmaps if necessary for exif tags
- File exifTempFile = getExifTempFile(request.jpeg_images.get(0));
for(int i=0;i
Open Camera uses the AndroidX/Jetpack libraries, under Apache license version 2.0.
+The following files are used in Open Camera:
-Version 1.48.2 (Work in progress) +Version 1.48.2 (2020/07/12) FIXED Manual focus and focus bracketing seekbars weren't being hidden when in immersive mode. FIXED Video subtitles would stop before end of video on some devices when using Storage Access -- GitLab From a6b413f000297ff559332b46fda6de1d55501e26 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 1 Aug 2020 14:40:40 +0100 Subject: [PATCH 030/430] Extra logging. --- .../cameracontroller/CameraController2.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 59dec115c..51eb06a43 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -2148,10 +2148,15 @@ public class CameraController2 extends CameraController { } private List convertFocusModesToValues(int [] supported_focus_modes_arr, float minimum_focus_distance) { - if( MyDebug.LOG ) + if( MyDebug.LOG ) { Log.d(TAG, "convertFocusModesToValues()"); - if( supported_focus_modes_arr.length == 0 ) + Log.d(TAG, "supported_focus_modes_arr: " + Arrays.toString(supported_focus_modes_arr)); + } + if( supported_focus_modes_arr.length == 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "no supported focus modes"); return null; + } List supported_focus_modes = new ArrayList<>(); for(Integer supported_focus_mode : supported_focus_modes_arr) supported_focus_modes.add(supported_focus_mode); @@ -2176,6 +2181,9 @@ public class CameraController2 extends CameraController { } if( supported_focus_modes.contains(CaptureRequest.CONTROL_AF_MODE_OFF) ) { output_modes.add("focus_mode_infinity"); + if( MyDebug.LOG ) { + Log.d(TAG, " supports focus_mode_infinity"); + } if( minimum_focus_distance > 0.0f ) { output_modes.add("focus_mode_manual2"); if( MyDebug.LOG ) { @@ -5467,6 +5475,8 @@ public class CameraController2 extends CameraController { return; } Integer focus_mode = previewBuilder.get(CaptureRequest.CONTROL_AF_MODE); + if( MyDebug.LOG ) + Log.d(TAG, "focus mode: " + (focus_mode == null ? "null" : focus_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) -- GitLab From 5962ad660f037be1344871ac035b1d5d3c4a7f0e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 1 Aug 2020 21:56:42 +0100 Subject: [PATCH 031/430] Add logging. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index f30b3a8f6..11118d56e 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -1805,6 +1805,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Wed, 5 Aug 2020 23:23:44 +0100 Subject: [PATCH 032/430] Fixes for running Camera2 tests on Android emulator with LIMITED support. --- _docs/history.html | 6 ++ .../cameracontroller/CameraController2.java | 66 +++++++++++++++++-- .../opencamera/preview/Preview.java | 6 +- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index a6ad4af1b..832180765 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -48,6 +48,12 @@ +Version 1.48.3 (Work in progress) + +FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. +FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices + with LIMITED Camera2 API support. + Version 1.48.2 (2020/07/12) FIXED Manual focus and focus bracketing seekbars weren't being hidden when in immersive mode. diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 51eb06a43..13ba6d0da 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -80,6 +80,7 @@ public class CameraController2 extends CameraController { private boolean supports_optical_stabilization; private boolean supports_photo_video_recording; private boolean supports_white_balance_temperature; + private String initial_focus_mode; // if non-null, focus mode to use if not set by Preview (rather than relying on the Builder template's default, which can be one that isn't supported, at least on Android emulator with its LIMITED camera!) private final static int tonemap_log_max_curve_points_c = 64; private final static float [] jtvideo_values_base = new float[] { @@ -307,9 +308,11 @@ public class CameraController2 extends CameraController { private int antibanding = CameraMetadata.CONTROL_AE_ANTIBANDING_MODE_AUTO; private boolean has_edge_mode; private int edge_mode = CameraMetadata.EDGE_MODE_FAST; + private boolean has_default_edge_mode; private Integer default_edge_mode; private boolean has_noise_reduction_mode; private int noise_reduction_mode = CameraMetadata.NOISE_REDUCTION_MODE_FAST; + private boolean has_default_noise_reduction_mode; private Integer default_noise_reduction_mode; private int white_balance_temperature = 5000; // used for white_balance == CONTROL_AWB_MODE_OFF private String flash_value = "flash_off"; @@ -555,12 +558,14 @@ public class CameraController2 extends CameraController { private boolean setEdgeMode(CaptureRequest.Builder builder) { if( MyDebug.LOG ) { Log.d(TAG, "setEdgeMode"); + Log.d(TAG, "has_default_edge_mode: " + has_default_edge_mode); Log.d(TAG, "default_edge_mode: " + default_edge_mode); } boolean changed = false; if( has_edge_mode ) { - if( default_edge_mode == null ) { + if( !has_default_edge_mode ) { // save the default_edge_mode edge_mode + has_default_edge_mode = true; default_edge_mode = builder.get(CaptureRequest.EDGE_MODE); if( MyDebug.LOG ) Log.d(TAG, "default_edge_mode: " + default_edge_mode); @@ -584,7 +589,7 @@ public class CameraController2 extends CameraController { // need EDGE_MODE_OFF to avoid a "glow" effect builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_OFF); } - else if( default_edge_mode != null ) { + else if( has_default_edge_mode ) { if( builder.get(CaptureRequest.EDGE_MODE) != null && !builder.get(CaptureRequest.EDGE_MODE).equals(default_edge_mode) ) { builder.set(CaptureRequest.EDGE_MODE, default_edge_mode); changed = true; @@ -596,12 +601,14 @@ public class CameraController2 extends CameraController { private boolean setNoiseReductionMode(CaptureRequest.Builder builder) { if( MyDebug.LOG ) { Log.d(TAG, "setNoiseReductionMode"); + Log.d(TAG, "has_default_noise_reduction_mode: " + has_default_noise_reduction_mode); Log.d(TAG, "default_noise_reduction_mode: " + default_noise_reduction_mode); } boolean changed = false; if( has_noise_reduction_mode ) { - if( default_noise_reduction_mode == null ) { + if( !has_default_noise_reduction_mode ) { // save the default_noise_reduction_mode noise_reduction_mode + has_default_noise_reduction_mode = true; default_noise_reduction_mode = builder.get(CaptureRequest.NOISE_REDUCTION_MODE); if( MyDebug.LOG ) Log.d(TAG, "default_noise_reduction_mode: " + default_noise_reduction_mode); @@ -625,7 +632,7 @@ public class CameraController2 extends CameraController { // need NOISE_REDUCTION_MODE_OFF to avoid excessive blurring builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF); } - else if( default_noise_reduction_mode != null ) { + else if( has_default_noise_reduction_mode ) { if( builder.get(CaptureRequest.NOISE_REDUCTION_MODE) != null && !builder.get(CaptureRequest.NOISE_REDUCTION_MODE).equals(default_noise_reduction_mode)) { builder.set(CaptureRequest.NOISE_REDUCTION_MODE, default_noise_reduction_mode); changed = true; @@ -767,6 +774,11 @@ public class CameraController2 extends CameraController { Log.d(TAG, "change af mode to " + af_mode); builder.set(CaptureRequest.CONTROL_AF_MODE, af_mode); } + else { + if( MyDebug.LOG ) { + Log.d(TAG, "af mode left at " + builder.get(CaptureRequest.CONTROL_AF_MODE)); + } + } } private void setFocusDistance(CaptureRequest.Builder builder) { @@ -2619,6 +2631,22 @@ public class CameraController2 extends CameraController { if( camera_features.supported_focus_values != null && camera_features.supported_focus_values.contains("focus_mode_manual2") ) { camera_features.supports_focus_bracketing = true; } + if( camera_features.supported_focus_values != null ) { + // prefer continuous focus mode + if( camera_features.supported_focus_values.contains("focus_mode_continuous_picture") ) { + initial_focus_mode = "focus_mode_continuous_picture"; + } + else { + // just go with the first one + initial_focus_mode = camera_features.supported_focus_values.get(0); + } + if( MyDebug.LOG ) + Log.d(TAG, "initial_focus_mode: " + initial_focus_mode); + } + else { + initial_focus_mode = null; + } + camera_features.max_num_focus_areas = characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); camera_features.is_exposure_lock_supported = true; @@ -4723,6 +4751,8 @@ public class CameraController2 extends CameraController { @Override public boolean setFocusAndMeteringArea(List areas) { + if( MyDebug.LOG ) + Log.d(TAG, "setFocusAndMeteringArea"); 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); @@ -4768,6 +4798,8 @@ public class CameraController2 extends CameraController { @Override public void clearFocusAndMetering() { + if( MyDebug.LOG ) + Log.d(TAG, "clearFocusAndMetering"); Rect sensor_rect = getViewableRect(); boolean has_focus = false; boolean has_metering = false; @@ -4807,12 +4839,20 @@ public class CameraController2 extends CameraController { e.printStackTrace(); } } + if( MyDebug.LOG ) { + Log.d(TAG, "af_regions: " + Arrays.toString(camera_settings.af_regions)); + Log.d(TAG, "ae_regions: " + Arrays.toString(camera_settings.ae_regions)); + } } @Override public List getFocusAreas() { if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) == 0 ) return null; + if( camera_settings.af_regions == null ) { + // needed to fix failure on Android emulator in testTakePhotoContinuousNoTouch - can happen when CONTROL_MAX_REGIONS_AF > 0, but Camera only has 1 focus mode so Preview doesn't set focus areas + return null; + } MeteringRectangle [] metering_rectangles = previewBuilder.get(CaptureRequest.CONTROL_AF_REGIONS); if( metering_rectangles == null ) return null; @@ -4833,6 +4873,10 @@ public class CameraController2 extends CameraController { public List getMeteringAreas() { if( characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) == 0 ) return null; + if( camera_settings.ae_regions == null ) { + // needed to fix failure on Android emulator in testTakePhotoContinuousNoTouch - can happen when CONTROL_MAX_REGIONS_AF > 0, but Camera only has 1 focus mode so Preview doesn't set focus areas + return null; + } MeteringRectangle [] metering_rectangles = previewBuilder.get(CaptureRequest.CONTROL_AE_REGIONS); if( metering_rectangles == null ) return null; @@ -5315,6 +5359,20 @@ public class CameraController2 extends CameraController { public void startPreview() throws CameraControllerException { if( MyDebug.LOG ) Log.d(TAG, "startPreview"); + + if( !camera_settings.has_af_mode && initial_focus_mode != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "user didn't specify focus, so set to: " + initial_focus_mode); + // If the caller hasn't set a focus mode, but focus modes are supported, it's still better to explicitly set one rather than leaving to the + // builder's default - e.g., problem on Android emulator with LIMITED camera where it only supported infinity focus (CONTROL_AF_MODE_OFF), but + // the preview builder defaults to CONTROL_AF_MODE_CONTINUOUS_PICTURE! This meant we froze when trying to take a photo, because we thought + // we were in continuous picture mode and so waited in state STATE_WAITING_AUTOFOCUS, but the focus never occurred. + // Ideally the caller to CameraController2 (Preview) should always explicitly set a focus mode if at least 1 focus mode is supported. At the + // time of writing, Preview only sets a focus if at least 2 focus modes are supported. But even if we fix that in future, still good to have + // well defined behaviour at the CameraController level. + setFocusValue(initial_focus_mode); + } + synchronized( background_camera_lock ) { if( captureSession != null ) { try { diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index fb19e007c..c7738bec5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -4447,7 +4447,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "found no existing focus_value"); // here we set the default values for focus mode // note if updating default focus value for photo mode, also update MainActivityTest.setToDefault() - updateFocus(is_video ? "focus_mode_continuous_video" : "focus_mode_continuous_picture", true, true, auto_focus); + if( !updateFocus(is_video ? "focus_mode_continuous_video" : "focus_mode_continuous_picture", true, true, auto_focus) ) { + if( MyDebug.LOG ) + Log.d(TAG, "continuous focus not supported, so fall back to first"); + updateFocus(0, true, true, auto_focus); + } } } -- GitLab From 9e3bcaea1383681b8982c6f00dfe9fdb96c3b813 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Wed, 5 Aug 2020 23:24:02 +0100 Subject: [PATCH 033/430] Update gradle version. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 8e0dd8301..dad9ef0a6 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:4.0.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a6111dc9b..d25d07942 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Apr 11 14:09:12 BST 2020 +#Fri Jul 31 23:57:49 BST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip -- GitLab From 6e48ee00728473737177b140d4eb146ad225ee33 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 5 Aug 2020 23:24:23 +0100 Subject: [PATCH 034/430] Fixes for tests running on Android emulator. --- .../opencamera/test/MainActivityTest.java | 145 ++++++++++++------ .../opencamera/ui/DrawPreview.java | 4 + 2 files changed, 100 insertions(+), 49 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 11118d56e..17f53c89f 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -83,6 +83,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 = 1000) { assertTrue(mPreview.isPreviewBitmapEnabled()); } @@ -2738,8 +2747,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 = 1000) { assertFalse(mPreview.isPreviewBitmapEnabled()); assertFalse(mPreview.refreshPreviewBitmapTaskIsRunning()); @@ -2876,16 +2887,19 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Thu, 6 Aug 2020 22:34:07 +0100 Subject: [PATCH 035/430] Add coments for individual tests failing on Android emulator. --- .../opencamera/test/MainActivityTest.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 17f53c89f..59cca52f0 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -2388,6 +2388,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Thu, 6 Aug 2020 23:06:56 +0100 Subject: [PATCH 036/430] Clarify on cookies. --- _docs/privacy_oc.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_docs/privacy_oc.html b/_docs/privacy_oc.html index 38972821d..eec123c87 100644 --- a/_docs/privacy_oc.html +++ b/_docs/privacy_oc.html @@ -83,12 +83,14 @@ save resultant files such as photos and videos to your device. Although Open Camera is ad-free, the Open Camera website has ads via Google Adsense: Third party vendors, including Google, use cookies to serve ads based on a user's previous visits to this website or other websites. Google's use of advertising cookies enables it and its partners to serve ads based on people's visit to this sites and/or other sites on the Internet. You may opt out of personalised -advertising by visiting Google's Ads Settings. Alternatively, you can -opt out of some third-party vendors' uses of cookies for personalised advertising by visiting +advertising by visiting Google's Ads Settings. The cookies of other third-party +vendors or ad networks may also be used to serve ads. You can opt out of some third-party vendors' uses of cookies for personalised advertising by visiting www.aboutads.info.
Update: I have instructed Google to not display personalised ads to users in the EEA.
+Note that cookies are still used for serving even non-personalised ads.
+The Open Camera website also uses Google Analytics which uses cookies, please see their Privacy Policy for more details.
-- GitLab From 29abc6f61c022e6a54607870b97aeb3a97ceeeab Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Fri, 7 Aug 2020 23:30:36 +0100 Subject: [PATCH 037/430] Add comment about fake flash for oneplus devices. --- app/src/main/java/net/sourceforge/opencamera/MainActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 659070efd..452fbb8b2 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -772,6 +772,7 @@ public class MainActivity extends Activity { // to work okay, testing on S7 on RTL, but still keeping the fake flash mode in place for these devices, until we're sure of good // behaviour // update for testing on Galaxy S10e: still needs fake flash + // has also been reported to me that OnePlus 8 and 8 Pro have problems with flash on Camera2 API unless fake flash enabled if( MyDebug.LOG ) Log.d(TAG, "set fake flash for camera2"); SharedPreferences.Editor editor = sharedPreferences.edit(); -- GitLab From 91afea29d1a1f3c99b6be955bc77489d50247be0 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 8 Aug 2020 15:51:33 +0100 Subject: [PATCH 038/430] Fix comment, no longer using picFile as temporary storage when using saveUri. --- app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 29478ac60..622c0be67 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2303,9 +2303,8 @@ public class ImageSaver extends Thread { main_activity.savingImage(true); - // If saveUri is non-null, then: - // Before Android 7, picFile is a temporary file which we use for saving exif tags too, and then we redirect the picFile to saveUri. - // On Android 7+, picFile is null - we can write the exif tags direct to the saveUri. + // If using SAF or image_capture_intent is true, only saveUri is non-null + // Otherwise, only picFile is non-null File picFile = null; Uri saveUri = null; try { -- GitLab From 297fec77d3d0f47559c03ff3fbb646826dd3ed8e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 8 Aug 2020 16:24:46 +0100 Subject: [PATCH 039/430] Refactor, use ImageColumns.DATA instead of hardcoded string _data. --- app/src/main/java/net/sourceforge/opencamera/StorageUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 0a22d84ac..2935d175e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -488,7 +488,7 @@ public class StorageUtils { } private String getDataColumn(Uri uri, String selection, String [] selectionArgs) { - final String column = "_data"; + final String column = MediaStore.Images.ImageColumns.DATA; final String[] projection = { column }; -- GitLab From a9a8b82d091f905247aae14ccc75903d4da7dea8 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 8 Aug 2020 16:26:01 +0100 Subject: [PATCH 040/430] Remove redundant codepaths, picFile is never non-null when saveUri is non-null. This was only needed when we needed separate codepaths for pre-Android 7, due to using non-support-library version of ExifInterface. --- .../java/net/sourceforge/opencamera/ImageSaver.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 622c0be67..cef8a6759 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2474,9 +2474,6 @@ public class ImageSaver extends Thread { } if( saveUri != null ) { - if( picFile != null ) { - copyFileToUri(main_activity, saveUri, picFile); - } success = true; broadcastSAFFile(saveUri, request.image_capture_intent); } @@ -2594,15 +2591,6 @@ public class ImageSaver extends Thread { 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); -- GitLab From 4be498a075f5cd49965d31ebe7d91fd6f45f6123 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 8 Aug 2020 21:53:13 +0100 Subject: [PATCH 041/430] Refactor code to new getImageMimeType(). --- .../sourceforge/opencamera/StorageUtils.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 2935d175e..58f094f08 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -714,27 +714,35 @@ public class StorageUtils { } } + /** Return the mime type corresponding to the supplied extension. Supports images only, not video. + */ + String getImageMimeType(String extension) { + String mimeType; + switch (extension) { + case "dng": + mimeType = "image/dng"; + //mimeType = "image/x-adobe-dng"; + break; + case "webp": + mimeType = "image/webp"; + break; + case "png": + mimeType = "image/png"; + break; + default: + mimeType = "image/jpeg"; + break; + } + return mimeType; + } + // only valid if isUsingSAF() @TargetApi(Build.VERSION_CODES.LOLLIPOP) Uri createOutputMediaFileSAF(int type, String suffix, String extension, Date current_date) throws IOException { String mimeType; switch (type) { case MEDIA_TYPE_IMAGE: - switch (extension) { - case "dng": - mimeType = "image/dng"; - //mimeType = "image/x-adobe-dng"; - break; - case "webp": - mimeType = "image/webp"; - break; - case "png": - mimeType = "image/png"; - break; - default: - mimeType = "image/jpeg"; - break; - } + mimeType = getImageMimeType(extension); break; case MEDIA_TYPE_VIDEO: switch( extension ) { -- GitLab From 5e7d160b8150c91fca9ce0ac206dde6b5ac4cf92 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 15 Aug 2020 14:59:24 +0100 Subject: [PATCH 042/430] Close the ParcelFileDescriptors! --- _docs/history.html | 4 ++ .../sourceforge/opencamera/ImageSaver.java | 63 +++++++++++++++---- .../opencamera/MyApplicationInterface.java | 19 +++++- .../sourceforge/opencamera/StorageUtils.java | 12 +++- .../opencamera/preview/Preview.java | 42 +++++++++++-- 5 files changed, 121 insertions(+), 19 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 832180765..c0939f360 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -50,6 +50,10 @@ Version 1.48.3 (Work in progress) +FIXED Photos would sometimes fail to save on some devices with Storage Access Framework, when some + options were enabled (options like DRO, HDR, auto-level, photostamp that require + post-processing; custom Exif tags like artist or copyright; or when using geotagging with + Camera2 API). FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices with LIMITED Camera2 API support. diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index cef8a6759..80297dfcb 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2439,12 +2439,24 @@ public class ImageSaver extends Thread { } else { ParcelFileDescriptor parcelFileDescriptor = main_activity.getContentResolver().openFileDescriptor(saveUri, "rw"); - if( parcelFileDescriptor != null ) { - FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - setExifFromData(request, data, fileDescriptor); + try { + if( parcelFileDescriptor != null ) { + FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + setExifFromData(request, data, fileDescriptor); + } + else { + Log.e(TAG, "failed to create ParcelFileDescriptor for saveUri: " + saveUri); + } } - else { - Log.e(TAG, "failed to create ParcelFileDescriptor for saveUri: " + saveUri); + finally { + if( parcelFileDescriptor != null ) { + try { + parcelFileDescriptor.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } } } } @@ -3189,6 +3201,8 @@ public class ImageSaver extends Thread { * being used by the ExifInterace! * This didn't cause any known bugs, but good practice to fix, similar to the issue reported in * https://sourceforge.net/p/opencamera/tickets/417/ . + * Also important to call the close() method when done with it, to close the + * ParcelFileDescriptor (if one was created). */ private static class ExifInterfaceHolder { // suppress warning - no it can't be local or removed, see documentation above about the garbage collector! @@ -3204,6 +3218,18 @@ public class ImageSaver extends Thread { ExifInterface getExif() { return this.exif; } + + void close() { + if( this.pfd != null ) { + try { + this.pfd.close(); + } + catch(IOException e) { + Log.e(TAG, "failed to close parcelfiledescriptor"); + e.printStackTrace(); + } + } + } } /** Creates a new exif interface for reading and writing. @@ -3211,6 +3237,7 @@ public class ImageSaver extends Thread { * tags too. * The returned ExifInterfaceHolder will always be non-null, but the contained getExif() may * return null if this method was unable to create the exif interface. + * The caller should call close() on the returned ExifInterfaceHolder when no longer required. */ private ExifInterfaceHolder createExifInterface(File picFile, Uri saveUri) throws IOException { ParcelFileDescriptor parcelFileDescriptor = null; @@ -3249,10 +3276,15 @@ public class ImageSaver extends Thread { Log.d(TAG, "add additional exif info"); try { ExifInterfaceHolder exif_holder = createExifInterface(picFile, saveUri); - ExifInterface exif = exif_holder.getExif(); - if( exif != null ) { - modifyExif(exif, request.type == Request.Type.JPEG, request.using_camera2, request.current_date, request.store_location, request.store_geo_direction, request.geo_direction, request.custom_tag_artist, request.custom_tag_copyright, request.level_angle, request.pitch_angle, request.store_ypr); - exif.saveAttributes(); + try { + ExifInterface exif = exif_holder.getExif(); + if( exif != null ) { + modifyExif(exif, request.type == Request.Type.JPEG, request.using_camera2, request.current_date, request.store_location, request.store_geo_direction, request.geo_direction, request.custom_tag_artist, request.custom_tag_copyright, request.level_angle, request.pitch_angle, request.store_ypr); + exif.saveAttributes(); + } + } + finally { + exif_holder.close(); } } catch(NoClassDefFoundError exception) { @@ -3269,10 +3301,15 @@ public class ImageSaver extends Thread { Log.d(TAG, "remove GPS timestamp hack"); try { ExifInterfaceHolder exif_holder = createExifInterface(picFile, saveUri); - ExifInterface exif = exif_holder.getExif(); - if( exif != null ) { - fixGPSTimestamp(exif, request.current_date); - exif.saveAttributes(); + try { + ExifInterface exif = exif_holder.getExif(); + if( exif != null ) { + fixGPSTimestamp(exif, request.current_date); + exif.saveAttributes(); + } + } + finally { + exif_holder.close(); } } catch(NoClassDefFoundError exception) { diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 1c51ba66b..dfe0d68fb 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2100,6 +2100,15 @@ public class MyApplicationInterface extends BasicApplicationInterface { } writer = null; } + if( pfd_saf != null ) { + try { + pfd_saf.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + pfd_saf = null; + } } return super.cancel(); } @@ -2177,7 +2186,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { // create thumbnail long debug_time = System.currentTimeMillis(); Bitmap thumbnail = null; - ParcelFileDescriptor pfd_saf; // keep a reference to this as long as retriever, to avoid risk of pfd_saf being garbage collected + ParcelFileDescriptor pfd_saf = null; // keep a reference to this as long as retriever, to avoid risk of pfd_saf being garbage collected MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { if( video_method == VIDEOMETHOD_FILE ) { @@ -2202,6 +2211,14 @@ public class MyApplicationInterface extends BasicApplicationInterface { catch(RuntimeException ex) { // ignore } + try { + if( pfd_saf != null ) { + pfd_saf.close(); + } + } + catch(IOException e) { + e.printStackTrace(); + } } if( thumbnail != null ) { ImageButton galleryButton = main_activity.findViewById(R.id.gallery); diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 58f094f08..06f8da3e8 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -1055,13 +1055,14 @@ public class StorageUtils { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private long freeMemorySAF() { Uri treeUri = applicationInterface.getStorageUtils().getTreeUriSAF(); + ParcelFileDescriptor pfd = null; if( MyDebug.LOG ) Log.d(TAG, "treeUri: " + treeUri); try { Uri docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); if( MyDebug.LOG ) Log.d(TAG, "docUri: " + docUri); - ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(docUri, "r"); + pfd = context.getContentResolver().openFileDescriptor(docUri, "r"); if( pfd == null ) { // just in case Log.e(TAG, "pfd is null!"); throw new FileNotFoundException(); @@ -1087,6 +1088,15 @@ public class StorageUtils { // now. e.printStackTrace(); } + finally { + try { + if( pfd != null ) + pfd.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } return -1; } diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index c7738bec5..8cb1c0ac1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -176,8 +176,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu 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 + /** Stores the file (or similar) to record a video. + * Important to call close() when the video recording is finished, to free up any resources + * (e.g., supplied ParcelFileDescriptor). + */ private static class VideoFileInfo { - // stores the file (or similar) to record a video private final int video_method; private final Uri video_uri; // for VIDEOMETHOD_SAF or VIDEOMETHOD_URI private final String video_filename; // for VIDEOMETHOD_FILE @@ -195,6 +198,17 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu this.video_filename = video_filename; this.video_pfd_saf = video_pfd_saf; } + + void close() { + if( this.video_pfd_saf != null ) { + try { + this.video_pfd_saf.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } } private VideoFileInfo videoFileInfo = new VideoFileInfo(); private VideoFileInfo nextVideoFileInfo; // used for Android 8+ to handle seamless restart (see MediaRecorder.setNextOutputFile()) @@ -963,9 +977,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // 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"); + videoFileInfo.close(); applicationInterface.deleteUnusedVideo(videoFileInfo.video_method, videoFileInfo.video_uri, videoFileInfo.video_filename); videoFileInfo = new VideoFileInfo(); + if( nextVideoFileInfo != null ) + nextVideoFileInfo.close(); nextVideoFileInfo = 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 ) { @@ -988,11 +1005,13 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu video_recorder_is_paused = false; applicationInterface.cameraInOperation(false, true); 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) + videoFileInfo.close(); applicationInterface.stoppedVideo(videoFileInfo.video_method, videoFileInfo.video_uri, videoFileInfo.video_filename); if( nextVideoFileInfo != null ) { // if nextVideoFileInfo is not-null, it means we received MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING but not // MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED, so it is the application responsibility to create the zero-size // video file that will have been created + nextVideoFileInfo.close(); applicationInterface.deleteUnusedVideo(nextVideoFileInfo.video_method, nextVideoFileInfo.video_uri, nextVideoFileInfo.video_filename); } videoFileInfo = new VideoFileInfo(); @@ -5136,6 +5155,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu catch(IOException e) { Log.e(TAG, "failed to setNextOutputFile"); e.printStackTrace(); + info.close(); } } } @@ -5150,6 +5170,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.e(TAG, "received MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED but nextVideoFileInfo is null"); } else { + videoFileInfo.close(); applicationInterface.restartedVideo(videoFileInfo.video_method, videoFileInfo.video_uri, videoFileInfo.video_filename); videoFileInfo = nextVideoFileInfo; nextVideoFileInfo = null; @@ -5281,11 +5302,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private VideoFileInfo createVideoFile(String extension) { if( MyDebug.LOG ) Log.d(TAG, "createVideoFile"); + VideoFileInfo video_file_info = null; + ParcelFileDescriptor video_pfd_saf = null; try { int method = applicationInterface.createOutputVideoMethod(); Uri video_uri = null; String video_filename = null; - ParcelFileDescriptor video_pfd_saf = null; if( MyDebug.LOG ) Log.d(TAG, "method? " + method); if( method == ApplicationInterface.VIDEOMETHOD_FILE ) { @@ -5310,14 +5332,26 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu video_uri = uri; } - return new VideoFileInfo(method, video_uri, video_filename, video_pfd_saf); + video_file_info = new VideoFileInfo(method, video_uri, video_filename, video_pfd_saf); } catch(IOException e) { if( MyDebug.LOG ) Log.e(TAG, "Couldn't create media video file; check storage permissions?"); e.printStackTrace(); } - return null; + finally { + if( video_file_info == null && video_pfd_saf != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "failed, so clean up video_pfd_saf"); + try { + video_pfd_saf.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + return video_file_info; } /** Start video recording. -- GitLab From d1c11549fe9bee484357c864a3bd9424bd32541b Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 15 Aug 2020 17:13:58 +0100 Subject: [PATCH 043/430] Refactor ints to new enum VideoMethod. --- .../opencamera/MyApplicationInterface.java | 38 +++++++++---------- .../preview/ApplicationInterface.java | 16 ++++---- .../preview/BasicApplicationInterface.java | 6 +-- .../opencamera/preview/Preview.java | 22 +++++------ 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index dfe0d68fb..ce9d9ed26 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -270,7 +270,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @Override - public int createOutputVideoMethod() { + public VideoMethod createOutputVideoMethod() { String action = main_activity.getIntent().getAction(); if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { if( MyDebug.LOG ) @@ -281,17 +281,17 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( intent_uri != null ) { if( MyDebug.LOG ) Log.d(TAG, "save to: " + intent_uri); - return VIDEOMETHOD_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; + return VideoMethod.FILE; } boolean using_saf = storageUtils.isUsingSAF(); - return using_saf ? VIDEOMETHOD_SAF : VIDEOMETHOD_FILE; + return using_saf ? VideoMethod.SAF : VideoMethod.FILE; } @Override @@ -1896,9 +1896,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { // ability to switch between auto and manual main_activity.getMainUI().setupExposureUI(); } - final int video_method = this.createOutputVideoMethod(); + final VideoMethod video_method = this.createOutputVideoMethod(); boolean dategeo_subtitles = getVideoSubtitlePref().equals("preference_video_subtitle_yes"); - if( dategeo_subtitles && video_method != ApplicationInterface.VIDEOMETHOD_URI ) { + 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(); @@ -2046,7 +2046,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { try { synchronized( this ) { if( writer == null ) { - if( video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + if( video_method == ApplicationInterface.VideoMethod.FILE ) { String subtitle_filename = last_video_file.getAbsolutePath(); subtitle_filename = getSubtitleFilename(subtitle_filename); writer = new FileWriter(subtitle_filename); @@ -2129,7 +2129,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @Override - public void stoppedVideo(final int video_method, final Uri uri, final String filename) { + public void stoppedVideo(final VideoMethod video_method, final Uri uri, final String filename) { if( MyDebug.LOG ) { Log.d(TAG, "stoppedVideo"); Log.d(TAG, "video_method " + video_method); @@ -2160,7 +2160,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { String action = main_activity.getIntent().getAction(); if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { - if( done && video_method == VIDEOMETHOD_FILE ) { + 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 { @@ -2169,9 +2169,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { 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 ) { + // 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 ) @@ -2189,7 +2189,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { ParcelFileDescriptor pfd_saf = null; // keep a reference to this as long as retriever, to avoid risk of pfd_saf being garbage collected MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { - if( video_method == VIDEOMETHOD_FILE ) { + if( video_method == VideoMethod.FILE ) { File file = new File(filename); retriever.setDataSource(file.getPath()); } @@ -2252,7 +2252,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @Override - public void restartedVideo(final int video_method, final Uri uri, final String filename) { + public void restartedVideo(final VideoMethod video_method, final Uri uri, final String filename) { if( MyDebug.LOG ) { Log.d(TAG, "restartedVideo"); Log.d(TAG, "video_method " + video_method); @@ -2262,7 +2262,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { broadcastVideo(video_method, uri, filename); } - private boolean broadcastVideo(final int video_method, final Uri uri, final String filename) { + private boolean broadcastVideo(final VideoMethod video_method, final Uri uri, final String filename) { if( MyDebug.LOG ) { Log.d(TAG, "broadcastVideo"); Log.d(TAG, "video_method " + video_method); @@ -2270,7 +2270,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "filename " + filename); } boolean done = false; - if( video_method == VIDEOMETHOD_FILE ) { + if( video_method == VideoMethod.FILE ) { if( filename != null ) { File file = new File(filename); storageUtils.broadcastFile(file, false, true, true); @@ -2296,18 +2296,18 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @Override - public void deleteUnusedVideo(final int video_method, final Uri uri, final String filename) { + public void deleteUnusedVideo(final VideoMethod video_method, final Uri uri, final String filename) { if( MyDebug.LOG ) { Log.d(TAG, "deleteUnusedVideo"); Log.d(TAG, "video_method " + video_method); Log.d(TAG, "uri " + uri); Log.d(TAG, "filename " + filename); } - if( video_method == VIDEOMETHOD_FILE ) { trashImage(false, uri, filename, false); + if( video_method == VideoMethod.FILE ) { } - else if( video_method == VIDEOMETHOD_SAF ) { trashImage(true, uri, filename, false); + else if( video_method == VideoMethod.SAF ) { } // else can't delete Uri } diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java index d7a3f4ce8..495a7948c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java @@ -31,15 +31,17 @@ public interface ApplicationInterface { 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 + enum VideoMethod { + FILE, // video will be saved to a file + SAF, // video will be saved using Android 5's Storage Access Framework + URI // 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 + VideoMethod createOutputVideoMethod(); // return a VideoMethod value to specify how to create a video file File createOutputVideoFile(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_FILE; extension is the recommended filename extension for the chosen video type Uri createOutputVideoSAF(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_SAF; extension is the recommended filename extension for the chosen video type Uri createOutputVideoUri(); // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_URI @@ -177,9 +179,9 @@ public interface ApplicationInterface { 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 restartedVideo(final int video_method, final Uri uri, final String filename); // called after a seamless restart (supported on Android 8+) has occurred - in this case stoppedVideo() is only called for the final video file; this method is instead called for all earlier video file segments - void deleteUnusedVideo(final int video_method, final Uri uri, final String filename); // application should delete the requested video (which will correspond to a video file previously returned via the createOutputVideo*() methods), either because it is corrupt or unused + void stoppedVideo(final VideoMethod 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 restartedVideo(final VideoMethod video_method, final Uri uri, final String filename); // called after a seamless restart (supported on Android 8+) has occurred - in this case stoppedVideo() is only called for the final video file; this method is instead called for all earlier video file segments + void deleteUnusedVideo(final VideoMethod video_method, final Uri uri, final String filename); // application should delete the requested video (which will correspond to a video file previously returned via the createOutputVideo*() methods), either because it is corrupt or unused 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 diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java index 58395005f..d3ad53479 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java @@ -424,16 +424,16 @@ public abstract class BasicApplicationInterface implements ApplicationInterface } @Override - public void stoppedVideo(int video_method, Uri uri, String filename) { + public void stoppedVideo(VideoMethod video_method, Uri uri, String filename) { } @Override - public void restartedVideo(final int video_method, final Uri uri, final String filename) { + public void restartedVideo(final VideoMethod video_method, final Uri uri, final String filename) { } @Override - public void deleteUnusedVideo(final int video_method, final Uri uri, final String filename) { + public void deleteUnusedVideo(final VideoMethod video_method, final Uri uri, final String filename) { } @Override diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 8cb1c0ac1..b5767b654 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -181,18 +181,18 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu * (e.g., supplied ParcelFileDescriptor). */ private static class VideoFileInfo { - private final int video_method; - private final Uri video_uri; // for VIDEOMETHOD_SAF or VIDEOMETHOD_URI - private final String video_filename; // for VIDEOMETHOD_FILE - private final ParcelFileDescriptor video_pfd_saf; // for VIDEOMETHOD_SAF + private final ApplicationInterface.VideoMethod video_method; + private final Uri video_uri; // for VideoMethod.SAF or VideoMethod.URI + private final String video_filename; // for VideoMethod.FILE + private final ParcelFileDescriptor video_pfd_saf; // for VideoMethod.SAF VideoFileInfo() { - this.video_method = ApplicationInterface.VIDEOMETHOD_FILE; + this.video_method = ApplicationInterface.VideoMethod.FILE; this.video_uri = null; this.video_filename = null; this.video_pfd_saf = null; } - VideoFileInfo(int video_method, Uri video_uri, String video_filename, ParcelFileDescriptor video_pfd_saf) { + VideoFileInfo(ApplicationInterface.VideoMethod video_method, Uri video_uri, String video_filename, ParcelFileDescriptor video_pfd_saf) { this.video_method = video_method; this.video_uri = video_uri; this.video_filename = video_filename; @@ -5141,7 +5141,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu try { //if( true ) // throw new IOException(); // test - if( info.video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + if( info.video_method == ApplicationInterface.VideoMethod.FILE ) { video_recorder.setNextOutputFile(new File(info.video_filename)); } else { @@ -5305,12 +5305,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu VideoFileInfo video_file_info = null; ParcelFileDescriptor video_pfd_saf = null; try { - int method = applicationInterface.createOutputVideoMethod(); + ApplicationInterface.VideoMethod method = applicationInterface.createOutputVideoMethod(); Uri video_uri = null; String video_filename = null; if( MyDebug.LOG ) Log.d(TAG, "method? " + method); - if( method == ApplicationInterface.VIDEOMETHOD_FILE ) { + if( method == ApplicationInterface.VideoMethod.FILE ) { /*if( true ) throw new IOException(); // test*/ File videoFile = applicationInterface.createOutputVideoFile(extension); @@ -5320,7 +5320,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } else { Uri uri; - if( method == ApplicationInterface.VIDEOMETHOD_SAF ) { + if( method == ApplicationInterface.VideoMethod.SAF ) { uri = applicationInterface.createOutputVideoSAF(extension); } else { @@ -5480,7 +5480,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "actual video_max_duration: " + video_max_duration); local_video_recorder.setMaxDuration((int)video_max_duration); - if( videoFileInfo.video_method == ApplicationInterface.VIDEOMETHOD_FILE ) { + if( videoFileInfo.video_method == ApplicationInterface.VideoMethod.FILE ) { local_video_recorder.setOutputFile(videoFileInfo.video_filename); } else { -- GitLab From 5daf5f0c171c695dfd6014ed7d660f6e6b0e8c79 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 15 Aug 2020 17:34:01 +0100 Subject: [PATCH 044/430] More refactoring related to converting int to VideoMethod enum. --- .../net/sourceforge/opencamera/MyApplicationInterface.java | 2 +- .../opencamera/preview/ApplicationInterface.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index ce9d9ed26..ba8011b38 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2046,7 +2046,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { try { synchronized( this ) { if( writer == null ) { - if( video_method == ApplicationInterface.VideoMethod.FILE ) { + if( video_method == VideoMethod.FILE ) { String subtitle_filename = last_video_file.getAbsolutePath(); subtitle_filename = getSubtitleFilename(subtitle_filename); writer = new FileWriter(subtitle_filename); diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java index 495a7948c..3fa467eef 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java @@ -42,9 +42,9 @@ public interface ApplicationInterface { 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) VideoMethod createOutputVideoMethod(); // return a VideoMethod value to specify how to create a video file - File createOutputVideoFile(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_FILE; extension is the recommended filename extension for the chosen video type - Uri createOutputVideoSAF(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_SAF; extension is the recommended filename extension for the chosen video type - Uri createOutputVideoUri(); // will be called if createOutputVideoUsingSAF() returns VIDEOMETHOD_URI + File createOutputVideoFile(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VideoMethod.FILE; extension is the recommended filename extension for the chosen video type + Uri createOutputVideoSAF(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VideoMethod.SAF; extension is the recommended filename extension for the chosen video type + 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 -- GitLab From 08226fe5e558252971146fb3f76eb7adbd0c4c9a Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 15 Aug 2020 21:21:16 +0100 Subject: [PATCH 045/430] Refactor ImageSaver.broadcastSAFFile to use common code in StorageUtils.broadcastUri. --- .../sourceforge/opencamera/ImageSaver.java | 27 ++----------------- .../opencamera/MyApplicationInterface.java | 2 +- .../sourceforge/opencamera/StorageUtils.java | 16 +++++++++-- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 80297dfcb..22d1a0fe1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2637,34 +2637,11 @@ public class ImageSaver extends Thread { private void broadcastSAFFile(Uri saveUri, boolean image_capture_intent) { if( MyDebug.LOG ) Log.d(TAG, "broadcastSAFFile"); - /* 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. - Note this will no longer work on Android Q's scoped storage (getFileFromDocumentUriSAF will return null). - But NEW_PICTURE etc are no longer sent on Android 7+ anyway. - */ StorageUtils storageUtils = main_activity.getStorageUtils(); - File real_file = storageUtils.getFileFromDocumentUriSAF(saveUri, false); - if( MyDebug.LOG ) - Log.d(TAG, "real_file: " + real_file); + File real_file = storageUtils.broadcastUri(saveUri, true, false, true, image_capture_intent); 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); - } } /** As setExifFromFile, but can read the Exif tags directly from the jpeg data, and to a file descriptor, rather than a file. @@ -3054,7 +3031,7 @@ public class ImageSaver extends Thread { storageUtils.broadcastFile(picFile, true, false, false); } else { - storageUtils.broadcastUri(saveUri, true, false, false); + storageUtils.broadcastUri(saveUri, true, false, false, false); } } catch(FileNotFoundException e) { diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index ba8011b38..a186febed 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2280,7 +2280,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { else { if( uri != null ) { // see note in onPictureTaken() for where we call broadcastFile for SAF photos - File real_file = storageUtils.broadcastUri(uri, false, true, true); + File real_file = storageUtils.broadcastUri(uri, false, true, true, false); if( real_file != null ) { main_activity.test_last_saved_image = real_file.getAbsolutePath(); } diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 06f8da3e8..6a331f631 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -266,9 +266,20 @@ public class StorageUtils { /** Wrapper for broadcastFile, when we only have a Uri (e.g., for SAF) */ - public File broadcastUri(final Uri uri, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned) { + public File broadcastUri(final Uri uri, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned, final boolean image_capture_intent) { if( MyDebug.LOG ) Log.d(TAG, "broadcastUri: " + uri); + /* 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. + Note this will no longer work on Android Q's scoped storage (getFileFromDocumentUriSAF will return null). + But NEW_PICTURE etc are no longer sent on Android 7+ anyway. + */ File real_file = getFileFromDocumentUriSAF(uri, false); if( MyDebug.LOG ) Log.d(TAG, "real_file: " + real_file); @@ -280,9 +291,10 @@ public class StorageUtils { broadcastFile(real_file, is_new_picture, is_new_video, set_last_scanned); return real_file; } - else { + else if( !image_capture_intent ) { if( MyDebug.LOG ) Log.d(TAG, "announce SAF uri"); + // shouldn't do this for an image capture intent - e.g., causes crash when calling from Google Keep announceUri(uri, is_new_picture, is_new_video); } return null; -- GitLab From 03a74f9d20d95eaf377f0e1b46de610373b2d5fa Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 15 Aug 2020 21:22:06 +0100 Subject: [PATCH 046/430] Refactor code to new getVideoMimeType(). --- .../sourceforge/opencamera/StorageUtils.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 6a331f631..d85916c2f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -748,6 +748,24 @@ public class StorageUtils { return mimeType; } + /** Return the mime type corresponding to the supplied extension. Supports video only, not images. + */ + String getVideoMimeType(String extension) { + String mimeType; + switch( extension ) { + case "3gp": + mimeType = "video/3gpp"; + break; + case "webm": + mimeType = "video/webm"; + break; + default: + mimeType = "video/mp4"; + break; + } + return mimeType; + } + // only valid if isUsingSAF() @TargetApi(Build.VERSION_CODES.LOLLIPOP) Uri createOutputMediaFileSAF(int type, String suffix, String extension, Date current_date) throws IOException { @@ -757,17 +775,7 @@ public class StorageUtils { mimeType = getImageMimeType(extension); break; case MEDIA_TYPE_VIDEO: - switch( extension ) { - case "3gp": - mimeType = "video/3gpp"; - break; - case "webm": - mimeType = "video/webm"; - break; - default: - mimeType = "video/mp4"; - break; - } + mimeType = getVideoMimeType(extension); break; case MEDIA_TYPE_PREFS: case MEDIA_TYPE_GYRO_INFO: -- GitLab From bc3f9c592dd58ad11942bb52ec81a06c497319a7 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 21 Aug 2020 23:11:14 +0100 Subject: [PATCH 047/430] Add comment. --- .../java/net/sourceforge/opencamera/LocationSupplier.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java b/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java index 3ecebb605..99fbd4b04 100644 --- a/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java +++ b/app/src/main/java/net/sourceforge/opencamera/LocationSupplier.java @@ -74,6 +74,10 @@ public class LocationSupplier { } } + /** If adding extra calls to this, consider whether explicit user permission is required, and whether + * privacy policy needs updating. + * @return Returns null if location not available. + */ public Location getLocation() { return getLocation(null); } -- GitLab From 67cc499a67cf445b48281d788e0b494da4d68f4d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 22 Aug 2020 14:38:43 +0100 Subject: [PATCH 048/430] Refactor to change video boolean to new UriType enum. --- .../sourceforge/opencamera/StorageUtils.java | 115 ++++++++++++++---- 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index d85916c2f..bc577192f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -810,12 +810,17 @@ public class StorageUtils { } } - private Media getLatestMediaCore(Uri baseUri, String bucket_id, boolean video) { + private enum UriType { + MEDIASTORE_IMAGES, + MEDIASTORE_VIDEOS + }; + + private Media getLatestMediaCore(Uri baseUri, String bucket_id, UriType uri_type) { if( MyDebug.LOG ) { Log.d(TAG, "getLatestMediaCore"); Log.d(TAG, "baseUri: " + baseUri); Log.d(TAG, "bucket_id: " + bucket_id); - Log.d(TAG, "video: " + video); + Log.d(TAG, "uri_type: " + uri_type); } Media media = null; @@ -826,31 +831,59 @@ public class StorageUtils { final int column_orientation_c = 4; // for images only*/ final int column_name_c = 2; // filename (without path), including extension final int column_orientation_c = 3; // for images only - String [] projection = video ? - new String[] {VideoColumns._ID, VideoColumns.DATE_TAKEN, VideoColumns.DISPLAY_NAME} : - new String[] {ImageColumns._ID, ImageColumns.DATE_TAKEN, ImageColumns.DISPLAY_NAME, ImageColumns.ORIENTATION}; + String [] projection; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + projection = new String[] {ImageColumns._ID, ImageColumns.DATE_TAKEN, ImageColumns.DISPLAY_NAME, ImageColumns.ORIENTATION}; + break; + case MEDIASTORE_VIDEOS: + projection = new String[] {VideoColumns._ID, VideoColumns.DATE_TAKEN, VideoColumns.DISPLAY_NAME}; + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); + } // for images, we need to search for JPEG/etc and RAW, to support RAW only mode (even if we're not currently in that mode, it may be that previously the user did take photos in RAW only mode) /*String selection = video ? "" : ImageColumns.MIME_TYPE + "='image/jpeg' OR " + ImageColumns.MIME_TYPE + "='image/webp' OR " + ImageColumns.MIME_TYPE + "='image/png' OR " + ImageColumns.MIME_TYPE + "='image/x-adobe-dng'";*/ String selection = ""; - if( bucket_id != null ) - selection = (video ? VideoColumns.BUCKET_ID : ImageColumns.BUCKET_ID) + " = " + bucket_id; - if( !video ) { - boolean and = selection.length() > 0; - if( and ) - selection += " AND ( "; - selection += ImageColumns.MIME_TYPE + "='image/jpeg' OR " + - ImageColumns.MIME_TYPE + "='image/webp' OR " + - ImageColumns.MIME_TYPE + "='image/png' OR " + - ImageColumns.MIME_TYPE + "='image/x-adobe-dng'"; - if( and ) - selection += " )"; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + { + if( bucket_id != null ) + selection = ImageColumns.BUCKET_ID + " = " + bucket_id; + boolean and = selection.length() > 0; + if( and ) + selection += " AND ( "; + selection += ImageColumns.MIME_TYPE + "='image/jpeg' OR " + + ImageColumns.MIME_TYPE + "='image/webp' OR " + + ImageColumns.MIME_TYPE + "='image/png' OR " + + ImageColumns.MIME_TYPE + "='image/x-adobe-dng'"; + if( and ) + selection += " )"; + break; + } + case MEDIASTORE_VIDEOS: + if( bucket_id != null ) + selection = VideoColumns.BUCKET_ID + " = " + bucket_id; + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); } if( MyDebug.LOG ) Log.d(TAG, "selection: " + selection); - String order = video ? VideoColumns.DATE_TAKEN + " DESC," + VideoColumns._ID + " DESC" : ImageColumns.DATE_TAKEN + " DESC," + ImageColumns._ID + " DESC"; + String order; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + order = ImageColumns.DATE_TAKEN + " DESC," + ImageColumns._ID + " DESC"; + break; + case MEDIASTORE_VIDEOS: + order = VideoColumns.DATE_TAKEN + " DESC," + VideoColumns._ID + " DESC"; + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); + } Cursor cursor = null; // we know we only want the most recent image - however we may need to scan forward if we find a RAW, to see if there's @@ -974,11 +1007,26 @@ public class StorageUtils { long id = cursor.getLong(column_id_c); long date = cursor.getLong(column_date_taken_c); - int orientation = video ? 0 : cursor.getInt(column_orientation_c); + int orientation = (uri_type == UriType.MEDIASTORE_IMAGES) ? cursor.getInt(column_orientation_c) : 0; Uri uri = ContentUris.withAppendedId(baseUri, id); String filename = cursor.getString(column_name_c); if( MyDebug.LOG ) - Log.d(TAG, "found most recent uri for " + (video ? "video" : "images") + ": " + uri); + Log.d(TAG, "found most recent uri for " + uri_type + ": " + uri); + + boolean video; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + video = false; + break; + case MEDIASTORE_VIDEOS: + video = true; + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); + } + if( MyDebug.LOG ) + Log.d(TAG, "video: " + video); + media = new Media(id, video, uri, date, orientation, filename); } else { @@ -1001,10 +1049,10 @@ public class StorageUtils { return media; } - private Media getLatestMedia(boolean video) { + private Media getLatestMedia(UriType uri_type) { 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 ) { + Log.d(TAG, "getLatestMedia: " + uri_type); // 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 @@ -1024,20 +1072,33 @@ public class StorageUtils { if( MyDebug.LOG ) Log.d(TAG, "bucket_id: " + bucket_id); - Uri baseUri = video ? Video.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - Media media = getLatestMediaCore(baseUri, bucket_id, video); + Uri baseUri; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + break; + case MEDIASTORE_VIDEOS: + baseUri = Video.Media.EXTERNAL_CONTENT_URI; + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); + } + + if( MyDebug.LOG ) + Log.d(TAG, "baseUri: " + baseUri); + Media media = getLatestMediaCore(baseUri, bucket_id, uri_type); if( media == null && bucket_id != null ) { if( MyDebug.LOG ) Log.d(TAG, "fall back to checking any folder"); - media = getLatestMediaCore(baseUri, null, video); + media = getLatestMediaCore(baseUri, null, uri_type); } return media; } Media getLatestMedia() { - Media image_media = getLatestMedia(false); - Media video_media = getLatestMedia(true); + Media image_media = getLatestMedia(UriType.MEDIASTORE_IMAGES); + Media video_media = getLatestMedia(UriType.MEDIASTORE_VIDEOS); Media media = null; if( image_media != null && video_media == null ) { if( MyDebug.LOG ) -- GitLab From daa5412061ab787e9742e9a538eff823cef7c889 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 22 Aug 2020 19:46:07 +0100 Subject: [PATCH 049/430] Refactor code to new loadThumbnailFromUri(). --- .../sourceforge/opencamera/MainActivity.java | 129 ++++++++++-------- 1 file changed, 71 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 452fbb8b2..b090d1c4a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -2935,6 +2935,76 @@ public class MainActivity extends Activity { container.setVisibility(show ? View.GONE : View.VISIBLE); } + /** Loads a thumbnail from the supplied image uri (not videos). Note this loads from the bitmap + * rather than reading from MediaStore. Therefore this works with SAF uris as well as + * MediaStore uris, as well as allowing control over the resolution of the thumbnail. + * If sample_factor is 1, this returns a bitmap scaled to match the display resolution. If + * sample_factor is greater than 1, it will be scaled down to a lower resolution. + */ + private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor) { + Bitmap thumbnail = null; + try { + //thumbnail = MediaStore.Images.Media.getBitmap(getContentResolver(), media.uri); + // only need to load a bitmap as large as the screen size + BitmapFactory.Options options = new BitmapFactory.Options(); + InputStream is = getContentResolver().openInputStream(uri); + // get dimensions + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + int bitmap_width = options.outWidth; + int bitmap_height = options.outHeight; + Point display_size = new Point(); + Display display = getWindowManager().getDefaultDisplay(); + display.getSize(display_size); + if( MyDebug.LOG ) { + Log.d(TAG, "bitmap_width: " + bitmap_width); + Log.d(TAG, "bitmap_height: " + bitmap_height); + Log.d(TAG, "display width: " + display_size.x); + Log.d(TAG, "display height: " + display_size.y); + } + // align dimensions + if( display_size.x < display_size.y ) { + //noinspection SuspiciousNameCombination + display_size.set(display_size.y, display_size.x); + } + if( bitmap_width < bitmap_height ) { + int dummy = bitmap_width; + //noinspection SuspiciousNameCombination + bitmap_width = bitmap_height; + bitmap_height = dummy; + } + if( MyDebug.LOG ) { + Log.d(TAG, "bitmap_width: " + bitmap_width); + Log.d(TAG, "bitmap_height: " + bitmap_height); + Log.d(TAG, "display width: " + display_size.x); + Log.d(TAG, "display height: " + display_size.y); + } + // only care about height, to save worrying about different aspect ratios + options.inSampleSize = 1; + while( bitmap_height / (2*options.inSampleSize) >= display_size.y ) { + options.inSampleSize *= 2; + } + options.inSampleSize *= sample_factor; + if( MyDebug.LOG ) { + Log.d(TAG, "inSampleSize: " + options.inSampleSize); + } + options.inJustDecodeBounds = false; + // need a new inputstream, see https://stackoverflow.com/questions/2503628/bitmapfactory-decodestream-returning-null-when-options-are-set + is.close(); + is = getContentResolver().openInputStream(uri); + thumbnail = BitmapFactory.decodeStream(is, null, options); + if( thumbnail == null ) { + Log.e(TAG, "decodeStream returned null bitmap for ghost image last"); + } + is.close(); + } + catch(IOException e) { + Log.e(TAG, "failed to load bitmap for ghost image last"); + e.printStackTrace(); + } + return thumbnail; + } + /** Shows the default "blank" gallery icon, when we don't have a thumbnail available. */ private void updateGalleryIconToBlank() { @@ -2997,64 +3067,7 @@ public class MainActivity extends Activity { if( ghost_image_last && !media.video ) { if( MyDebug.LOG ) Log.d(TAG, "load full size bitmap for ghost image last photo"); - try { - //thumbnail = MediaStore.Images.Media.getBitmap(getContentResolver(), media.uri); - // only need to load a bitmap as large as the screen size - BitmapFactory.Options options = new BitmapFactory.Options(); - InputStream is = getContentResolver().openInputStream(media.uri); - // get dimensions - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, options); - int bitmap_width = options.outWidth; - int bitmap_height = options.outHeight; - Point display_size = new Point(); - Display display = getWindowManager().getDefaultDisplay(); - display.getSize(display_size); - if( MyDebug.LOG ) { - Log.d(TAG, "bitmap_width: " + bitmap_width); - Log.d(TAG, "bitmap_height: " + bitmap_height); - Log.d(TAG, "display width: " + display_size.x); - Log.d(TAG, "display height: " + display_size.y); - } - // align dimensions - if( display_size.x < display_size.y ) { - //noinspection SuspiciousNameCombination - display_size.set(display_size.y, display_size.x); - } - if( bitmap_width < bitmap_height ) { - int dummy = bitmap_width; - //noinspection SuspiciousNameCombination - bitmap_width = bitmap_height; - bitmap_height = dummy; - } - if( MyDebug.LOG ) { - Log.d(TAG, "bitmap_width: " + bitmap_width); - Log.d(TAG, "bitmap_height: " + bitmap_height); - Log.d(TAG, "display width: " + display_size.x); - Log.d(TAG, "display height: " + display_size.y); - } - // only care about height, to save worrying about different aspect ratios - options.inSampleSize = 1; - while( bitmap_height / (2*options.inSampleSize) >= display_size.y ) { - options.inSampleSize *= 2; - } - if( MyDebug.LOG ) { - Log.d(TAG, "inSampleSize: " + options.inSampleSize); - } - options.inJustDecodeBounds = false; - // need a new inputstream, see https://stackoverflow.com/questions/2503628/bitmapfactory-decodestream-returning-null-when-options-are-set - is.close(); - is = getContentResolver().openInputStream(media.uri); - thumbnail = BitmapFactory.decodeStream(is, null, options); - if( thumbnail == null ) { - Log.e(TAG, "decodeStream returned null bitmap for ghost image last"); - } - is.close(); - } - catch(IOException e) { - Log.e(TAG, "failed to load bitmap for ghost image last"); - e.printStackTrace(); - } + thumbnail = loadThumbnailFromUri(media.uri, 1); } if( thumbnail == null ) { try { -- GitLab From 7ba1f04c563ff41461f5be7a5362b8fe3fae6854 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 22 Aug 2020 20:16:36 +0100 Subject: [PATCH 050/430] Refactor - ensure we always set is_video from media.video. --- .../main/java/net/sourceforge/opencamera/MainActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index b090d1c4a..566fe76c5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -3064,6 +3064,9 @@ public class MainActivity extends Activity { 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 + + is_video = media.video; + if( ghost_image_last && !media.video ) { if( MyDebug.LOG ) Log.d(TAG, "load full size bitmap for ghost image last photo"); @@ -3075,7 +3078,6 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) Log.d(TAG, "load thumbnail for video"); thumbnail = MediaStore.Video.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Video.Thumbnails.MINI_KIND, null); - is_video = true; } else { if( MyDebug.LOG ) -- GitLab From 76bfb060d9c02e9fc78a7a32d68d5d7ee44a3dbd Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 23 Aug 2020 13:36:21 +0100 Subject: [PATCH 051/430] Some refactoring. --- .../opencamera/MyApplicationInterface.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index a186febed..c6b5e4aba 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -86,7 +86,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { private boolean panorama_dir_left_to_right = true; // direction of panorama (set after we've captured two images) private File last_video_file = null; - private Uri last_video_file_saf = null; + private Uri last_video_file_uri = null; private final Timer subtitleVideoTimer = new Timer(); private TimerTask subtitleVideoTimerTask; @@ -290,8 +290,12 @@ public class MyApplicationInterface extends BasicApplicationInterface { // 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; + else if( storageUtils.isUsingSAF() ) { + return VideoMethod.SAF; + } + else { + return VideoMethod.FILE; + } } @Override @@ -302,8 +306,8 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public Uri createOutputVideoSAF(String extension) throws IOException { - last_video_file_saf = storageUtils.createOutputMediaFileSAF(StorageUtils.MEDIA_TYPE_VIDEO, "", extension, new Date()); - return last_video_file_saf; + last_video_file_uri = storageUtils.createOutputMediaFileSAF(StorageUtils.MEDIA_TYPE_VIDEO, "", extension, new Date()); + return last_video_file_uri; } @Override @@ -2053,8 +2057,8 @@ public class MyApplicationInterface extends BasicApplicationInterface { } else { if( MyDebug.LOG ) - Log.d(TAG, "last_video_file_saf: " + last_video_file_saf); - String subtitle_filename = storageUtils.getFileName(last_video_file_saf); + Log.d(TAG, "last_video_file_uri: " + last_video_file_uri); + String subtitle_filename = storageUtils.getFileName(last_video_file_uri); 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 pfd_saf = getContext().getContentResolver().openFileDescriptor(subtitle_uri, "w"); -- GitLab From 9f323c1c49811c218a3dd8651e9647f5f4d52dc7 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 23 Aug 2020 16:47:15 +0100 Subject: [PATCH 052/430] Refactor and simplify code for preferring non-DNG to DNG images from mediastore. --- .../sourceforge/opencamera/StorageUtils.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index bc577192f..3b29d4143 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -810,6 +810,17 @@ public class StorageUtils { } } + private static boolean filenameIsRaw(String filename) { + return filename.toLowerCase(Locale.US).endsWith(".dng"); + } + + private static String filenameWithoutExtension(String filename) { + String filename_without_ext = filename.toLowerCase(Locale.US); + if( filename_without_ext.indexOf(".") > 0 ) + filename_without_ext = filename_without_ext.substring(0, filename_without_ext.lastIndexOf(".")); + return filename_without_ext; + } + private enum UriType { MEDIASTORE_IMAGES, MEDIASTORE_VIDEOS @@ -947,14 +958,12 @@ public class StorageUtils { Log.d(TAG, "filename: " + filename); } // in theory now that we use DISPLAY_NAME instead of DATA (for path), this should always be non-null, but check just in case - if( filename != null && filename.toLowerCase(Locale.US).endsWith(".dng") ) { + if( filename != null && filenameIsRaw(filename) ) { if( MyDebug.LOG ) Log.d(TAG, "try to find a non-RAW version of the DNG"); int dng_pos = cursor.getPosition(); boolean found_non_raw = false; - String filename_without_ext = filename.toLowerCase(Locale.US); - if( filename_without_ext.indexOf(".") > 0 ) - filename_without_ext = filename_without_ext.substring(0, filename_without_ext.lastIndexOf(".")); + String filename_without_ext = filenameWithoutExtension(filename); if( MyDebug.LOG ) Log.d(TAG, "filename_without_ext: " + filename_without_ext); while( cursor.moveToNext() ) { @@ -966,9 +975,7 @@ public class StorageUtils { Log.d(TAG, "done scanning, couldn't find filename"); break; } - String next_filename_without_ext = next_filename.toLowerCase(Locale.US); - if( next_filename_without_ext.indexOf(".") > 0 ) - next_filename_without_ext = next_filename_without_ext.substring(0, next_filename_without_ext.lastIndexOf(".")); + String next_filename_without_ext = filenameWithoutExtension(next_filename); if( MyDebug.LOG ) Log.d(TAG, "next_filename_without_ext: " + next_filename_without_ext); if( !filename_without_ext.equals(next_filename_without_ext) ) { @@ -978,21 +985,16 @@ public class StorageUtils { break; } // so we've found another file with matching filename - is it a JPEG/etc? - if( next_filename.toLowerCase(Locale.US).endsWith(".jpg") ) { - if( MyDebug.LOG ) - Log.d(TAG, "found equivalent jpeg"); - found_non_raw = true; - break; - } - else if( next_filename.toLowerCase(Locale.US).endsWith(".webp") ) { + // we've already restricted the query to the image types we're interested in, so + // only need to check that it isn't another DNG (which would be strange, as it + // would mean a duplicate filename, but check just in case!) + if( filenameIsRaw(next_filename) ) { if( MyDebug.LOG ) - Log.d(TAG, "found equivalent webp"); - found_non_raw = true; - break; + Log.d(TAG, "found another dng!"); } - else if( next_filename.toLowerCase(Locale.US).endsWith(".png") ) { + else { if( MyDebug.LOG ) - Log.d(TAG, "found equivalent png"); + Log.d(TAG, "found equivalent non-dng"); found_non_raw = true; break; } -- GitLab From d4d56ef5fd173a1340bdc6d4a6b67420a1e83e32 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 28 Aug 2020 13:42:34 +0100 Subject: [PATCH 053/430] Refactor code to rotateForExif(). --- .../sourceforge/opencamera/MainActivity.java | 63 +++++++++++++++++++ .../opencamera/ui/DrawPreview.java | 53 +--------------- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 566fe76c5..ea36f227e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -67,6 +67,8 @@ import android.renderscript.RenderScript; import android.speech.tts.TextToSpeech; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import androidx.exifinterface.media.ExifInterface; + import android.util.Log; import android.view.Display; import android.view.GestureDetector; @@ -2935,6 +2937,67 @@ public class MainActivity extends Activity { container.setVisibility(show ? View.GONE : View.VISIBLE); } + /** Rotates the supplied bitmap according to the orientation tag stored in the exif data. If no + * rotation is required, the input bitmap is returned. If rotation is required, the input + * bitmap is recycled. + * @param uri Uri containing the JPEG with Exif information to use. + */ + public Bitmap rotateForExif(Bitmap bitmap, Uri uri) throws IOException { + ExifInterface exif; + InputStream inputStream = null; + try { + inputStream = this.getContentResolver().openInputStream(uri); + exif = new ExifInterface(inputStream); + } + finally { + if( inputStream != null ) + inputStream.close(); + } + + if( exif != null ) { + int exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + boolean needs_tf = false; + int exif_orientation = 0; + // see 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 ) { + if( MyDebug.LOG ) + Log.d(TAG, " need to rotate bitmap due to exif orientation tag"); + 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; + } + } + } + return bitmap; + } + /** Loads a thumbnail from the supplied image uri (not videos). Note this loads from the bitmap * rather than reading from MediaStore. Therefore this works with SAF uris as well as * MediaStore uris, as well as allowing control over the resolution of the thumbnail. diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java index 57036149b..0138c98c2 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java @@ -755,58 +755,7 @@ public class DrawPreview { // now need to take exif orientation into account, as some devices or camera apps store the orientation in the exif tag, // which getBitmap() doesn't account for - ExifInterface exif; - InputStream inputStream = null; - try { - inputStream = main_activity.getContentResolver().openInputStream(uri); - exif = new ExifInterface(inputStream); - } - finally { - if( inputStream != null ) - inputStream.close(); - } - - if( exif != null ) { - int exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); - boolean needs_tf = false; - int exif_orientation = 0; - // see 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 ) { - if( MyDebug.LOG ) - Log.d(TAG, " need to rotate bitmap due to exif orientation tag"); - 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; - } - } - } + bitmap = main_activity.rotateForExif(bitmap, uri); return bitmap; } -- GitLab From 73a7600f741f9db634aadd970835b1e14b45235c Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 28 Aug 2020 13:42:59 +0100 Subject: [PATCH 054/430] Update function comment. --- app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 22d1a0fe1..40a5d26db 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -3069,8 +3069,9 @@ public class ImageSaver extends Thread { return success; } - /** Rotates the supplied bitmap according to the orientation tag stored in the exif data.. If no - * rotation is required, the input bitmap is returned. + /** Rotates the supplied bitmap according to the orientation tag stored in the exif data. If no + * rotation is required, the input bitmap is returned. If rotation is required, the input + * bitmap is recycled. * @param data Jpeg data containing the Exif information to use. */ private Bitmap rotateForExif(Bitmap bitmap, byte [] data) { -- GitLab From 14ab86d313cf35fab23944c8b7b8cd0b2c9345e4 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 28 Aug 2020 22:53:26 +0100 Subject: [PATCH 055/430] Refactor to avoid callers using File API unnecessarily, when they only want the path as a string. --- .../sourceforge/opencamera/MainActivity.java | 16 ++++---- .../sourceforge/opencamera/StorageUtils.java | 39 +++++++++++++++++-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index ea36f227e..c7ebd9c4d 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -3486,9 +3486,9 @@ public class MainActivity extends Activity { Log.d(TAG, "update folder history for saf"); updateFolderHistorySAF(treeUri.toString()); - File file = applicationInterface.getStorageUtils().getImageFolder(); + String file = applicationInterface.getStorageUtils().getImageFolderPath(); if( file != null ) { - preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + file.getAbsolutePath()); + preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + file); } } catch(SecurityException e) { @@ -3716,9 +3716,9 @@ public class MainActivity extends Activity { String folder_name = history.get(history.size() - 1 - i); if( applicationInterface.getStorageUtils().isUsingSAF() ) { // try to get human readable form if possible - File file = applicationInterface.getStorageUtils().getFileFromDocumentUriSAF(Uri.parse(folder_name), true); - if( file != null ) { - folder_name = file.getAbsolutePath(); + String file_name = applicationInterface.getStorageUtils().getFilePathFromDocumentUriSAF(Uri.parse(folder_name), true); + if( file_name != null ) { + folder_name = file_name; } } items[index++] = folder_name; @@ -3791,9 +3791,9 @@ public class MainActivity extends Activity { 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(); + String file_name = applicationInterface.getStorageUtils().getFilePathFromDocumentUriSAF(Uri.parse(save_folder), true); + if( file_name != null ) { + save_folder_name = file_name; } } preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name); diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 3b29d4143..c4a81f8c6 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -333,8 +333,28 @@ public class StorageUtils { return new File(context.getExternalFilesDir(null), "backups"); } - // 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 + /** Only valid if isUsingSAF() + * Returns the absolute path (in File format) of the SAF folder. + * Only use this for needing e.g. human-readable strings for UI. + * This should not be used to create a File - instead, use getFileFromDocumentUriSAF(). + */ + /** Valid if whether or not isUsingSAF(). + * Returns the absolute path (in File format) of the image save folder. + * Only use this for needing e.g. human-readable strings for UI. + * This should not be used to create a File - instead, use getImageFolder(). + * 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) + public String getImageFolderPath() { + File file = getImageFolder(); + return file == null ? null : file.getAbsolutePath(); + } + + /** 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; @@ -372,6 +392,17 @@ public class StorageUtils { return file; } + /** Only valid if isUsingSAF() + * Returns the absolute path (in File format) of the SAF folder. + * Only use this for needing e.g. human-readable strings for UI. + * This should not be used to create a File - instead, use getFileFromDocumentUriSAF(). + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public String getFilePathFromDocumentUriSAF(Uri uri, boolean is_folder) { + File file = getFileFromDocumentUriSAF(uri, is_folder); + return file == null ? null : file.getAbsolutePath(); + } + /** 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, or that * the File can be obtained anyway. @@ -1064,12 +1095,12 @@ public class StorageUtils { return null; } - File save_folder = getImageFolder(); // may be null if using SAF + String save_folder = getImageFolderPath(); // may be null if using SAF if( MyDebug.LOG ) Log.d(TAG, "save_folder: " + save_folder); String bucket_id = null; if( save_folder != null ) { - bucket_id = String.valueOf(save_folder.getAbsolutePath().toLowerCase().hashCode()); + bucket_id = String.valueOf(save_folder.toLowerCase().hashCode()); } if( MyDebug.LOG ) Log.d(TAG, "bucket_id: " + bucket_id); -- GitLab From 47fae3cd00fd461075c8a029f55ce385ff9b5ccb Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 28 Aug 2020 22:53:51 +0100 Subject: [PATCH 056/430] Fix misleading comment. --- app/src/main/java/net/sourceforge/opencamera/StorageUtils.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index c4a81f8c6..348c511f3 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -277,8 +277,6 @@ public class StorageUtils { 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. - Note this will no longer work on Android Q's scoped storage (getFileFromDocumentUriSAF will return null). - But NEW_PICTURE etc are no longer sent on Android 7+ anyway. */ File real_file = getFileFromDocumentUriSAF(uri, false); if( MyDebug.LOG ) -- GitLab From 556c67c54f4e3b6953543f84636d75fa210c024d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 29 Aug 2020 13:58:56 +0100 Subject: [PATCH 057/430] New test testTakeVideoSubtitlesSAF. --- .../opencamera/test/MainActivityTest.java | 21 +++++++++++++++++++ .../opencamera/test/VideoTests.java | 1 + 2 files changed, 22 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 59cca52f0..3dd7962fe 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -6308,6 +6308,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sat, 29 Aug 2020 14:10:51 +0100 Subject: [PATCH 058/430] Refactor last_images_saf boolean to LastImagesType enum. --- .../opencamera/MyApplicationInterface.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index c6b5e4aba..1d11c34fa 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -97,7 +97,11 @@ public class MyApplicationInterface extends BasicApplicationInterface { // store to avoid calling PreferenceManager.getDefaultSharedPreferences() repeatedly private final SharedPreferences sharedPreferences; - private boolean last_images_saf; // whether the last images array are using SAF or not + private enum LastImagesType { + FILE, + SAF + } + private LastImagesType last_images_type = LastImagesType.FILE; // whether the last images array are using File API, SAF /** This class keeps track of the images saved in this batch, for use with Pause Preview option, so we can share or trash images. */ @@ -2307,11 +2311,11 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "uri " + uri); Log.d(TAG, "filename " + filename); } - trashImage(false, uri, filename, false); if( video_method == VideoMethod.FILE ) { + trashImage(LastImagesType.FILE, uri, filename, false); } - trashImage(true, uri, filename, false); else if( video_method == VideoMethod.SAF ) { + trashImage(LastImagesType.SAF, uri, filename, false); } // else can't delete Uri } @@ -3261,7 +3265,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "addLastImage: " + file); Log.d(TAG, "share?: " + share); } - last_images_saf = false; + last_images_type = LastImagesType.FILE; LastImage last_image = new LastImage(file.getAbsolutePath(), share); last_images.add(last_image); } @@ -3271,7 +3275,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "addLastImageSAF: " + uri); Log.d(TAG, "share?: " + share); } - last_images_saf = true; + last_images_type = LastImagesType.SAF; LastImage last_image = new LastImage(uri, share); last_images.add(last_image); } @@ -3279,7 +3283,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { void clearLastImages() { if( MyDebug.LOG ) Log.d(TAG, "clearLastImages"); - last_images_saf = false; + last_images_type = LastImagesType.FILE; last_images.clear(); drawPreview.clearLastImage(); } @@ -3322,13 +3326,13 @@ public class MyApplicationInterface extends BasicApplicationInterface { } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void trashImage(boolean image_saf, Uri image_uri, String image_name, boolean from_user) { + private void trashImage(LastImagesType image_type, Uri image_uri, String image_name, boolean from_user) { if( MyDebug.LOG ) Log.d(TAG, "trashImage"); Preview preview = main_activity.getPreview(); - if( image_saf && image_uri != null ) { + if( image_type == LastImagesType.SAF && image_uri != null ) { if( MyDebug.LOG ) - Log.d(TAG, "Delete: " + image_uri); + Log.d(TAG, "Delete SAF: " + image_uri); File file = storageUtils.getFileFromDocumentUriSAF(image_uri, false); // need to get file before deleting it, as fileFromDocumentUriSAF may depend on the file still existing try { if( !DocumentsContract.deleteDocument(main_activity.getContentResolver(), image_uri) ) { @@ -3374,12 +3378,12 @@ public class MyApplicationInterface extends BasicApplicationInterface { void trashLastImage() { if( MyDebug.LOG ) - Log.d(TAG, "trashImage"); + Log.d(TAG, "trashLastImage"); Preview preview = main_activity.getPreview(); if( preview.isPreviewPaused() ) { for(int i=0;i Date: Sat, 29 Aug 2020 14:15:45 +0100 Subject: [PATCH 059/430] Refactor to new method setLastMediaScanned(). --- .../java/net/sourceforge/opencamera/StorageUtils.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 348c511f3..1411e443e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -73,6 +73,12 @@ public class StorageUtils { last_media_scanned = null; } + void setLastMediaScanned(Uri uri) { + last_media_scanned = uri; + if( MyDebug.LOG ) + Log.d(TAG, "set last_media_scanned to " + last_media_scanned); + } + /** Sends the intents to announce the new file to other Android applications. E.g., cloud storage applications like * OwnCloud use this to listen for new photos/videos to automatically upload. * Note that on Android 7 onwards, these broadcasts are deprecated and won't have any effect - see: @@ -239,9 +245,7 @@ public class StorageUtils { 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); + setLastMediaScanned(uri); } announceUri(uri, is_new_picture, is_new_video); applicationInterface.scannedFile(file, uri); -- GitLab From cd562deb9194735260e2423816727422eba01f93 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 29 Aug 2020 15:30:36 +0100 Subject: [PATCH 060/430] No longer need to ignore these warnings. --- app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 3 +-- .../net/sourceforge/opencamera/MyApplicationInterface.java | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 40a5d26db..a8a2a12ec 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -3183,8 +3183,7 @@ public class ImageSaver extends Thread { * ParcelFileDescriptor (if one was created). */ private static class ExifInterfaceHolder { - // suppress warning - no it can't be local or removed, see documentation above about the garbage collector! - @SuppressWarnings({"FieldCanBeLocal", "unused"}) + // see documentation above about keeping hold of pdf due to the garbage collector! private final ParcelFileDescriptor pfd; private final ExifInterface exif; diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 1d11c34fa..c5c01ba9e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -1916,7 +1916,6 @@ public class MyApplicationInterface extends BasicApplicationInterface { final boolean store_geo_direction = getGeodirectionPref(); class SubtitleVideoTimerTask extends TimerTask { // need to keep a reference to pfd_saf for as long as writer, to avoid getting garbage collected - see https://sourceforge.net/p/opencamera/tickets/417/ - @SuppressWarnings("FieldCanBeLocal") private ParcelFileDescriptor pfd_saf; private OutputStreamWriter writer; private int count = 1; -- GitLab From e1e0e0d542c73a28cba5c180db133454bbd80515 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 29 Aug 2020 15:31:55 +0100 Subject: [PATCH 061/430] Ignore deprecation warnings for ZoomControls. --- .../sourceforge/opencamera/test/MainActivityTest.java | 1 + .../java/net/sourceforge/opencamera/MainActivity.java | 9 +++++++++ .../main/java/net/sourceforge/opencamera/ui/MainUI.java | 1 + 3 files changed, 11 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 3dd7962fe..668a52d4f 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -9822,6 +9822,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sat, 29 Aug 2020 15:32:56 +0100 Subject: [PATCH 062/430] Fix method comments. --- .../java/net/sourceforge/opencamera/StorageUtils.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 1411e443e..75dbe27b2 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -335,12 +335,7 @@ public class StorageUtils { return new File(context.getExternalFilesDir(null), "backups"); } - /** Only valid if isUsingSAF() - * Returns the absolute path (in File format) of the SAF folder. - * Only use this for needing e.g. human-readable strings for UI. - * This should not be used to create a File - instead, use getFileFromDocumentUriSAF(). - */ - /** Valid if whether or not isUsingSAF(). + /** Valid whether or not isUsingSAF(). * Returns the absolute path (in File format) of the image save folder. * Only use this for needing e.g. human-readable strings for UI. * This should not be used to create a File - instead, use getImageFolder(). @@ -353,7 +348,7 @@ public class StorageUtils { return file == null ? null : file.getAbsolutePath(); } - /** Valid if whether or not isUsingSAF(). + /** Valid 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. */ -- GitLab From 394d22ac6b04b6473fb9cf41026a8cb42968c093 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 29 Aug 2020 15:39:37 +0100 Subject: [PATCH 063/430] Fix unnecessary semicolon for enum. --- app/src/main/java/net/sourceforge/opencamera/StorageUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 75dbe27b2..190d8ae3b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -852,7 +852,7 @@ public class StorageUtils { private enum UriType { MEDIASTORE_IMAGES, MEDIASTORE_VIDEOS - }; + } private Media getLatestMediaCore(Uri baseUri, String bucket_id, UriType uri_type) { if( MyDebug.LOG ) { -- GitLab From f31f9e44adb0e80597fbd6b8b36b9ab591d4c51e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 29 Aug 2020 15:41:04 +0100 Subject: [PATCH 064/430] Ignore warning for duplicate switch branch. --- app/src/main/java/net/sourceforge/opencamera/StorageUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 190d8ae3b..648ee05c9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -918,6 +918,7 @@ public class StorageUtils { order = ImageColumns.DATE_TAKEN + " DESC," + ImageColumns._ID + " DESC"; break; case MEDIASTORE_VIDEOS: + //noinspection DuplicateBranchesInSwitch order = VideoColumns.DATE_TAKEN + " DESC," + VideoColumns._ID + " DESC"; break; default: -- GitLab From 5da6eb950d5f8323f31b3b7c2788812a9cf795df Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 29 Aug 2020 20:43:25 +0100 Subject: [PATCH 065/430] Fix crash when failing to crop, fail gracefully instead. --- _docs/history.html | 2 ++ .../net/sourceforge/opencamera/test/MainActivityTest.java | 5 +++++ .../main/java/net/sourceforge/opencamera/ImageSaver.java | 4 ++-- .../java/net/sourceforge/opencamera/PanoramaProcessor.java | 6 ++++++ .../sourceforge/opencamera/PanoramaProcessorException.java | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index c0939f360..0d0377df4 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -50,6 +50,8 @@ Version 1.48.3 (Work in progress) +FIXED Possible crash for panorama if failing to crop due to poor transformations; now fails + gracefully. FIXED Photos would sometimes fail to save on some devices with Storage Access Framework, when some options were enabled (options like DRO, HDR, auto-level, photostamp that require post-processing; custom Exif tags like artist or copyright; or when using geotagging with diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 668a52d4f..0641a9cbd 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -11737,6 +11737,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2Date: Mon, 31 Aug 2020 13:18:05 +0100 Subject: [PATCH 066/430] Target Android 10 - for now opting out of scoped storage, but also committing some code changes for scoped storage, currently disabled. --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 1 + .../sourceforge/opencamera/ImageSaver.java | 116 +++++++- .../sourceforge/opencamera/MainActivity.java | 71 ++++- .../opencamera/MyApplicationInterface.java | 135 ++++++++- .../opencamera/MyPreferenceFragment.java | 4 + .../opencamera/PermissionHandler.java | 10 + .../sourceforge/opencamera/StorageUtils.java | 271 +++++++++++++++++- .../preview/ApplicationInterface.java | 2 + .../opencamera/preview/Preview.java | 7 +- .../opencamera/ui/FolderChooserDialog.java | 21 +- 11 files changed, 610 insertions(+), 32 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f5a659e21..4dafc09f6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 28 + compileSdkVersion 29 compileOptions.encoding = 'UTF-8' defaultConfig { applicationId "net.sourceforge.opencamera" minSdkVersion 15 - targetSdkVersion 28 + targetSdkVersion 29 renderscriptTargetApi 21 //renderscriptSupportModeEnabled true // don't use support library as it bloats the APK, and we don't need pre-4.4 support diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8b0a81db0..afb1438b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ android:name=".OpenCameraApplication" android:theme="@style/AppTheme" android:largeHeap="true" + android:requestLegacyExternalStorage="true" > = Build.VERSION_CODES.Q ? + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) : + MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + contentValues = new ContentValues(); + String picName = storageUtils.createMediaFilename(StorageUtils.MEDIA_TYPE_IMAGE, filename_suffix, 0, "." + extension, request.current_date); + if( MyDebug.LOG ) + Log.d(TAG, "picName: " + picName); + contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, picName); + String mime_type = storageUtils.getImageMimeType(extension); + if( MyDebug.LOG ) + Log.d(TAG, "mime_type: " + mime_type); + contentValues.put(MediaStore.Images.Media.MIME_TYPE, mime_type); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + String relative_path = storageUtils.getSaveRelativeFolder(); + if( MyDebug.LOG ) + Log.d(TAG, "relative_path: " + relative_path); + contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, relative_path); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 1); + } + + saveUri = main_activity.getContentResolver().insert(folder, contentValues); + if( MyDebug.LOG ) + Log.d(TAG, "saveUri: " + saveUri); + if( saveUri == null ) { + throw new IOException(); + } + } else { picFile = storageUtils.createOutputMediaFile(StorageUtils.MEDIA_TYPE_IMAGE, filename_suffix, extension, request.current_date); if( MyDebug.LOG ) @@ -2487,7 +2531,29 @@ public class ImageSaver extends Thread { if( saveUri != null ) { success = true; - broadcastSAFFile(saveUri, request.image_capture_intent); + + if( use_media_store ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + contentValues.clear(); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 0); + main_activity.getContentResolver().update(saveUri, contentValues, null, null); + } + + // no need to broadcast when using mediastore method + if( !request.image_capture_intent ) { + if( MyDebug.LOG ) + Log.d(TAG, "announce mediastore uri"); + // in theory this is pointless, as announceUri no longer does anything on Android 7+, + // and mediastore method is only used on Android 10+, but keep this just in case + // announceUri does something in future + storageUtils.announceUri(saveUri, true, false); + // we also want to save the uri - we can use the media uri directly, rather than having to scan it + storageUtils.setLastMediaScanned(saveUri); + } + } + else { + broadcastSAFFile(saveUri, request.image_capture_intent); + } } } } @@ -2520,6 +2586,9 @@ public class ImageSaver extends Thread { else if( success && storageUtils.isUsingSAF() ){ applicationInterface.addLastImageSAF(saveUri, share_image); } + else if( success && use_media_store ){ + applicationInterface.addLastImageMediaStore(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 ) { @@ -2983,6 +3052,8 @@ public class ImageSaver extends Thread { try { File picFile = null; Uri saveUri = null; + boolean use_media_store = false; + ContentValues contentValues = null; // used if using scoped storage String suffix = "_"; String filename_suffix = (request.force_suffix) ? suffix + (request.suffix_offset) : ""; @@ -2993,6 +3064,26 @@ public class ImageSaver extends Thread { // 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 if( MainActivity.useScopedStorage() ) { + use_media_store = true; + Uri folder = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) : + MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + contentValues = new ContentValues(); + String picName = storageUtils.createMediaFilename(StorageUtils.MEDIA_TYPE_IMAGE, filename_suffix, 0, ".dng", request.current_date); + contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, picName); + contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/dng"); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, storageUtils.getSaveRelativeFolder()); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 1); + } + + saveUri = main_activity.getContentResolver().insert(folder, contentValues); + if( MyDebug.LOG ) + Log.d(TAG, "saveUri: " + saveUri); + if( saveUri == null ) + throw new IOException(); + } else { picFile = storageUtils.createOutputMediaFile(StorageUtils.MEDIA_TYPE_IMAGE, filename_suffix, "dng", request.current_date); if( MyDebug.LOG ) @@ -3026,10 +3117,27 @@ public class ImageSaver extends Thread { else if( storageUtils.isUsingSAF() ){ applicationInterface.addLastImageSAF(saveUri, raw_only); } + else if( success && use_media_store ){ + applicationInterface.addLastImageMediaStore(saveUri, raw_only); + } if( saveUri == null ) { storageUtils.broadcastFile(picFile, true, false, false); } + else if( use_media_store ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + contentValues.clear(); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 0); + main_activity.getContentResolver().update(saveUri, contentValues, null, null); + } + + // no need to broadcast when using mediastore method + + // in theory this is pointless, as announceUri no longer does anything on Android 7+, + // and mediastore method is only used on Android 10+, but keep this just in case + // announceUri does something in future + storageUtils.announceUri(saveUri, true, false); + } else { storageUtils.broadcastUri(saveUri, true, false, false, false); } diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 808b82324..921b74787 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -38,6 +38,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -678,6 +679,14 @@ public class MainActivity extends Activity { Log.d(TAG, "onCreate: total time for Activity startup: " + (System.currentTimeMillis() - debug_time)); } + /** Whether to use codepaths that are compatible with scoped storage. + */ + public static boolean useScopedStorage() { + return false; + //return true; + //return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + public int getNavigationGap() { return want_no_limits ? navigation_gap : 0; } @@ -3003,8 +3012,9 @@ public class MainActivity extends Activity { * MediaStore uris, as well as allowing control over the resolution of the thumbnail. * If sample_factor is 1, this returns a bitmap scaled to match the display resolution. If * sample_factor is greater than 1, it will be scaled down to a lower resolution. + * @param mediastore Whether the uri is for a mediastore uri or not. */ - private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor) { + private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor, boolean mediastore) { Bitmap thumbnail = null; try { //thumbnail = MediaStore.Images.Media.getBitmap(getContentResolver(), media.uri); @@ -3060,6 +3070,13 @@ public class MainActivity extends Activity { Log.e(TAG, "decodeStream returned null bitmap for ghost image last"); } is.close(); + + if( !mediastore ) { + // When loading from a mediastore, the bitmap already seems to have the correct orientation. + // But when loading from a saf uri, we need to apply the rotation. + // E.g., test on Galaxy S10e with ghost image last image option, when using SAF, in portrait orientation, after pause/resume. + thumbnail = rotateForExif(thumbnail, uri); + } } catch(IOException e) { Log.e(TAG, "failed to load bitmap for ghost image last"); @@ -3133,11 +3150,49 @@ public class MainActivity extends Activity { if( ghost_image_last && !media.video ) { if( MyDebug.LOG ) Log.d(TAG, "load full size bitmap for ghost image last photo"); - thumbnail = loadThumbnailFromUri(media.uri, 1); + thumbnail = loadThumbnailFromUri(media.uri, 1, media.mediastore); } if( thumbnail == null ) { try { - if( media.video ) { + if( !media.mediastore ) { + if( media.video ) { + if( MyDebug.LOG ) + Log.d(TAG, "load thumbnail for video from SAF uri"); + ParcelFileDescriptor pfd_saf = null; // keep a reference to this as long as retriever, to avoid risk of pfd_saf being garbage collected + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + pfd_saf = getContentResolver().openFileDescriptor(media.uri, "r"); + retriever.setDataSource(pfd_saf.getFileDescriptor()); + thumbnail = retriever.getFrameAtTime(-1); + } + catch(Exception e) { + Log.d(TAG, "failed to load video thumbnail"); + e.printStackTrace(); + } + finally { + try { + retriever.release(); + } + catch(RuntimeException ex) { + // ignore + } + try { + if( pfd_saf != null ) { + pfd_saf.close(); + } + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "load thumbnail for photo from SAF uri"); + thumbnail = loadThumbnailFromUri(media.uri, 4, media.mediastore); + } + } + else if( media.video ) { if( MyDebug.LOG ) Log.d(TAG, "load thumbnail for video"); thumbnail = MediaStore.Video.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Video.Thumbnails.MINI_KIND, null); @@ -3307,13 +3362,19 @@ public class MainActivity extends Activity { Log.d(TAG, "go to latest media"); StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia(); if( media != null ) { - uri = media.uri; + if( MyDebug.LOG ) + Log.d(TAG, "latest uri:" + media.uri); + uri = media.getMediaStoreUri(this); + if( MyDebug.LOG ) + Log.d(TAG, "media uri:" + uri); is_raw = media.filename != null && media.filename.toLowerCase(Locale.US).endsWith(".dng"); } } - if( uri != null ) { + if( uri != null && !MainActivity.useScopedStorage() ) { // check uri exists + // note, with scoped storage this isn't reliable when using SAF - since we don't actually have permission to access mediastore URIs that + // were created via Storage Access Framework, even though Open Camera was the application that saved them(!) if( MyDebug.LOG ) { Log.d(TAG, "found most recent uri: " + uri); Log.d(TAG, "is_raw: " + is_raw); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index c5c01ba9e..e913ba717 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -23,6 +23,7 @@ import net.sourceforge.opencamera.ui.DrawPreview; import android.annotation.TargetApi; import android.app.Activity; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -99,9 +100,10 @@ public class MyApplicationInterface extends BasicApplicationInterface { private enum LastImagesType { FILE, - SAF + SAF, + MEDIASTORE } - private LastImagesType last_images_type = LastImagesType.FILE; // whether the last images array are using File API, SAF + private LastImagesType last_images_type = LastImagesType.FILE; // whether the last images array are using File API, SAF or MediaStore /** This class keeps track of the images saved in this batch, for use with Pause Preview option, so we can share or trash images. */ @@ -297,6 +299,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { else if( storageUtils.isUsingSAF() ) { return VideoMethod.SAF; } + else if( MainActivity.useScopedStorage() ) { + return VideoMethod.MEDIASTORE; + } else { return VideoMethod.FILE; } @@ -314,6 +319,38 @@ public class MyApplicationInterface extends BasicApplicationInterface { return last_video_file_uri; } + @Override + public Uri createOutputVideoMediaStore(String extension) throws IOException { + Uri folder = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? + MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) : + MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + ContentValues contentValues = new ContentValues(); + String filename = storageUtils.createMediaFilename(StorageUtils.MEDIA_TYPE_VIDEO, "", 0, "." + extension, new Date()); + if( MyDebug.LOG ) + Log.d(TAG, "filename: " + filename); + contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, filename); + String mime_type = storageUtils.getVideoMimeType(extension); + if( MyDebug.LOG ) + Log.d(TAG, "mime_type: " + mime_type); + contentValues.put(MediaStore.Video.Media.MIME_TYPE, mime_type); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + String relative_path = storageUtils.getSaveRelativeFolder(); + if( MyDebug.LOG ) + Log.d(TAG, "relative_path: " + relative_path); + contentValues.put(MediaStore.Video.Media.RELATIVE_PATH, relative_path); + contentValues.put(MediaStore.Video.Media.IS_PENDING, 1); + } + + last_video_file_uri = main_activity.getContentResolver().insert(folder, contentValues); + if( MyDebug.LOG ) + Log.d(TAG, "uri: " + last_video_file_uri); + if( last_video_file_uri == null ) { + throw new IOException(); + } + + return last_video_file_uri; + } + @Override public Uri createOutputVideoUri() { String action = main_activity.getIntent().getAction(); @@ -967,7 +1004,11 @@ public class MyApplicationInterface extends BasicApplicationInterface { 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) + // 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). + // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; + // when using scoped storage, we should no longer hit this codepath, but we are still using this for older Android + // versions. + @SuppressWarnings("deprecation") File storage = Environment.getExternalStorageDirectory(); if( MyDebug.LOG ) Log.d(TAG, "compare to: " + storage.getAbsolutePath()); @@ -1918,6 +1959,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { // need to keep a reference to pfd_saf for as long as writer, to avoid getting garbage collected - see https://sourceforge.net/p/opencamera/tickets/417/ private ParcelFileDescriptor pfd_saf; private OutputStreamWriter writer; + private Uri uri; private int count = 1; private long min_video_time_from = 0; @@ -2058,13 +2100,40 @@ public class MyApplicationInterface extends BasicApplicationInterface { subtitle_filename = getSubtitleFilename(subtitle_filename); writer = new FileWriter(subtitle_filename); } - else { + else if( video_method == VideoMethod.SAF || video_method == VideoMethod.MEDIASTORE ) { if( MyDebug.LOG ) Log.d(TAG, "last_video_file_uri: " + last_video_file_uri); String subtitle_filename = storageUtils.getFileName(last_video_file_uri); 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 - pfd_saf = getContext().getContentResolver().openFileDescriptor(subtitle_uri, "w"); + if( video_method == VideoMethod.SAF ) { + uri = storageUtils.createOutputFileSAF(subtitle_filename, ""); // don't set a mimetype, as we don't want it to append a new extension + } + else { + Uri folder = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? + MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) : + MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, subtitle_filename); + // set mime type - it's unclear if .SRT files have an official mime type, but (a) we must set a mime type otherwise + // resultant files are named "*.srt.mp4", and (b) the mime type must be video/*, otherwise we get exception: + // "java.lang.IllegalArgumentException: MIME type text/plain cannot be inserted into content://media/external_primary/video/media; expected MIME type under video/*" + // and we need the file to be saved in the same folder (in DCIM/ ) as the video + contentValues.put(MediaStore.Images.Media.MIME_TYPE, "video/x-srt"); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + String relative_path = storageUtils.getSaveRelativeFolder(); + if( MyDebug.LOG ) + Log.d(TAG, "relative_path: " + relative_path); + contentValues.put(MediaStore.Video.Media.RELATIVE_PATH, relative_path); + contentValues.put(MediaStore.Video.Media.IS_PENDING, 1); + } + uri = main_activity.getContentResolver().insert(folder, contentValues); + if( uri == null ) { + throw new IOException(); + } + } + if( MyDebug.LOG ) + Log.d(TAG, "uri: " + uri); + pfd_saf = getContext().getContentResolver().openFileDescriptor(uri, "w"); writer = new FileWriter(pfd_saf.getFileDescriptor()); } } @@ -2116,6 +2185,13 @@ public class MyApplicationInterface extends BasicApplicationInterface { } pfd_saf = null; } + if( video_method == VideoMethod.MEDIASTORE ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Video.Media.IS_PENDING, 0); + main_activity.getContentResolver().update(uri, contentValues, null, null); + } + } } return super.cancel(); } @@ -2161,6 +2237,14 @@ public class MyApplicationInterface extends BasicApplicationInterface { subtitleVideoTimerTask = null; } + if( video_method == VideoMethod.MEDIASTORE ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Video.Media.IS_PENDING, 0); + main_activity.getContentResolver().update(uri, contentValues, null, null); + } + } + boolean done = broadcastVideo(video_method, uri, filename); if( MyDebug.LOG ) Log.d(TAG, "done? " + done); @@ -2178,7 +2262,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { // 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 ) { + if( video_method == VideoMethod.SAF || video_method == VideoMethod.MEDIASTORE ) { output = new Intent(); output.setData(uri); if( MyDebug.LOG ) @@ -2277,7 +2361,22 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "filename " + filename); } boolean done = false; - if( video_method == VideoMethod.FILE ) { + if( video_method == VideoMethod.MEDIASTORE ) { + // no need to broadcast when using mediastore + + if( uri != null ) { + // in theory this is pointless, as announceUri no longer does anything on Android 7+, + // and mediastore method is only used on Android 10+, but keep this just in case + // announceUri does something in future + storageUtils.announceUri(uri, false, true); + + // we also want to save the uri - we can use the media uri directly, rather than having to scan it + storageUtils.setLastMediaScanned(uri); + + done = true; + } + } + else if( video_method == VideoMethod.FILE ) { if( filename != null ) { File file = new File(filename); storageUtils.broadcastFile(file, false, true, true); @@ -2316,6 +2415,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { else if( video_method == VideoMethod.SAF ) { trashImage(LastImagesType.SAF, uri, filename, false); } + else if( video_method == VideoMethod.MEDIASTORE ) { + trashImage(LastImagesType.MEDIASTORE, uri, filename, false); + } // else can't delete Uri } @@ -2733,6 +2835,8 @@ public class MyApplicationInterface extends BasicApplicationInterface { public boolean needsStoragePermission() { if( MyDebug.LOG ) Log.d(TAG, "needsStoragePermission"); + if( MainActivity.useScopedStorage() ) + return false; // no longer need storage permission with scoped storage - and shouldn't request it either return true; } @@ -3279,6 +3383,16 @@ public class MyApplicationInterface extends BasicApplicationInterface { last_images.add(last_image); } + void addLastImageMediaStore(Uri uri, boolean share) { + if( MyDebug.LOG ) { + Log.d(TAG, "addLastImageMediaStore: " + uri); + Log.d(TAG, "share?: " + share); + } + last_images_type = LastImagesType.MEDIASTORE; + LastImage last_image = new LastImage(uri, share); + last_images.add(last_image); + } + void clearLastImages() { if( MyDebug.LOG ) Log.d(TAG, "clearLastImages"); @@ -3357,6 +3471,11 @@ public class MyApplicationInterface extends BasicApplicationInterface { e.printStackTrace(); } } + else if( image_type == LastImagesType.MEDIASTORE && image_uri != null ) { + if( MyDebug.LOG ) + Log.d(TAG, "Delete MediaStore: " + image_uri); + main_activity.getContentResolver().delete(image_uri, null, null); + } else if( image_name != null ) { if( MyDebug.LOG ) Log.d(TAG, "Delete: " + image_name); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java index a479872cc..aaee8c12d 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java @@ -1947,6 +1947,10 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared fragment.setModeFolder(false); fragment.setExtension(".xml"); fragment.setStartFolder(main_activity.getStorageUtils().getSettingsFolder()); + if( MainActivity.useScopedStorage() ) { + // since we use File API to load, don't allow going outside of the application's folder, as we won't be able to read those files! + fragment.setMaxParent(main_activity.getExternalFilesDir(null)); + } fragment.show(getFragmentManager(), "FOLDER_FRAGMENT"); } } diff --git a/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java b/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java index 632befe94..6d0c9e089 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java +++ b/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java @@ -40,6 +40,11 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); return; } + else if( MainActivity.useScopedStorage() ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for scoped storage!"); + return; + } boolean ok = true; String [] permissions = null; @@ -124,6 +129,11 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); return; } + else if( MainActivity.useScopedStorage() ) { + if( MyDebug.LOG ) + Log.e(TAG, "shouldn't be requesting permissions for scoped storage!"); + return; + } if( ActivityCompat.shouldShowRequestPermissionRationale(main_activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) ) { // Show an explanation to the user *asynchronously* -- don't block diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 648ee05c9..1b1fa01c5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -36,6 +36,7 @@ import android.provider.MediaStore.Video.VideoColumns; import android.provider.OpenableColumns; import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; + import android.system.Os; import android.system.StructStatVfs; import android.util.Log; @@ -55,7 +56,13 @@ public class StorageUtils { private final MyApplicationInterface applicationInterface; private Uri last_media_scanned; - private final static File base_folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + private final static String RELATIVE_FOLDER_BASE = Environment.DIRECTORY_DCIM; + // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; + // when using scoped storage, we should no longer be trying to read/write this codepath with File API, but we are still using + // this for older Android versions. + // Furthermore, sometimes we still use this with scoped storage for valid purposes, e.g., to pass to StatFs in freeMemory(). + @SuppressWarnings("deprecation") + private final static File base_folder = Environment.getExternalStoragePublicDirectory(RELATIVE_FOLDER_BASE); // for testing: public volatile boolean failed_to_scan; @@ -103,6 +110,7 @@ public class StorageUtils { if( MyDebug.LOG ) // this code only used for debugging/logging { + @SuppressWarnings("deprecation") // this is only debug code 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 ) { @@ -114,6 +122,7 @@ public class StorageUtils { Log.e(TAG, "Couldn't resolve given uri [2]: " + uri); } else { + @SuppressWarnings("deprecation") // this is only debug code 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)); @@ -368,6 +377,26 @@ public class StorageUtils { return file; } + // only valid if !isUsingSAF() + String getSaveRelativeFolder() { + String folder_name = getSaveLocation(); + return getSaveRelativeFolder(folder_name); + } + + // only valid if !isUsingSAF() + private static String getSaveRelativeFolder(String folder_name) { + 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.startsWith("/") ) { + return folder_name; + } + else { + return RELATIVE_FOLDER_BASE + File.separator + folder_name; + } + } + public static File getBaseFolder() { return base_folder; } @@ -379,7 +408,6 @@ public class StorageUtils { // 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); } @@ -407,6 +435,8 @@ public class StorageUtils { * 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/ + Note that when using Android Q's scoped storage, the returned File will be inaccessible. However we still sometimes call this, + e.g., to scan with mediascanner or get a human readable string for the path. Also note that this will return null for media store Uris with Android Q's scoped storage: https://developer.android.com/preview/privacy/scoped-storage "The DATA column is redacted for each file in the media store." */ @@ -440,6 +470,10 @@ public class StorageUtils { File [] storagePoints = new File("/storage").listFiles(); if( "primary".equalsIgnoreCase(type) ) { + // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; + // when using scoped storage, as noted above we should no longer be trying to read/write this path with File API; + // and otherwise we are still using this for older Android versions. + @SuppressWarnings("deprecation") final File externalStorage = Environment.getExternalStorageDirectory(); file = new File(externalStorage, path); } @@ -528,6 +562,9 @@ public class StorageUtils { } private String getDataColumn(Uri uri, String selection, String [] selectionArgs) { + // DATA is redacted with scoped storage, but that's fine - the callers of this method are documented that they may return + // no filename, and these callers have been reviewed for their use on Android 10+ with scoped storage + @SuppressWarnings("deprecation") final String column = MediaStore.Images.ImageColumns.DATA; final String[] projection = { column @@ -601,7 +638,7 @@ public class StorageUtils { return result; } - private String createMediaFilename(int type, String suffix, int count, String extension, Date current_date) { + String createMediaFilename(int type, String suffix, int count, String extension, Date current_date) { String index = ""; if( count > 0 ) { index = "_" + count; // try to find a unique filename @@ -821,14 +858,16 @@ public class StorageUtils { } static class Media { - final long id; + final boolean mediastore; // whether uri is from mediastore + final long id; // for mediastore==true only final boolean video; final Uri uri; final long date; - final int orientation; + final int orientation; // for mediastore==true, video==false only final String filename; // this should correspond to DISPLAY_NAME (so available with scoped storage) - so this includes file extension, but not full path - Media(long id, boolean video, Uri uri, long date, int orientation, String filename) { + Media(boolean mediastore, long id, boolean video, Uri uri, long date, int orientation, String filename) { + this.mediastore = mediastore; this.id = id; this.video = video; this.uri = uri; @@ -836,6 +875,24 @@ public class StorageUtils { this.orientation = orientation; this.filename = filename; } + + /** Returns a mediastore uri. If this Media object was not created by a mediastore uri, then + * this will try to convert using MediaStore.getMediaUri(), but if this fails the function + * will return null. + */ + Uri getMediaStoreUri(Context context) { + if( this.mediastore ) + return this.uri; + else { + try { + return MediaStore.getMediaUri(context, this.uri); + } + catch(Exception e) { + e.printStackTrace(); + } + return null; + } + } } private static boolean filenameIsRaw(String filename) { @@ -869,7 +926,7 @@ public class StorageUtils { final int column_name_c = 3; // filename (without path), including extension final int column_orientation_c = 4; // for images only*/ final int column_name_c = 2; // filename (without path), including extension - final int column_orientation_c = 3; // for images only + final int column_orientation_c = 3; // for mediastore images only String [] projection; switch( uri_type ) { case MEDIASTORE_IMAGES: @@ -882,6 +939,7 @@ public class StorageUtils { throw new RuntimeException("unknown uri_type: " + uri_type); } // for images, we need to search for JPEG/etc and RAW, to support RAW only mode (even if we're not currently in that mode, it may be that previously the user did take photos in RAW only mode) + // if updating this code for supported mime types, remember to also update getLatestMediaSAF() /*String selection = video ? "" : ImageColumns.MIME_TYPE + "='image/jpeg' OR " + ImageColumns.MIME_TYPE + "='image/webp' OR " + ImageColumns.MIME_TYPE + "='image/png' OR " + @@ -1058,7 +1116,21 @@ public class StorageUtils { if( MyDebug.LOG ) Log.d(TAG, "video: " + video); - media = new Media(id, video, uri, date, orientation, filename); + media = new Media(true, id, video, uri, date, orientation, filename); + + if( MyDebug.LOG ) { + // debug + if( cursor.moveToFirst() ) { + do { + long this_id = cursor.getLong(column_id_c); + long this_date = cursor.getLong(column_date_taken_c); + Uri this_uri = ContentUris.withAppendedId(baseUri, this_id); + String this_filename = cursor.getString(column_name_c); + Log.d(TAG, "Date: " + this_date + " ID: " + this_id + " Name: " + this_filename + " Uri: " + this_uri); + } + while( cursor.moveToNext() ); + } + } } else { if( MyDebug.LOG ) @@ -1077,17 +1149,192 @@ public class StorageUtils { } } + if( MyDebug.LOG ) + Log.d(TAG, "return latest media: " + media); + return media; + } + + /** Used when using Storage Access Framework AND scoped storage. + * This is because with scoped storage, we don't request READ_EXTERNAL_STORAGE (as + * recommended). It's meant to be the case that applications should still be able to see files + * that they own - but whilst this is true when images are saved using mediastore API, this is + * NOT true when saving with Storage Access Framework - they don't show up in mediastore + * queries (even though they've definitely been added to the mediastore). So instead we read + * using the SAF uri, and if we need the media uri (e.g., to pass to Gallery application), use + * Media.getMediaStoreUri(). What a mess! + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private Media getLatestMediaSAF(Uri treeUri) { + if (MyDebug.LOG) + Log.d(TAG, "getLatestMedia: " + treeUri); + + Media media = null; + + Uri baseUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); + if( MyDebug.LOG ) + Log.d(TAG, "baseUri: " + baseUri); + + final int column_id_c = 0; + final int column_date_c = 1; + final int column_name_c = 2; // filename (without path), including extension + final int column_mime_c = 3; + String [] projection = new String[] {DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE}; + + // Note, it appears that when querying DocumentsContract, basic query functionality like selection, ordering, are ignored(!). + // See: https://stackoverflow.com/questions/52770188/how-to-filter-the-results-of-a-query-with-buildchilddocumentsuriusingtree + // https://stackoverflow.com/questions/56263620/contentresolver-query-on-documentcontract-lists-all-files-disregarding-selection + // So, we have to do it ourselves. + + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(baseUri, projection, null, null, null); + if( cursor != null && cursor.moveToFirst() ) { + if( MyDebug.LOG ) + Log.d(TAG, "found: " + cursor.getCount()); + + Uri latest_uri = null; + long latest_date = 0; + String latest_filename = null; + boolean latest_is_video = false; + + // as well as scanning for the most recent image, we also keep track of the most recent non-RAW image, + // in case we want to prefer that when the most recent + Uri nonraw_latest_uri = null; + long nonraw_latest_date = 0; + String nonraw_latest_filename = null; + + do { + long this_date = cursor.getLong(column_date_c); + + String doc_id = cursor.getString(column_id_c); + Uri this_uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, doc_id); + String this_mime_type = cursor.getString(column_mime_c); + + // if updating this code for allowed mime types, also update corresponding code in getLatestMediaCore() + boolean is_allowed; + boolean this_is_video; + switch( this_mime_type ) { + case "image/jpeg": + case "image/webp": + case "image/png": + case "image/x-adobe-dng": + is_allowed = true; + this_is_video = false; + break; + case "video/3gpp": + case "video/webm": + case "video/mp4": + // n.b., perhaps we should just allow video/*, but we should still disallow .SRT files! + is_allowed = true; + this_is_video = true; + break; + default: + // skip unwanted file format + is_allowed = false; + this_is_video = false; + break; + } + if( !is_allowed ) { + continue; + } + + String this_filename = cursor.getString(column_name_c); + if( MyDebug.LOG ) { + Log.d(TAG, "Date: " + this_date + " doc_id: " + doc_id + " Name: " + this_filename + " Uri: " + this_uri); + } + + if( latest_uri == null || this_date > latest_date ) { + latest_uri = this_uri; + latest_date = this_date; + latest_filename = this_filename; + latest_is_video = this_is_video; + } + if( !this_is_video && !filenameIsRaw(this_filename) ) { + if( nonraw_latest_uri == null || this_date > nonraw_latest_date ) { + nonraw_latest_uri = this_uri; + nonraw_latest_date = this_date; + nonraw_latest_filename = this_filename; + } + } + } + while( cursor.moveToNext() ); + + if( latest_uri == null ) { + if( MyDebug.LOG ) + Log.e(TAG, "couldn't find latest uri"); + } + else { + if( MyDebug.LOG ) { + Log.d(TAG, "latest_uri: " + latest_uri); + Log.d(TAG, "nonraw_latest_uri: " + nonraw_latest_uri); + } + + if( !latest_is_video && filenameIsRaw(latest_filename) && nonraw_latest_uri != null ) { + // prefer non-RAW to RAW? check filenames without extensions match + String filename_without_ext = filenameWithoutExtension(latest_filename); + String next_filename_without_ext = filenameWithoutExtension(nonraw_latest_filename); + if( MyDebug.LOG ) { + Log.d(TAG, "filename_without_ext: " + filename_without_ext); + Log.d(TAG, "next_filename_without_ext: " + next_filename_without_ext); + } + if( filename_without_ext.equals(next_filename_without_ext) ) { + if( MyDebug.LOG ) + Log.d(TAG, "prefer non-RAW to RAW"); + latest_uri = nonraw_latest_uri; + latest_date = nonraw_latest_date; + latest_filename = nonraw_latest_filename; + // video is unchanged + } + } + + media = new Media(false,0, latest_is_video, latest_uri, latest_date, 0, latest_filename); + } + + /*if( MyDebug.LOG ) { + // debug + if( cursor.moveToFirst() ) { + do { + long this_id = cursor.getLong(column_id_c); + long this_date = cursor.getLong(column_date_taken_c); + Uri this_uri = ContentUris.withAppendedId(baseUri, this_id); + String this_filename = cursor.getString(column_name_c); + Log.d(TAG, "Date: " + this_date + " ID: " + this_id + " Name: " + this_filename + " Uri: " + this_uri); + } + while( cursor.moveToNext() ); + } + }*/ + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "mediastore returned no media"); + } + } + catch(Exception e) { + if( MyDebug.LOG ) + Log.e(TAG, "Exception trying to find latest media"); + e.printStackTrace(); + } + finally { + if( cursor != null ) { + cursor.close(); + } + } + + if( MyDebug.LOG ) + Log.d(TAG, "return latest media: " + media); return media; } private Media getLatestMedia(UriType uri_type) { if( MyDebug.LOG ) - if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) { Log.d(TAG, "getLatestMedia: " + uri_type); + if( !MainActivity.useScopedStorage() && 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() + // update for scoped storage: here we should no longer need READ_EXTERNAL_STORAGE (which we won't have), instead we'll only be able to see + // media created by Open Camera, which is fine if( MyDebug.LOG ) Log.e(TAG, "don't have READ_EXTERNAL_STORAGE permission"); return null; @@ -1128,6 +1375,11 @@ public class StorageUtils { } Media getLatestMedia() { + if( MainActivity.useScopedStorage() && this.isUsingSAF() ) { + Uri treeUri = this.getTreeUriSAF(); + return getLatestMediaSAF(treeUri); + } + Media image_media = getLatestMedia(UriType.MEDIASTORE_IMAGES); Media video_media = getLatestMedia(UriType.MEDIASTORE_VIDEOS); Media media = null; @@ -1221,6 +1473,7 @@ public class StorageUtils { // if we fail for SAF, don't fall back to the methods below, as this may be incorrect (especially for external SD card) return freeMemorySAF(); } + // n.b., StatFs still seems to work with Android 10's scoped storage... (and there doesn't seem to be an official non-File based equivalent) try { File folder = getImageFolder(); if( folder == null ) { diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java index 3fa467eef..5de45821b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java @@ -34,6 +34,7 @@ public interface ApplicationInterface { enum VideoMethod { FILE, // video will be saved to a file SAF, // video will be saved using Android 5's Storage Access Framework + MEDIASTORE, // video will be saved to the supplied MediaStore Uri URI // video will be written to the supplied Uri } @@ -44,6 +45,7 @@ public interface ApplicationInterface { VideoMethod createOutputVideoMethod(); // return a VideoMethod value to specify how to create a video file File createOutputVideoFile(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VideoMethod.FILE; extension is the recommended filename extension for the chosen video type Uri createOutputVideoSAF(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VideoMethod.SAF; extension is the recommended filename extension for the chosen video type + Uri createOutputVideoMediaStore(String extension) throws IOException; // will be called if createOutputVideoUsingSAF() returns VideoMethod.MEDIASTORE; extension is the recommended filename extension for the chosen video type 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 diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index b5767b654..ded73bcbd 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -182,9 +182,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu */ private static class VideoFileInfo { private final ApplicationInterface.VideoMethod video_method; - private final Uri video_uri; // for VideoMethod.SAF or VideoMethod.URI + private final Uri video_uri; // for VideoMethod.SAF, VideoMethod.URI or VideoMethod.MEDIASTORE private final String video_filename; // for VideoMethod.FILE - private final ParcelFileDescriptor video_pfd_saf; // for VideoMethod.SAF + private final ParcelFileDescriptor video_pfd_saf; // for VideoMethod.SAF, VideoMethod.URI or VideoMethod.MEDIASTORE VideoFileInfo() { this.video_method = ApplicationInterface.VideoMethod.FILE; @@ -5323,6 +5323,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu if( method == ApplicationInterface.VideoMethod.SAF ) { uri = applicationInterface.createOutputVideoSAF(extension); } + else if( method == ApplicationInterface.VideoMethod.MEDIASTORE ) { + uri = applicationInterface.createOutputVideoMediaStore(extension); + } else { uri = applicationInterface.createOutputVideoUri(); } diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java index 62f89e377..a846ccb88 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java @@ -43,6 +43,7 @@ public class FolderChooserDialog extends DialogFragment { private File start_folder = new File(""); private File current_folder; + private File max_parent; // if non-null, don't show the Parent option if viewing this folder (so the user can't go above that folder) private AlertDialog folder_dialog; private ListView list; private String chosen_folder; @@ -199,6 +200,12 @@ public class FolderChooserDialog extends DialogFragment { this.start_folder = start_folder; } + public void setMaxParent(File max_parent) { + if( MyDebug.LOG ) + Log.d(TAG, "setMaxParent: " + max_parent); + this.max_parent = max_parent; + } + public void setShowNewFolderButton(boolean show_new_folder_button) { this.show_new_folder_button = show_new_folder_button; } @@ -236,9 +243,19 @@ public class FolderChooserDialog extends DialogFragment { // 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)); + if( new_folder.getParentFile() != null ) { + if( max_parent != null && max_parent.equals(new_folder) ) { + // don't show parent option + } + else { + listed_files.add(new FileWrapper(new_folder.getParentFile(), getResources().getString(R.string.parent_folder), 0)); + } + } if( show_dcim_shortcut ) { + // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; + // when using scoped storage, we should no longer be using this folder dialog with DCIM shortcut, but we are still + // using this for older Android versions. + @SuppressWarnings("deprecation") 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)); -- GitLab From 8db7618d142487dd48400fa4ecebbe46c7cddf98 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 31 Aug 2020 15:22:14 +0100 Subject: [PATCH 067/430] Fix last release, shouldn't be block request for all permissions with scoped storage! --- .../java/net/sourceforge/opencamera/PermissionHandler.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java b/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java index 6d0c9e089..80f8ef5bd 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java +++ b/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java @@ -40,11 +40,6 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); return; } - else if( MainActivity.useScopedStorage() ) { - if( MyDebug.LOG ) - Log.e(TAG, "shouldn't be requesting permissions for scoped storage!"); - return; - } boolean ok = true; String [] permissions = null; -- GitLab From da80bd64147dab5f72382ffa1f901045cbc8fdcd Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 31 Aug 2020 15:27:32 +0100 Subject: [PATCH 068/430] Show toast for location icon being clicked on/off, to be consistent with other icons. --- .../main/java/net/sourceforge/opencamera/MainActivity.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 921b74787..3b9f0b438 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -160,6 +160,7 @@ public class MainActivity extends Activity { private final ToastBoxer white_balance_lock_toast = new ToastBoxer(); private final ToastBoxer exposure_lock_toast = new ToastBoxer(); private final ToastBoxer audio_control_toast = new ToastBoxer(); + private final ToastBoxer store_location_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 private String push_info_toast_text; // can be used to "push" extra text to the info text for showPhotoVideoToast() @@ -1472,6 +1473,9 @@ public class MainActivity extends Activity { applicationInterface.getDrawPreview().updateSettings(); // because we cache the geotagging setting initLocation(); // required to enable or disable GPS, also requests permission if necessary this.closePopup(); + + String message = getResources().getString(R.string.preference_location) + ": " + getResources().getString(value ? R.string.on : R.string.off); + preview.showToast(store_location_toast, message); } public void clickedTextStamp(View view) { -- GitLab From e3bb158076c2863a7f752590c901af3229720aff Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 31 Aug 2020 17:48:11 +0100 Subject: [PATCH 069/430] Fix crash on EXTERNAL devices with Camera2 API that didn't support querying the view angles. --- _docs/history.html | 1 + .../cameracontroller/CameraControllerManager2.java | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/_docs/history.html b/_docs/history.html index 0d0377df4..9a8a3fa64 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -52,6 +52,7 @@ Version 1.48.3 (Work in progress) FIXED Possible crash for panorama if failing to crop due to poor transformations; now fails gracefully. +FIXED Crash on EXTERNAL devices with Camera2 API that didn't support querying the view angles. FIXED Photos would sometimes fail to save on some devices with Storage Access Framework, when some options were enabled (options like DRO, HDR, auto-level, photostamp that require post-processing; custom Exif tags like artist or copyright; or when using geotagging with diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java index 85e799fb9..719237bdb 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraControllerManager2.java @@ -121,6 +121,15 @@ public class CameraControllerManager2 extends CameraControllerManager { SizeF physical_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); android.util.Size pixel_size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); float [] focal_lengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); + if( active_size == null || physical_size == null || pixel_size == null || focal_lengths == null || focal_lengths.length == 0 ) { + // in theory this should never happen according to the documentation, but I've had a report of physical_size (SENSOR_INFO_PHYSICAL_SIZE) + // being null on an EXTERNAL Camera2 device, see https://sourceforge.net/p/opencamera/tickets/754/ + if( MyDebug.LOG ) { + Log.e(TAG, "can't get camera view angles"); + } + // fall back to a default + return new SizeF(55.0f, 43.0f); + } //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]))); float frac_x = ((float)active_size.width())/(float)pixel_size.getWidth(); -- GitLab From 03faeb812f29fe2d85cb11494cf483b51a59054f Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 31 Aug 2020 19:56:51 +0100 Subject: [PATCH 070/430] Refactor common code to new isVideoCaptureIntent(). --- .../opencamera/MyApplicationInterface.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index e913ba717..e3f5dbf05 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -277,8 +277,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public VideoMethod createOutputVideoMethod() { - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); Bundle myExtras = main_activity.getIntent().getExtras(); @@ -353,8 +352,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public Uri createOutputVideoUri() { - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); Bundle myExtras = main_activity.getIntent().getExtras(); @@ -635,8 +633,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public String getVideoQualityPref() { - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); if( main_activity.getIntent().hasExtra(MediaStore.EXTRA_VIDEO_QUALITY) ) { @@ -689,8 +686,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public String getVideoFPSPref() { // if check for EXTRA_VIDEO_QUALITY, if set, best to fall back to default FPS - see corresponding code in getVideoQualityPref - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); if( main_activity.getIntent().hasExtra(MediaStore.EXTRA_VIDEO_QUALITY) ) { @@ -889,8 +885,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public long getVideoMaxDurationPref() { - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); if( main_activity.getIntent().hasExtra(MediaStore.EXTRA_DURATION_LIMIT) ) { @@ -935,8 +930,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "getVideoMaxFileSizeUserPref"); - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); if( main_activity.getIntent().hasExtra(MediaStore.EXTRA_SIZE_LIMIT) ) { @@ -964,8 +958,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { } private boolean getVideoRestartMaxFileSizeUserPref() { - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { if( MyDebug.LOG ) Log.d(TAG, "from video capture intent"); if( main_activity.getIntent().hasExtra(MediaStore.EXTRA_SIZE_LIMIT) ) { @@ -2249,8 +2242,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "done? " + done); - String action = main_activity.getIntent().getAction(); - if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( isVideoCaptureIntent() ) { 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 } @@ -3019,6 +3011,17 @@ public class MyApplicationInterface extends BasicApplicationInterface { return image_capture_intent; } + boolean isVideoCaptureIntent() { + boolean video_capture_intent = false; + String action = main_activity.getIntent().getAction(); + if( MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + if( MyDebug.LOG ) + Log.d(TAG, "from video capture intent"); + video_capture_intent = true; + } + return video_capture_intent; + } + /** Whether the photos will be part of a burst, even if we're receiving via the non-burst callbacks. */ private boolean forceSuffix(PhotoMode photo_mode) { -- GitLab From 79fd10676b785cb7e7712b8d936d72617275851e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 4 Sep 2020 22:31:12 +0100 Subject: [PATCH 071/430] Restrict DCIM usage to show_dcim_shortcut. --- .../opencamera/ui/FolderChooserDialog.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java index a846ccb88..f9c85b409 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java @@ -185,12 +185,17 @@ public class FolderChooserDialog extends DialogFragment { // 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( show_dcim_shortcut ) { if( MyDebug.LOG ) - Log.d(TAG, "can't even read DCIM?!"); - refreshList(new File("/")); + Log.d(TAG, "fall back to DCIM"); + // 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; -- GitLab From 3932c3f09ba3b4eccee635f109bdd34131cca629 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 4 Sep 2020 22:31:30 +0100 Subject: [PATCH 072/430] MediaStore.getMediaUri() only available on Android 10+. --- .../main/java/net/sourceforge/opencamera/StorageUtils.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 1b1fa01c5..30304539a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -885,7 +885,10 @@ public class StorageUtils { return this.uri; else { try { - return MediaStore.getMediaUri(context, this.uri); + // should only have allowed mediastore==null when using scoped storage + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + return MediaStore.getMediaUri(context, this.uri); + } } catch(Exception e) { e.printStackTrace(); -- GitLab From e96dfddb1e0c2db6c03c3f3182e4bc8a1c748c9b Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 4 Sep 2020 22:35:03 +0100 Subject: [PATCH 073/430] Remove copyFileToUri() no longer used. --- .../sourceforge/opencamera/ImageSaver.java | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 3f949ee25..bae1e7be8 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2571,6 +2571,7 @@ public class ImageSaver extends Thread { } catch(SecurityException e) { // received security exception from copyFileToUri()->openOutputStream() from Google Play + // update: no longer have copyFileToUri() (as no longer use temporary files for SAF), but might as well keep this if( MyDebug.LOG ) Log.e(TAG, "security exception writing file: " + e.getMessage()); e.printStackTrace(); @@ -3540,36 +3541,6 @@ public class ImageSaver extends Thread { 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() { -- GitLab From 402d25bf6bb65c45cd376ca30f5e66e6eabb845c Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 4 Sep 2020 23:33:24 +0100 Subject: [PATCH 074/430] Fix comments. --- .../main/java/net/sourceforge/opencamera/ui/PopupView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java b/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java index e26a716f7..cd9c7812e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java @@ -771,8 +771,8 @@ public class PopupView extends LinearLayout { @Override public void run() { if (MyDebug.LOG) - Log.d(TAG, "update settings due to resolution change"); main_activity.updateForSettings("", true); // keep the popupview open + Log.d(TAG, "update settings due to video capture rate change"); } }; @@ -812,7 +812,7 @@ public class PopupView extends LinearLayout { old_video_capture_rate_index = video_capture_rate_index; if( keep_popup ) { - // make it easier to scroll through the list of resolutions without a pause each time + // make it easier to scroll through the list of capture rates without a pause each time handler.removeCallbacks(update_runnable); handler.postDelayed(update_runnable, 400); } -- GitLab From f944402bc09639762eb47e570e19ea7d95be437a Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 5 Sep 2020 19:26:18 +0100 Subject: [PATCH 075/430] Display camera orientation in About. --- .../main/java/net/sourceforge/opencamera/MainActivity.java | 1 + .../net/sourceforge/opencamera/MyPreferenceFragment.java | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 3b9f0b438..c95d7f3b4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -2114,6 +2114,7 @@ public class MainActivity extends Activity { bundle.putInt("nCameras", preview.getCameraControllerManager().getNumberOfCameras()); bundle.putString("camera_api", this.preview.getCameraAPI()); bundle.putBoolean("using_android_l", this.preview.usingCamera2API()); + bundle.putInt("camera_orientation", this.preview.getCameraController().getCameraOrientation()); bundle.putString("photo_mode_string", getPhotoModeString(applicationInterface.getPhotoMode(), true)); bundle.putBoolean("supports_auto_stabilise", this.supports_auto_stabilise); bundle.putBoolean("supports_flash", this.preview.supportsFlash()); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java index aaee8c12d..19dd102ed 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java @@ -105,6 +105,10 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared if( MyDebug.LOG ) Log.d(TAG, "using_android_l: " + using_android_l); + final int camera_orientation = bundle.getInt("camera_orientation"); + if( MyDebug.LOG ) + Log.d(TAG, "camera_orientation: " + camera_orientation); + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); final boolean supports_auto_stabilise = bundle.getBoolean("supports_auto_stabilise"); @@ -1160,6 +1164,8 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared about_string.append(is_multi_cam); about_string.append("\nCamera API: "); about_string.append(camera_api); + about_string.append("\nCamera orientation: "); + about_string.append(camera_orientation); about_string.append("\nPhoto mode: "); about_string.append(photo_mode_string==null ? "UNKNOWN" : photo_mode_string); { -- GitLab From 184c9f568f68f8c5898dde1550887dad985a1926 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 5 Sep 2020 20:57:41 +0100 Subject: [PATCH 076/430] Fix last release, camera controller may be null when going to settings. --- .../main/java/net/sourceforge/opencamera/MainActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index c95d7f3b4..695429225 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -2114,7 +2114,9 @@ public class MainActivity extends Activity { bundle.putInt("nCameras", preview.getCameraControllerManager().getNumberOfCameras()); bundle.putString("camera_api", this.preview.getCameraAPI()); bundle.putBoolean("using_android_l", this.preview.usingCamera2API()); - bundle.putInt("camera_orientation", this.preview.getCameraController().getCameraOrientation()); + if( this.preview.getCameraController() != null ) { + bundle.putInt("camera_orientation", this.preview.getCameraController().getCameraOrientation()); + } bundle.putString("photo_mode_string", getPhotoModeString(applicationInterface.getPhotoMode(), true)); bundle.putBoolean("supports_auto_stabilise", this.supports_auto_stabilise); bundle.putBoolean("supports_flash", this.preview.supportsFlash()); -- GitLab From e4ab2851ab128c68461cb2e177a1efd1daa48c3a Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 5 Sep 2020 23:14:16 +0100 Subject: [PATCH 077/430] Close camera (via putting preview into paused state) when in "background" (settings/dialog) etc. --- _docs/history.html | 1 + .../opencamera/test/MainActivityTest.java | 30 +++++++++++++-- .../sourceforge/opencamera/MainActivity.java | 38 ++++++++++++++----- .../preview/ApplicationInterface.java | 2 +- .../opencamera/preview/Preview.java | 15 +++++++- .../net/sourceforge/opencamera/ui/MainUI.java | 4 +- .../sourceforge/opencamera/ui/PopupView.java | 14 +++---- 7 files changed, 80 insertions(+), 24 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 9a8a3fa64..24890db03 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -60,6 +60,7 @@ FIXED Photos would sometimes fail to save on some devices with Storage Access FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices with LIMITED Camera2 API support. +UPDATE Camera now closed when in settings or preview otherwise in background. Version 1.48.2 (2020/07/12) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 0641a9cbd..d40259442 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -202,7 +202,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sun, 6 Sep 2020 00:26:01 +0100 Subject: [PATCH 078/430] Use enum instead of hardcoded number for API level. --- .../net/sourceforge/opencamera/remotecontrol/DeviceScanner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java index c09518fc6..d443aec56 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java @@ -103,7 +103,7 @@ public class DeviceScanner extends ListActivity { // In real life most of bluetooth LE devices associated with location, so without this // permission the sample shows nothing in most cases // Also see https://stackoverflow.com/questions/33045581/location-needs-to-be-enabled-for-bluetooth-low-energy-scanning-on-android-6-0 - int permissionCoarse = Build.VERSION.SDK_INT >= 23 ? + int permissionCoarse = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? ContextCompat .checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) : PackageManager.PERMISSION_GRANTED; -- GitLab From 57665b580a80b8b85c6dfc575d0ee646eb58b0a5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 6 Sep 2020 00:26:37 +0100 Subject: [PATCH 079/430] Need ACCESS_FINE_LOCATION on Android 10+ for bluetooth. --- .../sourceforge/opencamera/remotecontrol/DeviceScanner.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java index d443aec56..9abcac162 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java @@ -103,9 +103,11 @@ public class DeviceScanner extends ListActivity { // In real life most of bluetooth LE devices associated with location, so without this // permission the sample shows nothing in most cases // Also see https://stackoverflow.com/questions/33045581/location-needs-to-be-enabled-for-bluetooth-low-energy-scanning-on-android-6-0 + // Update: on Android 10+, ACCESS_FINE_LOCATION is needed: https://developer.android.com/about/versions/10/privacy/changes#location-telephony-bluetooth-wifi + String permission_needed = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? Manifest.permission.ACCESS_FINE_LOCATION : Manifest.permission.ACCESS_COARSE_LOCATION; int permissionCoarse = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? ContextCompat - .checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) : + .checkSelfPermission(this, permission_needed) : PackageManager.PERMISSION_GRANTED; if( permissionCoarse == PackageManager.PERMISSION_GRANTED ) { @@ -127,6 +129,7 @@ public class DeviceScanner extends ListActivity { // Also note that if we did want to only request ACCESS_COARSE_LOCATION here, we'd need to declare that permission // explicitly in the AndroidManifest.xml, otherwise the dialog to request permission is never shown (and the permission // is denied automatically). + // Update: on Android 10+, ACCESS_FINE_LOCATION is needed anyway: https://developer.android.com/about/versions/10/privacy/changes#location-telephony-bluetooth-wifi 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 -- GitLab From de772159b2d401fe933d5f250820852359c66f7b Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 6 Sep 2020 15:33:16 +0100 Subject: [PATCH 080/430] More logging, comments. --- .../opencamera/remotecontrol/BluetoothLeService.java | 6 ++++++ .../remotecontrol/BluetoothRemoteControl.java | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java index 6cacf161b..b30220dfc 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java @@ -79,6 +79,8 @@ public class BluetoothLeService extends Service { * Android BLE stack and API (just knowing the MAC is not enough on * many phones).*/ private void triggerScan() { + if( MyDebug.LOG ) + Log.d(TAG, "triggerScan"); // Stops scanning after a pre-defined scan period. bluetoothHandler.postDelayed(new Runnable() { @Override @@ -305,12 +307,16 @@ public class BluetoothLeService extends Service { @Override public boolean onUnbind(Intent intent) { + if( MyDebug.LOG ) + Log.d(TAG, "onUnbind"); close(); return super.onUnbind(intent); } public boolean initialize() { + if( MyDebug.LOG ) + Log.d(TAG, "initialize"); if( bluetoothManager == null ) { bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if( bluetoothManager == null ) { diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java index eb25c2fab..b04595531 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java @@ -41,6 +41,8 @@ public class BluetoothRemoteControl { @Override public void onServiceConnected(ComponentName componentName, IBinder service) { + if( MyDebug.LOG ) + Log.d(TAG, "onServiceConnected"); if( Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 ) { // BluetoothLeService requires Android 4.3+ return; @@ -54,8 +56,17 @@ public class BluetoothRemoteControl { bluetoothLeService.connect(remoteDeviceAddress); } + /** Called when a connection to the Service has been lost. This typically happens when the + * process hosting the service has crashed or been killed. + * So in particular, note this isn't the inverse to onServiceConnected() - whilst + * onServiceConnected is always called (after the service receives onBind()), upon normal + * disconnection (after we call unbindService()), the service receives onUnbind(), but + * onServiceDisconnected is not called under normal operation. + */ @Override public void onServiceDisconnected(ComponentName componentName) { + if( MyDebug.LOG ) + Log.d(TAG, "onServiceDisconnected"); Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { -- GitLab From e3e4e499665049086957cdf6b33b286450f030cb Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 6 Sep 2020 16:28:56 +0100 Subject: [PATCH 081/430] Avoid risks that we could connect to bluetooth (and so startLeScan) from service when in background; ensure service not started when in background. --- .../sourceforge/opencamera/MainActivity.java | 4 ++ .../remotecontrol/BluetoothLeService.java | 60 ++++++++++++++++++- .../remotecontrol/BluetoothRemoteControl.java | 17 +++++- .../remotecontrol/DeviceScanner.java | 31 +++++++++- 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 3e5f53966..c86d49706 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -4628,6 +4628,10 @@ public class MainActivity extends Activity { return this.camera_in_background; } + public boolean isAppPaused() { + return this.app_is_paused; + } + public BluetoothRemoteControl getBluetoothRemoteControl() { return bluetoothRemoteControl; } diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java index b30220dfc..7b8bcdef3 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothLeService.java @@ -33,6 +33,7 @@ import java.util.UUID; public class BluetoothLeService extends Service { private final static String TAG = "BluetoothLeService"; + private boolean is_bound; // whether service is bound private BluetoothManager bluetoothManager; private BluetoothAdapter bluetoothAdapter; private String device_address; @@ -81,6 +82,16 @@ public class BluetoothLeService extends Service { private void triggerScan() { if( MyDebug.LOG ) Log.d(TAG, "triggerScan"); + + if( !is_bound ) { + // Don't allow calls to startLeScan() (which requires location permission) when service + // not bound, as application may be in background! + // In theory this shouldn't be needed here, as we also check is_bound in connect(), but + // have it here too just to be safe. + Log.e(TAG, "triggerScan shouldn't be called when service not bound"); + return; + } + // Stops scanning after a pre-defined scan period. bluetoothHandler.postDelayed(new Runnable() { @Override @@ -122,6 +133,13 @@ public class BluetoothLeService extends Service { } void attemptReconnect() { + if( !is_bound ) { + // We check is_bound in connect() itself, but seems pointless to even try if we + // know the service is unbound (and if it's later bound again, we'll try connecting + // again anyway without needing this). + Log.e(TAG, "don't attempt to reconnect when service not bound"); + } + Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { @@ -301,7 +319,7 @@ public class BluetoothLeService extends Service { @Override public IBinder onBind(Intent intent) { if( MyDebug.LOG ) - Log.d(TAG, "Starting OpenCamera Bluetooth Service"); + Log.d(TAG, "onBind"); return mBinder; } @@ -309,14 +327,22 @@ public class BluetoothLeService extends Service { public boolean onUnbind(Intent intent) { if( MyDebug.LOG ) Log.d(TAG, "onUnbind"); + this.is_bound = false; close(); return super.onUnbind(intent); } - + /** Only call this after service is bound (from ServiceConnection.onServiceConnected())! + */ public boolean initialize() { if( MyDebug.LOG ) Log.d(TAG, "initialize"); + + // in theory we'd put this in onBind(), to be more symmetric with onUnbind() where we + // set to false - but unclear whether onBind() is always called before + // ServiceConnection.onServiceConnected(). + this.is_bound = true; + if( bluetoothManager == null ) { bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if( bluetoothManager == null ) { @@ -347,6 +373,36 @@ public class BluetoothLeService extends Service { Log.d(TAG, "address is null"); return false; } + else if( !is_bound ) { + // Don't allow calls to startLeScan() via triggerScan() (which requires location + // permission) when service not bound, as application may be in background! + // And it doesn't seem sensible to even allow connecting if service not bound. + // Under normal operation this isn't needed, but there are calls to connect() that can + // happen from postDelayed() or TimerTask in this class, so a risk that they call + // connect() after the service is unbound! + Log.e(TAG, "connect shouldn't be called when service not bound"); + return false; + } + + + // test code for infinite looping, seeing if this runs in background: + /*if( address.equals("undefined") ) { + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "trying connect again from postdelayed"); + connect(address); + } + }, 1000); + } + + if( address.equals("undefined") ) { + // test - only needed if we've hacked BluetoothRemoteControl.remoteEnabled() to not check for being undefined + if( MyDebug.LOG ) + Log.d(TAG, "address is undefined"); + return false; + }*/ if( address.equals(device_address) && bluetoothGatt != null ) { bluetoothGatt.disconnect(); diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java index b04595531..e6d4a0261 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/BluetoothRemoteControl.java @@ -47,6 +47,17 @@ public class BluetoothRemoteControl { // BluetoothLeService requires Android 4.3+ return; } + if( main_activity.isAppPaused() ) { + if( MyDebug.LOG ) + Log.d(TAG, "but app is now paused"); + // Unclear if this could happen - possibly if app pauses immediately after starting + // the service, but before we connect? In theory we should then unbind the service, + // but seems safer not to try to call initialize or connect. + // This will mean the BluetoothLeService still thinks it's unbound (is_bound will + // be left false), but find, that just means we'll enforce not trying to connect at + // a later stage). + return; + } bluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService(); if( !bluetoothLeService.initialize() ) { Log.e(TAG, "Unable to initialize Bluetooth"); @@ -243,7 +254,10 @@ public class BluetoothRemoteControl { return; } Intent gattServiceIntent = new Intent(main_activity, BluetoothLeService.class); - if( remoteEnabled()) { + // Check isAppPaused() just to be safe - in theory shouldn't be needed, but don't want to + // start up the service if we're in background! (And we might as well then try to stop the + // service instead.) + if( !main_activity.isAppPaused() && remoteEnabled() ) { if( MyDebug.LOG ) Log.d(TAG, "Remote enabled, starting service"); main_activity.bindService(gattServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); @@ -298,6 +312,7 @@ public class BluetoothRemoteControl { boolean remote_enabled = sharedPreferences.getBoolean(PreferenceKeys.EnableRemote, false); remoteDeviceType = sharedPreferences.getString(PreferenceKeys.RemoteType, "undefined"); remoteDeviceAddress = sharedPreferences.getString(PreferenceKeys.RemoteName, "undefined"); + //return remote_enabled; // test - if using this, also need to enable test code in BluetoothLeService.connect() return remote_enabled && !remoteDeviceAddress.equals("undefined"); } } diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java index 9abcac162..9eb0a1ffd 100644 --- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java +++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java @@ -210,7 +210,7 @@ public class DeviceScanner extends ListActivity { @Override protected void onPause() { if( MyDebug.LOG ) - Log.d(TAG, "pause..."); + Log.d(TAG, "onPause"); super.onPause(); if( is_scanning ) { scanLeDevice(false); @@ -218,6 +218,33 @@ public class DeviceScanner extends ListActivity { } } + @Override + protected void onStop() { + if( MyDebug.LOG ) + Log.d(TAG, "onStop"); + super.onStop(); + + // we do this in onPause, but done here again just to be certain! + if( is_scanning ) { + scanLeDevice(false); + leDeviceListAdapter.clear(); + } + } + + @Override + protected void onDestroy() { + if( MyDebug.LOG ) + Log.d(TAG, "onDestroy"); + + // we do this in onPause, but done here again just to be certain! + if( is_scanning ) { + scanLeDevice(false); + leDeviceListAdapter.clear(); + } + + super.onDestroy(); + } + @Override protected void onListItemClick(ListView l, View v, int position, long id) { final BluetoothDevice device = leDeviceListAdapter.getDevice(position); @@ -243,6 +270,8 @@ public class DeviceScanner extends ListActivity { bluetoothHandler.postDelayed(new Runnable() { @Override public void run() { + if( MyDebug.LOG ) + Log.d(TAG, "stop scanning after delay"); is_scanning = false; bluetoothAdapter.stopLeScan(mLeScanCallback); invalidateOptionsMenu(); -- GitLab From 8c1824eae74c3fd5f075b2f7a0d3082cfa3d150d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 11 Sep 2020 21:02:37 +0100 Subject: [PATCH 082/430] Fix crash when failing to save with mediastore. --- .../main/java/net/sourceforge/opencamera/ImageSaver.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index bae1e7be8..f4fc132a2 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2577,6 +2577,13 @@ public class ImageSaver extends Thread { e.printStackTrace(); main_activity.getPreview().showToast(null, R.string.failed_to_save_photo); } + catch(IllegalArgumentException e) { + // can happen for mediastore method if invalid ContentResolver.insert() call + if( MyDebug.LOG ) + Log.e(TAG, "IllegalArgumentException writing file: " + e.getMessage()); + e.printStackTrace(); + main_activity.getPreview().showToast(null, R.string.failed_to_save_photo); + } if( raw_only ) { // no saved image to record -- GitLab From c16582b6ac1adffde642d213921dd980109cd0e0 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 11 Sep 2020 21:02:58 +0100 Subject: [PATCH 083/430] Add method doc. --- app/src/main/java/net/sourceforge/opencamera/MainActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index c86d49706..60901faf8 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -3706,6 +3706,8 @@ public class MainActivity extends Activity { } } + /** Update the save folder. + */ void updateSaveFolder(String new_save_location) { if( MyDebug.LOG ) Log.d(TAG, "updateSaveFolder: " + new_save_location); -- GitLab From 49fd5df80db10b05639a3ce2c4fe6ed25c852908 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 11 Sep 2020 21:03:07 +0100 Subject: [PATCH 084/430] Add comment. --- .../main/java/net/sourceforge/opencamera/preview/Preview.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index a4b6435dc..0f35d521e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -152,7 +152,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu //private boolean ui_placement_right = true; - private boolean app_is_paused = true; + private boolean app_is_paused = true; // whether Preview.onPause() is called - note this could include the application pausing the preview, even if the application isn't in background private boolean has_surface; private boolean has_aspect_ratio; private double aspect_ratio; -- GitLab From c3394be9cfcb0903c97a99f203597643bfc354f5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 11 Sep 2020 21:48:09 +0100 Subject: [PATCH 085/430] Pausing the preview when going to dialog or settings shouldn't prevent it from creating toasts. --- .../sourceforge/opencamera/MainActivity.java | 2 +- .../opencamera/preview/Preview.java | 31 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 60901faf8..84b4f6c70 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -2921,7 +2921,7 @@ public class MainActivity extends Activity { applicationInterface.getLocationSupplier().freeLocationListeners(); // similarly we close the camera - preview.onPause(); + preview.onPause(false); } private void showWhenLocked(boolean show) { diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 0f35d521e..2b420a3e4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -152,7 +152,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu //private boolean ui_placement_right = true; - private boolean app_is_paused = true; // whether Preview.onPause() is called - note this could include the application pausing the preview, even if the application isn't in background + private boolean app_is_paused = true; // whether activity is paused + private boolean is_paused = true; // whether Preview.onPause() is called - note this could include the application pausing the preview, even if app_is_paused==false private boolean has_surface; private boolean has_aspect_ratio; private double aspect_ratio; @@ -1449,9 +1450,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } return; } - if( this.app_is_paused ) { + if( this.is_paused ) { if( MyDebug.LOG ) { - Log.d(TAG, "don't open camera as app is paused"); + Log.d(TAG, "don't open camera as paused"); } return; } @@ -3856,9 +3857,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu public void draw(Canvas canvas) { /*if( MyDebug.LOG ) Log.d(TAG, "draw()");*/ - if( this.app_is_paused ) { + if( this.is_paused ) { /*if( MyDebug.LOG ) - Log.d(TAG, "draw(): app is paused");*/ + Log.d(TAG, "draw(): paused");*/ return; } /*if( true ) // test @@ -7198,11 +7199,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu return camera_controller.getAPI(); } + /** Call when activity is resumed. + */ public void onResume() { if( MyDebug.LOG ) Log.d(TAG, "onResume"); recreatePreviewBitmap(); this.app_is_paused = false; + this.is_paused = false; cameraSurface.onResume(); if( canvasView != null ) canvasView.onResume(); @@ -7224,10 +7228,23 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } + /** Call when activity is paused. + */ public void onPause() { + onPause(true); + } + + /** Call when activity is paused, or the application wants to put the Preview into a paused + * state (closing the camera etc). + * @param activity_is_pausing Set to true if this is called because the activity is being paused; + * set to false if the activity is not pausing. + */ + public void onPause(boolean activity_is_pausing) { if( MyDebug.LOG ) Log.d(TAG, "onPause"); - this.app_is_paused = true; + this.is_paused = true; + if( activity_is_pausing ) + this.app_is_paused = true; // note, if activity_is_paused==false, we don't change app_is_paused, in case app was paused indicated via a separate call to onPause if( camera_open_state == CameraOpenState.CAMERAOPENSTATE_OPENING ) { if( MyDebug.LOG ) Log.d(TAG, "cancel open_camera_task"); @@ -8250,7 +8267,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu final long refresh_time = (want_zebra_stripes || want_focus_peaking) ? 40 : refresh_histogram_rate_ms; long time_now = System.currentTimeMillis(); if( want_preview_bitmap && preview_bitmap != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && - !app_is_paused && !applicationInterface.isPreviewInBackground() && + !is_paused && !applicationInterface.isPreviewInBackground() && !refreshPreviewBitmapTaskIsRunning() && time_now > last_preview_bitmap_time_ms + refresh_time ) { if( MyDebug.LOG ) Log.d(TAG, "refreshPreviewBitmap"); -- GitLab From e267e757895f434cc212099de9d593bb28ec7800 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 14:29:59 +0100 Subject: [PATCH 086/430] Refactor preference key strings to fields in PreferenceKeys. --- .../main/java/net/sourceforge/opencamera/MainActivity.java | 4 ++-- .../main/java/net/sourceforge/opencamera/PreferenceKeys.java | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 84b4f6c70..eaf5d7565 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -286,11 +286,11 @@ public class MainActivity extends Activity { 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()); + save_location_history = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistoryBasePreferenceKey, 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()); + save_location_history_saf = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistorySAFBasePreferenceKey, getStorageUtils().getSaveLocationSAF()); } if( MyDebug.LOG ) Log.d(TAG, "onCreate: time after updating folder history: " + (System.currentTimeMillis() - debug_time)); diff --git a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java index cc8732db0..178b87427 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java +++ b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -206,6 +206,10 @@ public class PreferenceKeys { public static final String SaveLocationSAFPreferenceKey = "preference_save_location_saf"; + public static final String SaveLocationHistoryBasePreferenceKey = "save_location_history"; + + public static final String SaveLocationHistorySAFBasePreferenceKey = "save_location_history_saf"; + public static final String SavePhotoPrefixPreferenceKey = "preference_save_photo_prefix"; public static final String SaveVideoPrefixPreferenceKey = "preference_save_video_prefix"; -- GitLab From 94f1494d0522c33d5748e93ebf889359be8dfc3e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 14:33:01 +0100 Subject: [PATCH 087/430] Code for simpler save location format should only be for the save photo/video locations. --- .../opencamera/ui/FolderChooserDialog.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java index f9c85b409..fae2072b6 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java @@ -318,12 +318,14 @@ public class FolderChooserDialog extends DialogFragment { 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( this.show_dcim_shortcut ) { + File base_folder = StorageUtils.getBaseFolder(); + 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); -- GitLab From 1494746bfe3a59f168d8c7cb22b868268e7b043d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 14:33:38 +0100 Subject: [PATCH 088/430] Update for scoped storage. --- .../java/net/sourceforge/opencamera/StorageUtils.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 30304539a..541d6974a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -378,23 +378,20 @@ public class StorageUtils { } // only valid if !isUsingSAF() + // returns a form for use with RELATIVE_PATH (scoped storage) String getSaveRelativeFolder() { String folder_name = getSaveLocation(); return getSaveRelativeFolder(folder_name); } // only valid if !isUsingSAF() + // returns a form for use with RELATIVE_PATH (scoped storage) private static String getSaveRelativeFolder(String folder_name) { 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.startsWith("/") ) { - return folder_name; - } - else { - return RELATIVE_FOLDER_BASE + File.separator + folder_name; - } + return RELATIVE_FOLDER_BASE + File.separator + folder_name; } public static File getBaseFolder() { -- GitLab From 6d75a113645041301acf044b58fff19c9c4a6abf Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 14:35:32 +0100 Subject: [PATCH 089/430] Refactor to new saveFolderIsFull(). --- .../java/net/sourceforge/opencamera/StorageUtils.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 541d6974a..adb1df17f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -398,6 +398,13 @@ public class StorageUtils { return base_folder; } + /** Whether the save photo/video location is in a form that represents a full path, or a + * sub-folder in DCIM/. + */ + static boolean saveFolderIsFull(String folder_name) { + return folder_name.startsWith("/"); + } + // only valid if !isUsingSAF() private static File getImageFolder(String folder_name) { File file; @@ -405,7 +412,7 @@ public class StorageUtils { // ignore final '/' character folder_name = folder_name.substring(0, folder_name.length()-1); } - if( folder_name.startsWith("/") ) { + if( saveFolderIsFull(folder_name) ) { file = new File(folder_name); } else { @@ -1501,7 +1508,7 @@ public class StorageUtils { if( !isUsingSAF() ) { // getSaveLocation() only valid if !isUsingSAF() String folder_name = getSaveLocation(); - if( !folder_name.startsWith("/") ) { + if( !saveFolderIsFull(folder_name) ) { File folder = getBaseFolder(); StatFs statFs = new StatFs(folder.getAbsolutePath()); long blocks, size; -- GitLab From 9bf674c2ea1b0e8e1d9f610bbda75e116a73210f Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 15:12:22 +0100 Subject: [PATCH 090/430] Fix outdated comment. --- app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index f4fc132a2..5e646a27a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -3357,8 +3357,8 @@ public class ImageSaver extends Thread { /** Makes various modifications to the saved image file, according to the preferences in request. * This method is used when saving directly from the JPEG data rather than a bitmap. - * If picFile==null, then saveUri must be non-null (and the Android version must be Android 7 - * or later), and will be used instead to write the exif tags too. + * If picFile==null, then saveUri must be non-null, and will be used instead to write the exif + * tags too. */ private void updateExif(Request request, File picFile, Uri saveUri) throws IOException { if( MyDebug.LOG ) -- GitLab From 25d90d05a312b0bdcd40dd24dfa2a496bc86c690 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 15:53:26 +0100 Subject: [PATCH 091/430] Refactor to use new StorageUtils.saveFolderIsFull(). --- .../java/net/sourceforge/opencamera/MyApplicationInterface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index e3f5dbf05..e0d821749 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -993,7 +993,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "saving to: " + folder_name); boolean is_internal = false; - if( !folder_name.startsWith("/") ) { + if( !StorageUtils.saveFolderIsFull(folder_name) ) { is_internal = true; } else { -- GitLab From e29baa7b877e64624e29682f8c692f6aaaf54d40 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 15:59:41 +0100 Subject: [PATCH 092/430] Update broadcastUri() to return String instead of File. --- .../main/java/net/sourceforge/opencamera/ImageSaver.java | 6 +++--- .../net/sourceforge/opencamera/MyApplicationInterface.java | 6 +++--- .../main/java/net/sourceforge/opencamera/StorageUtils.java | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 5e646a27a..79fb83e39 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2715,9 +2715,9 @@ public class ImageSaver extends Thread { if( MyDebug.LOG ) Log.d(TAG, "broadcastSAFFile"); StorageUtils storageUtils = main_activity.getStorageUtils(); - File real_file = storageUtils.broadcastUri(saveUri, true, false, true, image_capture_intent); - if( real_file != null ) { - main_activity.test_last_saved_image = real_file.getAbsolutePath(); + String real_file_path = storageUtils.broadcastUri(saveUri, true, false, true, image_capture_intent); + if( real_file_path != null ) { + main_activity.test_last_saved_image = real_file_path; } } diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index e0d821749..6f8f9987e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2378,9 +2378,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { else { if( uri != null ) { // see note in onPictureTaken() for where we call broadcastFile for SAF photos - File real_file = storageUtils.broadcastUri(uri, false, true, true, false); - if( real_file != null ) { - main_activity.test_last_saved_image = real_file.getAbsolutePath(); + String real_file_path = storageUtils.broadcastUri(uri, false, true, true, false); + if( real_file_path != null ) { + main_activity.test_last_saved_image = real_file_path; } done = true; } diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index adb1df17f..3323172c9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -278,8 +278,9 @@ public class StorageUtils { } /** Wrapper for broadcastFile, when we only have a Uri (e.g., for SAF) + * @return If non-null, this contains the path of the file we found for the Uri. */ - public File broadcastUri(final Uri uri, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned, final boolean image_capture_intent) { + public String broadcastUri(final Uri uri, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned, final boolean image_capture_intent) { if( MyDebug.LOG ) Log.d(TAG, "broadcastUri: " + uri); /* We still need to broadcastFile for SAF for two reasons: @@ -300,7 +301,7 @@ public class StorageUtils { //Uri media_uri = broadcastFileRaw(real_file, current_date, location); //announceUri(media_uri, is_new_picture, is_new_video); broadcastFile(real_file, is_new_picture, is_new_video, set_last_scanned); - return real_file; + return (real_file==null ? null : real_file.getAbsolutePath()); } else if( !image_capture_intent ) { if( MyDebug.LOG ) -- GitLab From 5a8e449fa4d523b6c344edafd01a4400b42f0021 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 16:50:09 +0100 Subject: [PATCH 093/430] Fix auto-restart on max filesize when using scoped storage mediastore method. --- .../opencamera/MyApplicationInterface.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 6f8f9987e..adab4f299 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -952,6 +952,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { e.printStackTrace(); video_max_filesize = 0; } + //video_max_filesize = 1024*1024; // test if( MyDebug.LOG ) Log.d(TAG, "video_max_filesize: " + video_max_filesize); return video_max_filesize; @@ -2230,14 +2231,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { subtitleVideoTimerTask = null; } - if( video_method == VideoMethod.MEDIASTORE ) { - if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.Video.Media.IS_PENDING, 0); - main_activity.getContentResolver().update(uri, contentValues, null, null); - } - } - + completeVideo(video_method, uri); boolean done = broadcastVideo(video_method, uri, filename); if( MyDebug.LOG ) Log.d(TAG, "done? " + done); @@ -2342,9 +2336,23 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "uri " + uri); Log.d(TAG, "filename " + filename); } + completeVideo(video_method, uri); broadcastVideo(video_method, uri, filename); } + /** Called when we've finished recording to a video file, to do any necessary cleanup for the + * file. + */ + private void completeVideo(final VideoMethod video_method, final Uri uri) { + if( video_method == VideoMethod.MEDIASTORE ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Video.Media.IS_PENDING, 0); + main_activity.getContentResolver().update(uri, contentValues, null, null); + } + } + } + private boolean broadcastVideo(final VideoMethod video_method, final Uri uri, final String filename) { if( MyDebug.LOG ) { Log.d(TAG, "broadcastVideo"); -- GitLab From fd02ebf6e1591d52b3de16d2d52670a016d0830a Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 17:21:14 +0100 Subject: [PATCH 094/430] Fix scoped storage mediastore method when called from video capture intent. --- .../opencamera/MyApplicationInterface.java | 26 +++++++++++++++++-- .../sourceforge/opencamera/StorageUtils.java | 14 +++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index adab4f299..b095a6ec5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -292,8 +292,14 @@ public class MyApplicationInterface extends BasicApplicationInterface { // 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; + if( MainActivity.useScopedStorage() ) { + // can't use file method with scoped storage + return VideoMethod.MEDIASTORE; + } + else { + // 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; + } } else if( storageUtils.isUsingSAF() ) { return VideoMethod.SAF; @@ -2398,9 +2404,25 @@ public class MyApplicationInterface extends BasicApplicationInterface { if( MyDebug.LOG ) Log.d(TAG, "test_n_videos_scanned is now: " + test_n_videos_scanned); } + + if( video_method == VideoMethod.MEDIASTORE && isVideoCaptureIntent() ) { + finishVideoIntent(uri); + } return done; } + /** For use when called from a video capture intent. This returns the supplied uri to the + * caller, and finishes the activity. + */ + void finishVideoIntent(Uri uri) { + if( MyDebug.LOG ) + Log.d(TAG, "finishVideoIntent:" + uri); + Intent output = new Intent(); + output.setData(uri); + main_activity.setResult(Activity.RESULT_OK, output); + main_activity.finish(); + } + @Override public void deleteUnusedVideo(final VideoMethod video_method, final Uri uri, final String filename) { if( MyDebug.LOG ) { diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 3323172c9..6ba243473 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -259,17 +259,15 @@ public class StorageUtils { announceUri(uri, is_new_picture, is_new_video); applicationInterface.scannedFile(file, uri); - // it seems caller apps seem to prefer the content:// Uri rather than one based on a File + // If called from video intent, if not using scoped-storage, we'll have saved using File API (even if user preference is SAF), see + // MyApplicationInterface.createOutputVideoMethod(). + // 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 + // So we pass the uri back to the caller here. 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(); + if( !MainActivity.useScopedStorage() && MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) { + applicationInterface.finishVideoIntent(uri); } } } -- GitLab From 6f42591d9e552f0f154dbe34d124a6bc97025e67 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 13 Sep 2020 17:21:45 +0100 Subject: [PATCH 095/430] Add logging. --- .../main/java/net/sourceforge/opencamera/preview/Preview.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 2b420a3e4..75112342f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -1012,6 +1012,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // if nextVideoFileInfo is not-null, it means we received MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING but not // MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED, so it is the application responsibility to create the zero-size // video file that will have been created + if( MyDebug.LOG ) + Log.d(TAG, "delete ununused next video file"); nextVideoFileInfo.close(); applicationInterface.deleteUnusedVideo(nextVideoFileInfo.video_method, nextVideoFileInfo.video_uri, nextVideoFileInfo.video_filename); } -- GitLab From a0ff93d6ff39763903436990b5ab6a05909a5a02 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 14 Sep 2020 21:16:57 +0100 Subject: [PATCH 096/430] Add comment for testTakePhotoFrontCameraAll(), can be unstable on Android emulator. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index d40259442..0c8588b58 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -4537,6 +4537,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Mon, 14 Sep 2020 21:17:11 +0100 Subject: [PATCH 097/430] Add logging. --- .../main/java/net/sourceforge/opencamera/preview/Preview.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 75112342f..e66917422 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -6325,6 +6325,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu 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(); + if( MyDebug.LOG ) + Log.d(TAG, "switch back to: " + current_ui_focus_value); camera_controller.setFocusValue(current_ui_focus_value); } else { -- GitLab From 55d07cd820e178bdff39941dc12c501aa1ce0712 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 14 Sep 2020 22:04:14 +0100 Subject: [PATCH 098/430] Check whether non-SAF save locations are compatible with scoped storage. --- .../sourceforge/opencamera/MainActivity.java | 255 +++++++++++++++++- .../opencamera/MyPreferenceFragment.java | 18 ++ .../opencamera/SaveLocationHistory.java | 15 ++ 3 files changed, 281 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index eaf5d7565..0fe118179 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -70,6 +70,9 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.exifinterface.media.ExifInterface; +import android.text.InputFilter; +import android.text.InputType; +import android.text.Spanned; import android.util.Log; import android.view.Display; import android.view.GestureDetector; @@ -287,6 +290,7 @@ public class MainActivity extends Activity { Log.d(TAG, "onCreate: time after setting window flags: " + (System.currentTimeMillis() - debug_time)); save_location_history = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistoryBasePreferenceKey, getStorageUtils().getSaveLocation()); + checkSaveLocations(); if( applicationInterface.getStorageUtils().isUsingSAF() ) { if( MyDebug.LOG ) Log.d(TAG, "create new SaveLocationHistory for SAF"); @@ -961,6 +965,153 @@ public class MainActivity extends Activity { } } + /** Handles users updating to a version with scoped storage (this could be Android 10 users upgrading + * to the version of Open Camera with scoped storage; or users who later upgrade to Android 10). + * With scoped storage, we no longer support saving outside of DCIM/ when not using SAF. + * This updates if necessary both the current save location, and the save folder history. + */ + private void checkSaveLocations() { + if( MyDebug.LOG ) + Log.d(TAG, "checkSaveLocations"); + if( useScopedStorage() ) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean any_changes = false; + String save_location = getStorageUtils().getSaveLocation(); + CheckSaveLocationResult res = checkSaveLocation(save_location); + if( !res.res ) { + if( MyDebug.LOG ) + Log.d(TAG, "save_location not valid with scoped storage: " + save_location); + String new_folder; + if( res.alt == null ) { + // no alternative, fall back to default + new_folder = "OpenCamera"; + } + else { + // replace with the alternative + if( MyDebug.LOG ) + Log.d(TAG, "alternative: " + res.alt); + new_folder = res.alt; + } + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.SaveLocationPreferenceKey, new_folder); + editor.apply(); + any_changes = true; + } + + // now check history + // go backwards so we can remove easily + for(int i=save_location_history.size()-1;i>=0;i--) { + String this_location = save_location_history.get(i); + res = checkSaveLocation(this_location); + if( !res.res ) { + if( MyDebug.LOG ) + Log.d(TAG, "save_location in history " + i + " not valid with scoped storage: " + this_location); + if( res.alt == null ) { + // no alternative, remove + save_location_history.remove(i); + } + else { + // replace with the alternative + if( MyDebug.LOG ) + Log.d(TAG, "alternative: " + res.alt); + save_location_history.set(i, res.alt); + } + any_changes = true; + } + } + + if( any_changes ) { + this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), false); + } + } + } + + /** Result from checkSaveLocation. Ideally we'd just use android.util.Pair, but that's not mocked + * for use in unit tests. + * See checkSaveLocation() for documentation. + */ + public static class CheckSaveLocationResult { + final boolean res; + final String alt; + + public CheckSaveLocationResult(boolean res, String alt) { + this.res = res; + this.alt = alt; + } + + @Override + public boolean equals(Object o) { + if( !(o instanceof CheckSaveLocationResult) ) { + return false; + } + CheckSaveLocationResult that = (CheckSaveLocationResult)o; + // stop dumb inspection that suggests replacing warning with an error(!) (Objects class is not available on all API versions) + // and the other inspection suggests replacing with code that would cause a nullpointerexception + //noinspection EqualsReplaceableByObjectsCall,StringEquality + return that.res == this.res && ( (that.alt == this.alt) || (that.alt != null && that.alt.equals(this.alt) ) ); + //return that.res == this.res && ( (that.alt == this.alt) || (that.alt != null && that.alt.equals(this.alt) ) ); + } + + @Override + public int hashCode() { + return (res ? 1249 : 1259) ^ (alt == null ? 0 : alt.hashCode()); + } + + @Override + public String toString() { + return "CheckSaveLocationResult{" + res + " , " + alt + "}"; + } + } + + public static CheckSaveLocationResult checkSaveLocation(final String folder) { + return checkSaveLocation(folder, null); + } + + /** Checks to see if the supplied folder (in the format as used by our preferences) is supported + * with scoped storage. + * @return The Boolean is always non-null, and returns whether the save location is valid. + * If the return is false, then if the String is non-null, this stores an alternative + * form that is valid. If null, there is no valid alternative. + * @param base_folder This should normally be null, but can be used to specify manually the + * folder instead of using StorageUtils.getBaseFolder() - needed for unit + * tests as Environment class (for Environment.getExternalStoragePublicDirectory()) + * is not mocked. + */ + public static CheckSaveLocationResult checkSaveLocation(final String folder, String base_folder) { + /*if( MyDebug.LOG ) + Log.d(TAG, "DCIM path: " + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());*/ + if( StorageUtils.saveFolderIsFull(folder) ) { + if( MyDebug.LOG ) + Log.d(TAG, "checkSaveLocation for full path: " + folder); + // But still check to see if the full path is part of DCIM. Since when using the + // file dialog method with non-scoped storage, if the user specifies multiple subfolders + // e.g. DCIM/blah_a/blah_b, we don't spot that in FolderChooserDialog.useFolder(), and + // instead still store that as the full path. + + if( base_folder == null ) + base_folder = StorageUtils.getBaseFolder().getAbsolutePath(); + // strip '/' as last character - makes it easier to also spot cases where the folder is the + // DCIM folder, but doesn't have a '/' last character + if( base_folder.length() >= 1 && base_folder.charAt(base_folder.length()-1) == '/' ) + base_folder = base_folder.substring(0, base_folder.length()-1); + if( MyDebug.LOG ) + Log.d(TAG, " compare to base_folder: " + base_folder); + String alt_folder = null; + if( folder.startsWith(base_folder) ) { + alt_folder = folder.substring(base_folder.length()); + // also need to strip the first '/' if it exists + if( alt_folder.length() >= 1 && alt_folder.charAt(0) == '/' ) + alt_folder = alt_folder.substring(1); + } + + return new CheckSaveLocationResult(false, alt_folder); + } + else { + // already in expected format (indicates a sub-folder of DCIM) + return new CheckSaveLocationResult(true, null); + } + } + private void preloadIcons(int icons_id) { long debug_time = 0; if( MyDebug.LOG ) { @@ -3752,6 +3903,79 @@ public class MainActivity extends Activity { } } + /** Processes a user specified save folder. This should be used with the non-SAF scoped storage + * method, where the user types a folder directly. + */ + public static String processUserSaveLocation(String folder) { + // filter repeated '/', e.g., replace // with /: + String strip = "//"; + while( folder.length() >= 1 && folder.contains(strip) ) { + folder = folder.replaceAll(strip, "/"); + } + + if( folder.length() >= 1 && folder.charAt(0) == '/' ) { + // strip '/' as first character - as absolute paths not allowed with scoped storage + // whilst we do block entering a '/' as first character in the InputFilter, users could + // get around this (e.g., put a '/' as second character, then delete the first character) + folder = folder.substring(1); + } + + if( folder.length() >= 1 && folder.charAt(folder.length()-1) == '/' ) { + // strip '/' as last character - MediaStore will ignore it, but seems cleaner to strip it out anyway + // (we still need to allow '/' as last character in the InputFilter, otherwise users won't be able to type it whilst writing a subfolder) + folder = folder.substring(0, folder.length()-1); + } + + return folder; + } + + /** Creates a dialog builder for specifying a save folder dialog (used when not using SAF, + * and on scoped storage, as an alternative to using FolderChooserDialog). + */ + public AlertDialog.Builder createSaveFolderDialog() { + final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); + alertDialog.setTitle(R.string.preference_save_location); + + final EditText editText = new EditText(this); + editText.setInputType(InputType.TYPE_CLASS_TEXT); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + editText.setText(sharedPreferences.getString(PreferenceKeys.SaveLocationPreferenceKey, "OpenCamera")); + InputFilter filter = new InputFilter() { + // whilst Android seems to allow any characters on internal memory, SD cards are typically formatted with FAT32 + String disallowed = "|\\?*<\":>"; + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + for(int i=start;i Date: Mon, 14 Sep 2020 22:07:36 +0100 Subject: [PATCH 099/430] Unit tests for checking save locations being compatible with scoped storage. --- .../sourceforge/opencamera/StorageUtils.java | 12 +-- .../sourceforge/opencamera/test/UnitTest.java | 85 +++++++++++++++++++ 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 6ba243473..9583172c8 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -57,12 +57,6 @@ public class StorageUtils { private Uri last_media_scanned; private final static String RELATIVE_FOLDER_BASE = Environment.DIRECTORY_DCIM; - // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; - // when using scoped storage, we should no longer be trying to read/write this codepath with File API, but we are still using - // this for older Android versions. - // Furthermore, sometimes we still use this with scoped storage for valid purposes, e.g., to pass to StatFs in freeMemory(). - @SuppressWarnings("deprecation") - private final static File base_folder = Environment.getExternalStoragePublicDirectory(RELATIVE_FOLDER_BASE); // for testing: public volatile boolean failed_to_scan; @@ -394,6 +388,12 @@ public class StorageUtils { } public static File getBaseFolder() { + // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; + // when using scoped storage, we should no longer be trying to read/write this codepath with File API, but we are still using + // this for older Android versions. + // Furthermore, sometimes we still use this with scoped storage for valid purposes, e.g., to pass to StatFs in freeMemory(). + @SuppressWarnings("deprecation") + final File base_folder = Environment.getExternalStoragePublicDirectory(RELATIVE_FOLDER_BASE); return base_folder; } diff --git a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java index 17b04cec0..c0e87f9c0 100644 --- a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java +++ b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java @@ -2,6 +2,7 @@ package net.sourceforge.opencamera.test; import android.media.CamcorderProfile; +import net.sourceforge.opencamera.MainActivity; import net.sourceforge.opencamera.MyApplicationInterface; import net.sourceforge.opencamera.cameracontroller.CameraController; import net.sourceforge.opencamera.cameracontroller.CameraController2; @@ -786,4 +787,88 @@ public class UnitTest { assertEquals(1.0f/(0.369070f*(0.2f-0.1f) + 0.1f), focus_distances.get(1), 1.0e-5); assertEquals(1.0f/0.2f, focus_distances.get(0), 1.0e-5); } + + /** Tests MainActivity.processUserSaveLocation() (used for checking save locations when specifying user folders with non-SAF scoped storage). + */ + @Test + public void testprocessUserSaveLocation() { + assertEquals("OpenCamera", MainActivity.processUserSaveLocation("OpenCamera")); + + assertEquals("OpenCamera", MainActivity.processUserSaveLocation("OpenCamera/")); + + assertEquals("", MainActivity.processUserSaveLocation("")); + + assertEquals("", MainActivity.processUserSaveLocation("/")); + + assertEquals("blah_a/blah_b", MainActivity.processUserSaveLocation("blah_a/blah_b")); + + assertEquals("blah_a/blah_b", MainActivity.processUserSaveLocation("blah_a/blah_b/")); + + assertEquals("blah_a/blah_b", MainActivity.processUserSaveLocation("blah_a//blah_b")); + + assertEquals("blah_a/blah_b", MainActivity.processUserSaveLocation("blah_a///blah_b")); + + assertEquals("blah_a/blah_b/blah_c", MainActivity.processUserSaveLocation("blah_a///blah_b/blah_c//")); + + assertEquals("OpenCamera", MainActivity.processUserSaveLocation("/OpenCamera")); + + assertEquals("OpenCamera", MainActivity.processUserSaveLocation("//OpenCamera")); + + assertEquals("OpenCamera", MainActivity.processUserSaveLocation("///OpenCamera")); + + assertEquals("blah_a/blah_b/blah_c", MainActivity.processUserSaveLocation("/blah_a///blah_b/blah_c//")); + + } + + /** Tests MainActivity.checkSaveLocation() (used for checking save locations when updating to scoped storage). + */ + @Test + public void testCheckSaveLocation() { + Log.d(TAG, "testCheckSaveLocation"); + + // Should be same as Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + // - needed as Environment is not mocked. + final String dcim_path = "/storage/emulated/0/DCIM"; + + MainActivity.CheckSaveLocationResult res; + + res = MainActivity.checkSaveLocation(""); + assertEquals(new MainActivity.CheckSaveLocationResult(true, null), res); + + res = MainActivity.checkSaveLocation("OpenCamera"); + assertEquals(new MainActivity.CheckSaveLocationResult(true, null), res); + + res = MainActivity.checkSaveLocation("blah_a/blah_b"); + assertEquals(new MainActivity.CheckSaveLocationResult(true, null), res); + + res = MainActivity.checkSaveLocation("OpenCamera/"); + assertEquals(new MainActivity.CheckSaveLocationResult(true, null), res); + + res = MainActivity.checkSaveLocation("blah_a/blah_b/"); + assertEquals(new MainActivity.CheckSaveLocationResult(true, null), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/DCIM/OpenCamera/subfolder/", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, "OpenCamera/subfolder/"), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/DCIM/OpenCamera/subfolder", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, "OpenCamera/subfolder"), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/DCIM/OpenCamera/", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, "OpenCamera/"), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/DCIM/OpenCamera", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, "OpenCamera"), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/DCIM/", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, ""), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/DCIM", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, ""), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0/Pictures", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, null), res); + + res = MainActivity.checkSaveLocation("/storage/emulated/0", dcim_path); + assertEquals(new MainActivity.CheckSaveLocationResult(false, null), res); + } } -- GitLab From 7baf058f7b193641a1bcb94e144b47f0a1a1fb0d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 14 Sep 2020 22:07:58 +0100 Subject: [PATCH 100/430] Tests for checking save locations being compatible with scoped storage. --- .../opencamera/test/MainActivityTest.java | 134 ++++++++++++++++++ .../opencamera/test/MainTests.java | 6 + 2 files changed, 140 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 0c8588b58..577d10e57 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -10409,6 +10409,140 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Mon, 14 Sep 2020 22:08:26 +0100 Subject: [PATCH 101/430] Fix crash in testSaveFolderHistorySAF() when run with scoped storage. --- .../sourceforge/opencamera/StorageUtils.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 9583172c8..f4336acc0 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -1176,7 +1176,23 @@ public class StorageUtils { Media media = null; - Uri baseUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); + Uri baseUri = null; + try { + String parentDocUri = DocumentsContract.getTreeDocumentId(treeUri); + baseUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocUri); + } + catch(Exception e) { + // DocumentsContract.getTreeDocumentId throws IllegalArgumentException if the uri is + // invalid. Unclear if this can happen in practice - this happens in test + // testSaveFolderHistorySAF() but only because we test a dummy invalid SAF uri. But + // seems no harm catching it in case this can happen (e.g., especially if restoring + // backed up preferences from a different device?) Better to just show nothing in the + // thumbnail, rather than crashing! + // N.B., we catch Exception is otherwise compiler complains IllegalArgumentException + // isn't ever thrown - even though it is!? + Log.e(TAG, "Exception using treeUri: " + treeUri); + return media; + } if( MyDebug.LOG ) Log.d(TAG, "baseUri: " + baseUri); -- GitLab From 305770781c5293f46cd1aa12f05fd826e250057e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 14 Sep 2020 22:09:57 +0100 Subject: [PATCH 102/430] Updates for running tests with scoped storage. --- .../opencamera/test/MainActivityTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 577d10e57..dc08a892e 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -10297,6 +10297,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Tue, 15 Sep 2020 22:59:02 +0100 Subject: [PATCH 103/430] Add logging. --- .../java/net/sourceforge/opencamera/MyApplicationInterface.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index b095a6ec5..d78f2821c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2350,6 +2350,8 @@ public class MyApplicationInterface extends BasicApplicationInterface { * file. */ private void completeVideo(final VideoMethod video_method, final Uri uri) { + if( MyDebug.LOG ) + Log.d(TAG, "completeVideo"); if( video_method == VideoMethod.MEDIASTORE ) { if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { ContentValues contentValues = new ContentValues(); -- GitLab From 699e33c85aaeb651ffb2c7ef489f6c716329a115 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Tue, 15 Sep 2020 23:27:31 +0100 Subject: [PATCH 104/430] Update testing for scoped storage. --- .../opencamera/test/MainActivityTest.java | 385 +++++++++++------- .../sourceforge/opencamera/ImageSaver.java | 7 +- .../sourceforge/opencamera/MainActivity.java | 3 +- .../opencamera/MyApplicationInterface.java | 5 +- .../sourceforge/opencamera/StorageUtils.java | 11 +- 5 files changed, 250 insertions(+), 161 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index dc08a892e..a902e2b2b 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -39,6 +39,7 @@ import android.annotation.TargetApi; import android.content.Intent; import android.content.SharedPreferences; //import android.content.res.AssetManager; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; //import android.graphics.BitmapFactory; @@ -53,10 +54,12 @@ import android.location.Location; import android.media.CamcorderProfile; import androidx.exifinterface.media.ExifInterface; import android.media.MediaScannerConnection; +import android.net.Uri; import android.os.Build; import android.os.Environment; //import android.os.Environment; import android.preference.PreferenceManager; +import android.provider.DocumentsContract; import android.provider.MediaStore; import android.renderscript.Allocation; import androidx.annotation.RequiresApi; @@ -2056,12 +2059,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 mediaFilesinSaveFolder(Uri baseUri, String bucket_id, UriType uri_type) { + List files = new ArrayList<>(); + final int column_name_c = 0; // filename (without path), including extension + + String [] projection; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + projection = new String[] {MediaStore.Images.ImageColumns.DISPLAY_NAME}; + break; + case MEDIASTORE_VIDEOS: + projection = new String[] {MediaStore.Video.VideoColumns.DISPLAY_NAME}; + break; + case STORAGE_ACCESS_FRAMEWORK: + projection = new String[] {DocumentsContract.Document.COLUMN_DISPLAY_NAME}; + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); + } + + String selection = ""; + switch( uri_type ) { + case MEDIASTORE_IMAGES: + selection = MediaStore.Images.ImageColumns.BUCKET_ID + " = " + bucket_id; + break; + case MEDIASTORE_VIDEOS: + selection = MediaStore.Video.VideoColumns.BUCKET_ID + " = " + bucket_id; + break; + case STORAGE_ACCESS_FRAMEWORK: + break; + default: + throw new RuntimeException("unknown uri_type: " + uri_type); + } + Log.d(TAG, "selection: " + selection); + + Cursor cursor = mActivity.getContentResolver().query(baseUri, projection, selection, null, null); + if( cursor != null && cursor.moveToFirst() ) { + Log.d(TAG, "found: " + cursor.getCount()); + + do { + String name = cursor.getString(column_name_c); + files.add(name); + } + while( cursor.moveToNext() ); + } + + if( cursor != null ) { + cursor.close(); + } + + return files; + } + + /** Returns an array of filenames (not including full path) in the current save folder. + */ + private String [] filesInSaveFolder() { + Log.d(TAG, "filesInSaveFolder"); + if( MainActivity.useScopedStorage() ) { + List files = new ArrayList<>(); + if( mActivity.getStorageUtils().isUsingSAF() ) { + // See documentation for StorageUtils.getLatestMediaSAF() - for some reason with scoped storage when not having READ_EXTERNAL_STORAGE, + // we can't query the mediastore for files saved via SAF! + Uri treeUri = mActivity.getStorageUtils().getTreeUriSAF(); + Uri baseUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); + files.addAll( mediaFilesinSaveFolder(baseUri, null, UriType.STORAGE_ACCESS_FRAMEWORK) ); + } + else { + String save_folder = mActivity.getStorageUtils().getImageFolderPath(); + String bucket_id = String.valueOf(save_folder.toLowerCase().hashCode()); + files.addAll( mediaFilesinSaveFolder(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, bucket_id, UriType.MEDIASTORE_IMAGES) ); + files.addAll( mediaFilesinSaveFolder(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, bucket_id, UriType.MEDIASTORE_VIDEOS) ); + } + + if( files.size() == 0 ) { + return null; + } + else { + return files.toArray(new String[0]); + } + } + else { + File folder = mActivity.getImageFolder(); + File [] files = folder.listFiles(); + if( files == null ) + return null; + String [] filenames = new String[files.length]; + for(int i=0;i -1 ) { @@ -9335,6 +9445,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 = Build.VERSION_CODES.LOLLIPOP ) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -328,7 +325,7 @@ public class StorageUtils { } // only valid if isUsingSAF() - private Uri getTreeUriSAF() { + public Uri getTreeUriSAF() { String folder_name = getSaveLocationSAF(); return Uri.parse(folder_name); } @@ -600,7 +597,7 @@ public class StorageUtils { * See https://developer.android.com/guide/topics/providers/document-provider.html and * http://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content . */ - String getFileName(Uri uri) { + public String getFileName(Uri uri) { if( MyDebug.LOG ) { Log.d(TAG, "getFileName: " + uri); Log.d(TAG, "uri has path: " + uri.getPath()); -- GitLab From 88fa2497361db436a198854d87f9c6c3bf5e34d5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 18 Sep 2020 00:05:40 +0100 Subject: [PATCH 105/430] Remove redundant code, as we always call stoppingVideo() which does these lines. --- .../sourceforge/opencamera/MyApplicationInterface.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 4992e7c54..498515711 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2513,10 +2513,6 @@ public class MyApplicationInterface extends BasicApplicationInterface { error_message = getContext().getResources().getString(R.string.failed_to_record_video); } main_activity.getPreview().showToast(null, error_message); - ImageButton view = 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 @@ -2539,11 +2535,9 @@ public class MyApplicationInterface extends BasicApplicationInterface { @Override public void onFailedCreateVideoFileError() { + if( MyDebug.LOG ) + Log.d(TAG, "onFailedCreateVideoFileError"); main_activity.getPreview().showToast(null, R.string.failed_to_save_video); - ImageButton view = 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 -- GitLab From 13b79abd4d52146df6c619776133891b8dbc2305 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Sep 2020 01:22:21 +0100 Subject: [PATCH 106/430] Delete video file that might have been created if fail to start video; extra tests for this. --- _docs/history.html | 1 + .../opencamera/test/MainActivityTest.java | 44 +++++++++++++++++++ .../opencamera/test/VideoTests.java | 3 ++ .../opencamera/preview/Preview.java | 32 +++++++++++--- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 24890db03..23ed20d99 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -57,6 +57,7 @@ FIXED Photos would sometimes fail to save on some devices with Storage Access options were enabled (options like DRO, HDR, auto-level, photostamp that require post-processing; custom Exif tags like artist or copyright; or when using geotagging with Camera2 API). +FIXED Corrupt videos could be left over if video failed to start. FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices with LIMITED Camera2 API support. diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index a902e2b2b..77744e587 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -6207,6 +6207,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sat, 19 Sep 2020 13:41:34 +0100 Subject: [PATCH 107/430] Fix typo. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 23ed20d99..cfddf010b 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -61,7 +61,7 @@ FIXED Corrupt videos could be left over if video failed to start. FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices with LIMITED Camera2 API support. -UPDATE Camera now closed when in settings or preview otherwise in background. +UPDATED Camera now closed when in settings or preview otherwise in background. Version 1.48.2 (2020/07/12) -- GitLab From 5f6bb1a5bbf59d25ec663fd84c757b29f7547f1e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Sep 2020 22:37:36 +0100 Subject: [PATCH 108/430] Clarify only JPEG format supports Exif metadata. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86065472d..9bd25bbf5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -892,7 +892,7 @@ Seekbar to change exposure compensation -Select the file format used for saving photos. This affects \"standard\" (not RAW) photos. Note that PNG format is not truly lossless, instead it is converted from a JPEG at 100%% quality.\n%s +Select the file format used for saving photos. This affects \"standard\" (not RAW) photos. Only JPEG supports saving Exif metadata. Note that PNG format is not truly lossless, instead it is converted from a JPEG at 100%% quality.\n%s Camera API Select Camera2 API to enable extra features such as manual modes for exposure, focus, white balance, along with RAW (if supported by the device). Changing the API will cause a restart.\n%s -- GitLab From 9d904c3c1c36f6460b0c0eac17235a3e6baeae73 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 19 Sep 2020 22:51:44 +0100 Subject: [PATCH 109/430] Show toast for speech recognition voice control more often. --- .../sourceforge/opencamera/MainActivity.java | 4 +--- .../sourceforge/opencamera/SpeechControl.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 10ba00f7d..cc6ba8838 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -1786,9 +1786,7 @@ public class MainActivity extends Activity { } } if( has_audio_permission ) { - String toast_string = this.getResources().getString(R.string.speech_recognizer_started) + "\n" + - this.getResources().getString(R.string.speech_recognizer_extra_info); - preview.showToast(audio_control_toast, toast_string); + speechControl.showToast(true); speechControl.startSpeechRecognizerIntent(); speechControl.speechRecognizerStarted(); } diff --git a/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java b/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java index 63800e7ee..7f02942f1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java +++ b/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java @@ -24,6 +24,9 @@ class SpeechControl { private SpeechRecognizer speechRecognizer; private boolean speechRecognizerIsStarted; + private boolean shown_toast; + private long last_toast_time_ms; + SpeechControl(final MainActivity main_activity) { this.main_activity = main_activity; } @@ -32,12 +35,25 @@ class SpeechControl { if( MyDebug.LOG ) Log.d(TAG, "startSpeechRecognizerIntent"); if( speechRecognizer != null ) { + showToast(false); 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); } } + void showToast(boolean force) { + if( MyDebug.LOG ) + Log.d(TAG, "speechRecognizerStarted"); + if( force || !shown_toast || System.currentTimeMillis() > last_toast_time_ms + 10000 ) { + shown_toast = true; + last_toast_time_ms = System.currentTimeMillis(); + String toast_string = main_activity.getResources().getString(R.string.speech_recognizer_started) + "\n" + + main_activity.getResources().getString(R.string.speech_recognizer_extra_info); + main_activity.getPreview().showToast(main_activity.getAudioControlToast(), toast_string); + } + } + void speechRecognizerStarted() { if( MyDebug.LOG ) Log.d(TAG, "speechRecognizerStarted"); @@ -50,6 +66,7 @@ class SpeechControl { Log.d(TAG, "speechRecognizerStopped"); main_activity.getMainUI().audioControlStopped(); speechRecognizerIsStarted = false; + shown_toast = false; } void initSpeechRecognizer() { -- GitLab From e34d97276ed77f9dce2116872843790e7a55b3fa Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Sep 2020 23:31:11 +0100 Subject: [PATCH 110/430] Android Studio seems to have changed what it complains about. --- .../opencamera/test/MainActivityTest.java | 3 --- .../sourceforge/opencamera/HDRProcessor.java | 1 - .../net/sourceforge/opencamera/ImageSaver.java | 1 - .../sourceforge/opencamera/MainActivity.java | 9 --------- .../opencamera/MyApplicationInterface.java | 4 ---- .../opencamera/PanoramaProcessor.java | 7 ++----- .../sourceforge/opencamera/StorageUtils.java | 17 +++-------------- .../sourceforge/opencamera/preview/Preview.java | 2 +- .../sourceforge/opencamera/ui/DrawPreview.java | 2 +- .../opencamera/ui/FolderChooserDialog.java | 4 ---- .../net/sourceforge/opencamera/ui/MainUI.java | 1 - 11 files changed, 7 insertions(+), 44 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 77744e587..98e9183c7 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -9998,7 +9998,6 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs, String output_name, String gyro_debug_info_filename, float panorama_pics_per_screen, float camera_angle_x, float camera_angle_y, float gyro_tol_degrees) throws IOException, InterruptedException { Log.d(TAG, "subTestPanorama"); diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index e6b23cbd6..d097b0644 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -3031,7 +3031,6 @@ public class HDRProcessor { * @param allocation_in The input allocation. * @param width The width of the allocation. */ - @SuppressWarnings("unused") @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private float computeSharpness(Allocation allocation_in, int width, long time_s) { if( MyDebug.LOG ) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index ea280f92f..b3c533c2b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -1172,7 +1172,6 @@ public class ImageSaver extends Thread { @SuppressWarnings("WeakerAccess") public static class GyroDebugInfo { - @SuppressWarnings("unused") public static class GyroImageDebugInfo { public float [] vectorRight; // X axis public float [] vectorUp; // Y axis diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index cc6ba8838..b107d35df 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -4425,26 +4425,20 @@ public class MainActivity extends Activity { Log.d(TAG, "set up zoom"); if( MyDebug.LOG ) Log.d(TAG, "has_zoom? " + preview.supportsZoom()); - @SuppressWarnings("deprecation") // "This functionality and UI is better handled with custom views and layouts" doesn't really work here - anyhow, we no longer use ZoomControl by default ZoomControls zoomControls = findViewById(R.id.zoom); SeekBar zoomSeekBar = findViewById(R.id.zoom_seekbar); if( preview.supportsZoom() ) { if( sharedPreferences.getBoolean(PreferenceKeys.ShowZoomControlsPreferenceKey, false) ) { - //noinspection deprecation zoomControls.setIsZoomInEnabled(true); - //noinspection deprecation zoomControls.setIsZoomOutEnabled(true); - //noinspection deprecation zoomControls.setZoomSpeed(20); - //noinspection deprecation zoomControls.setOnZoomInClickListener(new View.OnClickListener(){ public void onClick(View v){ zoomIn(); } }); - //noinspection deprecation zoomControls.setOnZoomOutClickListener(new View.OnClickListener(){ public void onClick(View v){ zoomOut(); @@ -4614,15 +4608,12 @@ public class MainActivity extends Activity { } }); - @SuppressWarnings("deprecation") // "This functionality and UI is better handled with custom views and layouts" doesn't really work here - anyhow, we no longer use ZoomControl by default ZoomControls seek_bar_zoom = findViewById(R.id.exposure_seekbar_zoom); - //noinspection deprecation seek_bar_zoom.setOnZoomInClickListener(new View.OnClickListener(){ public void onClick(View v){ changeExposure(1); } }); - //noinspection deprecation seek_bar_zoom.setOnZoomOutClickListener(new View.OnClickListener(){ public void onClick(View v){ changeExposure(-1); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 498515711..596498974 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -1005,10 +1005,6 @@ public class MyApplicationInterface extends BasicApplicationInterface { } 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). - // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; - // when using scoped storage, we should no longer hit this codepath, but we are still using this for older Android - // versions. - @SuppressWarnings("deprecation") File storage = Environment.getExternalStorageDirectory(); if( MyDebug.LOG ) Log.d(TAG, "compare to: " + storage.getAbsolutePath()); diff --git a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java index 2cc5cede9..ed2f74045 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java @@ -218,7 +218,7 @@ public class PanoramaProcessor { * RGBA_8888. */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private List createLaplacianPyramid(ScriptC_pyramid_blending script, Bitmap bitmap, int n_levels, @SuppressWarnings("unused") String name) { + private List createLaplacianPyramid(ScriptC_pyramid_blending script, Bitmap bitmap, int n_levels, String name) { if( MyDebug.LOG ) Log.d(TAG, "createLaplacianPyramid"); long time_s = 0; @@ -497,7 +497,6 @@ public class PanoramaProcessor { } } - @SuppressWarnings("unused") @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void saveAllocation(String name, Allocation allocation) { Bitmap bitmap; @@ -780,7 +779,7 @@ public class PanoramaProcessor { } } - private static void computeDistancesBetweenMatches(List matches, int st_indx, int nd_indx, int feature_descriptor_radius, @SuppressWarnings("unused") List bitmaps, int [] pixels0, int [] pixels1) { + private static void computeDistancesBetweenMatches(List matches, int st_indx, int nd_indx, int feature_descriptor_radius, List bitmaps, int [] pixels0, int [] pixels1) { final int wid = 2*feature_descriptor_radius+1; final int wid2 = wid*wid; for(int indx=st_indx;indx bitmaps, long time_s) { List histogramInfos = new ArrayList<>(); diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 075a9f447..305b9a8e9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -104,7 +104,7 @@ public class StorageUtils { if( MyDebug.LOG ) // this code only used for debugging/logging { - @SuppressWarnings("deprecation") // this is only debug code + @SuppressLint("InlinedApi") // complains this constant only available on API 29 (even though it was available on older versions, but looks like it was moved?) 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 ) { @@ -116,10 +116,10 @@ public class StorageUtils { Log.e(TAG, "Couldn't resolve given uri [2]: " + uri); } else { - @SuppressWarnings("deprecation") // this is only debug code 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)); + @SuppressLint("InlinedApi") // complains this constant only available on API 29 (even though it was available on older versions, but looks like it was moved?) 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); @@ -385,11 +385,6 @@ public class StorageUtils { } public static File getBaseFolder() { - // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; - // when using scoped storage, we should no longer be trying to read/write this codepath with File API, but we are still using - // this for older Android versions. - // Furthermore, sometimes we still use this with scoped storage for valid purposes, e.g., to pass to StatFs in freeMemory(). - @SuppressWarnings("deprecation") final File base_folder = Environment.getExternalStoragePublicDirectory(RELATIVE_FOLDER_BASE); return base_folder; } @@ -470,10 +465,6 @@ public class StorageUtils { File [] storagePoints = new File("/storage").listFiles(); if( "primary".equalsIgnoreCase(type) ) { - // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; - // when using scoped storage, as noted above we should no longer be trying to read/write this path with File API; - // and otherwise we are still using this for older Android versions. - @SuppressWarnings("deprecation") final File externalStorage = Environment.getExternalStorageDirectory(); file = new File(externalStorage, path); } @@ -562,9 +553,6 @@ public class StorageUtils { } private String getDataColumn(Uri uri, String selection, String [] selectionArgs) { - // DATA is redacted with scoped storage, but that's fine - the callers of this method are documented that they may return - // no filename, and these callers have been reviewed for their use on Android 10+ with scoped storage - @SuppressWarnings("deprecation") final String column = MediaStore.Images.ImageColumns.DATA; final String[] projection = { column @@ -914,6 +902,7 @@ public class StorageUtils { MEDIASTORE_VIDEOS } + @SuppressLint("InlinedApi") // complains MediaColumns constants only available on API 29 (even though it was available on older versions, but looks like it was moved?); for some reason doesn't allow putting this at the actual comments?! private Media getLatestMediaCore(Uri baseUri, String bucket_id, UriType uri_type) { if( MyDebug.LOG ) { Log.d(TAG, "getLatestMediaCore"); diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 9de72a598..635f54380 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -5027,7 +5027,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, "takePicturePressed exit"); } - private void takePictureOnTimer(final long timer_delay, @SuppressWarnings("unused") boolean repeated) { + private void takePictureOnTimer(final long timer_delay, boolean repeated) { if( MyDebug.LOG ) { Log.d(TAG, "takePictureOnTimer"); Log.d(TAG, "timer_delay: " + timer_delay); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java index 0138c98c2..c6e33f10f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java @@ -2753,7 +2753,7 @@ public class DrawPreview { } } - private void drawGyroSpot(Canvas canvas, float distance_x, float distance_y, @SuppressWarnings("unused") float dir_x, @SuppressWarnings("unused") float dir_y, int radius_dp, boolean outline) { + private void drawGyroSpot(Canvas canvas, float distance_x, float distance_y, float dir_x, float dir_y, int radius_dp, boolean outline) { if( outline ) { p.setStyle(Paint.Style.STROKE); p.setStrokeWidth(stroke_width); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java index fae2072b6..0a70aef45 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/FolderChooserDialog.java @@ -257,10 +257,6 @@ public class FolderChooserDialog extends DialogFragment { } } if( show_dcim_shortcut ) { - // The reason given for deprecation is wrong - the path will only be inaccessible when also running on Android 10; - // when using scoped storage, we should no longer be using this folder dialog with DCIM shortcut, but we are still - // using this for older Android versions. - @SuppressWarnings("deprecation") 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)); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java index 135b64c29..a7901e5c9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java @@ -1934,7 +1934,6 @@ public class MainUI { if( main_activity.getPreview().supportsExposures() ) { exposure_seek_bar.setVisibility(View.VISIBLE); - @SuppressWarnings("deprecation") // "This functionality and UI is better handled with custom views and layouts" doesn't really work here - anyhow, we no longer use ZoomControl by default ZoomControls seek_bar_zoom = main_activity.findViewById(R.id.exposure_seekbar_zoom); seek_bar_zoom.setVisibility(View.VISIBLE); } -- GitLab From 7c6c766f62ef8e490410e06f9d4809b095fb5933 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Sep 2020 23:32:49 +0100 Subject: [PATCH 111/430] Fix Android warnings. --- .../main/java/net/sourceforge/opencamera/ImageSaver.java | 2 -- .../main/java/net/sourceforge/opencamera/MainActivity.java | 7 ++++--- .../main/java/net/sourceforge/opencamera/StorageUtils.java | 2 +- .../opencamera/cameracontroller/CameraController.java | 3 +++ .../net/sourceforge/opencamera/preview/VideoProfile.java | 3 +++ .../java/net/sourceforge/opencamera/ui/DrawPreview.java | 1 - .../net/sourceforge/opencamera/ui/FolderChooserDialog.java | 1 + 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index b3c533c2b..446aa89fb 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -6,7 +6,6 @@ import net.sourceforge.opencamera.cameracontroller.RawImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileDescriptor; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -30,7 +29,6 @@ import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.content.ContentValues; -import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index b107d35df..2cd42d583 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -1058,6 +1058,7 @@ public class MainActivity extends Activity { return (res ? 1249 : 1259) ^ (alt == null ? 0 : alt.hashCode()); } + @NonNull @Override public String toString() { return "CheckSaveLocationResult{" + res + " , " + alt + "}"; @@ -1502,7 +1503,7 @@ public class MainActivity extends Activity { } @Override - public void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(@NonNull Configuration newConfig) { if( MyDebug.LOG ) Log.d(TAG, "onConfigurationChanged()"); // configuration change can include screen orientation (landscape/portrait) when not locked (when settings is open) @@ -3941,7 +3942,7 @@ public class MainActivity extends Activity { editText.setText(sharedPreferences.getString(PreferenceKeys.SaveLocationPreferenceKey, "OpenCamera")); InputFilter filter = new InputFilter() { // whilst Android seems to allow any characters on internal memory, SD cards are typically formatted with FAT32 - String disallowed = "|\\?*<\":>"; + final String disallowed = "|\\?*<\":>"; public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { for(int i=start;i Date: Sat, 19 Sep 2020 23:37:32 +0100 Subject: [PATCH 112/430] Update ExifInterface version. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4dafc09f6..bb37d78ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,6 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.2.0' + implementation 'androidx.exifinterface:exifinterface:1.3.0' testImplementation 'junit:junit:4.12' } -- GitLab From 74bf944f24c9b090c9bd3a4eb8d344a93d2024b4 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 20 Sep 2020 17:35:36 +0100 Subject: [PATCH 113/430] Clarify use addresses option also applies to video subtitles. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd25bbf5..c8d8b9aa1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -905,7 +905,7 @@ be sent to remote servers to perform speech recognition. \n%s If possible, use an address when stamping GPS locations. + If possible, obtain an address from GPS locations (for photo stamp or video subtitles). This option requires an Internet connection. Note that if enabled, this requires that your device transmits location data across the Internet to a third party in order to convert GPS coordinates to an address. See https://developer.android.com/reference/android/location/Geocoder . -- GitLab From 73ebbd9f040ada893d3236c32a1dc28d6a8ed9ac Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 20 Sep 2020 17:36:47 +0100 Subject: [PATCH 114/430] Clarification for use addresses option (already in privacy policy, but might as well repeat here). --- _docs/help.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index a1aebe85c..0d33ff7df 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -918,8 +918,12 @@ formatting. Also used for Video settings/"Video subtitles". Use addresses - If "Stamp photos" and "Store location data" is enabled, this allows the location to be displayed on the resultant photo in the form of an address, either as well as or instead of GPS - coordinates. Note that an Internet connection is required for this to succeed. Also used for - Video settings/"Video subtitles".
+ coordinates. Similarly also used for Video settings/"Video subtitles". + An Internet connection is required for this to succeed. + Note that if enabled, this requires that your device transmits location data across the Internet to a third party + in order to convert GPS coordinates to an address (this uses the + Geocoder API). +Distance unit - If "Stamp photos" is enabled, this controls whether to use metres (m) or feet (ft) when recording the GPS altitude. Also used for Video settings/"Video subtitles".
-- GitLab From a723ea1f3c56674f3d712ed25e31f17758bcf9d0 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sun, 20 Sep 2020 17:37:58 +0100 Subject: [PATCH 115/430] Don't try geocoder calls if app already paused. --- .../sourceforge/opencamera/ImageSaver.java | 33 ++++++++++++++++++- .../sourceforge/opencamera/MainActivity.java | 2 ++ .../opencamera/MyApplicationInterface.java | 8 ++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 446aa89fb..791cb9e14 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -81,6 +81,11 @@ public class ImageSaver extends Thread { private final static int queue_cost_dng_c = 6; //private final static int queue_cost_dng_c = 1; + // Should be same as MainActivity.app_is_paused, but we keep our own copy to make threading easier (otherwise, all + // accesses of MainActivity.app_is_paused would need to be synchronized). + // Access to app_is_paused should always be synchronized to this (i.e., the ImageSaver class). + private boolean app_is_paused = true; + // for testing; must be volatile for test project reading the state // n.b., avoid using static, as static variables are shared between different instances of an application, // and won't be reset in subsequent tests in a suite! @@ -436,6 +441,22 @@ public class ImageSaver extends Thread { return n_real_images_to_save; } + /** Application has paused. + */ + void onPause() { + synchronized(this) { + app_is_paused = true; + } + } + + /** Application has resumed. + */ + void onResume() { + synchronized(this) { + app_is_paused = false; + } + } + void onDestroy() { if( MyDebug.LOG ) Log.d(TAG, "onDestroy"); @@ -2132,9 +2153,19 @@ public class ImageSaver extends Thread { Address address = null; if( request.store_location && !request.preference_stamp_geo_address.equals("preference_stamp_geo_address_no") ) { + boolean block_geocoder; + synchronized(this) { + block_geocoder = app_is_paused; + } // try to find an address // n.b., if we update the class being used, consider whether the info on Geocoder in preference_stamp_geo_address_summary needs updating - if( Geocoder.isPresent() ) { + if( block_geocoder ) { + // seems safer to not try to initiate potential network connections (via geocoder) if Open Camera + // has paused and we're still saving images + if( MyDebug.LOG ) + Log.d(TAG, "don't call geocoder for photostamp as app is paused"); + } + else if( Geocoder.isPresent() ) { if( MyDebug.LOG ) Log.d(TAG, "geocoder is present"); Geocoder geocoder = new Geocoder(main_activity, Locale.getDefault()); diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 2cd42d583..e5c8005b6 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -1395,6 +1395,7 @@ public class MainActivity extends Activity { speechControl.initSpeechRecognizer(); initLocation(); initGyroSensors(); + applicationInterface.getImageSaver().onResume(); soundPoolManager.initSound(); soundPoolManager.loadSound(R.raw.mybeep); soundPoolManager.loadSound(R.raw.mybeep_hi); @@ -1485,6 +1486,7 @@ public class MainActivity extends Activity { applicationInterface.getLocationSupplier().freeLocationListeners(); applicationInterface.stopPanorama(true); // in practice not needed as we should stop panorama when camera is closed, but good to do it explicitly here, before disabling the gyro sensors applicationInterface.getGyroSensor().disableSensors(); + applicationInterface.getImageSaver().onPause(); soundPoolManager.releaseSound(); applicationInterface.clearLastImages(); // this should happen when pausing the preview, but call explicitly just to be safe applicationInterface.getDrawPreview().clearGhostImage(); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 596498974..8463370d5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2026,7 +2026,13 @@ public class MyApplicationInterface extends BasicApplicationInterface { Address address = null; if( store_location && !preference_stamp_geo_address.equals("preference_stamp_geo_address_no") ) { // try to find an address - if( Geocoder.isPresent() ) { + if( main_activity.isAppPaused() ) { + // seems safer to not try to initiate potential network connections (via geocoder) if Open Camera + // is paused - this shouldn't happen, since we stop video when paused, but just to be safe + if( MyDebug.LOG ) + Log.d(TAG, "don't call geocoder for video subtitles as app is paused?!"); + } + else if( Geocoder.isPresent() ) { if( MyDebug.LOG ) Log.d(TAG, "geocoder is present"); Geocoder geocoder = new Geocoder(main_activity, Locale.getDefault()); -- GitLab From e7120885133db81d6258d5b1289d508870342940 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 25 Sep 2020 13:10:06 +0100 Subject: [PATCH 116/430] Fix sluggish UI with denying camera/storage permissions with "Don't ask again". --- _docs/history.html | 1 + .../opencamera/PermissionHandler.java | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/_docs/history.html b/_docs/history.html index cfddf010b..04fdba668 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -61,6 +61,7 @@ FIXED Corrupt videos could be left over if video failed to start. FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices with LIMITED Camera2 API support. +FIXED UI would become sluggish if camera or storage permission denied with "Don't ask again". UPDATED Camera now closed when in settings or preview otherwise in background. Version 1.48.2 (2020/07/12) diff --git a/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java b/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java index 80f8ef5bd..25cc40bc6 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java +++ b/app/src/main/java/net/sourceforge/opencamera/PermissionHandler.java @@ -24,6 +24,26 @@ public class PermissionHandler { final private static int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 2; final private static int MY_PERMISSIONS_REQUEST_LOCATION = 3; + private boolean camera_denied; // whether the user requested to deny a camera permission + private long camera_denied_time_ms; // if denied, the time when this occurred + private boolean storage_denied; // whether the user requested to deny a camera permission + private long storage_denied_time_ms; // if denied, the time when this occurred + private boolean audio_denied; // whether the user requested to deny a camera permission + private long audio_denied_time_ms; // if denied, the time when this occurred + private boolean location_denied; // whether the user requested to deny a camera permission + private long location_denied_time_ms; // if denied, the time when this occurred + // In some cases there can be a problem if the user denies a permission, we then get an onResume() + // (since application goes into background when showing system UI to request permission) at which + // point we try to request permission again! This would happen for camera and storage permissions. + // Whilst that isn't necessarily wrong, there would also be a problem if the user says + // "Don't ask again", we get stuck in a loop repeatedly asking the OS for permission (and it + // repeatedly being automatically denied) causing the UI to become sluggish. + // So instead we only try asking again if not within deny_delay_ms of the user denying that + // permission. + // Time shouldn't be too long, as the user might restart and then not be asked again for camera + // or storage permission. + final private static long deny_delay_ms = 1000; + PermissionHandler(MainActivity main_activity) { this.main_activity = main_activity; } @@ -101,6 +121,11 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); return; } + else if( camera_denied && System.currentTimeMillis() < camera_denied_time_ms + deny_delay_ms ) { + if( MyDebug.LOG ) + Log.d(TAG, "too soon since user last denied permission"); + return; + } if( ActivityCompat.shouldShowRequestPermissionRationale(main_activity, Manifest.permission.CAMERA) ) { // Show an explanation to the user *asynchronously* -- don't block @@ -129,6 +154,11 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for scoped storage!"); return; } + else if( storage_denied && System.currentTimeMillis() < storage_denied_time_ms + deny_delay_ms ) { + if( MyDebug.LOG ) + Log.d(TAG, "too soon since user last denied permission"); + return; + } if( ActivityCompat.shouldShowRequestPermissionRationale(main_activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) ) { // Show an explanation to the user *asynchronously* -- don't block @@ -152,6 +182,11 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); return; } + else if( audio_denied && System.currentTimeMillis() < audio_denied_time_ms + deny_delay_ms ) { + if( MyDebug.LOG ) + Log.d(TAG, "too soon since user last denied permission"); + return; + } if( ActivityCompat.shouldShowRequestPermissionRationale(main_activity, Manifest.permission.RECORD_AUDIO) ) { // Show an explanation to the user *asynchronously* -- don't block @@ -175,6 +210,11 @@ public class PermissionHandler { Log.e(TAG, "shouldn't be requesting permissions for pre-Android M!"); return; } + else if( location_denied && System.currentTimeMillis() < location_denied_time_ms + deny_delay_ms ) { + if( MyDebug.LOG ) + Log.d(TAG, "too soon since user last denied permission"); + return; + } if( ActivityCompat.shouldShowRequestPermissionRationale(main_activity, Manifest.permission.ACCESS_FINE_LOCATION) || ActivityCompat.shouldShowRequestPermissionRationale(main_activity, Manifest.permission.ACCESS_COARSE_LOCATION) ) { @@ -215,6 +255,8 @@ public class PermissionHandler { else { if( MyDebug.LOG ) Log.d(TAG, "camera permission denied"); + camera_denied = true; + camera_denied_time_ms = System.currentTimeMillis(); // permission denied, boo! Disable the // functionality that depends on this permission. // Open Camera doesn't need to do anything: the camera will remain closed @@ -235,6 +277,8 @@ public class PermissionHandler { else { if( MyDebug.LOG ) Log.d(TAG, "storage permission denied"); + storage_denied = true; + storage_denied_time_ms = System.currentTimeMillis(); // permission denied, boo! Disable the // functionality that depends on this permission. // Open Camera doesn't need to do anything: the camera will remain closed @@ -255,6 +299,8 @@ public class PermissionHandler { else { if( MyDebug.LOG ) Log.d(TAG, "record audio permission denied"); + audio_denied = true; + audio_denied_time_ms = System.currentTimeMillis(); // permission denied, boo! Disable the // functionality that depends on this permission. // no need to do anything @@ -276,6 +322,8 @@ public class PermissionHandler { else { if( MyDebug.LOG ) Log.d(TAG, "location permission denied"); + location_denied = true; + location_denied_time_ms = System.currentTimeMillis(); // permission denied, boo! Disable the // functionality that depends on this permission. // for location, seems best to turn the option back off -- GitLab From b0f44420ad48bf037c888650f6d938c1b0ee780f Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 25 Sep 2020 13:15:44 +0100 Subject: [PATCH 117/430] Improve wording for storage permission rationale. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8d8b9aa1..a93fdb811 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,7 +304,7 @@ PERMISSIONS NOT AVAILABLE Permission required Camera permission is required to enable the camera -Storage read/write permission is required to save photos +Storage read/write permission is required to save resultant files such as photos or videos Microphone permission is required to record video with audio, as well as use the \"Audio control\" options Location permission is required for geotagging (storing location data in photos and videos). Location permission is also required for connecting to Bluetooth LE remote control devices. -- GitLab From a1cfd6ddae2916c7ee646585232c91281ac1d877 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Fri, 25 Sep 2020 13:43:57 +0100 Subject: [PATCH 118/430] Refactor common code to use new getHumanReadableSaveFolder(); for scoped storage non-SAF display DCIM instead of blank line. --- .../sourceforge/opencamera/MainActivity.java | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index e5c8005b6..6a496da44 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -3859,7 +3859,7 @@ public class MainActivity extends Activity { } } - /** Update the save folder. + /** Update the save folder (for non-SAF methods). */ void updateSaveFolder(String new_save_location) { if( MyDebug.LOG ) @@ -3876,7 +3876,8 @@ public class MainActivity extends Activity { 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()); + String save_folder_name = getHumanReadableSaveFolder(this.applicationInterface.getStorageUtils().getSaveLocation()); + this.preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name); } } } @@ -4013,6 +4014,26 @@ public class MainActivity extends Activity { } } + /** Returns a human readable string for the save_folder (as stored in the preferences). + */ + private String getHumanReadableSaveFolder(String save_folder) { + if( applicationInterface.getStorageUtils().isUsingSAF() ) { + // try to get human readable form if possible + String file_name = applicationInterface.getStorageUtils().getFilePathFromDocumentUriSAF(Uri.parse(save_folder), true); + if( file_name != null ) { + save_folder = file_name; + } + } + else { + // The strings can either be a sub-folder of DCIM, or (pre-scoped-storage) a full path, so normally either can be displayed. + // But with scoped storage, an empty string is used to mean DCIM, so seems clearer to say that instead of displaying a blank line! + if( MainActivity.useScopedStorage() && save_folder.length() == 0 ) { + save_folder = "DCIM"; + } + } + return save_folder; + } + /** 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. */ @@ -4045,13 +4066,7 @@ public class MainActivity extends Activity { // history is stored in order most-recent-last for(int i=0;i Date: Sat, 26 Sep 2020 15:30:20 +0100 Subject: [PATCH 119/430] Update Chinese Simplified translation. --- _docs/credits.html | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 302 ++++++++++++++------- 2 files changed, 202 insertions(+), 102 deletions(-) diff --git a/_docs/credits.html b/_docs/credits.html index 82f8745e8..813c0d1f6 100644 --- a/_docs/credits.html +++ b/_docs/credits.html @@ -60,7 +60,7 @@ - Changing icons for pause/resume video by Johan Ejdemark ( johanejdemark AT hotmail DOT com).
- Azerbaijani translation by Eldost ( l-dost AT mail DOT ru ).
- Brazilian tranlation by Kaio Duarte.
-- Chinese Simplified translation by Michael Lu ( yeskky AT gmail DOT com ) and tumuyan ( tumuyan AT gmail DOT com ).
+- Chinese Simplified translation by Michael Lu ( yeskky AT gmail DOT com ), tumuyan ( tumuyan AT gmail DOT com ) and Tommy He.
- Chinese Traditional translation by You-Cheng Hsieh ( yochenhsieh AT gmail DOT com ) and Hsiu-Ming Chang.
- Belarusian translation by Zmicer Turok.
- Czech translation by Jaroslav Svoboda ( multi DOT flexi AT gmail DOT com , http://jaroslavsvoboda.eu ).
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4bfdf8ed8..df7b1689e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -18,7 +18,7 @@重复做 连接到相机失败 错误,视频文件可能已经损坏 -在你的设备上不被支持 +不支持您的设备 未知错误,视频挂起 服务器死亡,视频挂起 视频达到最大时长 @@ -72,7 +72,7 @@m 相机特效 -自动稳定 +自动水平 图像将被旋转,使它们自动水平(仅照片) (拍照更慢,并且在内存过低的设备上失败) 应用一个颜色效果 应用选定的颜色效果到照片 @@ -190,7 +190,7 @@文本样式 水印文本的样式\n%s 视频分辨率 -强制 4K UHD 视频 (实验) +强制 4K UHD 视频 (仅在部分设备可用) 为后置相机录像启用 3840x2160 分辨率 - 仅当你的设备支持时才能工作,并且如果不支持可能崩溃! 启用视频稳定 视频稳定减少在预览和录像中由于相机移动引起的晃动。 @@ -239,8 +239,8 @@闪光模式 删除上一张照片 共享照片 - -../ (上层目录) + +父文件夹 新建文件夹 不能写到这个文件夹 不能访问这个文件夹 @@ -293,7 +293,7 @@默认 默认 -yyyy-mm-dd +yyyy-mm-dd (ISO 8601) dd/mm/yyyy mm/dd/yyyy 无 @@ -302,12 +302,12 @@自动闪光灯 开启闪光灯 闪光灯常亮 -红眼 +防红眼 摄像机同侧 外部麦克风(如果存在) 默认麦克风 -优化语音 +针对语音通话优化 默认 100kbps @@ -375,19 +375,19 @@重置水平校正 高品质 快速 -小尺寸 +低限度 默认 关闭 处理设置… 降噪算法 -处理… +处理中… 仅JPEG JPEG和DNG(RAW) 仅DNG(RAW) 照片数量 照片数量 -速度 -普通 +速度优先 +正常 关闭慢动作 启用慢动作 @@ -398,7 +398,7 @@低 中 高 -位置 +未知 距离单位 英制 公制 @@ -407,7 +407,7 @@相机驱动程序提供的降噪算法。降噪算法会试图去除相机捕获的噪点(特别是黑暗环境),改善照片质量。 注意这与降噪照片模式无关,实际上在NR照片模式下会忽略此设置。\n%s -锐化算法(Edge mode) +锐化算法 相机驱动程序提供的锐化算法。可以提高捕获图像的清晰度和细节。(在NR拍照模式下忽略此设置)\n%s 是否在应用程序更新后提示新功能和改进的信息 自动水平会自动旋转照片,使他们显示水平.\n\n注意这意味着图像分辨率会略低(因为需要旋转和裁切)。 @@ -417,7 +417,7 @@在取景框内显示一张半透明的图片,辅助你对正镜头。\n%s 关闭 上一张照片 -选一张图片 +选定的图片 校正 显示等分线 显示水平等分线 @@ -425,7 +425,7 @@显示罗盘方向线 暂停录像 继续录像 -拍照按钮 +显示“拍照按钮” 显示拍照、录像按钮。除非你有其他方法(比如硬件快门、使用音量键),否则不应该取消勾选 @@ -453,9 +453,9 @@此选项可以校准设备的加速度计,从而使自动水平和屏幕显示的角度/水平度正常工作 -请将您的设备放在平坦的水平面上,然后按下“校准”按钮\n\n按下“重置”按钮可以从设备中删除校准数据。 -快速HDR/expo曝光 -允许更快地完成HDR/expo拍照。如果您的设备在使用HDR/expo模式拍照时出现了问题,请禁用此功能。 +请将您的设备放在平坦的水平面上(横竖屏均可),然后按下“校准”按钮\n\n按下“重置”按钮可以从设备中删除校准数据。\n\n按下“返回”按钮可取消。 +快速HDR/包围曝光 +允许更快地完成HDR/包围拍照。如果您的设备在使用HDR/包围模式拍照时出现了问题,请禁用此功能。 在设备上没有找到文件选择器,所以不能使用辅助图像功能。 无法打开文件 @@ -465,19 +465,18 @@视频字幕 -创建一个纯粹日期和时间的字幕文件(.SET格式)\n如果启用了定位/方向数据,同时也会记录GPS信息.\n%s +创建一个存储日期和时间的字幕文件(.SET格式);如果启用了定位/方向数据,同时也会记录GPS信息.\n%s -针对语音识别进行优化 +针对语音识别优化 相机发生了严重错误 -记录相机参数 -保存录像的参数到flat(log)\n%s -Off -Low -Medium -Strong -Extra strong -Log profile +视频图片配置文件 +为视频模式设定标准或者平铺的图片配置文件\n%s +Log (低) +Log (中等) +Log (强) +Log (超强) +Log 配置文件 在屏幕左侧 在屏幕右侧 在屏幕顶端 @@ -488,10 +487,10 @@高速 -艺术家(Artist") -存储在艺术家(Artist)标签内的文本(仅限JPEG格式)。默认留空。 -版权(Copyright) -存储在版权(Copyright)标签内的文本(仅限JPEG格式)。默认留空。 +艺术家 +存储在艺术家标签内的文本(仅限JPEG格式)。留空代表不保存。 +版权 +存储在版权标签内的文本(仅限JPEG格式)。留空代表不保存。 暂停录像 @@ -502,49 +501,49 @@麦克风初始化失败 HDR照片创建失败 -自动·Auto -多云·Cloudy -日光·Daylight -荧光灯·Fluorescent -白炽灯·Incandescent -阴翳·Shade -星光·Twilight -温暖·Warm -手动·Manual - -动作·Action -条码·Barcode -沙滩·Beach -烛光·Candlelight -自动·Auto -篝火·Fireworks -横向·Landscape -夜晚·Night -夜间人像 -聚会·Party -人像·Portrait -雪景·Snow -运动·Sports +自动 +阴天 +日光 +荧光灯 +白炽灯 +阴影 +星光 +温暖 +手动 + +动作 +条码 +沙滩 +烛光 +自动 +烟火 +风景 +夜景 +夜景肖像 +聚会 +肖像 +雪景 +运动 稳定拍照 -黄昏·Sunset -剧院·Theatre - -湖蓝·Aqua -黑板·Blackboard -黑白·Mono -负片·Negative -无·None -反色·Posterize -棕·Sepia -过爆·Solarize -白板·Whiteboard +黄昏 +剧院 + +湖蓝 +黑板 +黑白 +负片 +无 +反色 +棕褐色 +过曝 +白板 新功能: 捐赠 -face -faces +人脸 +人脸 未处理 @@ -638,26 +637,26 @@自动开启屏幕补光 打开屏幕补光 -屏幕高亮 +屏幕常亮 -去除条纹 +去除摩尔纹 使用算法消除闪烁造成的摩尔纹\n%s -Auto +自动 50Hz 60Hz -Off +关闭 -配置管理 +配置管理器 -导出配置 -导出Open Camera所有选项的设定值一个文件中 +保存配置 +保存Open Camera所有选项的至文件中 保存的文件名 恢复配置 -导入配置文件到Open Camera。注意这会覆盖现在的所有设定值! -这个选项允许你选择一个先前导出的配置文件并导入到Open Camera中。需要注意这个文件载入后会覆盖当前所有选项的设定值! +恢复配置文件到Open Camera。注意这会覆盖现在的所有设定值! +这个选项允许你选择一个先前保存的配置文件并恢复到Open Camera中。需要注意这个文件载入后会覆盖当前所有选项的设定值! 视频格式 录像的编码和格式\n%s @@ -668,9 +667,9 @@3GPP WebM (不支持音频) -导出的配置 -导出配置失败 -导入配置失败 +保存的配置 +保存配置失败 +恢复配置失败 这个设备没有可用的文件选择器,不支持这个选项 照片格式 @@ -681,8 +680,8 @@HDR增强对比度 HDR如何使用增强对比度的算法。在动态范围非常大的场景中能够提高成像品质。当然这也可以让照片多出一种HDR的味道。\n%s 关闭 -自动 -打开 +智能开启 +一直开启 功能按钮在左侧 功能按钮在右侧 @@ -692,8 +691,8 @@是否在屏幕上显示人脸识别的开关 打开人脸识别 关闭人脸识别 -人脸识别已经打开 -人脸识别已经关闭 +人脸识别已启用 +人脸识别已禁用 显示自动水平的按钮 是否在屏幕上显示自动水平的开关。开启自动水平后,照片会根据水平面自动旋转裁切。 @@ -704,8 +703,8 @@是否在屏幕上显示照片水印的开关 使用水印 禁用水印 -水印已经启用 -水印已经禁用 +水印已启用 +水印已禁用 显示文本水印的按钮 @@ -715,8 +714,8 @@是否在屏幕上显示白平衡锁定的开关 锁定白平衡 解锁白平衡 -白平衡已经锁定 -白平衡已经解锁 +白平衡已锁定 +白平衡已解锁 显示曝光锁 是否在屏幕上显示曝光锁定的开关 @@ -737,7 +736,7 @@带背景色的文字 全景 -全景(Panorama) +全景 BLE蓝牙控制设备… 启用BLE蓝牙控制 @@ -749,9 +748,9 @@不支持蓝牙 未知设备 断开控制时熄屏 -提示:打开Open Camera前,请把默认亮度跳到最低 +提示:打开Open Camera前,请把默认亮度调到最低 使用盐水计算深度 -设置正确的水的类型,可以提高水下拍摄的准确性 +设置正确的水下兔笼的类型,可以提高水下拍摄时深度计算的准确性 控制器已连接 Kraken智能家居 @@ -770,13 +769,13 @@是否显示颜色直方图\n%s 关闭 RGB色彩 -亮度(luminance) +亮度 值(最大) -强度(平均)intensity -Lightness (Average of min-max) +强度(平均) +光亮度 (最小与最大之间的平均值) -Show zebra stripes -If this option is enabled, zebra stripes will be drawn on-screen showing where the camera preview is over exposed.\n%s +显示斑马条纹 +如果启用该选项,将会在相机预览屏幕中在过曝区域绘制斑马条纹。\n%s 关闭 70% 80% @@ -795,7 +794,7 @@焦点峰值色彩 高亮显示对焦的边缘时使用的颜色\n%s -Remaining +剩余 成像预览… @@ -807,8 +806,8 @@RAW模式也使用包围曝光模式。如果禁用,只有拍摄JPEG照片时使用包围曝光。 -允许RAW包围焦距 -RAW模式也是用包围焦距模式。如果禁用,只有拍摄JPEF照片时使用包围焦距。 +允许RAW包围对焦 +RAW模式也是用包围对焦模式。如果禁用,只有拍摄JPEF照片时使用包围对焦。 循环RAW的模式 显示RAW按钮 @@ -842,4 +841,105 @@只隐藏屏幕上的虚拟导航按钮 隐藏图形用户界面 隐藏一切 +150Mbps +200Mbps +2 +3 +4 +5 +6 +8 +10 +12 +15 +20 +25 +30 +40 +50 +100 +150 +200 +2 +3 +4 +5 +6 +8 +10 +12 +15 +20 +s +DRO +默认 +降噪模式 +普通 +低照度 +2 (2:1) +2GB +5GB +9GB +包围对焦已取消 +0.5秒 +上一个 +下一个 +ft +Log (精细) +伽马 +sRGB +REC709 +选择遥控设备 +仅暗淡屏幕上的导航按钮 +4K UHD +比特率 +帧率 +慢动作 +允许长按动作 +是否允许长按动作(例如长按图库来改变存储位置)。 +显示相机 ID +在屏幕上显示当前相机的 ID +光圈 +多相机图标 +改变曝光补偿的进度条 +选择保存照片的文件格式。这将影响“标准”(非 RAW)照片。注意 PNG 格式并非真正无损,它是从 JPEG 按 100% 质量转换而成的。%s +Camera API +选择 Camera2 API 来启用额外功能,比如手动曝光、对焦、白平衡及 RAW(如果设备支持)。改变 API 将重启应用。%s +原始 Camera API +Camera2 API +视频伽马值 +如果视频图标预置文件被设定为伽马时的伽马值 %s +1.6 +1.8 +1.9 +2.0 +2.1 +2.2 +2.3 +2.4 +2.6 +2.8 +辅助图片透明度 +用于辅助图片的透明度值。较低的值代表更透明,较高的值代表更不透明。%s +外部相机 +超广角 +切换至外部相机 +切换相机 +在多个相机间切换 +改变当前首选项值的进度条 +斑马条纹前景颜色 +显示斑马条纹时前景使用的颜色。%s +斑马条纹背景颜色 +显示斑马条纹时背景使用的颜色。%s +黑色 +红色 +橙色 +半透明 +白色 +如果启用,使用分开的按钮在前置/后置相机之间切换,且在多个前置/后置之间切换。如果禁用,切换相机按钮将在所有相机之间循环。 +如果可行的话,在标注 GPS 位置时使用一个地址。该选项需要互联网连接。注意一旦启用,这将要求您的设备传输位置数据至互联网上的第三方来实现将 GPS 坐标转换为地址的操作。参阅 https://developer.android.com/reference/android/location/Geocoder 。 \n%s +保存三轴运动信息 +在照片的 Exit 用户备注字段保存设备的三轴运动信息(仅适用于JPEG格式) +根据噪音或者语音指令拍照。当启用后,使用屏幕上的麦克风按钮来启动监听。注意语音指令选项使用 Android 语音识别服务:当使用该选项的时候,语音数据将极有可能被发送至远程服务来实现指令识别。\n%s +点击来对焦,然后按下蓝色相机按钮以拍照。要禁用最大屏幕亮度,查阅设置/屏幕GUI/\"强制最大亮度\"选项。要在 Android 5 之后的设备上保存至 SD 卡,查阅设置/更多相机控制/\"使用存储访问架构\"。如需更多帮助,在设置中点击“在线帮助”。 -- GitLab From 60cf2d4975e7cb69d4d8d2111eac3abf8f913ce0 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 26 Sep 2020 15:32:03 +0100 Subject: [PATCH 120/430] Put %s after newline to be consistent. --- app/src/main/res/values-zh-rCN/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index df7b1689e..f0bfb75c9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -902,13 +902,13 @@ 光圈 多相机图标 改变曝光补偿的进度条 -选择保存照片的文件格式。这将影响“标准”(非 RAW)照片。注意 PNG 格式并非真正无损,它是从 JPEG 按 100% 质量转换而成的。%s +选择保存照片的文件格式。这将影响“标准”(非 RAW)照片。注意 PNG 格式并非真正无损,它是从 JPEG 按 100% 质量转换而成的。\n%s Camera API -选择 Camera2 API 来启用额外功能,比如手动曝光、对焦、白平衡及 RAW(如果设备支持)。改变 API 将重启应用。%s +选择 Camera2 API 来启用额外功能,比如手动曝光、对焦、白平衡及 RAW(如果设备支持)。改变 API 将重启应用。\n%s 原始 Camera API Camera2 API 视频伽马值 -如果视频图标预置文件被设定为伽马时的伽马值 %s +如果视频图标预置文件被设定为伽马时的伽马值\n%s 1.6 1.8 1.9 @@ -920,7 +920,7 @@2.6 2.8 辅助图片透明度 -用于辅助图片的透明度值。较低的值代表更透明,较高的值代表更不透明。%s +用于辅助图片的透明度值。较低的值代表更透明,较高的值代表更不透明。\n%s 外部相机 超广角 切换至外部相机 @@ -928,9 +928,9 @@在多个相机间切换 改变当前首选项值的进度条 斑马条纹前景颜色 -显示斑马条纹时前景使用的颜色。%s +显示斑马条纹时前景使用的颜色。\n%s 斑马条纹背景颜色 -显示斑马条纹时背景使用的颜色。%s +显示斑马条纹时背景使用的颜色。\n%s 黑色 红色 橙色 -- GitLab From 0b72e17bdccaf9286c8bf2bf5320a0d0e7c45eeb Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Mon, 28 Sep 2020 13:59:15 +0100 Subject: [PATCH 121/430] Fix HDR problems due to tonemap_scale_c too small; add tests testHDR58, testHDR59, testHDR60 for this. --- _docs/history.html | 2 + .../sourceforge/opencamera/test/HDRTests.java | 3 + .../opencamera/test/MainActivityTest.java | 69 +++++++++++++++++-- .../sourceforge/opencamera/HDRProcessor.java | 11 ++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 04fdba668..b9ecae895 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -57,6 +57,8 @@ FIXED Photos would sometimes fail to save on some devices with Storage Access options were enabled (options like DRO, HDR, auto-level, photostamp that require post-processing; custom Exif tags like artist or copyright; or when using geotagging with Camera2 API). +FIXED Fix for HDR scenes with both very bright and very dark regions, result would be over + exposed. FIXED Corrupt videos could be left over if video failed to start. FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java index 9efcae467..2aec0e595 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java @@ -74,6 +74,9 @@ public class HDRTests { suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR55")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR56")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR57")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR58")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR59")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR60")); return suite; } } diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 98e9183c7..c047c456e 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -13254,7 +13254,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR58/IMG_20190911_210146_0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR58/IMG_20190911_210146_1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR58/IMG_20190911_210146_2.jpg") ); + + HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR58_output.jpg", false, 1250, 1000000000L/10); + //HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR58_output.jpg", false, 1250, 1000000000L/10, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP); + + checkHistogramDetails(hdrHistogramDetails, 11, 119, 255); + } + + /** Tests HDR algorithm on test samples "testHDR59". + */ + public void testHDR59() throws IOException, InterruptedException { + Log.d(TAG, "testHDR59"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR59/IMG_20190911_210154_0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR59/IMG_20190911_210154_1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR59/IMG_20190911_210154_2.jpg") ); + + HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR59_output.jpg", false, 1250, 1000000000L/10); + //HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR59_output.jpg", false, 1250, 1000000000L/10, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP); + + //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255); + } + + /** Tests HDR algorithm on test samples "testHDR60". + */ + public void testHDR60() throws IOException, InterruptedException { + Log.d(TAG, "testHDR60"); + + setToDefault(); + + // list assets + List inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR60/IMG_20200507_020319_0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR60/IMG_20200507_020319_1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR60/IMG_20200507_020319_2.jpg") ); + + HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR60_output.jpg", false, 491, 1000000000L/10); + //HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR60_output.jpg", false, 491, 1000000000L/10, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP); + + //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255); + } + /** 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/ . diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index d097b0644..9207f110a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -797,8 +797,17 @@ public class HDRProcessor { final float tonemap_denom = ((float)median_target)/(float)median_brightness - (255.0f / max_possible_value); if( MyDebug.LOG ) Log.d(TAG, "tonemap_denom: " + tonemap_denom); - if( tonemap_denom != 0.0f ) // just in case + if( tonemap_denom != 0.0f ) { // just in case tonemap_scale_c = (255.0f - median_target) / tonemap_denom; + if( MyDebug.LOG ) + Log.d(TAG, "tonemap_scale_c (before setting min): " + tonemap_scale_c); + /*if( tonemap_scale_c < 0.5f*255.0f ) { + throw new RuntimeException("tonemap_scale_c: " + tonemap_scale_c); + }*/ + // important to set a min value, see testHDR58, testHDR59, testHDR60 - at least 0.25, but 0.5 works better: + //tonemap_scale_c = Math.max(tonemap_scale_c, 0.25f*255.0f); + tonemap_scale_c = Math.max(tonemap_scale_c, 0.5f*255.0f); + } //throw new RuntimeException(); // test } // Higher tonemap_scale_c values means darker results from the Reinhard tonemapping. -- GitLab From f3652fff5cba722aa8958be57e5d2756ee3a79de Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 28 Sep 2020 19:42:27 +0100 Subject: [PATCH 122/430] Resync (fix sorting code). --- .../sourceforge/opencamera/HDRProcessor.java | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index 9207f110a..0b99fac50 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -24,6 +24,8 @@ import android.renderscript.Script; import android.renderscript.ScriptIntrinsicHistogram; //import android.renderscript.ScriptIntrinsicResize; import android.renderscript.Type; + +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import android.util.Log; @@ -1818,10 +1820,18 @@ public class HDRProcessor { BitmapInfo bitmapInfo = new BitmapInfo(luminanceInfos[i], bitmaps.get(i), allocations[i], i); bitmapInfos.add(bitmapInfo); } + if( MyDebug.LOG ) { + Log.d(TAG, "before sorting:"); + for(int i=0;i () { @Override public int compare(BitmapInfo o1, BitmapInfo o2) { - return o1.luminanceInfo.median_value - o2.luminanceInfo.median_value; + // important to use the code in LuminanceInfo.compareTo(), as that's also tested via the unit test + // sortLuminanceInfo() + return o1.luminanceInfo.compareTo(o2.luminanceInfo); } }); bitmaps.clear(); @@ -1831,8 +1841,9 @@ public class HDRProcessor { allocations[i] = bitmapInfos.get(i).allocation; } if( MyDebug.LOG ) { + Log.d(TAG, "after sorting:"); for(int i=0;i { + final int min_value; final int median_value; final boolean noisy; - LuminanceInfo(int median_value, boolean noisy) { + public LuminanceInfo(int min_value, int median_value, boolean noisy) { + this.min_value = min_value; this.median_value = median_value; this.noisy = noisy; } + + @Override + @NonNull + public String toString() { + return "min: " + min_value + " , median: " + median_value + " , noisy: " + noisy; + } + + @Override + public int compareTo(LuminanceInfo o) { + int value = this.median_value - o.median_value; + if( value == 0 ) { + // fall back to using min_value + value = this.min_value - o.min_value; + } + return value; + } } private LuminanceInfo computeMedianLuminance(Bitmap bitmap, int mtb_x, int mtb_y, int mtb_width, int mtb_height) { @@ -2214,8 +2243,14 @@ public class HDRProcessor { int middle = total/2; int count = 0; boolean noisy = false; + int min_value = -1; for(int i=0;i<256;i++) { count += histo[i]; + if( min_value == -1 && histo[i] > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "min luminance " + i); + min_value = i; + } if( count >= middle ) { if( MyDebug.LOG ) Log.d(TAG, "median luminance " + i); @@ -2243,11 +2278,11 @@ public class HDRProcessor { Log.d(TAG, "too dark/noisy"); noisy = true; } - return new LuminanceInfo(i, noisy); + return new LuminanceInfo(min_value, i, noisy); } } Log.e(TAG, "computeMedianLuminance failed"); - return new LuminanceInfo(127, true); + return new LuminanceInfo(min_value, 127, true); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) -- GitLab From 052e7a937876b1754f2c7d4325099659b1ddadc8 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 28 Sep 2020 19:42:40 +0100 Subject: [PATCH 123/430] New sortLuminanceInfo. --- .../sourceforge/opencamera/test/UnitTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java index c0e87f9c0..b6857aad7 100644 --- a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java +++ b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java @@ -19,6 +19,7 @@ import org.junit.Test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -871,4 +872,69 @@ public class UnitTest { res = MainActivity.checkSaveLocation("/storage/emulated/0", dcim_path); assertEquals(new MainActivity.CheckSaveLocationResult(false, null), res); } + + @Test + public void sortLuminanceInfo() { + Log.d(TAG, "sortLuminanceInfo"); + + List luminanceInfos = new ArrayList<>(); + List luminanceInfosSorted; + + luminanceInfos.clear(); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, false)); + luminanceInfosSorted = new ArrayList<>(luminanceInfos); + Collections.sort(luminanceInfosSorted); + assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); + assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(0)); + assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(1)); + assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(2)); + + luminanceInfos.clear(); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, false)); + luminanceInfosSorted = new ArrayList<>(luminanceInfos); + Collections.sort(luminanceInfosSorted); + assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); + assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(0)); + assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(1)); + assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(2)); + + luminanceInfos.clear(); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, false)); + luminanceInfosSorted = new ArrayList<>(luminanceInfos); + Collections.sort(luminanceInfosSorted); + assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); + assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(0)); + assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(1)); + assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(2)); + + // case that requires using min value as well as median value + luminanceInfos.clear(); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(93, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(68, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(17, 193, false)); + luminanceInfosSorted = new ArrayList<>(luminanceInfos); + Collections.sort(luminanceInfosSorted); + assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); + assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(0)); + assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(1)); + assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(2)); + + // case that should never use min value + luminanceInfos.clear(); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(60, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(70, 240, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(80, 95, false)); + luminanceInfosSorted = new ArrayList<>(luminanceInfos); + Collections.sort(luminanceInfosSorted); + assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); + assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(0)); + assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(1)); + assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(2)); + } } -- GitLab From a8dfe557958c05914134704a254c214cece0a145 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 30 Sep 2020 23:28:58 +0100 Subject: [PATCH 124/430] Resync (fix sorting code for using hi_value); update sortLuminanceInfo for this. --- .../sourceforge/opencamera/HDRProcessor.java | 43 ++++++++++++++-- .../sourceforge/opencamera/test/UnitTest.java | 49 +++++++++++++------ 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index 0b99fac50..2041eec76 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -1856,6 +1856,14 @@ public class HDRProcessor { sort_cb.sortOrder(sort_order); } } + /*{ + // test + for(int i = 0; i < luminanceInfos.length-1; i++) { + if( luminanceInfos[i].compareTo(luminanceInfos[i+1]) == 0 ) { + throw new RuntimeException("this: " + luminanceInfos[i] + " , that: " + luminanceInfos[i+1]); + } + } + }*/ int median_brightness = -1; if( use_mtb ) { @@ -2174,18 +2182,20 @@ public class HDRProcessor { public static class LuminanceInfo implements Comparable { final int min_value; final int median_value; + final int hi_value; final boolean noisy; - public LuminanceInfo(int min_value, int median_value, boolean noisy) { + public LuminanceInfo(int min_value, int median_value, int hi_value, boolean noisy) { this.min_value = min_value; this.median_value = median_value; + this.hi_value = hi_value; this.noisy = noisy; } @Override @NonNull public String toString() { - return "min: " + min_value + " , median: " + median_value + " , noisy: " + noisy; + return "min: " + min_value + " , median: " + median_value + " , hi: " + hi_value + " , noisy: " + noisy; } @Override @@ -2195,6 +2205,10 @@ public class HDRProcessor { // fall back to using min_value value = this.min_value - o.min_value; } + if( value == 0 ) { + // fall back to using hi_value + value = this.hi_value - o.hi_value; + } return value; } } @@ -2243,7 +2257,26 @@ public class HDRProcessor { int middle = total/2; int count = 0; boolean noisy = false; - int min_value = -1; + int min_value = -1, hi_value = -1; + // first count backwards to get hi_value + for(int i=255;i>=0;i--) { + /*if( histo[i] > 0 ) { + if( MyDebug.LOG ) + Log.d(TAG, "max luminance " + i); + max_value = i; + break; + }*/ + count += histo[i]; + if( count >= total/10 ) { + if( MyDebug.LOG ) + Log.d(TAG, "hi luminance " + i); + hi_value = i; + break; + } + } + + // then count forwards to get min and median values + count = 0; for(int i=0;i<256;i++) { count += histo[i]; if( min_value == -1 && histo[i] > 0 ) { @@ -2278,11 +2311,11 @@ public class HDRProcessor { Log.d(TAG, "too dark/noisy"); noisy = true; } - return new LuminanceInfo(min_value, i, noisy); + return new LuminanceInfo(min_value, i, hi_value, noisy); } } Log.e(TAG, "computeMedianLuminance failed"); - return new LuminanceInfo(min_value, 127, true); + return new LuminanceInfo(min_value, 127, hi_value, true); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) diff --git a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java index b6857aad7..d19a039d2 100644 --- a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java +++ b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java @@ -881,9 +881,9 @@ public class UnitTest { List luminanceInfosSorted; luminanceInfos.clear(); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, 255, false)); luminanceInfosSorted = new ArrayList<>(luminanceInfos); Collections.sort(luminanceInfosSorted); assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); @@ -892,9 +892,9 @@ public class UnitTest { assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(2)); luminanceInfos.clear(); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80,255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, 255, false)); luminanceInfosSorted = new ArrayList<>(luminanceInfos); Collections.sort(luminanceInfosSorted); assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); @@ -903,9 +903,9 @@ public class UnitTest { assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(2)); luminanceInfos.clear(); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(33, 116, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 64, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(16, 80, 255, false)); luminanceInfosSorted = new ArrayList<>(luminanceInfos); Collections.sort(luminanceInfosSorted); assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); @@ -915,9 +915,9 @@ public class UnitTest { // case that requires using min value as well as median value luminanceInfos.clear(); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(93, 255, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(68, 255, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(17, 193, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(93, 255, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(68, 255, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(17, 193, 255, false)); luminanceInfosSorted = new ArrayList<>(luminanceInfos); Collections.sort(luminanceInfosSorted); assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); @@ -927,14 +927,33 @@ public class UnitTest { // case that should never use min value luminanceInfos.clear(); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(60, 255, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(70, 240, false)); - luminanceInfos.add(new HDRProcessor.LuminanceInfo(80, 95, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(60, 255, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(70, 240, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(80, 95, 255, false)); luminanceInfosSorted = new ArrayList<>(luminanceInfos); Collections.sort(luminanceInfosSorted); assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(0)); assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(1)); assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(2)); + + // case that requires using hi value as well as median and min values + luminanceInfos.clear(); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(17, 31, 100, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(34, 127, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(93, 255, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(68, 255, 255, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 0, 90, false)); + luminanceInfos.add(new HDRProcessor.LuminanceInfo(0, 0, 80, false)); + luminanceInfosSorted = new ArrayList<>(luminanceInfos); + Collections.sort(luminanceInfosSorted); + assertEquals(luminanceInfos.size(), luminanceInfosSorted.size()); + assertEquals(luminanceInfos.get(5), luminanceInfosSorted.get(0)); + assertEquals(luminanceInfos.get(4), luminanceInfosSorted.get(1)); + assertEquals(luminanceInfos.get(0), luminanceInfosSorted.get(2)); + assertEquals(luminanceInfos.get(1), luminanceInfosSorted.get(3)); + assertEquals(luminanceInfos.get(3), luminanceInfosSorted.get(4)); + assertEquals(luminanceInfos.get(2), luminanceInfosSorted.get(5)); + } } -- GitLab From 57e840296d75930a02cda93a954b8c1f261f203b Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 3 Oct 2020 16:21:05 +0100 Subject: [PATCH 125/430] Save debug csv files to application specific folder. --- app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index 2041eec76..8e27eae70 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -264,7 +264,7 @@ public class HDRProcessor { if( MyDebug.LOG ) { // log samples to a CSV file - File file = new File(Environment.getExternalStorageDirectory().getPath() + "/net.sourceforge.opencamera.hdr_samples_" + id + ".csv"); + File file = new File(context.getExternalFilesDir(null).getPath() + "/net.sourceforge.opencamera.hdr_samples_" + id + ".csv"); if( file.exists() ) { if( !file.delete() ) { // keep FindBugs happy by checking return argument -- GitLab From c49d660d03108920eada000c3b7be968e14df9bf Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 3 Oct 2020 18:15:51 +0100 Subject: [PATCH 126/430] Remove incorrect line (function only used for testing). --- .../opencamera/cameracontroller/CameraController2.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 13ba6d0da..f9ef7c698 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -4857,7 +4857,6 @@ public class CameraController2 extends CameraController { 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; -- GitLab From adbfa3b261cce254baf93435630350ff21561f50 Mon Sep 17 00:00:00 2001 From: Ilya Pogrebenko Date: Thu, 8 Oct 2020 13:38:22 +0300 Subject: [PATCH 127/430] Update russian translation --- app/src/main/res/values-ru/strings.xml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 425ee22b9..4a5f0e257 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -4,12 +4,12 @@ Open Camera Настройки Настройки всплывающих окон -Ок (Это сообщение больше не появится.) +Ок (Это сообщение больше не появится) Вкл Выкл -Выберите папку сохранения: +Выберите папку для сохранения: Очистить историю папки Очистить историю папки? Выбрать другую папку @@ -51,7 +51,7 @@Галерея недоступна Экран заблокирован,\nпроведите пальцем по экрану чтобы разблокировать Разблокировано -К сожалению, автостабилизация не\nподдерживается на данном устройстве +К сожалению, автостабилизация\nне поддерживается на данном устройстве Аудио выключено Макс. продолжительность Цветовой эффект @@ -60,7 +60,7 @@Угол Направление -Не удалось открыть камеру. +Не удалось открыть камеру Камера может быть использована другим приложением? ISO @@ -113,7 +113,7 @@Папка сохранения Имя папки для сохранения фото и видео (также может быть полный путь) Использовать Storage Access Framework -Использовать Storage Access Framework для сохранения фото и видео +Использовать Storage Access Framework для сохранения фото и видео Префикс для фото Префикс имен файлов для сохраняемых фото Префикс для видео @@ -159,8 +159,8 @@Отображение прямоугольника, показывающего соотношение сторон - полезно, если вы планируете обрезать фото/видео. Требуется режим фотосъемки WYSIWYG или режим видео\n%s Всплывающие сообщения Показывать всплывающие уведомления при работе -Показать эскиз анимации -Отображение перемещения эскиза анимации при съемке фотографии +Показать анимацию эскиза +Отображение анимации перемещения эскиза при съемке фотографии Показывать рамку во время фотосъемки Показывать рамку на экране для индикации фотосъемки Не выключать дисплей @@ -192,9 +192,9 @@Размер шрифта Размер шрифта для добавляемого на фотографию текста Цвет -Цвет для добавляемого на фотографию текста +Цвет текста добавляемого на фотографию Стиль -Стиль для добавляемого на фотографию текста\n%s +Стиль текста добавляемого на фотографию\n%s Использовать фоновый поток Сохранять фото в фоновом потоке (для ускорения работы) Разрешение видео @@ -225,7 +225,7 @@Разное Помощь в Интернете -Запустить веб-сайт Open Camera в вашем браузере +Открыть веб-сайт Open Camera в вашем браузере Пожертвовать Если вам нравится это приложение, киньте мне копейку. Вы можете сделать это, купив мое приложение. Спасибо! Использовать Camera2 API @@ -390,7 +390,7 @@45 минут 1 час -Показать кнопку съемки +Показать кнопку съёмки Отображение кнопки фото- и видеосъемки. Полезно, если на вашем устройстве уже есть аппаратная кнопка спуска затвора. Запись видео приостановлена @@ -405,7 +405,7 @@Автоматически Облачно -Светлый деньDaylight +Светлый день Свечение Яркое свечение Тень @@ -711,7 +711,7 @@Всегда Формат видео -Кодеки и формат видео-и аудиофайлов\n%s +Кодеки и формат видео- и аудиофайлов\n%s По умолчанию MPEG4 H264 -- GitLab From d8796ed87815a03edfd9e29a3cb6f7ff9cde244c Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 10 Oct 2020 16:39:13 +0100 Subject: [PATCH 128/430] Fixed possible misalignment for HDR scenes with very bright or very dark images; new testHDR61 for this. --- _docs/history.html | 1 + .../sourceforge/opencamera/test/HDRTests.java | 1 + .../opencamera/test/MainActivityTest.java | 25 ++++++++++++++++++- .../sourceforge/opencamera/HDRProcessor.java | 10 ++++++++ .../sourceforge/opencamera/ImageSaver.java | 2 +- app/src/main/rs/create_mtb.rs | 2 +- 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index b9ecae895..c77da514e 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -59,6 +59,7 @@ FIXED Photos would sometimes fail to save on some devices with Storage Access Camera2 API). FIXED Fix for HDR scenes with both very bright and very dark regions, result would be over exposed. +FIXED Fixed possible misalignment for HDR scenes with very bright or very dark images. FIXED Corrupt videos could be left over if video failed to start. FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java index 2aec0e595..ec247386e 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java @@ -77,6 +77,7 @@ public class HDRTests { suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR58")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR59")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR60")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testHDR61")); return suite; } } diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index c047c456e..d08c6329d 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -13554,7 +13554,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>(); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR61/IMG_20191111_145230_0.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR61/IMG_20191111_145230_1.jpg") ); + inputs.add( getBitmapFromFile(hdr_images_path + "testHDR61/IMG_20191111_145230_2.jpg") ); + + HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR61_output.jpg", false, 50, 1000000000L/5025); + + checkHistogramDetails(hdrHistogramDetails, 0, 86, 254); + + int [] exp_offsets_x = {0, 0, 1}; + int [] exp_offsets_y = {0, 0, -2}; + 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/ . diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index 8e27eae70..831d1cfc5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -1898,6 +1898,16 @@ public class HDRProcessor { mtb_allocations[i] = Allocation.createTyped(rs, Type.createXY(rs, Element.U8(rs), mtb_width, mtb_height)); + // avoid too low/high median_values, otherwise we'll detect dark or light pixels as "noisy" - needed for testHDR61 + final int min_diff_c = 4; // should be same value as in create_mtb.rs/create_mtb() + /*if( median_value < min_diff_c+1 || median_value > 255-(min_diff_c+1) ) { + throw new RuntimeException("image " + i + " has median_value: " + median_value); // test + }*/ + median_value = Math.max(median_value, min_diff_c+1); + median_value = Math.min(median_value, 255-(min_diff_c+1)); + if( MyDebug.LOG ) + Log.d(TAG, i + ": median_value is now: " + median_value); + // set parameters if( use_mtb ) createMTBScript.set_median_value(median_value); diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 791cb9e14..bde002461 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -1107,7 +1107,7 @@ public class ImageSaver extends Thread { default: // Using local contrast enhancement helps scenes where the dynamic range is very large, which tends to be when we choose // a short exposure time, due to fixing problems where some regions are too dark. - // This helps: testHDR11, testHDR19, testHDR34, testHDR53. + // This helps: testHDR11, testHDR19, testHDR34, testHDR53, testHDR61. // Using local contrast enhancement in all cases can increase noise in darker scenes. This problem would occur // (if we used local contrast enhancement) is: testHDR2, testHDR12, testHDR17, testHDR43, testHDR50, testHDR51, // testHDR54, testHDR55, testHDR56. diff --git a/app/src/main/rs/create_mtb.rs b/app/src/main/rs/create_mtb.rs index 2882d2b2d..52756eae3 100644 --- a/app/src/main/rs/create_mtb.rs +++ b/app/src/main/rs/create_mtb.rs @@ -19,7 +19,7 @@ void __attribute__((kernel)) create_mtb(uchar4 in, uint32_t x, uint32_t y) { diff = value - median_value; else diff = median_value - value; - if( diff <= 4 ) + if( diff <= 4 ) // should be same value as min_diff_c in HDRProcessor.autoAlignment() out = 127; else if( value <= median_value ) out = 0; -- GitLab From c878148a33c26c3b7751dcebc560c085b2a4e5d0 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 10 Oct 2020 16:39:35 +0100 Subject: [PATCH 129/430] Fix typo in comments. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index d08c6329d..e0679e097 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -14360,7 +14360,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sat, 10 Oct 2020 16:42:51 +0100 Subject: [PATCH 130/430] Use seekbar for more settings. --- _docs/history.html | 2 ++ .../opencamera/MyPreferenceFragment.java | 6 ++--- app/src/main/res/values/arrays.xml | 24 +++++++++---------- app/src/main/res/xml/preferences.xml | 10 ++++---- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index c77da514e..d70105705 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -65,6 +65,8 @@ FIXED Possible problem taking photos on some devices with LIMITED Camera2 API FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices with LIMITED Camera2 API support. FIXED UI would become sluggish if camera or storage permission denied with "Don't ask again". +UPDATED Use seekbar for more settings (audio control sensitivity, image quality, photo stamp font + size). UPDATED Camera now closed when in settings or preview otherwise in background. Version 1.48.2 (2020/07/12) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java index b79c2c79d..af2be815a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java @@ -285,9 +285,9 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared entries[i] = "" + (i+1) + "%"; values[i] = "" + (i+1); } - ListPreference lp = (ListPreference)findPreference("preference_quality"); - lp.setEntries(entries); - lp.setEntryValues(values); + ArraySeekBarPreference sp = (ArraySeekBarPreference)findPreference("preference_quality"); + sp.setEntries(entries); + sp.setEntryValues(values); } { diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 29a5ea272..cad7a66ef 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -826,22 +826,22 @@ - crop_guide_2.4
- - @string/preference_audio_noise_control_sensitivity_3
-- @string/preference_audio_noise_control_sensitivity_2
-- @string/preference_audio_noise_control_sensitivity_1
-- @string/preference_audio_noise_control_sensitivity_0
-- @string/preference_audio_noise_control_sensitivity_m1
-- @string/preference_audio_noise_control_sensitivity_m2
- @string/preference_audio_noise_control_sensitivity_m3
+- @string/preference_audio_noise_control_sensitivity_m2
+- @string/preference_audio_noise_control_sensitivity_m1
+- @string/preference_audio_noise_control_sensitivity_0
+- @string/preference_audio_noise_control_sensitivity_1
+- @string/preference_audio_noise_control_sensitivity_2
+- @string/preference_audio_noise_control_sensitivity_3
- - 3
-- 2
-- 1
-- 0
-- -1
-- -2
- -3
+- -2
+- -1
+- 0
+- 1
+- 2
+- 3
- @string/preference_video_output_format_default
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 87b792e9a..98223f2fe 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -168,7 +168,7 @@ android:defaultValue="none" /> -- - - - Date: Tue, 13 Oct 2020 23:13:22 +0100 Subject: [PATCH 131/430] Update gradle versions. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index dad9ef0a6..c260ed8c6 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d25d07942..a0d1cf5bc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jul 31 23:57:49 BST 2020 +#Tue Oct 13 22:07:50 BST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip -- GitLab From 56d358cd7725f8aadbcd515c2db47e2a92ef14cd Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Thu, 15 Oct 2020 22:32:05 +0100 Subject: [PATCH 132/430] Enable scoped storage for Android 10+. --- _docs/help.html | 42 ++++++++++++------- _docs/history.html | 13 ++++++ _docs/privacy_oc.html | 4 +- app/src/main/AndroidManifest.xml | 4 +- .../sourceforge/opencamera/MainActivity.java | 4 +- app/src/main/res/values/strings.xml | 4 +- 6 files changed, 48 insertions(+), 23 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index 0d33ff7df..383df266e 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -265,7 +265,7 @@ has a hardware menu button, pressing that should also open the settings.) photo/video (by default saved in the OpenCamera folder). If you get the message "No Gallery app available", then you should install a Gallery app. You can also "long press" on the Gallery icon - this will let you switch between the recent save locations, or take you straight to a -file dialog to choose a save location if additional locations have yet been defined. See +dialog to choose a save location if additional locations have yet been defined. See Save location under Settings/More camera controls for more details. Pause video - When recording @@ -540,19 +540,27 @@ be in portrait. This option allows fixing the camera to either be in portrait or auto-level is also enabled, it will have the effect of aligning photos to the nearest 90 degrees.
-Save location - Select the folder to store the photos in. Click on a folder -(or "Parent Folder") to navigate through the filesystem. Select "New Folder" to create a new folder in the currently -displayed folder. Select "Use Folder" to choose the currently displayed folder. Note that on Android, there are some -folders that cannot be written to - Open Camera will display a message if you try to use one of these folders. Once -you have specified a new save location, you can long press on the Gallery icon to quickly switch between recent save -locations. Note that if "Use Storage Access Framework" is selected, this option will instead show up the Android standard -file chooser - navigate to the desired folder, and click "SELECT". If you want to save to an SD card, see "How can I -save to my external SD card?" under the FAQ.
+Save location - Select the folder to store the photos in.
++
+- On Android 9 or earlier: This opens a file dialog. Click on a folder (or "Parent Folder") to navigate through + the filesystem. Select "New Folder" to create a new folder in the currently displayed folder. Select "Use Folder" + to choose the currently displayed folder. Note that on Android, there are some folders that cannot be written + to - Open Camera will display a message if you try to use one of these folders.
+- On Android 10 or later: This opens a dialog to type the name of the folder. This + will be a subfolder of DCIM on your internal storage. You can specify subfolders with the "/" + character. For example, specifying Camera/holiday will save inside DCIM/Camera/holiday/ + on your internal storage.
+- If "Use Storage Access Framework" is enabled: Then on any Android version, this option + will show up the Android standard file chooser - navigate to the desired folder, and click "SELECT" or + "ALLOW ACCESS" (wording varies depending on Android version).
+Once you have specified a new save location, you can long press on the Gallery icon to quickly switch between recent save + locations. If you want to save to an SD card, see "How can I save to my external SD card?" under the FAQ.
Use Storage Access Framework - If selected, Open Camera will instead use the Android Storage Access Framework. This -has some advantages, such as using the standard Android file picker, and being the only way to save to SD cards on Android 5 -- though it may be slightly slower to take photos. (Requires Android 5.0 or higher.)
+has some advantages, such as using the standard Android file picker, and being the only way to save to SD cards on Android 5+. +Furthermore on Android 10+, it is the only way to save outside of the DCIM/ folder. (Requires Android 5.0 or higher.)= Build.VERSION_CODES.Q; + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; } public int getNavigationGap() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a93fdb811..5f924ea7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,7 +113,7 @@ Save location Folder to save the photo/video files in Use Storage Access Framework -Whether to use Storage Access Framework for saving photos and videos. This should be enabled to allow saving to external SD cards. +Whether to use Storage Access Framework for saving photos and videos. This should be enabled to allow saving to external SD cards. On Android 10+ this option must be enabled if you want to save in a folder outside of DCIM. Save photo prefix The prefix to use for the save filenames for photos Save video prefix @@ -993,7 +993,7 @@Open Camera accesses camera sensor and microphone data to fulfil its purpose as a camera. Microphone is also used for the optional \"Audio control\". - \nAccess to files is needed to save resultant files such as photos and videos to your device. + \nAccess to files is needed (at least for Android 9 and earlier) to save resultant files such as photos and videos to your device. \nLocation permission is required for the optional geotagging features (for photos and videos, including stamp and subtitles options). When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files. -- GitLab From a4a02f39e9901190ba3dfa28902b5715757074f8 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 17 Oct 2020 18:57:29 +0100 Subject: [PATCH 133/430] Bump to version 79 / 1.48.3. --- app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 38e81a333..9d25de5b4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ -- GitLab From 1310b8efc433de0158d39f97cc09341dfb471a2e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 17 Oct 2020 19:08:34 +0100 Subject: [PATCH 134/430] Update credits for russian. --- _docs/credits.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/credits.html b/_docs/credits.html index 813c0d1f6..1cb705b99 100644 --- a/_docs/credits.html +++ b/_docs/credits.html @@ -74,7 +74,7 @@ - Norwegian Bokmål translation by Imre Kristoffer Eilertsen ( imreeil42 AT gmail DOT com ).
- Polish translation by Jacek Buczyński.
-- Russian translation by maksnogin ( maksnogin AT gmail DOT com ), Grigorii Chirkov, Dmitry Vahnin aka JSBmanD, Aleksey Khlybov.
+- Russian translation by maksnogin ( maksnogin AT gmail DOT com ), Grigorii Chirkov, Dmitry Vahnin aka JSBmanD, Aleksey Khlybov, Ilya Pogrebenko.
- Slovenian translation by Peter Klofutar.
- Spanish translation by Mario Sanoguera ( sanogueralorenzo AT gmail DOT com , https://play.google.com/store/apps/developer?id=Mario+Sanoguera ; Sebastian05067, https://forum.xda-developers.com/member.php?u=6302705 ) and Gonzalo Prieto Vega.
- Turkish translation by Serdar Erkoc ( serdarerkoc2004 AT yahoo DOT com ).
-- GitLab From e666b90ced916e5456554b7842cc15fca8996043 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 17 Oct 2020 20:30:09 +0100 Subject: [PATCH 135/430] Remove unstranslated strings. --- app/src/main/res/values-de/strings.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 291d085a3..551623254 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -613,9 +613,6 @@ Zeige den What\'s-New-Dialog Zeigt Info zu neuen Features und Verbesserungen wenn die App upgedatet wurde -Edge mode algorithm -Algorithm that the camera driver should use for applying edge enhancement. Edge enhancement improves sharpness and details in the captured image. (This setting is ignored in NR photo mode.)\n%s -Verarbeitungseinstellungen… Algorithmus zur Rauschunterdrückung @@ -920,9 +917,6 @@REC709 sRGB Gamma -JTVideo -JTLog -JTLog2 Video Gamma-Wert Gamma-Wert für Videos, wenn Video-Profil Gamma ausgewählt wurde\n%s 1,6 -- GitLab From b7dc6c680345acf474fde96c5473da33a019b141 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 17 Oct 2020 20:32:00 +0100 Subject: [PATCH 136/430] Update credits for german. --- _docs/credits.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/credits.html b/_docs/credits.html index 1cb705b99..7e056082c 100644 --- a/_docs/credits.html +++ b/_docs/credits.html @@ -65,7 +65,7 @@ - Belarusian translation by Zmicer Turok.
- Czech translation by Jaroslav Svoboda ( multi DOT flexi AT gmail DOT com , http://jaroslavsvoboda.eu ).
- French translation by Olivier Seiler ( oseiler AT nebuka DOT net ) and Eric Lassauge ( lassauge AT users DOT sf DOT net ).
-- German translation by Ronny Steiner, Sebastian Ahlborn, Carsten Schlote, Wilhelm Stein.
+- German translation by Ronny Steiner, Sebastian Ahlborn, Carsten Schlote, Wilhelm Stein, Jochen Wiesel.
- Greek translation by Wasilis Mandratzis-Walz.
- Hungarian translation by Báthory Péter.
- Italian tranlation by Valerio Bozzolan, Stefano Gualmo ( s DOT gualmo AT gmail DOT com ).
-- GitLab From dbe1fee87cf9396c0ca3d810b4a739651cf853db Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 17 Oct 2020 20:57:00 +0100 Subject: [PATCH 137/430] Update feature list - mention log profile. --- _docs/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_docs/index.html b/_docs/index.html index 2e1eb03c6..87a77451e 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -114,8 +114,8 @@ browsers --> - Apply date and timestamp, location coordinates, and custom text to photos; store date/time and location as video subtitles (.SRT).
- Panorama, including for front camera.
- Support for HDR (with auto-alignment and ghost removal) and Exposure Bracketing.
-- Support for Camera2 API: manual controls (with optional focus assist); burst mode; RAW (DNG) files; slow motion video.
-- Noise reduction (including low light night mode) and Dynamic range optimisation modes for better quality photos.
+- Support for Camera2 API: manual controls (with optional focus assist); burst mode; RAW (DNG) files; slow motion video; log profile video.
+- Noise reduction (including low light night mode) and Dynamic range optimisation modes.
- Options for on-screen histogram, zebra stripes, focus peaking.
- Focus bracketing mode.
- Completely free, and no ads in the app (I only run ads on the website). Open Source.
-- GitLab From 4b11b8f6f1a4b7bf47fa84cd485d84ac8e88900d Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sun, 25 Oct 2020 12:10:39 +0000 Subject: [PATCH 138/430] Update library versions. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bb37d78ef..4135f4080 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,6 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.0' - testImplementation 'junit:junit:4.12' + implementation 'androidx.exifinterface:exifinterface:1.3.1' + testImplementation 'junit:junit:4.13.1' } -- GitLab From ca804ba3136ae6de528816ea4bfc05ba7a0954ff Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 25 Oct 2020 12:15:27 +0000 Subject: [PATCH 139/430] Fix/ignore Android warnings. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 2 ++ app/src/main/AndroidManifest.xml | 3 ++- app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 2 ++ .../opencamera/cameracontroller/CameraController2.java | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index e0679e097..8dd942621 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -74,6 +74,8 @@ import android.widget.SeekBar; import android.widget.TextView; import android.widget.ZoomControls; +// ignore warning about "Call to Thread.sleep in a loop", this is only test code +@SuppressWarnings("BusyWait") public class MainActivityTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "MainActivityTest"; private MainActivity mActivity = null; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d25de5b4..c0b092e9f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,8 @@ + android:maxSdkVersion="28" + tools:ignore="ScopedStorage" /> diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index bde002461..369559a0f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -505,6 +505,8 @@ public class ImageSaver extends Thread { break; } if( test_slow_saving ) { + // ignore warning about "Call to Thread.sleep in a loop", this is only activated in test code + //noinspection BusyWait Thread.sleep(2000); } if( MyDebug.LOG ) { diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index f9ef7c698..88f080f65 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -63,7 +63,7 @@ public class CameraController2 extends CameraController { private final Context context; private CameraDevice camera; - private String cameraIdS; + private final String cameraIdS; private final boolean is_samsung; private final boolean is_samsung_s7; // Galaxy S7 or Galaxy S7 Edge -- GitLab From 71df7d1f9845622e6dceac72a5deaaa925d508fb Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 8 Nov 2020 13:32:18 +0000 Subject: [PATCH 140/430] Clarify. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index b1393ded3..d3806cc78 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -66,7 +66,7 @@ FIXED Possible problem with default edge mode and noise reduction mode behavio with LIMITED Camera2 API support. FIXED UI would become sluggish if camera or storage permission denied with "Don't ask again". UPDATED Now supporting "scoped storage" on Android 10+. This means storage permission is no longer - required. However this means the following changes: + required on Android 10+. However this means the following changes: * Saving outside of DCIM/ is no longer possible unless using the Storage Access Framework. If you had set up a custom save folder outside of DCIM/ and are on Android 10+, it will be reset to the default DCIM/OpenCamera/ folder. If you want to continue saving outside -- GitLab From 1eaff90aae98d71b40265f0b8af73a4a95f481da Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 8 Nov 2020 13:33:47 +0000 Subject: [PATCH 141/430] Fix wording. --- _docs/history.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index d3806cc78..8d874f4bc 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -67,11 +67,11 @@ FIXED Possible problem with default edge mode and noise reduction mode behavio FIXED UI would become sluggish if camera or storage permission denied with "Don't ask again". UPDATED Now supporting "scoped storage" on Android 10+. This means storage permission is no longer required on Android 10+. However this means the following changes: - * Saving outside of DCIM/ is no longer possible unless using the Storage Access Framework. - If you had set up a custom save folder outside of DCIM/ and are on Android 10+, it will - be reset to the default DCIM/OpenCamera/ folder. If you want to continue saving outside - of DCIM/, you can enable Settings/More camera controls/"Use Storage Access Framework" - and choose a new folder. + * Saving outside of DCIM/ is no longer possible unless using the Storage Access Framework + option. If you had set up a custom save folder outside of DCIM/ and are on Android 10+, + it will be reset to the default DCIM/OpenCamera/ folder. If you want to continue saving + outside of DCIM/, you can enable + Settings/More camera controls/"Use Storage Access Framework" and choose a new folder. * If using Video subtitles option, then the .SRT files will show up in gallery applications, unless Settings/More camera controls/"Use Storage Access Framework" is enabled. -- GitLab From 2dcc06c89f9442614666b1d2737e2268310c5ab5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 9 Nov 2020 22:58:16 +0000 Subject: [PATCH 142/430] Fix crash due to percent not escaped (bug introduced since last release). --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f0bfb75c9..f598a9ebd 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -902,7 +902,7 @@ 光圈 多相机图标 改变曝光补偿的进度条 -选择保存照片的文件格式。这将影响“标准”(非 RAW)照片。注意 PNG 格式并非真正无损,它是从 JPEG 按 100% 质量转换而成的。\n%s +选择保存照片的文件格式。这将影响“标准”(非 RAW)照片。注意 PNG 格式并非真正无损,它是从 JPEG 按 100%% 质量转换而成的。\n%s Camera API 选择 Camera2 API 来启用额外功能,比如手动曝光、对焦、白平衡及 RAW(如果设备支持)。改变 API 将重启应用。\n%s 原始 Camera API -- GitLab From 3481cda167cb373df279571cdba76812dfa9048a Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Mon, 9 Nov 2020 23:07:53 +0000 Subject: [PATCH 143/430] Update what's new text for 1.48.3. --- .../sourceforge/opencamera/MainActivity.java | 2 +- app/src/main/res/values/strings.xml | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 7b6930382..c66641ba1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -598,7 +598,7 @@ public class MainActivity extends Activity { // E.g., we have a "What's New" for 1.44 (64), but then push out a quick fix for 1.44.1 (65). We don't want to // show the dialog again to people who already received 1.44 (64), but we still want to show the dialog to people // upgrading from earlier versions. - int whats_new_version = 78; // 1.48.2 + int whats_new_version = 79; // 1.48.3 whats_new_version = Math.min(whats_new_version, version_code); // whats_new_version should always be <= version_code, but just in case! if( MyDebug.LOG ) { Log.d(TAG, "whats_new_version: " + whats_new_version); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5f924ea7a..a7ef9b77f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1033,11 +1033,20 @@ [This dialog is shown when Open Camera is updated. You can disable it under Settings/On screen GUI/Show What\'s New dialog.] - \n\nv1.48.2:\n + \n\nv1.48.3:\n -- GitLab From 526b631adcd71adf820c1ce3f0f2c53b2d2e8cbb Mon Sep 17 00:00:00 2001 From: Mark Harman-
- Manual focus and focus bracketing seekbars weren\'t being hidden when in immersive mode.\n
-- Fixed problem with video subtitles and Storage Access Framework on some devices.\n
-- Artist, Copyright exif tags option now supported for devices running Android 6 or earlier.\n
+- Now supporting \"scoped storage\" on Android 10+. This means the following changes for devices with Android 10+:\n
+- - Saving outside of DCIM/ is no longer possible unless using the Storage Access Framework + option. If you had set up a custom save folder outside of DCIM/ and are on Android 10+, + it will be reset to the default DCIM/OpenCamera/ folder. If you want to continue saving + outside of DCIM/, you can enable + Settings/More camera controls/\"Use Storage Access Framework\" and choose a new folder.\n
+- - If using Video subtitles option, then the .SRT files will show up in gallery + applications, unless Settings/More camera controls/\"Use Storage Access Framework\" is + enabled.\n
+- HDR improvements for some specific scenes.\n
+- Seekbar now used for more settings.\n
+- Bug fixes.\n
Date: Fri, 13 Nov 2020 23:24:22 +0000 Subject: [PATCH 144/430] Fixes. --- .../opencamera/test/MainActivityTest.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 8dd942621..fca451802 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -8019,6 +8019,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sat, 14 Nov 2020 14:04:30 +0000 Subject: [PATCH 145/430] Fix test bug (introduced since last version). --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index fca451802..0d65cf79a 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -12009,7 +12009,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Wed, 18 Nov 2020 22:58:30 +0000 Subject: [PATCH 146/430] Update date for release. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 8d874f4bc..8da1b163d 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -48,7 +48,7 @@ -Version 1.48.3 (Work in progress) +Version 1.48.3 (2020/11/18) FIXED Possible crash for panorama if failing to crop due to poor transformations; now fails gracefully. -- GitLab From e4566370a0d458ded809d9ce5b861178d0c42c6b Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Fri, 20 Nov 2020 22:41:34 +0000 Subject: [PATCH 147/430] Bump to version 80. --- _docs/history.html | 2 +- app/src/main/AndroidManifest.xml | 2 +- app/src/main/java/net/sourceforge/opencamera/MainActivity.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 8da1b163d..d825a7464 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -48,7 +48,7 @@ -Version 1.48.3 (2020/11/18) +Version 1.48.3 (2020/11/20) FIXED Possible crash for panorama if failing to crop due to poor transformations; now fails gracefully. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c0b092e9f..0864837d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index c66641ba1..bec1ac09c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -598,7 +598,7 @@ public class MainActivity extends Activity { // E.g., we have a "What's New" for 1.44 (64), but then push out a quick fix for 1.44.1 (65). We don't want to // show the dialog again to people who already received 1.44 (64), but we still want to show the dialog to people // upgrading from earlier versions. - int whats_new_version = 79; // 1.48.3 + int whats_new_version = 80; // 1.48.3 whats_new_version = Math.min(whats_new_version, version_code); // whats_new_version should always be <= version_code, but just in case! if( MyDebug.LOG ) { Log.d(TAG, "whats_new_version: " + whats_new_version); -- GitLab From 6781993e3aff7dace082704e5c8a4564ad3a7db5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Mon, 14 Dec 2020 21:13:13 +0000 Subject: [PATCH 148/430] Generalise wording. --- _docs/help.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index 383df266e..e65f349c4 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -530,9 +530,10 @@ see the "Max duration of video" option. Audio control sensitivity - This controls how sensitive Open Camera is to noises, if "Audio control" is set to "Loud noise". If you find it's taking photos too often unintentionally, or isn't responding to your sounds, try adjusting this option.
-Bluetooth LE remote control - Open Camera supports connecting to the Kraken Smart Housing via the options in -these settings. Once connected via Bluetooth, it should be possible to control Open Camera from the device. The on-screen -display of Open Camera will also display information from the Kraken (temperature and depth).
+Bluetooth LE remote control - Open Camera supports connecting to some specific "smart housing" cases via the + options in these settings. See "Remote device type" for supported types. At the time of writing, only one make/model + is supported. Once connected via Bluetooth, it should be possible to control Open Camera from the device. + The on-screen display of Open Camera will also display information from the housing (temperature and depth).
Lock photo/video orientation - Normally the orientation of the photo/video will be rotated by some multiple of 90 degree such that the orientation looks right - e.g. if your device is held in portrait, the resultant image/video will -- GitLab From dd4c2cf48e8e15b892719d9d2087bee0c4b38d67 Mon Sep 17 00:00:00 2001 From: Mark Harman
Date: Mon, 14 Dec 2020 21:13:25 +0000 Subject: [PATCH 149/430] Fix missing close bracket. --- _docs/help.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/help.html b/_docs/help.html index e65f349c4..6ec6a7d94 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -1236,7 +1236,7 @@ if you use this method, and then uninstall!
+Version 1.49 (Work in progress) + +UPDATED Improvement to Noise Reduction photo mode to avoid overexposing in lower light scenes. + Version 1.48.3 (2020/11/20) FIXED Possible crash for panorama if failing to crop due to poor transformations; now fails diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 88f080f65..d0433d012 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -5740,7 +5740,7 @@ public class CameraController2 extends CameraController { /** Sets up a builder to have manual exposure time, if supported. The exposure time will be * clamped to the allowed values, and manual ISO will also be set based on the current ISO value. */ - private void setManualExposureTime(CaptureRequest.Builder stillBuilder, long exposure_time) { + private void setManualExposureTime(CaptureRequest.Builder stillBuilder, long exposure_time, boolean set_iso, int new_iso) { if( MyDebug.LOG ) Log.d(TAG, "setManualExposureTime: " + exposure_time); Rangeexposure_time_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); // may be null on some devices @@ -5759,12 +5759,17 @@ public class CameraController2 extends CameraController { { // set ISO int iso = 800; - if( capture_result_has_iso ) + if( set_iso ) + iso = new_iso; + else if( capture_result_has_iso ) iso = capture_result_iso; // see https://sourceforge.net/p/opencamera/tickets/321/ - some devices may have auto ISO that's // outside of the allowed manual iso range! iso = Math.max(iso, iso_range.getLower()); iso = Math.min(iso, iso_range.getUpper()); + if( MyDebug.LOG ) { + Log.d(TAG, "iso: " + iso); + } stillBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, iso ); } if( capture_result_has_frame_duration ) @@ -5847,7 +5852,7 @@ public class CameraController2 extends CameraController { Log.d(TAG, "exposure_time_scale: " + exposure_time_scale); } modified_from_camera_settings = true; - setManualExposureTime(stillBuilder, exposure_time); + setManualExposureTime(stillBuilder, exposure_time, false, 0); } } //stillBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); @@ -6490,7 +6495,21 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "also set long exposure time"); modified_from_camera_settings = true; - setManualExposureTime(stillBuilder, exposure_time); + + boolean set_new_iso = false; + int new_iso = 0; + if( capture_result_has_exposure_time ) { + // ISO*exposure should be a constant + set_new_iso = true; + new_iso = (int)((capture_result_iso * capture_result_exposure_time)/exposure_time); + // but don't make ISO too low + new_iso = Math.max(new_iso, capture_result_iso/2); + new_iso = Math.max(new_iso, 1100); + if( MyDebug.LOG ) + Log.d(TAG, "... and set iso to " + new_iso); + } + + setManualExposureTime(stillBuilder, exposure_time, set_new_iso, new_iso); } else { if( MyDebug.LOG ) @@ -6517,7 +6536,7 @@ public class CameraController2 extends CameraController { Log.d(TAG, "exposure_time_scale: " + exposure_time_scale); } modified_from_camera_settings = true; - setManualExposureTime(stillBuilder, exposure_time); + setManualExposureTime(stillBuilder, exposure_time, false, 0); } } } -- GitLab From 34db494fbbab3af228deb85aa88ffcbe720e2eda Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 26 Dec 2020 16:47:41 +0000 Subject: [PATCH 151/430] Allow up to 1/3s manual exposure on some Samsung devices. --- _docs/history.html | 1 + .../cameracontroller/CameraController2.java | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index a3ddd3c81..737a828f1 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -50,6 +50,7 @@ Version 1.49 (Work in progress) +UPDATED Support longer exposure time (1/3s) on some Samsung Galaxy S devices. UPDATED Improvement to Noise Reduction photo mode to avoid overexposing in lower light scenes. Version 1.48.3 (2020/11/20) diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index d0433d012..05f212858 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -67,12 +67,14 @@ public class CameraController2 extends CameraController { private final boolean is_samsung; private final boolean is_samsung_s7; // Galaxy S7 or Galaxy S7 Edge + private final boolean is_samsung_galaxy_s; private CameraCharacteristics characteristics; // cached characteristics (use this for values that need to be frequently accessed, e.g., per frame, to improve performance); private int characteristics_sensor_orientation; private Facing characteristics_facing; + // camera features that we save (either to avoid repeatedly accessing, or we do our own modification) private Listzoom_ratios; private int current_zoom_value; private boolean supports_face_detect_mode_simple; @@ -81,6 +83,9 @@ public class CameraController2 extends CameraController { private boolean supports_photo_video_recording; private boolean supports_white_balance_temperature; private String initial_focus_mode; // if non-null, focus mode to use if not set by Preview (rather than relying on the Builder template's default, which can be one that isn't supported, at least on Android emulator with its LIMITED camera!) + private boolean supports_exposure_time; + private long min_exposure_time; + private long max_exposure_time; private final static int tonemap_log_max_curve_points_c = 64; private final static float [] jtvideo_values_base = new float[] { @@ -1757,9 +1762,11 @@ public class CameraController2 extends CameraController { this.is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung"); this.is_samsung_s7 = Build.MODEL.toLowerCase(Locale.US).contains("sm-g93"); + this.is_samsung_galaxy_s = is_samsung && Build.MODEL.toLowerCase(Locale.US).contains("sm-g"); if( MyDebug.LOG ) { Log.d(TAG, "is_samsung: " + is_samsung); Log.d(TAG, "is_samsung_s7: " + is_samsung_s7); + Log.d(TAG, "is_samsung_galaxy_s: " + is_samsung_galaxy_s); } thread = new HandlerThread("CameraBackground"); @@ -2716,9 +2723,19 @@ public class CameraController2 extends CameraController { camera_features.max_expo_bracketing_n_images = max_expo_bracketing_n_images; camera_features.min_exposure_time = exposure_time_range.getLower(); camera_features.max_exposure_time = exposure_time_range.getUpper(); + if( is_samsung_galaxy_s && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + // seems we can get away with longer exposure on some devices (e.g., Galaxy S10e claims only max of 0.1s, but works with 1/3s) + if( MyDebug.LOG ) + Log.d(TAG, "boost max_exposure_time, was: " + max_exposure_time); + camera_features.max_exposure_time = Math.max(camera_features.max_exposure_time, 1000000000L/3); + } } } } + // save to local fields: + 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; Range exposure_range = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); camera_features.min_exposure = exposure_range.getLower(); @@ -5743,11 +5760,8 @@ public class CameraController2 extends CameraController { private void setManualExposureTime(CaptureRequest.Builder stillBuilder, long exposure_time, boolean set_iso, int new_iso) { if( MyDebug.LOG ) Log.d(TAG, "setManualExposureTime: " + exposure_time); - Range exposure_time_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); // may be null on some devices Range iso_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); // may be null on some devices - if( exposure_time_range != null && iso_range != null ) { - long min_exposure_time = exposure_time_range.getLower(); - long max_exposure_time = exposure_time_range.getUpper(); + if( this.supports_exposure_time && iso_range != null ) { if( exposure_time < min_exposure_time ) exposure_time = min_exposure_time; if( exposure_time > max_exposure_time ) @@ -6136,14 +6150,7 @@ public class CameraController2 extends CameraController { 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/(double)n_half_images); - Range exposure_time_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); // may be null on some devices - 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); @@ -6157,7 +6164,7 @@ public class CameraController2 extends CameraController { // darker images for(int i=0;i Date: Tue, 29 Dec 2020 00:16:05 +0000 Subject: [PATCH 152/430] Update logging. --- .../main/java/net/sourceforge/opencamera/preview/Preview.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 635f54380..f95525f7c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -7659,7 +7659,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu public void setUIRotation(int ui_rotation) { if( MyDebug.LOG ) - Log.d(TAG, "setUIRotation"); + Log.d(TAG, "setUIRotation: " + ui_rotation); this.ui_rotation = ui_rotation; } -- GitLab From 724c291053da24eb04fa5eac21d003eff22b24d5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 2 Jan 2021 20:04:29 +0000 Subject: [PATCH 153/430] Fix warnings. --- .../net/sourceforge/opencamera/SettingsManager.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java b/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java index 17ffb84fa..9a5c0b7f0 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java +++ b/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java @@ -22,7 +22,6 @@ import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.Charset; import java.util.Map; -import java.util.Set; /** Code for options for saving and restoring settings. */ @@ -216,10 +215,9 @@ public class SettingsManager { xmlSerializer.startTag(null, doc_tag); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); - Set set = sharedPreferences.getAll().entrySet(); - for( Object aSet : set) { - Map.Entry entry = (Map.Entry) aSet; - String key = (String)entry.getKey(); + Map map = sharedPreferences.getAll(); + for( Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); Object value = entry.getValue(); if( key != null ) { String tag_type = null; @@ -245,9 +243,7 @@ public class SettingsManager { if( tag_type != null ) { xmlSerializer.startTag(null, tag_type); xmlSerializer.attribute(null, "key", key); - if( value != null ) { - xmlSerializer.attribute(null, "value", value.toString()); - } + xmlSerializer.attribute(null, "value", value.toString()); xmlSerializer.endTag(null, tag_type); } } -- GitLab From feb69ba2069af55ee2a3937ec876a2b6128b5029 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 13 Jan 2021 22:16:28 +0000 Subject: [PATCH 154/430] Update links. --- _docs/help.html | 3 --- _docs/index.html | 8 +------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index 6ec6a7d94..d94c73905 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -1131,9 +1131,6 @@ specified by using this option. Online help - Load this web page.
-Donate to support development - Loads the page for -my donation app.
-Camera API - If set to "Camera2 API", this enables support for the Camera2 API that was introduced in Android 5. Changing this setting will cause Open Camera to restart. Camera2 API enables more advanced features (including manual ISO/exposure, manual focus, HDR, exposure bracketing). diff --git a/_docs/index.html b/_docs/index.html index 87a77451e..534625f49 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -124,13 +124,8 @@ browsers -->
(Some features may not be available on all devices, as they may depend on hardware features, or the Android version.)
- + -Also see alternative download sites.
- -
-Open Camera is completely free, however if you wish you can show your appreciation by supporting me.
-
Open Camera Blog ~ @@ -142,7 +137,6 @@ browsers -->
- Requirements
- Instructions
-- Support me!
- Credits
- Privacy policy
- Licence and Terms of Service
-- GitLab From e81f522b90767f2afc70e382e72f3681c505ef01 Mon Sep 17 00:00:00 2001 From: Peter KlofutarDate: Sat, 16 Jan 2021 17:42:56 +0100 Subject: [PATCH 155/430] Update to the Slovenian translation --- app/src/main/res/values-sl/arrays.xml | 35 +- app/src/main/res/values-sl/strings.xml | 649 ++++++++++++++++++++----- 2 files changed, 563 insertions(+), 121 deletions(-) diff --git a/app/src/main/res/values-sl/arrays.xml b/app/src/main/res/values-sl/arrays.xml index 1a300e136..72e2f4ead 100644 --- a/app/src/main/res/values-sl/arrays.xml +++ b/app/src/main/res/values-sl/arrays.xml @@ -1,10 +1,21 @@ + + - drawable/focus_mode_auto
+- drawable/focus_mode_infinity
+- drawable/baseline_filter_vintage_white_48
+- drawable/focus_mode_locked
+- drawable/focus_mode_manual
+- drawable/focus_mode_fixed
+- drawable/focus_mode_edof
+- drawable/focus_mode_continuous_picture
+- drawable/focus_mode_continuous_video
+@@ -104,7 +115,7 @@ - Samodejno ostrenje
- Ostrenje v neskončnosti
- Ostrenje makro
-- Zaklenjeno ostrenje
+- Ostrenje je zaklenjeno.
- Ročno ostrenje
- Pritrjeno ostrenje
- Ostrenje EDOF
@@ -83,7 +94,7 @@- Ostrenje
- Povečaj/Pomanjšaj
- Spremeni raven osvetlitve
-- Vklopi/Izklopi samodejno izravnavo
+- Vklopi/Izklopi samodejno poravnavo
- Spremeni glasnost naprave
- Ne naredi ničesar
- 0
- 180
-+ - Rdeča
- Zelena
- Modra
@@ -132,7 +143,7 @@- Zlata (4)
- Trikotnik (1)
- Trikotnik (2)
-- Diagonala
+- Diagonale
- preference_grid_none
@@ -170,7 +181,7 @@- Privzeta
-- Stopinje/minute/sekunde
+- Stopinje/Minute/Sekunde
- Brez
@@ -205,6 +216,16 @@ +- 20
- 22
- 24
++ +- @string/preference_stamp_style_plain
+- @string/preference_stamp_style_shadowed
+- @string/preference_stamp_style_background
++ - preference_stamp_style_plain
+- preference_stamp_style_shadowed
+- preference_stamp_style_background
- Ne zaženi ponovno
@@ -244,8 +265,8 @@- Brez
-- Glasen šum
-- Glasovni ukaz: \"cheese\"
+- Glasen zvok
+- Glasovni ukaz: \"cheese\"
- none
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 38cb93fa2..859845f04 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -1,7 +1,7 @@- -- GitLab From 0dfb6e38e4e19e2d7e11232b93457a8510b0e6f5 Mon Sep 17 00:00:00 2001 From: Peter KlofutarOpen Camera +Open Camera Nastavitve Nastavitve pojavnih oken V redu (to sporočilo se ne bo več prikazovalo) @@ -12,17 +12,17 @@Izberite mesto za shranjevanje: Počisti zgodovino mape Počisti zgodovino mape? -Izberite drugo mapo +Izberite drugo mapo. Mesto za shranjevanje spremenjeno na: -Snemanje videoposnetka ustavljeno -ponovitev preostalih -Povezava s fotoaparatom je spodletela -Napaka, datoteka videoposnetka je morda pokvarjena -ni podprto na vaši napravi -Neznana napaka, videoposnetek ustavljen -Strežnik se je sesul, videoposnetek ustavljen -Videoposnetek je dosegel najdaljše trajanje -Videoposnetek je dosegel največjo velikost datoteke +Snemanje videoposnetka je ustavljeno. +ponovitev je preostalih. +Povezava s fotoaparatom je spodletela. +Napaka, datoteka videoposnetka je morda pokvarjena. +ni podprto na vaši napravi. +Neznana napaka, videoposnetek je ustavljen. +Strežnik se je sesul, videoposnetek je ustavljen. +Videoposnetek je dosegel najdaljše trajanje. +Videoposnetek je dosegel največjo velikost datoteke. Izavnava osvetlitve sl./s Razdalja ostrenja @@ -31,28 +31,28 @@Zadnji fotoaparat Fotografija Videoposnetek -Osvetlitev zaklenjena -Osvetlitev odklenjena -Samosprožilec preklican -Zaporedni način preklican -Odštevanje se je začelo -Shranjevanje datoteke videoposnetka je spodletelo -Snemanje videoposnetka je spodletelo -Snemanje videoposnetka se je začelo +Osvetlitev je zaklenjena. +Osvetlitev je odklenjena. +Samosprožilec je preklican. +Zaporedni način je preklican. +Odštevanje se je začelo. +Shranjevanje datoteke videoposnetka je spodletelo. +Snemanje videoposnetka je spodletelo. +Snemanje videoposnetka se je začelo. Oprostite -Shranjevanje fotografije je spodletelo -Shranjevanje fotografije v obliki RAW je spodletelo -Samodejna izravnava je spodletela -Zagon predogleda fotoaparata je spodletel -Slikanje fotografije -Slikanje fotografije je spodletelo -Lokacija GPS ni na voljo -Fotografija izbrisana -Program galerije ni na voljo -Zaslon je zaklenjen\nPovlecite ga za odklepanje -Odklenjen -Žal na tej napravi samodejna\nizravnava ni podprta -Zvok onemogočen +Shranjevanje fotografije je spodletelo. +Shranjevanje fotografije v obliki RAW je spodletelo. +Samodejna izravnava je spodletela. +Zagon predogleda fotoaparata je spodletel. +Slikanje fotografije. +Slikanje fotografije je spodletelo. +Lokacija GPS ni na voljo. +Fotografija je izbrisana. +Program za galerijo ni na voljo. +Zaslon je zaklenjen.\nPovlecite ga za odklepanje. +Odklenjen. +Žal na tej napravi samodejna\nizravnava ni podprta. +Zvok je onemogočen. Najdaljše trajanje Barvni učinek Način prizora @@ -61,7 +61,7 @@Kot Smer ODPIRANJE FOTOAPARATA JE SPODLETELO. -FOTOAPARAT MORDA UPORABLJA +ALI FOTOAPARAT MORDA UPORABLJA DRUG PROGRAM? ISO Povečava @@ -69,12 +69,12 @@[ZAKLENJENO: povlecite za odklepanje] -Dodajanje časovnega žiga na fotografijo je spodletelo +Dodajanje časovnega žiga na fotografijo je spodletelo. m Učinki fotoaparata Samodejno izravnaj -Slike bodo zavrtene za samodejno vodoravno poravnavo (samo fotografije) (slikanje fotografij je počasnejše in lahko spodleti na napravah s premalo pomnilnika) +Slike bodo zavrtene za samodejno vodoravno poravnavo (samo fotografije - slikanje fotografij je počasnejše in lahko spodleti na napravah s premalo pomnilnika) Uporabi barvni učinek Uporabi izbrani bavni učinek za fotografije Uporabi način prizora @@ -90,7 +90,7 @@Zaznavanje obrazov Uporabi zaznavanje obrazov namesto področij ostrenja -Nadzor fotoaparata +Nastavitve fotoaparata Samosprožilec Pisk samosprožilca Zapiskaj ob odštevanju samosprožilca ali zakasnitvi zaporednega načina @@ -98,21 +98,22 @@Glasovno odštevanje samosprožilca ali zakasnitve zaporednega načina (od 60-ih s naprej) Zaporedje Razmik zaporednega načina -Več nadzora fotoaparata … +Več nastavitev fotoaparata … Dotik za zajem Posnemi fotografijo z dotikom ali dvojnim dotikom predogleda Premor po slikanju fotografije Premor zaslona po slikanju fotografje z možnostjo deljenja ali izbrisa fotografije -Zvok zaslonke +Zvok zaklopa Predvajaj zvok ob slikanju fotografije Gumba za glasnost Možnosti nadzora zvoka +Nadzor občutljivosti zvoka -Možnost občutljivosti ravni šuma za zvok (glasen šum) +Občutljivost ravni za možnost zvoka (glasen zvok) Mesto za shranjevanje Mapa za shranjevanje datotek fotografij/videoposnetkov -Uporabi ogrodje za dostop do pomnilniške naprave -Ali naj se uporabi ogrodje za dostop do pomnilniške naprave za shranjevanje fotografij in videoposnetkov +Uporabi ogrodje za dostop do pom. naprave +Ali naj se za shranjevanje fotografij in videoposnetkov uporabi ogrodje za dostop do pomnilniške naprave. Omogočite to, da dovolite shranjevanje na zunanje kartice SD. Na Androidih 10+ morate to možnost omogočiti, če želite shranjevati v mapo zunaj mape DCIM. Predpona shranjenih fotografij Predpona imena datotek fotografij Predpona shranjenih videoposnetkov @@ -121,9 +122,9 @@Prikaži fotoaparat, ko je zaklenjeno Če je omogočeno, se bo Open Camera še vedno prikazoval nad zaklenjenim zaslonom (še vedno boste morali odkleniti za dostop do nastavitev, galerije itd.) Izvedi samodejno ostrenje ob zagonu -Ali naj se izvede samodejno ostrenje ob zagonu Open Camere. Če imate težavo z vklopom bliskavice ob zagonu, onemogočite to možnost +Ali naj se izvede samodejno ostrenje ob zagonu Open Camere. Če imate težavo z vklopom bliskavice ob zagonu, onemogočite to možnost Zakleni zaslon ob snemanju videoposnetkov -Ob snemanju videoposnetkov se bo gr. up. vmesnik zaklenil, da se prepreči naključna ustavitev snemanja. Povlecite zaslon za odklepanje. Upoštevajte, da se bo snemanje videoposnetkov vedno ustavilo, če se program postavi v ozadje ali zaslon izprazni. +Ob snemanju videoposnetkov se bo gr. up. vmesnik zaklenil, da se prepreči naključna ustavitev snemanja. Povlecite zaslon za odklepanje. Upoštevajte, da se bo snemanje videoposnetkov vedno ustavilo, če se program postavi v ozadje ali zaslon izprazni. Zavrti predogled Možnost vrtenja predogleda (ne bo vplivalo na nastale fotografije/videoposnetke)\n%s Gr. up. vmesnik na zaslonu … @@ -137,7 +138,7 @@Prikaži drsnik povečave Prikaži drsnik za nadzor povečave Prikaži ISO -Prikaži trenutno raven ISO. V načinu bliskavice bo simbol bliskavice tudi nakazal, da se bo sprožila bliskavica (zahteva API Camera2). +Prikaži trenutno raven ISO. V načinu bliskavice bo simbol bliskavice tudi nakazal, da se bo sprožila bliskavica (zahteva API Camera2). Prikaži nezaseden prostor Na zaslonu prikaži količino nezasedenega prostora na pomnilniški napravi Prikaži kot @@ -146,8 +147,8 @@Prikaži črto vodoravne ravni Barva poudarjanja kota/višine Barva poudarjanja, ko je fotoaparat skoraj vodoraven -Prikaži kompas -Na zaslonu prikaži kompas s smerjo naprave +Prikaži smer neba +Na zaslonu prikaži smer neba naprave Prikaži čas Na zaslonu prikaži trenutni čas Prikaži baterijo @@ -159,7 +160,7 @@Prikaži \"pojavna\" sporočila Ali naj se prikažejo \"pojavna\" sporočila Prikaži animacijo sličice -Prikaži premikajočo animacijo sličice med slikanjem fotografije +Prikaži premikajočo se animacijo sličice med slikanjem fotografije Prikaži obrobo med slikanjem fotografije Na zaslonu prikaži obrobo, da se nakaže slikanje fotografije Pusti zaslon vklopljen @@ -173,12 +174,12 @@Nastavitve lokacije … Ločljivost fotoaparata Kakovost slik -Nastavite kakovost shranjenih slik JPEG (privzeta vrednost je 90 %%)\n%s +Nastavite kakovost shranjenih slik JPEG ali WebP (privzeta vrednost je 90 %%). Nima učinka na obliko PNG.\n%s RAW -Shrani podatke o lokaciji (označevanje z zemljepisnim položajem) -Shrani podatke o lokaicji GPS v fotografijah/videoposnetkih -Shrani smer kompasa -Shrani smer komapasa GPS v fotografijah +Shrani podatke o lokaciji (označevanje z zemljepisno lego) +Shrani podatke o lokaciji GPS v fotografijah/videoposnetkih (v fotografijah se podatki o lokaciji lahko shranijo samo pri oblikah JPEG in DNG) +Shrani smer neba +Shrani smer neba GPS v fotografijah (samo oblika JPEG) Zahtevaj podatke o lokaciji Če so podatki o lokaciji omogočeni, snemaj fotografije/videoposnetke samo, če so ti podatki na voljo Časovni žig fotografij @@ -198,13 +199,13 @@Ali naj se fotografije shranjujejo z uporabo niti v ozadju (za hitrejše delovanje) Ločljivost videoposnetkov Vsili vidosposnetke 4K UHD (deluje samo na nekaterih napravah) -Omogoči ločljivost 3840x2160 za snemanje videoposnetkov z zadnjo kamero - ta možnost je zaobidenje, ki bo morda omogočila naprave 4K s kamerami 4K, ki te možnosti ne razkrijejo programom za kamero tretjih oseb. Za delovanje tega ni nobenega zagotovila, preizkusite pred uporabo. +Omogoči ločljivost 3840x2160 za snemanje videoposnetkov z zadnjo kamero - ta možnost je zaobidenje, ki bo morda omogočila naprave 4K s kamerami 4K, ki te možnosti ne razkrijejo programom za kamero tretjih oseb. Za delovanje tega ni nobenega zagotovila, preizkusite pred uporabo. Omogoči ustalitev videoposnetkov Ustaltev videoposnetkov zmanjša tresenje zaradi gibanja kamere tako v predogledu, kot v posnetku. Bitna hitrost videoposnetkov (pribl.) Nastavite pribl. hitrost videoposnetkov (višje vrednosti pomenijo boljšo kakovost, vendar zavzamejo več prostora na disku; snemanje videoposnetkov lahko spodleti, če bitna hitrost ni podprta)\n%s Hitrost sličic videoposnetkov (pribl.) -Nastavite hitrost sličic (sl./s) videoposnetkov (je lahko približna. Doseganje nastavljene vrednosti ni zagotovljeno in snemanje videoposnetkov lahko spodleti, če hitrost sličic ni podprta)\n%s +Nastavite hitrost sličic (sl./s) videoposnetkov (je lahko približna. Doseganje nastavljene vrednosti ni zagotovljeno in snemanje videoposnetkov lahko spodleti, če hitrost sličic ni podprta). Preverite nastali videoposnetek za dejansko uporabljeno hitrost sličic. Upoštevajte, da se ta nastavitev pri počasnih posnetkih prezre.\n%s Najdaljše trajanje videoposnetkov Snemanje videoposnetkov se bo ustavilo po tem času\n%s Ponovno zaženi videoposnetek po najdaljšem trajanju @@ -216,7 +217,7 @@Snemaj zvok Snemaj zvok ob snemanju vidoposnetkov Vir zvoka -Kateri mikrofon naj se uporabi za snemanje zvoka\n%s +Kateri mikrofon naj se uporabi za snemanje zvoka. Upoštevajte, da je podrobno vedenje te možnosti odvisno od načina vdelave te možnosti v napravo.\n%s Zvočni kanali Določite mono ali stereo za snemanje zvoka (stereo je podprt samo na nekaterih napravah) Svetilka med snemanjem videoposnetkov @@ -224,11 +225,11 @@Razno Spletna pomoč -Zaženi spletno stran Open Camera v mojem brskalniku +Zaženi spletno stran Open Camera v brskalniku Daruj za podporo razvoju -Če vam je ta program všeč, razmislite o darovanju, da podprete razvoj. To lahko naredite z nakupom mojega "programa za darovanje" - kliknite na to možnost za odpiranje strani z mojim programom. Hvala! +Če vam je ta program všeč, razmislite o darovanju, da podprete razvoj. To lahko naredite z nakupom mojega "programa za darovanje" - kliknite na to možnost za odpiranje strani z mojim programom. Hvala! Uporabi API Camera2 -Omogoči API Camera2 - ponuja dodatne značilnosti, vendar morda ne bo pravilno deloval na vseh napravah (lahko povzroči ponovni zagon) +Omogoči dodatne značilnosti, kot so ročni načini za osvetlitev, ostrino, ravnovesje beline, kot tudi obliko RAW (če jo naprava podpira), vendar morda ne bo pravilno delovala na vseh napravah (lahko povzročijo ponovni zagon) O programu Program in podatki za razhroščevanje Ponastavi nastavitve @@ -254,20 +255,19 @@Nadrejena mapa Nova mapa -V to mapo ni mogoče pisati -Dostop do te mape ni mogoč -Vnesite novo ime mape -Ustvarjanje mape je spodletelo -Mapa že obstaja - -Izberite mesto za shranjevanje -Ogrodje za dostop do pomnilniške naprave preklicano - -Ni mogoče shraniti v to mapo +V to mapo ni mogoče pisati. +Dostop do te mape ni mogoč. +Vnesite novo ime mape. +Ustvarjanje mape je spodletelo. +Mapa že obstaja. + +Izberite mesto za shranjevanje. +Ogrodje za dostop do pomnilniške naprave je preklicano. +Ni mogoče shraniti v to mapo. -Dovoljenje za mikrofon ni na voljo -Dovoljenje za lokacijo ni na voljo +Dovoljenje za mikrofon ni na voljo. +Dovoljenje za lokacijo ni na voljo. Začni snemati videoposnetek Ustavi snemanje videoposnetka @@ -276,48 +276,51 @@MB GB -Začni poslušati zvok -Ustavi poslušanje zvoka -Recite \"cheese\" -Naredite glasen zvok +Začni zaznavati zvok +Ustavi zaznavanje zvoka +Recite \"cheese\". +Naredite glasen zvok. -Samodejna izravnava bo zavrtela fotografije, da bodo videti poravnane.\n\nUpoštevajte, da se bo ločljivost slik rahlo zmanjšala (zaradi zahtevanega vrtenja in obrezovanja). -Datoteke DNG vsebujejo nestisnjene in neobdelane podatke iz fotoaparata.\n\nVečina programov za galerijo ne prepozna oblike DNG, zato jo uporabljajte s posebnimi programi.\n\nUpoštevajte, da se bodo razne možnosti obdelave, kot sta "Dodaj žig" in "Samodejno izravnaj", uporabile samo za slike JPEG in ne za slike DNG.\n\nDatoteke DNG zavzamejo veliko prosotra; za njihov izbris ali prenos lahko uporabite raziskovalca datotek.\n\nDatoteke DNG se shranjujejo samo v običajnem načinu ali načinu DRO. -Način HDR je uporaben za posnetke z velikimi odstopanji v svetlosti. Deluje tako, da zajame več posnetkov z različnimi osvetlitvami in jih potem združi v končno sliko.\n\nUpoštevajte, da HDR ni primeren za hitro gibajoče prizore in lahko povzroči barvna odstopanja.\n\nSlikanje fotografij v načinu HDR je počasnejše. +Samodejna poravnava bo zavrtela fotografije, da bodo videti poravnane.\n\nUpoštevajte, da se bo ločljivost slik rahlo zmanjšala (zaradi zahtevanega vrtenja in obrezovanja). +Datoteke DNG vsebujejo nestisnjene in neobdelane podatke iz fotoaparata.\n\nVečina programov za galerijo ne prepozna oblike DNG, zato jo uporabljajte s posebnimi programi.\n\nUpoštevajte, da se bodo razne možnosti obdelave, kot sta "Dodaj žig" in "Samodejno izravnaj", uporabile samo za slike, ki niso v obliki DNG (JPEG itd.). +Način HDR je uporaben za posnetke z velikimi odstopanji v svetlosti. Deluje tako, da zajame več posnetkov z različnimi osvetlitvami in jih potem združi v končno sliko.\n\nUpoštevajte, da HDR ni primeren za hitro gibajoče prizore in lahko povzroči barvna odstopanja.\n\nSlikanje fotografij v načinu HDR je počasnejše. Ne prikazuj več -Način za fotografije -Std +Način fotografiranja +Obič. Običajni HDR -Expo {} -Korektura samodejne osvetlitve +{} osvet. +Stopnjevanje osvetlitve +[]]] +Hitro zaporedje NR Zmanjševanje šuma Dodaj žig Shrani vse slike za način HDR -Če je omogočeno, bodo ob uporabi načina HDR posnete tri osnovne slike kot tudi končna slika HDR. Upoštevajte, da bo shranevanje počasnejše, posebno, če uporabljate možnosti, kot sta \"Dodaj žig\" ali samodejno izravnavo. - -DOVOLJENJA NISO NA VOLJO -Zahtevano je dovoljenje -Za omogočanje fotoaparata je zahtevano dovoljenje za fotoaparat -Za shranjevanje fotografij je zahtevano dovoljenje za branje/pisanje na pomnilniško napravo -Za snemanje videoposnetkov z zvokom je zahtevano dovoljenje za mikrofon -Za označevanje z lokacijo je zahtevano dovoljenje za lokacijo +Če je omogočeno, bodo ob uporabi načina HDR posnete tri osnovne slike kot tudi končna slika HDR. Upoštevajte, da bo shranevanje počasnejše, še posebno, če uporabljate možnosti, kot sta \"Dodaj žig\" ali samodejno izravnavo. + +DOVOLJENJA NISO NA VOLJO. +Zahtevano je dovoljenje. +Za omogočanje fotoaparata je zahtevano dovoljenje za fotoaparat. +Za shranjevanje fotografij je zahtevano dovoljenje za branje/pisanje na pomnilniško napravo. +Za snemanje videoposnetkov z zvokom in uporabo možnosti \"Nadzor zvoka\" je zahtevano dovoljenje za mikrofon. +Za označevanje z zemljepisno lego (shranjevanje podatkov o zemljepisni legi + v fotografijah in videoposnetkih) je zahtevano dovoljenje za lokacijo. To dovoljenje je zahtevano tudi za povezavo z napravami za oddaljeni nador Bluetooth LE. Možnosti razhroščevanja Možnosti razhroščevanja Uporabi nadomestni način bliskavice Omogočite, če se vaša naprava neobičajno vede z API-jem Camera2 -Korektura sam. osvetlitve -Število slik korekture samodejne osvetlitve\n%s -Korak kor. sam. osvetlitve +Stopnjevanje osvetlitve +Število slik stopnjevanja osvetlitve\n%s +Korak stopnjevanja osvetlitve Število pozitivnih/negativnih korakov za najtemnejšo/najsvetlejšo sliko\n%s Izvirne slike zmanjševanja šuma -Ali naj se izvirne slike shranijo v načinu zmanjševanja šuma. Upoštevajte, da se bodo pri tem slike shranjevale dalj časa.\n%s +Ali naj se izvirne slike shranijo v načinu zmanjševanja šuma. Upoštevajte, da se bodo pri tem slike shranjevale dalj časa.\n%s Ne shrani izvirnih slik Shrani eno izvirno sliko @@ -325,34 +328,34 @@s -Ni dovolj nezasedenega prostora za snemanje videoposnetka -Videoposnetek se je ustavil\nRaven baterije je kritično nizka +Ni dovolj nezasedenega prostora za snemanje videoposnetka. +Videoposnetek se je ustavil.\nRaven baterije je kritično nizka. Preveri za kritično stanje baterije -Ustavi snemanje videoposnetka, če je raven baterije kritično nizka. Zmanjša tveganje okvare videoposnetka, če se naprava izklopi zaradi pomanjkanja energije. +Ustavi snemanje videoposnetka, če je raven baterije kritično nizka. Zmanjša tveganje okvare videoposnetka, če se naprava izklopi zaradi pomanjkanja energije Temna Svetla -Hitrost zaslonke +Hitrost zaklopa Resna napaka fotoaparata -Omogoči hitro zaporedje HDR/kor. sam. osvetlitve -Omogoča hitrejše slikanje fotografij s HDR-om/kor. sam. osvetlitve. Onemogočite, če ima vaša naprava pri slikanju fotografij z načinoma HDR/kor. sam. osvetlitve težave. +Omogoči hitro zaporedje HDR/osvetlitve +Omogoča hitrejše slikanje fotografij s HDR-om/stopnjevanjem osvetlitvami. Onemogočite, če ima vaša naprava pri slikanju fotografij z načinoma HDR/stopnjevanje osvetlitve težave. Fotoaparat Posnemi videoposnetek Samoposnetek Zrcaljenje sprednjega fotoaparata -Ali naj se ob uporabi sprednjega fotoaparata slike zrcalijo\n%s +Ali naj se ob uporabi sprednjega fotoaparata nastale slike zrcalijo\n%s -Kot ravni umerjanja -Ta možnost umeri pospeškomer naprave za pravilno delovanje samodejne izravnave in prikaza ravni/kota na zaslonu. +Umeri kot ravni +Ta možnost umeri pospeškomer naprave za pravilno delovanje samodejne poravnave in prikaza ravni/kota na zaslonu. Napravo postavite na ravno vodoravno površino in izberite Umeri.\n\nPritisnite Ponastavi za odstranitev umerjanja iz naprave.\n\nPritisnite gumb Nazaj za preklic. Umeri Ponastavi -Raven umerjena -Ponastavitev ravni umerjanja +Raven je umerjena. +Umerejanje ravni je ponastavljeno. DRO @@ -389,17 +392,17 @@1 ura Prikaži ikono za slikanje fotografije -Prikaži ikono za slikanje fotografij in snemanje videoposnetkov. Uporabno, če ima vaša naprava fizični sprožilec, ki bi ga raje uporabili. +Prikaži ikono za slikanje fotografij in snemanje videoposnetkov. Uporabno, če ima vaša naprava fizični sprožilec, ki bi ga raje uporabili. -Videoposnetek v premoru +Premor videoposnetka Nadaljevanje videoposnetka Premor snemanja Nadaljuj snemanje Zajemanje … -Začenjanje poslušanja zvoka je spodletelo -Ustvarjanje slike HDR je spodletelo +Začenjanje zaznavanja zvoka je spodletelo. +Ustvarjanje slike HDR je spodletelo. Samodejno Oblačno @@ -438,6 +441,9 @@Solariziraj Bela tabla +Novosti: +Daruj +obraz obrazi @@ -445,28 +451,110 @@na desni strani zaslona na zgornji strani zaslona na spodnji strani zaslona +na sredini + +Visoka hitrost Privzeta + +Oznake EXIF po meri +Avtor +Besedilo, ki se shrani med metapodatke slike v oznako "Avtor" (samo oblika JPEG) +Avtorske pravice +Besedilo, ki se shrani med metapodatke slike v oznako "Avtorske pravice" (samo oblika JPEG) -Samo JPEG -JPEG in DNG (RAW) +Obdelovanje … + +Ne +Običajno in DNG (RAW) +Samo DNG (RAW) Privzeta yyyy-mm-dd dd.mm.yyyy mm/dd/yyyy Brez + +Št. fotografij +2 +3 +4 +5 +6 +8 +10 +12 +15 +20 + +Št. fotografij +2 +3 +4 +5 +6 +8 +10 +12 +15 +20 +25 +30 +40 +50 +100 +150 +200 -Bliskavica izk. +Hitrost +Običajna +Počasni posnetek je omogočen. +Počasni posnetek je onemogočen. + +Bliskavica je izklopljena. Samodejna bliskavica -Bliskavica vkl. +Bliskavica je vklopljena. Svetilka Zm. uč. rdečih oči +Sam. zaslonska bliskavica +Zaslonska bliskavica je vklopljena +Zaslonska svetilka + +Odstranjevanje pasov +Algoritmi za pomoč pri utripanju svetlobe.\n%s +Samodejno +50 Hz +60 Hz +Izk. + +Prekrivna slika +Prekrivna slika za pomoč pri uravnavi.\n%s +Izk. +Nazadje posneta fotografija +Izbrana slika + +Na tej napravi ni bilo mogoče najti pogovornega okna za iskanje datotek. Možnost prekrivne slike ni podprta +Te slike ni mogoče odpreti. + +Dovoli fotografiranje med snemanjem videoposnetka +Dovoli fotografiranje med snemanjem videoposnetka. Onemogočite to, če naletite na težave pri snemanju videoposnetkov, ko imate na napravi omogočen API Camera2. + +Profili slike videoposnetkov +Nastavite običajni ali difuzni profil za način videoposnetkov\n%s +Privzeti +Log (podrobno) +Log (nizko) +Log (srednje) +Log (močno) +Log (zelo močno) +Profil Log Kamera Zunanji mikrofon (če je prisoten) Privzeti vir zvoka -Optimizirano za glas +Optimizirano za glasovne klice +Optimizirano za prepoznavanje glasu +Neobdelano Privzeta 100 kb/s @@ -492,6 +580,63 @@80 Mb/s 90 Mb/s 100 Mb/s +150Mbps +200Mbps + +brez taporedja + +Prikaži merilnik ravni zvoka +Ali naj se med snemanjem videoposnetkov na zaslonu prikaže raven zvoka + +{} ostrenja +Stopnjevanje globine ostrenja + +Izvorna razdalja ostrenja +Ciljna razdalja ostrenja + +Dodaj neskončno razdaljo + +Smer neba +Kompas vaše naprave je treba umeriti, da se izboljša natančnost. To lahko storite z gibanjem naprave v obliki številke 8.\n\nTrenutna natančnost: + +Nezanesljiva +Nizka +Srednja +Visoka +Neznano + +Enote razdalje +Uporablja se za nadmorsko višino GPS pri žigih na fotografijah in pri podnapisih na videoposnetkih\n%s +Metri +Čevlji +čev. + +Prikaži pogovorno okno novosti +Ali naj se prikažejo podatki o novih značilnostih in izboljšavah ob posodobitvi programa + +Algoritem robnega načina +Algoritem, ki naj ga gonilnik fotoaparata uporabi za poudarjanje robov. Poudarjanje robov izboljša ostrino in podrobnosti zajete slike (ta nastavitev se prezre v načinu zmanjševanja šumov).\n%s + +Obdelovanje nastavitev … + +Algoritem zmanjševanja šumov +Algoritem, ki naj ga gonilnik fotoaparata uporabi za zmanjševanje šumov. Algortem za zmanjševanje šumov poizkuša izboljšati kakovost slike z odstranjevanjem odvečnega šuma, ki ga doda postopek zajema, še posebno v temnih pogojih (upoševajte, da to ni povezano z načinom odstranjevanja šuma, v katerem se ta nastavitev pravzaprev prezre).\n%s +Privzeto +Izk. +Najmanjše +Hitro +Visoka kakovost + +Pomoč pri ostrenju +Ali naj se predogled poveča pri ročnem uravnavanju ostrine\n%s +Izk. +2x +4x + +Povečaj ali pomanjšaj izravnavo osvetlitve + +Nazaj +Naprej Izk. 2x @@ -509,6 +654,7 @@Neomejeno Brez zakasnitve +0,5 s 1 s 2 s 3 s @@ -532,6 +678,11 @@300 MB 500 MB 1 GB +2 GB +5 GB +9 GB + +Stopnjevanje globine ostrenja je preklicano. Brez 1 (1:1) @@ -541,6 +692,7 @@1,5 (3:2) 1,78 (16:9) 1,85 (37:20) +2 (2:1) 2,33 (21:9) 2,35 (47:20) 2,4 (12:5) @@ -552,15 +704,284 @@-1 -2 -3 (nizka občutljivost) + +Izboljšava kontrasta HDR +Kdaj naj se uporabi algoritem za izboljšavo kontrasta v načinu HDR. To lahko izboljša videz v primerih zelo visokega dinamičnega razpona prizora in rezultatom doda "HDR-videz".\n%s +Izk. +Pametno +Vedno + +Oblika videposnetkov +Oblika datotek zvoka in videoposnetkov, in kodeki\n%s + +Privzeto +MPEG4 H264 +MPEG4 HEVC +3GPP +WebM (zvok ni podprt) + +Upravitelj nastavitev + +Shrani nastavitve +Shrani vse nastavitve Open Camere v datoteko +Ime ahranjenih nastavitev + +Obnovi nastavitve +Obnovi predhodno shranjene nastavitve. To bo prepisalo vse trenutne nastavitve s shranjenimi! +Ta možnost omogoča izbiro ene od datotek z nastavitvami, ki ste jih predhodno shranili. Upoštevajte, da bo nalaganje datoteke prepisalo vse trenutne nastavitve! + +Nastavitve so shranjene +Shranjevanje nastavitev je spodletelo. +Obnovitev nastavitev je spodletela. +Na tej napravi ni bilo mogoče najti pogovornega okna za izbiro datotek. Ta možnost ni podprta. + +Oblika slik +JPEG +WebP +PNG + + +Način zmanj. šuma +Običajni +Nizka svetloba Up. vmesnik za levičarje Up. vmesnik za desničarje +Ikone na vrhu (pokončno) + +Prikaži ikono za zaznavane obrazov +Ali naj se na zaslonu prikaže ikona za omogočanje/onemogočanje zaznavanja obrazov +Omogoči zaznavanje obrazov +Onemogoči zaznavanje obrazov +Zaznavanje obrazov je omogočeno. +Zaznavanje obrazov je onemogočeno. + +Prikaži ikono za samodejno poravnavo +Ali naj se na zaslonu prikaže ikona za omogočanje/onemogočanje samodejne poravnave. Ko je samodejna poravnava omogočena, se bodo fotografije zavrtele, da se samodejno poravnajo z obzorjem. +Omogoči samodejno poravnavo +Onemogoči samodejno poravnavo + +Prikaži ikono za žig na fotografijah +Ali naj se na zaslonu prikaže ikona za omogočanje/onemogočanje žiga na fotografijah +Omogoči žig na fotografijah +Onemogoči žig na fotografijah +Žig na fotografijah je omogočen. +Žig na fotografijah je onemogočen. + +Prikaži ikono za žig z besedilom po meri +Ali naj se na zaslonu prikaže ikona za določanje besedila po meri, ki naj se prikaže v žigu na fotografijah + +Prikaži ikono za zaklepanje ravnovesja beline +Ali naj se na zaslonu prikaže ikona za zaklepanje/odklepanje samodejnega uravnavanja beline +Zakleni ravnovesje beline +Odkleni ravnovesje beline +Ravnovesje beline je zaklenjeno. +Ravnovesje beline je odklenjeno. + +Prikaži ikono za zaklepanje samodejne osvetlitve +Ali naj se na zaslonu prikaže ikona za zaklepanje/odklepanje osvetlitve +Odkleni osvetlitev + +Način nizke svetlobe: držite fotoaparat pri miru + +Pano. +Panorama + +Oddaljeni nadzor Bluetooth LE … +Omogoči oddaljeni nadzor Bluetooth LE +Omogoči naprave za oddaljeni nadzor Bluetooth LE +Vrsta oddaljene naprave + +BLE ni podprt +Poišči naprave BLE +Bluetooth ni podprt. +Neznana naprava +Zatemni zaslon, če se povezava z oddaljeno napravo prekine +Nasvet: pred zagonom Open Camere privzeto svetlost nastavite na najmanjšo raven. +Za izračunavanje globine uporabi slano vodo +Izboljša natančnost pri vodotesnih ohišjih, če je izbrana pravilna vrsta vode. +Oddaljena naprava je povezana. + +Pametno ohišje Kraken + +Uporabi naslove + +Poleg koordinat GPS prikaži naslove, če jo možno +Prednost ima naslov pred koordinatami GPS +Ne prikaži naslova + +Prikaži ikono za shranjevanje podakov o lokaciji +Ali naj se na zaslonu prikaže ikona za omogočanje/onemogočanje podatkov o lokaciji (označevanje z zemljepisno lego) +Shrani podatke o lokaciji +Prenehaj shranjevati podatke o lokaciji + +Prikaži histogram +Ali naj se prikaže histogram barv na zaslonu\n%s +Izk. +Barve RGB +Svetilnost +Vrednost (največja) +Jakost (povprečno) +Svetlost (povprečje od najm.-najv.) + +Prikaži zebrine črte +Če je ta možnost omogočena, bodo na zaslonu prikazane zebrine črte, kjer je predogled slike preosvetljen.\n%s +Izk. +70 % +80 % +90 % +93 % +95 % +97 % +98 % +99 % +100 % + +Izboljšava ostrenja +Če je ta možnost omogočena, bodo na izostrenih robovih prikazani poudarki (orisi). To je v glavnem uporabno za ročno ostrenje in za pomoč pri določanju, kateri deli slike zo izostreni.\n%s +Izk. +Vkl. +Barva izboljšave ostrenja +Barva za prikaz na izostrenih robovih\n%s + +Preostalo + +Predogled … + +Preklop bliskavice +Prikaži ikono za bliskavico +Ali naj se na zaslonu namesto prikaza možnosti bliskavice v pojavnem oknu prikaže ikona za preklop med možnostmi bliskavice. Upoštevajte, da med uporabo te ikone svetilka ni na voljo. + +Dovoli obliko RAW za stopnjevanje osvetlitve +Ali naj se možnost RAW uporabi tudi v načinu stopnjevanja osvetlitve (ali v načinu HDR, kadar je omogočena možnost \"Shrani vse slike za način HDR\"). Če je ta možnost onemogočena, bodo v teh načinih slike shranjene samo v obliki JPEG. + +Dovoli RAW for stopnjevanje globine ostrenja +Ali naj se možnost RAW uporabi tudi v načinu stopnjevanja globine ostrenja. Če je ta možnost onemogočena, bodo v načinih stopnjevanja globine ostrenja shranjene samo slike JPEG. + +Preklop načinov RAW +Prikaži ikono RAW +Ali naj se na zaslonu prikaže ikona za preklop med načini RAW + +Trenutna oddaljena naprava: +Išči Osenčeno besedilo Golo besedilo +Besedilo z osenčenim ozadjem + +Shranjevanje slik … + +Dokončaj panoramo +Prekliči panoramo +Panorama je preklicana. +Ustvarjanje panoramske slike je spodletelo. + +Samodejno obreži panoramo +Obrezovanje nazobčanih robov panoramske slike\n%s + +Za zajem panoramske slike držite napravo v pokončnem položaju in kliknite za slikanje fotografije, da začnete panoramo. Nato premaknite napravo levo ali desno, da bel krog na sredini premaknate na modro piko. Ko se vsaka nova slika zajame, še naprej premikajte napravo, da pokrijete vsako novo modro piko.\n\nKliknite na kljukico, da shranite panoramo, ali križec, da jo prekličete.\n\nUpoštevajte, da traja nekaj časa, da se panoramske fotografije obdelajo in shranijo. + +Izvirne slike panorame +Ali naj se shranijo izvirne slike v načinu panorame. To bo podaljšalo čas shranjevanja slik panorame. Dovoli tudi možnost shranjevanja datoteke XML, ki je uporabna pri poročanju o težavah s panoramo.\n%s + +Ne shrani izvirnih slik +Shrani izvirne slike +Shrani izvirne slike in razhroščevalni XML + +ID + +Iskalna vrstica za spremembo izravnave osvetlitve + +Izberite obliko datototek za shranjevanje fotografij. To vpliva na fotografije v \"običajni\" obliki (ne v obliki RAW). Samo oblika JPEG podpira shranjevanje metapodatkov Exif. Upoštevajte, da oblika PNG ni popolnoma brezizgubna in se zaradi tega pretvori iz oblike JPEG pri 100 %% kakovosti.\n%s + +API fotoaparata +Izberite API Camera2, da omogočite dodatne značilnosti, kot so ročni načini za osvetlitev, ostrino, ravnovesje beline, kot tudi obliko RAW (če jo naprava podpira). Sprememba API-ja po ponovno zagnala program.\n%s +Izvirni API fotoaparata +API Camera2 + +Posnemite fotografijo/videposnetek, ko naprava zazna zvok ali glasovni ukaz. + Ko je omogočeno, uporabite zaslonski gumb za mikrofon, da vklopite/izklopite zaznavo zvoka. + Upoštevajte, da možnost glasovnega ukaza uporablja Androidovo storitev za prepoznavanje govora: med uporabo te možnosti se bodo + podatki verjetno poslali na oddaljene strežnike, da se opravi prepoznavanje govora. + \n%s + +Če je možno, pridobi naslov iz lokacij GPS (za žig na fotografijah ali podnapise v videoposnetkih). + Ta možnost zahteva internetno povezavo. + Upoštevajte, da če je omogočena, bo naprava preko interneta tretjim osebam poslala podatke o lokaciji, da koordinate + GPS pretvori v naslov. Glejte https://developer.android.com/reference/android/location/Geocoder . + \n%s + +Dotaknite se za izostritev in pritisnite moder gumb za fotoaparat, da posnamete fotografijo. + \n\nZa onemogočanje največje svetlosti zaslona glejte Nastavitve/Gr. up. vmesnik na zaslonu/\"Vsili največjo svetlost\". + Za shranjevanje na kartico SD na Androidih 5+ glejte Nastavitve/Več nastavitev fotoaparata/\"Uporabi ogrodje za dostop do pom. naprave\". + Za pomoč kliknite \"Spletna pomoč\" v nastavitvah. + + +Izberi oddaljeno napravo + +REC709 +sRGB +Gamma +JTVideo +JTLog +JTLog2 +Vrednost gama videoposnetkov +Vrednost gama, k naj se uporabi v videoposnetkih, če je profil slike videoposnetkov nastavljen na Gama\n%s +1,6 +1,8 +1,9 +2,0 +2,1 +2,2 +2,3 +2,4 +2,6 +2,8 Izk. -Skrij samo zaslonske gumbe za navidezno krmarjenje +Zatemni samo zaslonnske navidezne gumbe za krmarjenje +Skrij samo zaslonske navidezne gumbe za krmarjenje Skrij gr. up. vmsnik Skrij vse + +Prosojnost prekrivne slike +Vrednost alfa pri uporabi prekrivne slike. Nižja vrednost pomeni večjo prosojnost.\n%s + +Ospredna barva zebrinih črt +Barva, ki naj se uporabi za osprednje črte med prikazom zebrinih črt.\n%s +Ozadna barva zebrinih črt +Barva, ki naj se uporabi za ozadne črte med prikazom zebrinih črt.\n%s + +Črna +Rdeča +Oranžna +Prosojna +Bela + +Zunanji fotoaparat +Ultraširoko + +Preklopi na zunanji fotoaaprat +Preklopi fotoaparat +Preklopi med več fotoaparati +Ikona za več fotoaparatov +Če je omogočeno, uporabi ločene gumbe za preklop med sprednjim/zadnjim fotoaparatom in za preklop med več sprednjimi/zadnjimi fotoaparati. Če je onemogočeno, bo ikona preklapljala med vsemi fotoaparati. + +Prikaži ID fotoaparata +Prikaži ID trenutnega fotoaparata na zaslonu + +Dovoli dejanja dolgih pritiskov +Ali naj se dovolijo dejanja dolgih pritiskov (npr. dolg pritisk na galerijo za spremembo mesta shranjevanja). + +Zaslonka + +4K UHD +Bitna hitrost +Hitrost sličic +Počasni posnetek + +Shrani odklon, naklon in nagib +V uporabniško opombo Exif v fotografiji shrani odklon, naklon in nagib naprave (samo oblika JPEG) + +Iskalna vrstica za spremembo trenutne vrednosti nastavitve Date: Sat, 16 Jan 2021 18:32:02 +0100 Subject: [PATCH 156/430] Corrections to the Slovenian translation --- app/src/main/res/values-sl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 859845f04..c665bf0c0 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -129,7 +129,7 @@ Možnost vrtenja predogleda (ne bo vplivalo na nastale fotografije/videoposnetke)\n%s Gr. up. vmesnik na zaslonu … Velikost predogleda -Posavitev up. vmesnika +Postavitev up. vmesnika Vdelan način Prikaži povečavo Prikaži trenutno raven povečave na zaslonu (ob povečavi) -- GitLab From 84596c2d2a820f30012fe7e19ce70e1dc0dd0cd8 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Wed, 3 Feb 2021 23:22:16 +0000 Subject: [PATCH 157/430] Improvements to support different system orientations (although for now still locked to landscape). --- .../sourceforge/opencamera/MainActivity.java | 314 +++++++++++++- .../opencamera/MyApplicationInterface.java | 34 +- .../net/sourceforge/opencamera/MyDebug.java | 2 +- .../preview/ApplicationInterface.java | 2 +- .../preview/BasicApplicationInterface.java | 12 +- .../opencamera/preview/Preview.java | 72 ++-- .../opencamera/ui/DrawPreview.java | 238 ++++++++--- .../net/sourceforge/opencamera/ui/MainUI.java | 391 ++++++++++++++---- app/src/main/res/layout/activity_main.xml | 28 +- 9 files changed, 859 insertions(+), 234 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index bec1ac09c..06a0ba028 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -38,6 +38,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.AsyncTask; @@ -67,6 +68,7 @@ import android.content.res.Configuration; import android.renderscript.RenderScript; import android.speech.tts.TextToSpeech; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import androidx.exifinterface.media.ExifInterface; @@ -81,6 +83,7 @@ import android.view.KeyEvent; import android.view.Menu; import android.view.MotionEvent; import android.view.OrientationEventListener; +import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewConfiguration; @@ -199,6 +202,30 @@ public class MainActivity extends Activity { private static final float WATER_DENSITY_SALTWATER = 1.03f; private float mWaterDensity = 1.0f; + // whether to lock to landscape orientation, or allow switching between portrait and landscape orientations + public static final boolean lock_to_landscape = true; + //public static final boolean lock_to_landscape = false; + + // handling for lock_to_landscape==false: + + public enum SystemOrientation { + LANDSCAPE, + PORTRAIT, + REVERSE_LANDSCAPE + } + + private MyDisplayListener displayListener; + + private boolean has_cached_system_orientation; + private SystemOrientation cached_system_orientation; + + private boolean hasOldSystemOrientation; + private SystemOrientation oldSystemOrientation; + + private boolean has_cached_display_rotation; + private long cached_display_rotation_time_ms; + private int cached_display_rotation; + @Override protected void onCreate(Bundle savedInstanceState) { long debug_time = 0; @@ -211,13 +238,6 @@ public class MainActivity extends Activity { Log.d(TAG, "activity_count: " + activity_count); super.onCreate(savedInstanceState); - if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 ) { - // don't show orientation animations - WindowManager.LayoutParams layout = getWindow().getAttributes(); - layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; - getWindow().setAttributes(layout); - } - setContentView(R.layout.activity_main); PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // initialise any unset preferences to their default values if( MyDebug.LOG ) @@ -329,6 +349,26 @@ public class MainActivity extends Activity { if( MyDebug.LOG ) Log.d(TAG, "onCreate: time after creating preview: " + (System.currentTimeMillis() - debug_time)); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 ) { + // don't show orientation animations + // must be done after creating Preview (so we know if Camera2 API or not) + WindowManager.LayoutParams layout = getWindow().getAttributes(); + // If locked to landscape, ROTATION_ANIMATION_SEAMLESS/JUMPCUT has the problem that when going to + // Settings in portrait, we briefly see the UI change - this is because we set the flag + // to no longer lock to landscape, and that change happens to quickly. + // This isn't a problem when lock_to_landscape==false, and we want + // ROTATION_ANIMATION_SEAMLESS so that there is no/minimal pause from the preview when + // rotating the device. However if using old camera API, we get an ugly transition with + // ROTATION_ANIMATION_SEAMLESS (probably related to not using TextureView?) + if( lock_to_landscape || !preview.usingCamera2API() ) + layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; + else if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) + layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; + else + layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; + getWindow().setAttributes(layout); + } + // Setup multi-camera buttons (must be done after creating preview so we know which Camera API is being used, // and before initialising on-screen visibility). // We only allow the separate icon for switching cameras if: @@ -483,10 +523,14 @@ public class MainActivity extends Activity { decorView.getRootView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @Override public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - if( MyDebug.LOG ) - Log.d(TAG, "inset: " + insets.getSystemWindowInsetRight()); + if( MyDebug.LOG ) { + Log.d(TAG, "inset right: " + insets.getSystemWindowInsetRight()); + Log.d(TAG, "inset bottom: " + insets.getSystemWindowInsetBottom()); + } if( navigation_gap == 0 ) { - navigation_gap = insets.getSystemWindowInsetRight(); + SystemOrientation system_orientation = getSystemOrientation(); + boolean system_orientation_portrait = system_orientation == SystemOrientation.PORTRAIT; + navigation_gap = system_orientation_portrait ? insets.getSystemWindowInsetBottom() : insets.getSystemWindowInsetRight(); if( MyDebug.LOG ) Log.d(TAG, "navigation_gap is " + navigation_gap); // Sometimes when this callback is called, the navigation_gap may still be 0 even if @@ -1383,6 +1427,8 @@ public class MainActivity extends Activity { // 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); + registerDisplayListener(); + mSensorManager.registerListener(accelerometerListener, mSensorAccelerometer, SensorManager.SENSOR_DELAY_NORMAL); magneticSensor.registerMagneticListener(mSensorManager); orientationEventListener.enable(); @@ -1400,6 +1446,7 @@ public class MainActivity extends Activity { soundPoolManager.loadSound(R.raw.mybeep); soundPoolManager.loadSound(R.raw.mybeep_hi); + resetCachedSystemOrientation(); // just in case? mainUI.layoutUI(); updateGalleryIcon(); // update in case images deleted whilst idle @@ -1470,6 +1517,7 @@ public class MainActivity extends Activity { this.app_is_paused = true; mainUI.destroyPopup(); // important as user could change/reset settings from Android settings when pausing + unregisterDisplayListener(); mSensorManager.unregisterListener(accelerometerListener); magneticSensor.unregisterMagneticListener(mSensorManager); orientationEventListener.disable(); @@ -1504,16 +1552,239 @@ public class MainActivity extends Activity { } } + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) + private class MyDisplayListener implements DisplayManager.DisplayListener { + private int old_rotation; + + private MyDisplayListener() { + int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation(); + if( MyDebug.LOG ) { + Log.d(TAG, "MyDisplayListener"); + Log.d(TAG, "rotation: " + rotation); + } + old_rotation = rotation; + } + + @Override + public void onDisplayAdded(int displayId) { + } + + @Override + public void onDisplayRemoved(int displayId) { + } + + @Override + public void onDisplayChanged(int displayId) { + int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation(); + if( MyDebug.LOG ) { + Log.d(TAG, "onDisplayChanged: " + displayId); + Log.d(TAG, "rotation: " + rotation); + Log.d(TAG, "old_rotation: " + rotation); + } + if( ( rotation == Surface.ROTATION_0 && old_rotation == Surface.ROTATION_180 ) || + ( rotation == Surface.ROTATION_180 && old_rotation == Surface.ROTATION_0 ) || + ( rotation == Surface.ROTATION_90 && old_rotation == Surface.ROTATION_270 ) || + ( rotation == Surface.ROTATION_270 && old_rotation == Surface.ROTATION_90 ) + ) { + if( MyDebug.LOG ) + Log.d(TAG, "switched between landscape and reverse orientation"); + onSystemOrientationChanged(); + } + + old_rotation = rotation; + } + } + + /** Creates and registers a display listener, needed to handle switches between landscape and + * reverse landscape (without going via portrait) when lock_to_landscape==false. + */ + private void registerDisplayListener() { + if( MyDebug.LOG ) + Log.d(TAG, "registerDisplayListener"); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && !lock_to_landscape ) { + displayListener = new MyDisplayListener(); + DisplayManager displayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE); + displayManager.registerDisplayListener(displayListener, null); + } + } + + private void unregisterDisplayListener() { + if( MyDebug.LOG ) + Log.d(TAG, "unregisterDisplayListener"); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && displayListener != null ) { + DisplayManager displayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE); + displayManager.unregisterDisplayListener(displayListener); + displayListener = null; + } + } + @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { if( MyDebug.LOG ) - Log.d(TAG, "onConfigurationChanged()"); + Log.d(TAG, "onConfigurationChanged(): " + newConfig.orientation); // 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(); + // update: need this all the time when lock_to_landscape==false + onSystemOrientationChanged(); super.onConfigurationChanged(newConfig); } + private void onSystemOrientationChanged() { + if( MyDebug.LOG ) + Log.d(TAG, "onSystemOrientationChanged"); + + // n.b., need to call this first, before preview.setCameraDisplayOrientation(), since + // preview.setCameraDisplayOrientation() will call getDisplayRotation() and we don't want + // to be using the outdated cached value now that the rotation has changed! + resetCachedSystemOrientation(); + + preview.setCameraDisplayOrientation(); + if( !lock_to_landscape ) { + SystemOrientation newSystemOrientation = getSystemOrientation(); + if( hasOldSystemOrientation && oldSystemOrientation == newSystemOrientation ) { + if( MyDebug.LOG ) + Log.d(TAG, "onSystemOrientationChanged: orientation hasn't changed"); + } + else { + if( hasOldSystemOrientation ) { + // handle rotation animation + int start_rotation = getRotationFromSystemOrientation(oldSystemOrientation) - getRotationFromSystemOrientation(newSystemOrientation); + if( MyDebug.LOG ) + Log.d(TAG, "start_rotation: " + start_rotation); + if( start_rotation < -180 ) + start_rotation += 360; + else if( start_rotation > 180 ) + start_rotation -= 360; + mainUI.layoutUIWithRotation(start_rotation); + } + else { + mainUI.layoutUI(); + } + applicationInterface.getDrawPreview().updateSettings(); + + hasOldSystemOrientation = true; + oldSystemOrientation = newSystemOrientation; + } + } + } + + /** Returns the current system orientation. + * Note if lock_to_landscape is true, this always returns LANDSCAPE even if called when we're + * allowing configuration changes (e.g., in Settings or a dialog is showing). (This method, + * and hence calls to it, were added to support lock_to_landscape==false behaviour, and we + * want to avoid changing behaviour for lock_to_landscape==true behaviour.) + * Note that this also caches the orientation: firstly for performance (as this is called from + * DrawPreview), secondly to support REVERSE_LANDSCAPE, we don't want a sudden change if + * getDefaultDisplay().getRotation() changes after the configuration changes. + */ + public SystemOrientation getSystemOrientation() { + if( lock_to_landscape ) { + return SystemOrientation.LANDSCAPE; + } + if( has_cached_system_orientation ) { + return cached_system_orientation; + } + SystemOrientation result; + int system_orientation = getResources().getConfiguration().orientation; + if( MyDebug.LOG ) + Log.d(TAG, "system orientation: " + system_orientation); + switch( system_orientation ) { + case Configuration.ORIENTATION_LANDSCAPE: + result = SystemOrientation.LANDSCAPE; + // now try to distinguish between landscape and reverse landscape + + // check whether the display matches the landscape configuration, in case this is inconsistent? + Point display_size = new Point(); + Display display = getWindowManager().getDefaultDisplay(); + display.getSize(display_size); + if( display_size.x > display_size.y ) { + int rotation = getWindowManager().getDefaultDisplay().getRotation(); + if( MyDebug.LOG ) + Log.d(TAG, "rotation: " + rotation); + switch( rotation ) { + case Surface.ROTATION_0: + case Surface.ROTATION_90: + // landscape + if( MyDebug.LOG ) + Log.d(TAG, "landscape"); + break; + case Surface.ROTATION_180: + case Surface.ROTATION_270: + // reverse landscape + if( MyDebug.LOG ) + Log.d(TAG, "reverse landscape"); + result = SystemOrientation.REVERSE_LANDSCAPE; + break; + default: + if( MyDebug.LOG ) + Log.e(TAG, "unknown rotation: " + rotation); + break; + } + } + else { + if( MyDebug.LOG ) + Log.e(TAG, "display size not landscape: " + display_size); + } + break; + case Configuration.ORIENTATION_PORTRAIT: + result = SystemOrientation.PORTRAIT; + break; + default: + if( MyDebug.LOG ) + Log.e(TAG, "unknown system orientation: " + system_orientation); + result = SystemOrientation.LANDSCAPE; + break; + } + if( MyDebug.LOG ) + Log.d(TAG, "system orientation is now: " + result); + this.has_cached_system_orientation = true; + this.cached_system_orientation = result; + return result; + } + + /** Returns rotation in degrees (as a multiple of 90 degrees) corresponding to the supplied + * system orientation. + */ + public static int getRotationFromSystemOrientation(SystemOrientation system_orientation) { + int rotation; + if( system_orientation == MainActivity.SystemOrientation.PORTRAIT ) + rotation = 270; + else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) + rotation = 180; + else + rotation = 0; + return rotation; + } + + private void resetCachedSystemOrientation() { + this.has_cached_system_orientation = false; + this.has_cached_display_rotation = false; + } + + /** A wrapper for getWindowManager().getDefaultDisplay().getRotation(), except if + * lock_to_landscape==false, this checks for the display being inconsistent with the system + * orientation, and if so, returns a cached value. + */ + public int getDisplayRotation() { + if( lock_to_landscape ) { + return getWindowManager().getDefaultDisplay().getRotation(); + } + // we cache to reduce effect of annoying problem where rotation changes shortly before the + // configuration actually changes (several frames), so on-screen elements would briefly show + // in wrong location when device rotates from/to portrait and landscape; also not a bad idea + // to cache for performance anyway, to avoid calling + // getWindowManager().getDefaultDisplay().getRotation() every frame + long time_ms = System.currentTimeMillis(); + if( has_cached_display_rotation && time_ms < cached_display_rotation_time_ms + 1000 ) { + return cached_display_rotation; + } + has_cached_display_rotation = true; + int rotation = getWindowManager().getDefaultDisplay().getRotation(); + cached_display_rotation = rotation; + cached_display_rotation_time_ms = time_ms; + return rotation; + } + public void waitUntilImageQueueEmpty() { if( MyDebug.LOG ) Log.d(TAG, "waitUntilImageQueueEmpty"); @@ -2959,11 +3230,13 @@ public class MainActivity extends Activity { }*/ 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) + if( lock_to_landscape ) { + // 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) + } if( preview != null ) { - // also need to call setCameraDisplayOrientation, as this handles if the user switched from portrait to reverse landscape whilst in settings/etc + // also need to call preview.setCameraDisplayOrientation, as this handles if the user switched from portrait to reverse landscape whilst in settings/etc // as switching from reverse landscape back to landscape isn't detected in onConfigurationChanged // update: now probably irrelevant now that we close/reopen the camera, but keep it here anyway preview.setCameraDisplayOrientation(); @@ -3046,8 +3319,10 @@ public class MainActivity extends Activity { public void setWindowFlagsForSettings(boolean set_lock_protect) { if( MyDebug.LOG ) Log.d(TAG, "setWindowFlagsForSettings: " + set_lock_protect); - // allow screen rotation - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + if( lock_to_landscape ) { + // allow screen rotation + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } // revert to standard screen blank behaviour getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -4734,6 +5009,9 @@ public class MainActivity extends Activity { SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar); final int visibility = is_visible ? View.VISIBLE : View.GONE; focusSeekBar.setVisibility(visibility); + if( is_visible ) { + applicationInterface.getDrawPreview().updateSettings(); // needed so that we reset focus_seekbars_margin_left, as the focus seekbars can only be updated when visible + } } public void setManualWBSeekbar() { diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 8463370d5..450fc9524 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -51,6 +51,7 @@ import android.provider.Settings; import android.util.Log; import android.util.Pair; import android.view.MotionEvent; +import android.view.Surface; import android.view.View; import android.widget.ImageButton; @@ -1074,11 +1075,6 @@ public class MyApplicationInterface extends BasicApplicationInterface { return sharedPreferences.getString(PreferenceKeys.PreviewSizePreferenceKey, "preference_preview_size_wysiwyg"); } - @Override - public String getPreviewRotationPref() { - return sharedPreferences.getString(PreferenceKeys.RotatePreviewPreferenceKey, "0"); - } - @Override public String getLockOrientationPref() { if( getPhotoMode() == PhotoMode.Panorama ) @@ -1410,6 +1406,34 @@ public class MyApplicationInterface extends BasicApplicationInterface { return imageSaver.queueWouldBlock(n_raw, n_jpegs); } + /** Returns the ROTATION_* enum of the display relative to the natural device orientation, but + * also checks for the preview being rotated due to user preference + * RotatePreviewPreferenceKey. + */ + @Override + public int getDisplayRotation() { + // important to use cached rotation to reduce issues of incorrect focus square location when + // rotating device, due to strange Android behaviour where rotation changes shortly before + // the configuration actually changes + int rotation = main_activity.getDisplayRotation(); + + String rotate_preview = sharedPreferences.getString(PreferenceKeys.RotatePreviewPreferenceKey, "0"); + 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; + } + @Override public long getExposureTimePref() { return sharedPreferences.getLong(PreferenceKeys.ExposureTimePreferenceKey, CameraController.EXPOSURE_TIME_DEFAULT); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyDebug.java b/app/src/main/java/net/sourceforge/opencamera/MyDebug.java index 00e82f8f5..f6c4ff139 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyDebug.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyDebug.java @@ -4,5 +4,5 @@ package net.sourceforge.opencamera; * released versions. */ public class MyDebug { - public static final boolean LOG = false; + public static final boolean LOG = true; } diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java index 55b69c375..0a7887077 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java @@ -121,7 +121,6 @@ public interface ApplicationInterface { 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 @@ -141,6 +140,7 @@ public interface ApplicationInterface { double getCalibratedLevelAngle(); // set to non-zero to calibrate the accelerometer used for the level angles boolean canTakeNewPhoto(); // whether taking new photos is allowed (e.g., can return false if queue for processing images would become full) boolean imageQueueWouldBlock(int n_raw, int n_jpegs); // called during some burst operations, whether we can allow taking the supplied number of extra photos + int getDisplayRotation(); // same behaviour as Activity.getWindowManager().getDefaultDisplay().getRotation() (including returning a member of Surface.ROTATION_*), but allows application to modify e.g. for upside-down preview // Camera2 only modes: long getExposureTimePref(); // only called if getISOPref() is not "default" float getFocusDistancePref(boolean is_target_distance); diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java index d3ad53479..cf2a5f068 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java @@ -3,6 +3,7 @@ package net.sourceforge.opencamera.preview; import java.util.Date; import java.util.List; +import android.app.Activity; import android.graphics.Canvas; import android.location.Location; import android.net.Uri; @@ -188,11 +189,6 @@ public abstract class BasicApplicationInterface implements ApplicationInterface return "preference_preview_size_wysiwyg"; } - @Override - public String getPreviewRotationPref() { - return "0"; - } - @Override public String getLockOrientationPref() { return "none"; @@ -288,6 +284,12 @@ public abstract class BasicApplicationInterface implements ApplicationInterface return false; } + @Override + public int getDisplayRotation() { + Activity activity = (Activity)this.getContext(); + return activity.getWindowManager().getDefaultDisplay().getRotation(); + } + @Override public long getExposureTimePref() { return CameraController.EXPOSURE_TIME_DEFAULT; diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index f95525f7c..ffc44fda1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -333,8 +333,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private boolean supports_tonemap_curve; private float [] supported_apertures; private boolean has_focus_area; - private int focus_screen_x; - private int focus_screen_y; + private float focus_camera_x; + private float focus_camera_y; private long focus_complete_time = -1; private long focus_started_time = -1; private int focus_success = FOCUS_DONE; @@ -536,16 +536,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu 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]; - + /** Return a focus area from supplied point. Supplied coordinates should be in camera + * coordinates. + */ + private ArrayList getAreas(float focus_x, float focus_y) { 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(); @@ -660,13 +656,23 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // don't set focus areas on touch if the user is touching to unpause! if( camera_controller != null && !this.using_face_detection && !was_paused ) { this.has_focus_area = false; - ArrayList areas = getAreas(event.getX(), event.getY()); + + if( MyDebug.LOG ) { + Log.d(TAG, "x, y: " + event.getX() + ", " + event.getY()); + } + float [] coords = {event.getX(), event.getY()}; + calculatePreviewToCameraMatrix(); + preview_to_camera_matrix.mapPoints(coords); + float focus_x = coords[0]; + float focus_y = coords[1]; + ArrayList areas = getAreas(focus_x, focus_y); + 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(); + this.focus_camera_x = focus_x; + this.focus_camera_y = focus_y; } else { if( MyDebug.LOG ) @@ -919,13 +925,13 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } if( MyDebug.LOG ) Log.d(TAG, "textureview size: " + textureview_w + ", " + textureview_h); - int rotation = getDisplayRotation(); + int rotation = applicationInterface.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 ) { + if( rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270 ) { bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); float scale = Math.max( @@ -934,6 +940,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu matrix.postScale(scale, scale, centerX, centerY); matrix.postRotate(90 * (rotation - 2), centerX, centerY); } + else if( rotation == Surface.ROTATION_180 ) { + matrix.postRotate(180, centerX, centerY); + } cameraSurface.setTransform(matrix); } @@ -3691,36 +3700,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu 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 rotation = applicationInterface.getDisplayRotation(); int degrees = 0; switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; @@ -8430,7 +8415,12 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } public Pair getFocusPos() { - return new Pair<>(focus_screen_x, focus_screen_y); + // note, we don't store the screen coordinates, as they may become out of date in the + // screen orientation changes (if MainActivity.lock_to_landscape==false) + float [] coords = {focus_camera_x, focus_camera_y}; + calculateCameraToPreviewMatrix(); + camera_to_preview_matrix.mapPoints(coords); + return new Pair<>((int)coords[0], (int)coords[1]); } public int getMaxNumFocusAreas() { diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java index f5cf5a765..14fcb118f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -217,7 +218,7 @@ public class DrawPreview { private float view_angle_y_preview; private long last_view_angles_time; - private int take_photo_top; // coordinate (in canvas x coordinates) of top of the take photo icon + private int take_photo_top; // coordinate (in canvas x coordinates, or y coords if system_orientation_portrait==true) of top of the take photo icon private long last_take_photo_top_time; private int top_icon_shift; // shift that may be needed for on-screen text to avoid clashing with icons (when arranged "along top") @@ -379,18 +380,34 @@ public class DrawPreview { * *after* applying the rotation, when we want the top left of the icon as shown on screen. * This should not be called every frame but instead should be cached, due to cost of calling * view.getLocationOnScreen(). + * Update: For supporting landscape and portrait (if MainActivity.lock_to_landscape==false), + * instead this returns the top side if in portrait. Note though we still need to take rotation + * into account, as we still apply rotation to the icons when changing orienations (e.g., this + * is needed when rotating from reverse landscape to portrait, for on-screen text like level + * angle to be offset correctly above the shutter button (see take_photo_top) when the preview + * has a wide aspect ratio. */ private int getViewOnScreenX(View view) { view.getLocationOnScreen(gui_location); - int xpos = gui_location[0]; + + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + boolean system_orientation_portrait = system_orientation == MainActivity.SystemOrientation.PORTRAIT; + int xpos = gui_location[system_orientation_portrait ? 1 : 0]; int rotation = Math.round(view.getRotation()); // rotation can be outside [0, 359] if the user repeatedly rotates in same direction! rotation = (rotation % 360 + 360) % 360; // version of (rotation % 360) that work if rotation is -ve /*if( MyDebug.LOG ) Log.d(TAG, " mod rotation: " + rotation);*/ - if( rotation == 180 || rotation == 90 ) { - // annoying behaviour that getLocationOnScreen takes the rotation into account - xpos -= view.getWidth(); + // undo annoying behaviour that getLocationOnScreen takes the rotation into account + if( system_orientation_portrait ) { + if( rotation == 180 || rotation == 270 ) { + xpos -= view.getHeight(); + } + } + else { + if( rotation == 90 || rotation == 180 ) { + xpos -= view.getWidth(); + } } return xpos; } @@ -663,7 +680,7 @@ public class DrawPreview { last_take_photo_top_time = 0; // force take_photo_top to be recomputed last_top_icon_shift_time = 0; // for top_icon_shift to be recomputed - focus_seekbars_margin_left = -1; // just in case?? + focus_seekbars_margin_left = -1; // needed as the focus seekbars can only be updated when visible has_settings = true; } @@ -1021,7 +1038,7 @@ public class DrawPreview { } } - private void onDrawInfoLines(Canvas canvas, final int top_x, final int top_y, final int bottom_y, long time_ms) { + private void onDrawInfoLines(Canvas canvas, final int top_x, final int top_y, final int bottom_y, final int device_ui_rotation, long time_ms) { Preview preview = main_activity.getPreview(); CameraController camera_controller = preview.getCameraController(); int ui_rotation = preview.getUIRotation(); @@ -1035,15 +1052,16 @@ public class DrawPreview { final int gap_y = (int) (0 * scale + 0.5f); // convert dps to pixels final int icon_gap_y = (int) (2 * scale + 0.5f); // convert dps to pixels if( ui_rotation == 90 || ui_rotation == 270 ) { + // n.b., this is only for when lock_to_landscape==true, so we don't look at device_ui_rotation int diff = canvas.getWidth() - canvas.getHeight(); location_x += diff/2; location_y -= diff/2; } - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { location_y = canvas.getHeight() - location_y - (int) (20 * scale + 0.5f); } boolean align_right = false; - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x = canvas.getWidth() - location_x; p.setTextAlign(Paint.Align.RIGHT); align_right = true; @@ -1109,7 +1127,7 @@ public class DrawPreview { first_line_height = Math.max(first_line_height, height); } // update location_y for first line (time and camera id) - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { // upside-down portrait location_y -= first_line_height; } @@ -1144,7 +1162,7 @@ public class DrawPreview { } int height = applicationInterface.drawTextWithBackground(canvas, p, free_memory_gb_string, Color.WHITE, Color.BLACK, location_x, location_y, MyApplicationInterface.Alignment.ALIGNMENT_TOP, null, MyApplicationInterface.Shadow.SHADOW_OUTLINE, text_bounds_free_memory); height += gap_y; - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { location_y -= height; } else { @@ -1228,7 +1246,7 @@ public class DrawPreview { height += gap_y; // only move location_y if we actually print something (because on old camera API, even if the ISO option has // been enabled, we'll never be able to display the on-screen ISO) - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { location_y -= height; } else { @@ -1245,7 +1263,7 @@ public class DrawPreview { int location_x2 = location_x - flash_padding; final int icon_size = (int) (16 * scale + 0.5f); // convert dps to pixels - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 = location_x - icon_size + flash_padding; } @@ -1272,7 +1290,7 @@ public class DrawPreview { canvas.drawBitmap(location_off_bitmap, null, icon_dest, p); } - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1294,7 +1312,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(is_raw_only_pref ? raw_only_bitmap : raw_jpeg_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1311,7 +1329,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(face_detection_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1328,7 +1346,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(auto_stabilise_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1367,7 +1385,7 @@ public class DrawPreview { canvas.drawBitmap(bitmap, null, icon_dest, p); p.setColorFilter(null); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1387,7 +1405,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(photostamp_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1404,7 +1422,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(audio_disabled_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1422,7 +1440,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(capture_rate_factor < 1.0f ? slow_motion_bitmap : time_lapse_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1438,7 +1456,7 @@ public class DrawPreview { p.setAlpha(255); canvas.drawBitmap(high_speed_fps_bitmap, null, icon_dest, p); - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 -= icon_size + flash_padding; } else { @@ -1486,7 +1504,7 @@ public class DrawPreview { needs_flash_time = -1; } - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { location_y -= icon_gap_y; } else { @@ -1506,11 +1524,11 @@ public class DrawPreview { // n.b., if changing the histogram_height, remember to update focus_seekbar and // focus_bracketing_target_seekbar margins in activity_main.xml int location_x2 = location_x - flash_padding; - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { location_x2 = location_x - histogram_width + flash_padding; } icon_dest.set(location_x2 - flash_padding, location_y, location_x2 - flash_padding + histogram_width, location_y + histogram_height); - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { icon_dest.top -= histogram_height; icon_dest.bottom -= histogram_height; } @@ -1626,7 +1644,7 @@ public class DrawPreview { /** This includes drawing of the UI that requires the canvas to be rotated according to the preview's * current UI rotation. */ - private void drawUI(Canvas canvas, long time_ms) { + private void drawUI(Canvas canvas, int device_ui_rotation, long time_ms) { Preview preview = main_activity.getPreview(); CameraController camera_controller = preview.getCameraController(); int ui_rotation = preview.getUIRotation(); @@ -1635,6 +1653,8 @@ public class DrawPreview { double level_angle = preview.getLevelAngle(); boolean has_geo_direction = preview.hasGeoDirection(); double geo_direction = preview.getGeoDirection(); + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + boolean system_orientation_portrait = system_orientation == MainActivity.SystemOrientation.PORTRAIT; int text_base_y = 0; canvas.save(); @@ -1643,23 +1663,24 @@ public class DrawPreview { if( camera_controller != null && !preview.isPreviewPaused() ) { /*canvas.drawText("PREVIEW", canvas.getWidth() / 2, canvas.getHeight() / 2, p);*/ + int gap_y = (int) (20 * scale + 0.5f); // convert dps to pixels int text_y = (int) (16 * scale + 0.5f); // convert dps to pixels boolean avoid_ui = false; // fine tuning to adjust placement of text with respect to the GUI, depending on orientation - if( ui_placement == MainUI.UIPlacement.UIPLACEMENT_TOP && ( ui_rotation == 0 || ui_rotation == 180 ) ) { + if( ui_placement == MainUI.UIPlacement.UIPLACEMENT_TOP && ( device_ui_rotation == 0 || device_ui_rotation == 180 ) ) { text_base_y = canvas.getHeight() - (int)(0.1*gap_y); avoid_ui = true; } - else if( ui_rotation == ( ui_placement == MainUI.UIPlacement.UIPLACEMENT_RIGHT ? 0 : 180 ) ) { + else if( device_ui_rotation == ( ui_placement == MainUI.UIPlacement.UIPLACEMENT_RIGHT ? 0 : 180 ) ) { text_base_y = canvas.getHeight() - (int)(0.1*gap_y); avoid_ui = true; } - else if( ui_rotation == ( ui_placement == MainUI.UIPlacement.UIPLACEMENT_RIGHT ? 180 : 0 ) ) { + else if( device_ui_rotation == ( ui_placement == MainUI.UIPlacement.UIPLACEMENT_RIGHT ? 180 : 0 ) ) { text_base_y = canvas.getHeight() - (int)(2.5*gap_y); // leave room for GUI icons } - else if( ui_rotation == 90 || ui_rotation == 270 ) { - // ui_rotation 90 is upside down portrait + else if( device_ui_rotation == 90 || device_ui_rotation == 270 ) { + // 90 is upside down portrait // 270 is portrait if( last_take_photo_top_time == 0 || time_ms > last_take_photo_top_time + 1000 ) { @@ -1670,11 +1691,12 @@ public class DrawPreview { // align with "top" of the take_photo button, but remember to take the rotation into account! int view_left = getViewOnScreenX(view); preview.getView().getLocationOnScreen(gui_location); - int this_left = gui_location[0]; + int this_left = gui_location[system_orientation_portrait ? 1 : 0]; take_photo_top = view_left - this_left; last_take_photo_top_time = time_ms; /*if( MyDebug.LOG ) { + Log.d(TAG, "device_ui_rotation: " + device_ui_rotation); Log.d(TAG, "view_left: " + view_left); Log.d(TAG, "this_left: " + this_left); Log.d(TAG, "take_photo_top: " + take_photo_top); @@ -1682,7 +1704,9 @@ public class DrawPreview { } // diff_x is the difference from the centre of the canvas to the position we want - int diff_x = take_photo_top - canvas.getWidth()/2; + int max_x = system_orientation_portrait ? canvas.getHeight() : canvas.getWidth(); + int mid_x = max_x/2; + int diff_x = take_photo_top - mid_x; /*if( MyDebug.LOG ) { Log.d(TAG, "view left: " + view_left); @@ -1701,8 +1725,7 @@ public class DrawPreview { int diff_x = preview.getView().getRootView().getRight()/2 - offset_x; */ - int max_x = canvas.getWidth(); - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { // so we don't interfere with the top bar info (datetime, free memory, ISO) when upside down max_x -= (int)(2.5*gap_y); } @@ -1712,9 +1735,9 @@ public class DrawPreview { Log.d(TAG, "canvas.getWidth()/2 + diff_x: " + (canvas.getWidth()/2+diff_x)); Log.d(TAG, "max_x: " + max_x); }*/ - if( canvas.getWidth()/2 + diff_x > max_x ) { + if( mid_x + 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; + diff_x = max_x - mid_x; } text_base_y = canvas.getHeight()/2 + diff_x - (int)(0.5*gap_y); } @@ -1905,7 +1928,7 @@ public class DrawPreview { p.setTextSize(14 * scale + 0.5f); // convert dps to pixels p.setTextAlign(Paint.Align.CENTER); int pixels_offset_y = 2*text_y; // avoid overwriting the zoom - if( ui_rotation == 0 && applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.FocusBracketing ) { + if( device_ui_rotation == 0 && applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.FocusBracketing ) { // avoid clashing with the target focus bracketing seekbar in landscape orientation pixels_offset_y = 5*gap_y; } @@ -1986,10 +2009,19 @@ public class DrawPreview { // avoid computing every time, due to cost of calling View.getLocationOnScreen() /*if( MyDebug.LOG ) Log.d(TAG, "update cached top_icon_shift");*/ - int top_margin = getViewOnScreenX(top_icon) + top_icon.getWidth(); + int top_margin = getViewOnScreenX(top_icon); + if( system_orientation == MainActivity.SystemOrientation.LANDSCAPE ) + top_margin += top_icon.getWidth(); + else if( system_orientation == MainActivity.SystemOrientation.PORTRAIT ) + top_margin += top_icon.getHeight(); + // n.b., don't adjust top_margin for icon width/height for an reverse orientation preview.getView().getLocationOnScreen(gui_location); - int preview_left = gui_location[0]; + int preview_left = gui_location[system_orientation_portrait ? 1 : 0]; + if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) + preview_left += preview.getView().getWidth(); // actually want preview-right for reverse landscape this.top_icon_shift = top_margin - preview_left; + if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) + this.top_icon_shift = -this.top_icon_shift; /*if( MyDebug.LOG ) { Log.d(TAG, "top_icon.getRotation(): " + top_icon.getRotation()); Log.d(TAG, "preview_left: " + preview_left); @@ -2001,7 +2033,7 @@ public class DrawPreview { } if( this.top_icon_shift > 0 ) { - if( ui_rotation == 90 || ui_rotation == 270 ) { + if( device_ui_rotation == 90 || device_ui_rotation == 270 ) { // portrait top_y += top_icon_shift; } @@ -2031,15 +2063,51 @@ public class DrawPreview { if( MyDebug.LOG ) Log.d(TAG, "set focus_seekbars_margin_left to " + focus_seekbars_margin_left); + // "left" and "right" here are written assuming we're in landscape system orientation + View view = main_activity.findViewById(R.id.focus_seekbar); RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); - layoutParams.setMargins(focus_seekbars_margin_left, 0, 0, 0); + preview.getView().getLocationOnScreen(gui_location); + int preview_left = gui_location[system_orientation_portrait ? 1 : 0]; + if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) + preview_left += preview.getView().getWidth(); // actually want preview-right for reverse landscape + + view.getLocationOnScreen(gui_location); + int seekbar_right = gui_location[system_orientation_portrait ? 1 : 0]; + if( system_orientation == MainActivity.SystemOrientation.LANDSCAPE || system_orientation == MainActivity.SystemOrientation.PORTRAIT ) { + // n.b., we read view.getWidth() even if system_orientation is portrait, because the seekbar is rotated in portrait orientation + seekbar_right += view.getWidth(); + } + else { + // and for reversed landscape, the seekbar is rotated 180 degrees, and getLocationOnScreen() returns the location after the rotation + seekbar_right -= view.getWidth(); + } + + int min_seekbar_width = (int) (150 * scale + 0.5f); // convert dps to pixels + int new_seekbar_width; + if( system_orientation == MainActivity.SystemOrientation.LANDSCAPE || system_orientation == MainActivity.SystemOrientation.PORTRAIT ) { + new_seekbar_width = seekbar_right - (preview_left+focus_seekbars_margin_left); + } + else { + // reversed landscape + new_seekbar_width = preview_left - focus_seekbars_margin_left - seekbar_right; + } + new_seekbar_width = Math.max(new_seekbar_width, min_seekbar_width); + /*if( MyDebug.LOG ) { + Log.d(TAG, "preview_left: " + preview_left); + Log.d(TAG, "seekbar_right: " + seekbar_right); + Log.d(TAG, "new_seekbar_width: " + new_seekbar_width); + }*/ + layoutParams.width = new_seekbar_width; view.setLayoutParams(layoutParams); view = main_activity.findViewById(R.id.focus_bracketing_target_seekbar); layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); - layoutParams.setMargins(focus_seekbars_margin_left, 0, 0, 0); + layoutParams.width = new_seekbar_width; view.setLayoutParams(layoutParams); + + // need to update due to changing width of focus seekbars + main_activity.getMainUI().setFocusSeekbarsRotation(); } } @@ -2048,14 +2116,15 @@ public class DrawPreview { 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 ) { + // n.b., this is only for when lock_to_landscape==true, so we don't look at device_ui_rotation int diff = canvas.getWidth() - canvas.getHeight(); battery_x += diff/2; battery_y -= diff/2; } - if( ui_rotation == 90 ) { + if( device_ui_rotation == 90 ) { battery_y = canvas.getHeight() - battery_y - battery_height; } - if( ui_rotation == 180 ) { + if( device_ui_rotation == 180 ) { battery_x = canvas.getWidth() - battery_x - battery_width; } if( show_battery_pref ) { @@ -2091,14 +2160,16 @@ public class DrawPreview { top_x += (int) (10 * scale + 0.5f); // convert dps to pixels } - onDrawInfoLines(canvas, top_x, top_y, text_base_y, time_ms); + onDrawInfoLines(canvas, top_x, top_y, text_base_y, device_ui_rotation, time_ms); canvas.restore(); } - private void drawAngleLines(Canvas canvas, long time_ms) { + private void drawAngleLines(Canvas canvas, int device_ui_rotation, long time_ms) { Preview preview = main_activity.getPreview(); CameraController camera_controller = preview.getCameraController(); + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + boolean system_orientation_portrait = system_orientation == MainActivity.SystemOrientation.PORTRAIT; boolean has_level_angle = preview.hasLevelAngle(); boolean actual_show_angle_line_pref; if( photoMode == MyApplicationInterface.PhotoMode.Panorama ) { @@ -2111,28 +2182,35 @@ public class DrawPreview { boolean allow_angle_lines = camera_controller != null && !preview.isPreviewPaused(); if( allow_angle_lines && has_level_angle && ( actual_show_angle_line_pref || show_pitch_lines_pref || show_geo_direction_lines_pref ) ) { - int ui_rotation = preview.getUIRotation(); 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(); // n.b., must draw this without the standard canvas rotation - int radius_dps = (ui_rotation == 90 || ui_rotation == 270) ? 60 : 80; + int radius_dps = (device_ui_rotation == 90 || device_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(); + int rotation = main_activity.getDisplayRotation(); switch (rotation) { case Surface.ROTATION_90: - case Surface.ROTATION_270: angle -= 90.0; break; - case Surface.ROTATION_0: + case Surface.ROTATION_270: + angle += 90.0; + break; case Surface.ROTATION_180: + angle += 180.0; + break; + case Surface.ROTATION_0: default: break; } + /*if( MyDebug.LOG ) { + Log.d(TAG, "system_orientation: " + system_orientation); + Log.d(TAG, "rotation: " + rotation); + }*/ /*if( MyDebug.LOG ) { Log.d(TAG, "orig_level_angle: " + preview.getOrigLevelAngle()); Log.d(TAG, "angle: " + angle); @@ -2196,8 +2274,15 @@ public class DrawPreview { } } updateCachedViewAngles(time_ms); // ensure view_angle_x_preview, view_angle_y_preview are computed and up to date - float camera_angle_x = this.view_angle_x_preview; - float camera_angle_y = this.view_angle_y_preview; + float camera_angle_x, camera_angle_y; + if( system_orientation_portrait ) { + camera_angle_x = this.view_angle_y_preview; + camera_angle_y = this.view_angle_x_preview; + } + else { + camera_angle_x = this.view_angle_x_preview; + camera_angle_y = this.view_angle_y_preview; + } 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 ) { @@ -2215,7 +2300,7 @@ public class DrawPreview { 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_pref ) { - int pitch_radius_dps = (ui_rotation == 90 || ui_rotation == 270) ? 100 : 80; + int pitch_radius_dps = (device_ui_rotation == 90 || device_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 ) @@ -2257,7 +2342,7 @@ public class DrawPreview { } } if( has_geo_direction && has_pitch_angle && show_geo_direction_lines_pref ) { - int geo_radius_dps = (ui_rotation == 90 || ui_rotation == 270) ? 80 : 100; + int geo_radius_dps = (device_ui_rotation == 90 || device_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; @@ -2567,6 +2652,19 @@ public class DrawPreview { } } + // If MainActivity.lock_to_landscape==true, then the ui_rotation represents the orientation of the + // device; if MainActivity.lock_to_landscape==false then ui_rotation is always 0 as we don't need to + // apply any orientation ourselves. However, we're we do want to know the true rotation of the + // device, as it affects how certain elements of the UI are layed out. + int device_ui_rotation; + if( MainActivity.lock_to_landscape ) { + device_ui_rotation = ui_rotation; + } + else { + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + device_ui_rotation = MainActivity.getRotationFromSystemOrientation(system_orientation); + } + if( camera_controller != null && taking_picture && !front_screen_flash && take_photo_border_pref ) { p.setColor(Color.WHITE); p.setStyle(Paint.Style.STROKE); @@ -2635,9 +2733,9 @@ public class DrawPreview { doThumbnailAnimation(canvas, time_ms); - drawUI(canvas, time_ms); + drawUI(canvas, device_ui_rotation, time_ms); - drawAngleLines(canvas, time_ms); + drawAngleLines(canvas, device_ui_rotation, time_ms); doFocusAnimation(canvas, time_ms); @@ -2658,17 +2756,33 @@ public class DrawPreview { if( enable_gyro_target_spot && camera_controller != null ) { GyroSensor gyroSensor = main_activity.getApplicationInterface().getGyroSensor(); if( gyroSensor.isRecording() ) { + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + boolean system_orientation_portrait = system_orientation == MainActivity.SystemOrientation.PORTRAIT; for(float [] gyro_direction : gyro_directions) { gyroSensor.getRelativeInverseVector(transformed_gyro_direction, gyro_direction); gyroSensor.getRelativeInverseVector(transformed_gyro_direction_up, gyro_direction_up); // note that although X of gyro_direction represents left to right on the device, because we're in landscape mode, // this is y coordinates on the screen - float angle_x = - (float)Math.asin(transformed_gyro_direction[1]); - float angle_y = - (float)Math.asin(transformed_gyro_direction[0]); + float angle_x, angle_y; + if( system_orientation_portrait ) { + angle_x = (float)Math.asin(transformed_gyro_direction[0]); + angle_y = - (float)Math.asin(transformed_gyro_direction[1]); + } + else { + angle_x = - (float)Math.asin(transformed_gyro_direction[1]); + angle_y = - (float)Math.asin(transformed_gyro_direction[0]); + } if( Math.abs(angle_x) < 0.5f*Math.PI && Math.abs(angle_y) < 0.5f*Math.PI ) { updateCachedViewAngles(time_ms); // ensure view_angle_x_preview, view_angle_y_preview are computed and up to date - float camera_angle_x = this.view_angle_x_preview; - float camera_angle_y = this.view_angle_y_preview; + float camera_angle_x, camera_angle_y; + if( system_orientation_portrait ) { + camera_angle_x = this.view_angle_y_preview; + camera_angle_y = this.view_angle_x_preview; + } + else { + camera_angle_x = this.view_angle_x_preview; + camera_angle_y = this.view_angle_y_preview; + } 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))))); angle_scale_x *= preview.getZoomRatio(); diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java index a7901e5c9..953aecd30 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java @@ -14,6 +14,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; @@ -67,6 +68,7 @@ public class MainUI { private UIPlacement ui_placement = UIPlacement.UIPLACEMENT_RIGHT; private View top_icon = null; private boolean view_rotate_animation; + private float view_rotate_animation_start; // for MainActivity.lock_to_landscape==false private final static int view_rotate_animation_duration = 100; // duration in ms of the icon rotation animation private boolean immersive_mode; @@ -147,6 +149,12 @@ public class MainUI { if( !view_rotate_animation ) { view.setRotation(ui_rotation); } + if( !MainActivity.lock_to_landscape ) { + float start_rotation = view_rotate_animation_start + ui_rotation; + if( start_rotation >= 360.0f ) + start_rotation -= 360.0f; + view.setRotation(start_rotation); + } float rotate_by = ui_rotation - view.getRotation(); if( rotate_by > 181.0f ) rotate_by -= 360.0f; @@ -161,6 +169,16 @@ public class MainUI { layoutUI(false); } + public void layoutUIWithRotation(float view_rotate_animation_start) { + if( MyDebug.LOG ) + Log.d(TAG, "layoutUIWithRotation: " + view_rotate_animation_start); + this.view_rotate_animation = true; + this.view_rotate_animation_start = view_rotate_animation_start; + layoutUI(); + view_rotate_animation = false; + this.view_rotate_animation_start = 0.0f; + } + private UIPlacement computeUIPlacement() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); String ui_placement_string = sharedPreferences.getString(PreferenceKeys.UIPlacementPreferenceKey, "ui_top"); @@ -181,75 +199,136 @@ public class MainUI { debug_time = System.currentTimeMillis(); } + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + boolean system_orientation_portrait = system_orientation == MainActivity.SystemOrientation.PORTRAIT; + boolean system_orientation_reversed_landscape = system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE; + if( MyDebug.LOG ) { + Log.d(TAG, " system_orientation = " + system_orientation); + Log.d(TAG, " system_orientation_portrait? " + system_orientation_portrait); + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity); // we cache the preference_ui_placement to save having to check it in the draw() method this.ui_placement = computeUIPlacement(); 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; + int relative_orientation; + if( MainActivity.lock_to_landscape ) { + // 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; + 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); + } } - // 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); + else { + relative_orientation = 0; } final 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; + // naming convention for variables is for system_orientation==LANDSCAPE, right-handed UI + int align_left = system_orientation_portrait ? RelativeLayout.ALIGN_TOP : RelativeLayout.ALIGN_LEFT; + int align_right = system_orientation_portrait ? RelativeLayout.ALIGN_BOTTOM : RelativeLayout.ALIGN_RIGHT; + int align_top = system_orientation_portrait ? RelativeLayout.ALIGN_RIGHT : RelativeLayout.ALIGN_TOP; + int align_bottom = system_orientation_portrait ? RelativeLayout.ALIGN_LEFT : RelativeLayout.ALIGN_BOTTOM; + int left_of = system_orientation_portrait ? RelativeLayout.ABOVE : RelativeLayout.LEFT_OF; + int right_of = system_orientation_portrait ? RelativeLayout.BELOW : RelativeLayout.RIGHT_OF; + int above = system_orientation_portrait ? RelativeLayout.RIGHT_OF : RelativeLayout.ABOVE; + int below = system_orientation_portrait ? RelativeLayout.LEFT_OF : RelativeLayout.BELOW; + int ui_independent_left_of = left_of; + int ui_independent_right_of = right_of; + int ui_independent_above = above; + int ui_independent_below = below; + int align_parent_left = system_orientation_portrait ? RelativeLayout.ALIGN_PARENT_TOP : RelativeLayout.ALIGN_PARENT_LEFT; + int align_parent_right = system_orientation_portrait ? RelativeLayout.ALIGN_PARENT_BOTTOM : RelativeLayout.ALIGN_PARENT_RIGHT; + int align_parent_top = system_orientation_portrait ? RelativeLayout.ALIGN_PARENT_RIGHT : RelativeLayout.ALIGN_PARENT_TOP; + int align_parent_bottom = system_orientation_portrait ? RelativeLayout.ALIGN_PARENT_LEFT : RelativeLayout.ALIGN_PARENT_BOTTOM; + int center_horizontal = system_orientation_portrait ? RelativeLayout.CENTER_VERTICAL : RelativeLayout.CENTER_HORIZONTAL; + int center_vertical = system_orientation_portrait ? RelativeLayout.CENTER_HORIZONTAL : RelativeLayout.CENTER_VERTICAL; + int iconpanel_left_of = left_of; int iconpanel_right_of = right_of; - int above = RelativeLayout.ABOVE; - int below = RelativeLayout.BELOW; int iconpanel_above = above; int iconpanel_below = below; - int align_parent_left = RelativeLayout.ALIGN_PARENT_LEFT; - int align_parent_right = RelativeLayout.ALIGN_PARENT_RIGHT; int iconpanel_align_parent_left = align_parent_left; int iconpanel_align_parent_right = align_parent_right; - int align_parent_top = RelativeLayout.ALIGN_PARENT_TOP; - int align_parent_bottom = RelativeLayout.ALIGN_PARENT_BOTTOM; int iconpanel_align_parent_top = align_parent_top; int iconpanel_align_parent_bottom = align_parent_bottom; + + if( system_orientation_reversed_landscape ) { + int temp = align_left; + align_left = align_right; + align_right = temp; + temp = align_top; + align_top = align_bottom; + align_bottom = temp; + temp = left_of; + left_of = right_of; + right_of = temp; + temp = above; + above = below; + below = temp; + + ui_independent_left_of = left_of; + ui_independent_right_of = right_of; + ui_independent_above = above; + ui_independent_below = below; + + temp = align_parent_left; + align_parent_left = align_parent_right; + align_parent_right = temp; + temp = align_parent_top; + align_parent_top = align_parent_bottom; + align_parent_bottom = temp; + + iconpanel_left_of = left_of; + iconpanel_right_of = right_of; + iconpanel_above = above; + iconpanel_below = below; + iconpanel_align_parent_left = align_parent_left; + iconpanel_align_parent_right = align_parent_right; + iconpanel_align_parent_top = align_parent_top; + iconpanel_align_parent_bottom = align_parent_bottom; + } + if( ui_placement == UIPlacement.UIPLACEMENT_LEFT ) { - above = RelativeLayout.BELOW; - below = RelativeLayout.ABOVE; - align_parent_top = RelativeLayout.ALIGN_PARENT_BOTTOM; - align_parent_bottom = RelativeLayout.ALIGN_PARENT_TOP; + int temp = above; + above = below; + below = temp; + temp = align_parent_top; + align_parent_top = align_parent_bottom; + align_parent_bottom = temp; iconpanel_align_parent_top = align_parent_top; iconpanel_align_parent_bottom = align_parent_bottom; } else if( ui_placement == UIPlacement.UIPLACEMENT_TOP ) { - iconpanel_left_of = RelativeLayout.BELOW; - iconpanel_right_of = RelativeLayout.ABOVE; - iconpanel_above = RelativeLayout.LEFT_OF; - iconpanel_below = RelativeLayout.RIGHT_OF; + iconpanel_left_of = below; + iconpanel_right_of = above; + iconpanel_above = left_of; + iconpanel_below = right_of; //noinspection SuspiciousNameCombination - iconpanel_align_parent_left = RelativeLayout.ALIGN_PARENT_BOTTOM; + iconpanel_align_parent_left = align_parent_bottom; //noinspection SuspiciousNameCombination - iconpanel_align_parent_right = RelativeLayout.ALIGN_PARENT_TOP; + iconpanel_align_parent_right = align_parent_top; //noinspection SuspiciousNameCombination - iconpanel_align_parent_top = RelativeLayout.ALIGN_PARENT_LEFT; + iconpanel_align_parent_top = align_parent_left; //noinspection SuspiciousNameCombination - iconpanel_align_parent_bottom = RelativeLayout.ALIGN_PARENT_RIGHT; + iconpanel_align_parent_bottom = align_parent_right; } Point display_size = new Point(); @@ -257,6 +336,8 @@ public class MainUI { display.getSize(display_size); final int display_height = Math.min(display_size.x, display_size.y); + final float scale = main_activity.getResources().getDisplayMetrics().density; + /*int navigation_gap = 0; if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ) { final int display_width = Math.max(display_size.x, display_size.y); @@ -309,7 +390,7 @@ public class MainUI { layoutParams.addRule(below, 0); layoutParams.addRule(left_of, 0); layoutParams.addRule(right_of, 0); - layoutParams.setMargins(0, 0, navigation_gap, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); } @@ -417,7 +498,9 @@ public class MainUI { // is displayed (when taking a photo) if it is still shown left-most, rather than centred; also // needed for "pause preview" trash/icons to be shown properly (test by rotating the phone to update // the layout) - layoutParams.setMargins(0, this_view==first_visible_view ? 0 : margin/2, 0, this_view==last_visible_view ? 0 : margin/2); + int margin_first = this_view==first_visible_view ? 0 : margin/2; + int margin_last = this_view==last_visible_view ? 0 : margin/2; + setMarginsForSystemUI(layoutParams, 0, margin_first, 0, margin_last); layoutParams.width = button_size; layoutParams.height = button_size; this_view.setLayoutParams(layoutParams); @@ -443,7 +526,11 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(center_vertical, RelativeLayout.TRUE); + layoutParams.addRule(center_horizontal, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -451,12 +538,30 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(ui_independent_above, R.id.take_photo); + layoutParams.addRule(ui_independent_below, 0); + layoutParams.addRule(ui_independent_left_of, 0); + layoutParams.addRule(ui_independent_right_of, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); view = main_activity.findViewById(R.id.switch_multi_camera); layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + layoutParams.addRule(ui_independent_above, 0); + layoutParams.addRule(ui_independent_below, 0); + layoutParams.addRule(ui_independent_left_of, R.id.switch_camera); + layoutParams.addRule(ui_independent_right_of, 0); + layoutParams.addRule(align_top, R.id.switch_camera); + layoutParams.addRule(align_bottom, R.id.switch_camera); + layoutParams.addRule(align_left, 0); + layoutParams.addRule(align_right, 0); + { + int margin = (int) (5 * scale + 0.5f); // convert dps to pixels + setMarginsForSystemUI(layoutParams, 0, 0, margin, 0); + } view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -464,7 +569,13 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(ui_independent_above, R.id.take_photo); + layoutParams.addRule(ui_independent_below, 0); + layoutParams.addRule(ui_independent_left_of, 0); + layoutParams.addRule(ui_independent_right_of, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -472,7 +583,13 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(above, R.id.take_photo); + layoutParams.addRule(below, 0); + layoutParams.addRule(left_of, 0); + layoutParams.addRule(right_of, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -480,7 +597,13 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(ui_independent_above, 0); + layoutParams.addRule(ui_independent_below, R.id.take_photo); + layoutParams.addRule(ui_independent_left_of, 0); + layoutParams.addRule(ui_independent_right_of, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -488,7 +611,13 @@ public class MainUI { layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); layoutParams.addRule(align_parent_left, 0); layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); + layoutParams.addRule(align_parent_top, 0); + layoutParams.addRule(align_parent_bottom, 0); + layoutParams.addRule(ui_independent_above, 0); + layoutParams.addRule(ui_independent_below, R.id.take_photo); + layoutParams.addRule(ui_independent_left_of, 0); + layoutParams.addRule(ui_independent_right_of, 0); + setMarginsForSystemUI(layoutParams, 0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); setViewRotation(view, ui_rotation); @@ -498,65 +627,68 @@ public class MainUI { layoutParams.addRule(align_parent_right, RelativeLayout.TRUE); layoutParams.addRule(align_parent_top, 0); layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); - layoutParams.setMargins(0, 0, navigation_gap, 0); view.setLayoutParams(layoutParams); - view.setRotation(180.0f); // should always match the zoom_seekbar, so that zoom in and out are in the same directions + setFixedRotation(main_activity.findViewById(R.id.zoom), 0, 0, navigation_gap, 0); + view.setRotation(view.getRotation()+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.ShowZoomControlsPreferenceKey, 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_right, RelativeLayout.TRUE); layoutParams.addRule(align_parent_top, 0); layoutParams.addRule(align_parent_bottom, 0); - layoutParams.setMargins(0, 0, 0, 0); + layoutParams.addRule(above, R.id.zoom); + layoutParams.addRule(below, 0); + layoutParams.addRule(left_of, 0); + layoutParams.addRule(right_of, 0); + // margins set below in setFixedRotation() } 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); - layoutParams.setMargins(0, 0, navigation_gap, 0); + // margins set below in setFixedRotation() // 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); + layoutParams.addRule(left_of, 0); + layoutParams.addRule(right_of, 0); } view.setLayoutParams(layoutParams); + int margin = (int) (20 * scale + 0.5f); // convert dps to pixels + setFixedRotation(main_activity.findViewById(R.id.zoom_seekbar), 0, 0, margin+navigation_gap, 0); 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(above, 0); + layoutParams.addRule(below, 0); layoutParams.addRule(align_parent_top, 0); layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, 0); view.setLayoutParams(layoutParams); view = main_activity.findViewById(R.id.focus_bracketing_target_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(above, R.id.focus_seekbar); layoutParams.addRule(below, 0); view.setLayoutParams(layoutParams); + + setFocusSeekbarsRotation(); } if( !popup_container_only ) { // set seekbar info int width_dp; - if( ui_rotation == 0 || ui_rotation == 180 ) { + if( !system_orientation_portrait && (ui_rotation == 0 || ui_rotation == 180) ) { // landscape width_dp = 350; } @@ -571,7 +703,6 @@ public class MainUI { if( MyDebug.LOG ) Log.d(TAG, "width_dp: " + width_dp); 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 @@ -580,9 +711,12 @@ public class MainUI { view.setTranslationX(0.0f); view.setTranslationY(0.0f); - if( ui_rotation == 90 || ui_rotation == 270 ) { + if( system_orientation_portrait || ui_rotation == 90 || ui_rotation == 270 ) { // portrait - view.setTranslationX(2*height_pixels); + if( system_orientation_portrait ) + view.setTranslationY(2*height_pixels); + else + view.setTranslationX(2*height_pixels); } else if( ui_rotation == 0 ) { // landscape @@ -596,9 +730,12 @@ public class MainUI { /* // align sliders_container RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)view.getLayoutParams(); - if( ui_rotation == 90 || ui_rotation == 270 ) { + if( system_orientation_portrait || ui_rotation == 90 || ui_rotation == 270 ) { // portrait - view.setTranslationX(2*height_pixels); + if( system_orientation_portrait ) + view.setTranslationY(2*height_pixels); + else + view.setTranslationX(2*height_pixels); lp.addRule(left_of, 0); lp.addRule(right_of, 0); lp.addRule(above, 0); @@ -679,21 +816,35 @@ public class MainUI { RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); if( ui_placement == UIPlacement.UIPLACEMENT_TOP ) { layoutParams.addRule(align_right, 0); + layoutParams.addRule(align_bottom, 0); + layoutParams.addRule(align_left, 0); + layoutParams.addRule(align_top, 0); layoutParams.addRule(above, 0); layoutParams.addRule(below, 0); layoutParams.addRule(left_of, 0); layoutParams.addRule(right_of, R.id.popup); - layoutParams.addRule(align_parent_top, RelativeLayout.TRUE); - layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_top, system_orientation_portrait ? 0 : RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, system_orientation_portrait ? 0 : RelativeLayout.TRUE); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, 0); } else { layoutParams.addRule(align_right, R.id.popup); + layoutParams.addRule(align_bottom, 0); + layoutParams.addRule(align_left, 0); + layoutParams.addRule(align_top, 0); layoutParams.addRule(above, 0); layoutParams.addRule(below, R.id.popup); layoutParams.addRule(left_of, 0); layoutParams.addRule(right_of, 0); layoutParams.addRule(align_parent_top, 0); - layoutParams.addRule(align_parent_bottom, RelativeLayout.TRUE); + layoutParams.addRule(align_parent_bottom, system_orientation_portrait ? 0 : RelativeLayout.TRUE); + layoutParams.addRule(align_parent_left, 0); + layoutParams.addRule(align_parent_right, 0); + } + if( system_orientation_portrait ) { + // limit height so doesn't take up full height of screen + layoutParams.height = display_height; } view.setLayoutParams(layoutParams); @@ -734,6 +885,57 @@ public class MainUI { } } + /** Wrapper for layoutParams.setMargins, but where the margins are supplied for landscape orientation, + * and if in portrait these are automatically rotated. + */ + void setMarginsForSystemUI(RelativeLayout.LayoutParams layoutParams, int left, int top, int right, int bottom) { + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + if( system_orientation == MainActivity.SystemOrientation.PORTRAIT ) { + layoutParams.setMargins(bottom, left, top, right); + } + else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) { + layoutParams.setMargins(right, bottom, left, top); + } + else { + layoutParams.setMargins(left, top, right, bottom); + } + } + + /** Some views (e.g. seekbars and zoom controls) are ones where we want to have a fixed + * orientation as if in landscape mode, even if the system UI is portrait. So this method + * sets a rotation so that the view appears as if in landscape orentation, and also sets + * margins. + * Note that Android has poor support for a rotated seekbar - we use view.setRotation(), but + * this doesn't affect the bounds of the view! So as a hack, we modify the margins so the + * view is positioned correctly. For this to work, the view must have a specified width + * (which can be computed programmatically), rather than having both left and right sides being + * aligned to another view. + * The left/top/right/bottom margins should be supply for landscape orientation - these will + * be automatically rotated if we're actually in portrait orientation. + */ + private void setFixedRotation(View view, int left, int top, int right, int bottom) { + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); + int rotation = (360 - MainActivity.getRotationFromSystemOrientation(system_orientation)) % 360; + view.setRotation(rotation); + // set margins due to rotation + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)view.getLayoutParams(); + if( system_orientation == MainActivity.SystemOrientation.PORTRAIT ) { + int diff = (layoutParams.width-layoutParams.height)/2; + if( MyDebug.LOG ) + Log.d(TAG, "diff: " + diff); + setMarginsForSystemUI(layoutParams, +diff+left, -diff+top, +diff+right, -diff+bottom); + } + else { + setMarginsForSystemUI(layoutParams, left, top, right, bottom); + } + view.setLayoutParams(layoutParams); + } + + void setFocusSeekbarsRotation() { + setFixedRotation(main_activity.findViewById(R.id.focus_seekbar), 0, 0, 0, 0); + setFixedRotation(main_activity.findViewById(R.id.focus_bracketing_target_seekbar), 0, 0, 0, 0); + } + private void setPopupViewRotation(int ui_rotation, int display_height) { if( MyDebug.LOG ) Log.d(TAG, "setPopupViewRotation"); @@ -927,6 +1129,9 @@ public class MainUI { Log.d(TAG, "orientation: " + orientation); Log.d(TAG, "current_orientation: " + current_orientation); }*/ + if( !MainActivity.lock_to_landscape ) + return; + // if locked to landscape, we need to handle the orientation change ourselves if( orientation == OrientationEventListener.ORIENTATION_UNKNOWN ) return; int diff = Math.abs(orientation - current_orientation); @@ -2351,26 +2556,52 @@ public class MainUI { } UIPlacement ui_placement = computeUIPlacement(); + MainActivity.SystemOrientation system_orientation = main_activity.getSystemOrientation(); float pivot_x; float pivot_y; switch( ui_placement ) { case UIPLACEMENT_TOP: if( main_activity.getPreview().getUIRotation() == 270 ) { + // portrait (when not locked) pivot_x = 0.0f; pivot_y = 1.0f; } + else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) { + pivot_x = 1.0f; + pivot_y = 1.0f; + } else { pivot_x = 0.0f; pivot_y = 0.0f; } break; case UIPLACEMENT_LEFT: - pivot_x = 1.0f; - pivot_y = 1.0f; + if( system_orientation == MainActivity.SystemOrientation.PORTRAIT ) { + pivot_x = 0.0f; + pivot_y = 1.0f; + } + else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) { + pivot_x = 0.0f; + pivot_y = 0.0f; + } + else { + pivot_x = 1.0f; + pivot_y = 1.0f; + } break; default: - pivot_x = 1.0f; - pivot_y = 0.0f; + if( system_orientation == MainActivity.SystemOrientation.PORTRAIT ) { + pivot_x = 1.0f; + pivot_y = 1.0f; + } + else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE ) { + pivot_x = 0.0f; + pivot_y = 1.0f; + } + else { + pivot_x = 1.0f; + pivot_y = 0.0f; + } break; } ScaleAnimation animation = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, pivot_x, Animation.RELATIVE_TO_SELF, pivot_y); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ba6962524..0645f2ad5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -20,7 +20,6 @@ android:contentDescription="@string/take_photo" android:src="@drawable/take_photo_selector" android:onClick="clickedTakePhoto" - android:layout_centerVertical="true" android:background="@null" /> @@ -33,7 +32,6 @@ android:contentDescription="@string/switch_to_front_camera" android:src="@drawable/switch_camera" android:onClick="clickedSwitchCamera" - android:layout_above="@+id/take_photo" android:background="@drawable/circle_background" android:backgroundTint="@color/icons_background_tint" android:backgroundTintMode="src_in" @@ -47,15 +45,10 @@ android:paddingRight="15dp" android:paddingTop="12dp" android:paddingBottom="18dp" - android:layout_marginRight="5dp" android:scaleType="fitCenter" android:contentDescription="@string/switch_multi_camera" android:src="@drawable/baseline_add_a_photo_white_48" android:onClick="clickedSwitchMultiCamera" - android:layout_below="@+id/switch_video" - android:layout_toLeftOf="@+id/switch_camera" - android:layout_alignTop="@+id/switch_camera" - android:layout_alignBottom="@+id/switch_camera" android:background="@drawable/circle_background" android:backgroundTint="@color/icons_background_tint" android:backgroundTintMode="src_in" @@ -71,7 +64,6 @@ android:contentDescription="@string/pause_video" android:src="@drawable/ic_pause_circle_outline_white_48dp" android:onClick="clickedPauseVideo" - android:layout_above="@+id/take_photo" android:background="@null" android:visibility="gone" /> @@ -85,7 +77,6 @@ android:contentDescription="@string/cancel_panorama" android:src="@drawable/baseline_close_white_48" android:onClick="clickedCancelPanorama" - android:layout_above="@+id/take_photo" android:background="@null" android:visibility="gone" /> @@ -101,7 +92,6 @@ android:src="@drawable/take_video" android:alpha="0.55" android:onClick="clickedSwitchVideo" - android:layout_below="@+id/take_photo" android:background="@null" /> @@ -114,16 +104,18 @@ android:contentDescription="@string/take_photo" android:src="@drawable/take_photo_when_video_recording" android:onClick="clickedTakePhotoVideoSnapshot" - android:layout_below="@+id/take_photo" android:background="@null" android:visibility="gone" /> + @@ -132,10 +124,6 @@ android:contentDescription="@string/zoom" android:layout_width="150dp" android:layout_height="50dp" - android:layout_marginLeft="0dp" - android:layout_marginRight="20dp" - android:layout_marginTop="0dp" - android:layout_marginBottom="0dp" android:background="@color/seekbar_background" /> @@ -145,7 +133,7 @@ android:contentDescription="@string/focus_distance" android:layout_width="150dp" android:layout_height="50dp" - android:layout_marginLeft="135dp" + android:layout_marginLeft="0dp" android:layout_marginRight="0dp" android:layout_marginTop="0dp" android:layout_marginBottom="0dp" @@ -159,7 +147,7 @@ android:contentDescription="@string/focus_bracketing_target_distance" android:layout_width="150dp" android:layout_height="50dp" - android:layout_marginLeft="135dp" + android:layout_marginLeft="0dp" android:layout_marginRight="0dp" android:layout_marginTop="0dp" android:layout_marginBottom="0dp" @@ -619,8 +607,6 @@ android:id="@+id/popup_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignRight="@+id/popup" - android:layout_below="@+id/popup" android:fadeScrollbars="false" /> -- GitLab From fdbad024ab9358404f48b592bd8a0b2b1a479177 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Thu, 4 Feb 2021 23:29:39 +0000 Subject: [PATCH 158/430] Fix for moving getDisplayRotation to ApplicationInterface. --- .../net/sourceforge/opencamera/test/MainActivityTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 0d65cf79a..b7a961f54 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -10930,7 +10930,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sat, 6 Feb 2021 20:25:53 +0000 Subject: [PATCH 159/430] Add logging. --- .../cameracontroller/CameraController2.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 05f212858..193d50b79 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -7299,6 +7299,11 @@ public class CameraController2 extends CameraController { Log.d(TAG, "frameNumber: " + frameNumber); Log.d(TAG, "exposure time: " + request.get(CaptureRequest.SENSOR_EXPOSURE_TIME)); } + else if( getRequestTagType(request) == RequestTagType.CAPTURE_BURST_IN_PROGRESS ) { + Log.d(TAG, "onCaptureStarted: capture burst in progress"); + Log.d(TAG, "frameNumber: " + frameNumber); + Log.d(TAG, "exposure time: " + request.get(CaptureRequest.SENSOR_EXPOSURE_TIME)); + } } // n.b., we don't play the shutter sound here for RequestTagType.CAPTURE, as it typically sounds "too late" // (if ever we changed this, would also need to fix for burst, where we only set the RequestTagType.CAPTURE for the last image) @@ -7329,6 +7334,13 @@ public class CameraController2 extends CameraController { Log.d(TAG, "exposure time: " + request.get(CaptureRequest.SENSOR_EXPOSURE_TIME)); Log.d(TAG, "frame duration: " + request.get(CaptureRequest.SENSOR_FRAME_DURATION)); } + else if( getRequestTagType(request) == RequestTagType.CAPTURE_BURST_IN_PROGRESS ) { + Log.d(TAG, "onCaptureCompleted: capture burst in progress"); + Log.d(TAG, "sequenceId: " + result.getSequenceId()); + Log.d(TAG, "frameNumber: " + result.getFrameNumber()); + Log.d(TAG, "exposure time: " + request.get(CaptureRequest.SENSOR_EXPOSURE_TIME)); + Log.d(TAG, "frame duration: " + request.get(CaptureRequest.SENSOR_FRAME_DURATION)); + } } process(request, result); processCompleted(request, result); -- GitLab From b553fc802374f09688cc81a5baea1d07dc79b465 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 10 Feb 2021 23:02:52 +0000 Subject: [PATCH 160/430] Fix testTakeVideoMacro for scoped storage. --- .../net/sourceforge/opencamera/test/MainActivityTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index b7a961f54..83a9d1773 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -8205,8 +8205,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Thu, 11 Feb 2021 22:58:25 +0000 Subject: [PATCH 161/430] Fix for testPhotoNR on Galaxy S10e. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 83a9d1773..33ce06f54 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -3840,7 +3840,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Thu, 11 Feb 2021 22:59:22 +0000 Subject: [PATCH 162/430] Fixes for running with lock_to_landscape==false, when in portrait orientation. --- .../opencamera/test/MainActivityTest.java | 69 ++++++++++++++----- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 33ce06f54..0f961511a 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -887,11 +887,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 (int)(0.8*display_size.x)); - assertEquals(0, settingsButton.getTop()); - assertEquals(display_size.x, galleryButton.getRight()); - assertEquals(0, galleryButton.getTop()); + if( mActivity.getSystemOrientation() == MainActivity.SystemOrientation.PORTRAIT ) { + assertTrue(settingsButton.getBottom() > (int)(0.8*display_size.y)); + assertEquals(display_size.x, settingsButton.getRight()); + assertEquals(display_size.y-1, galleryButton.getBottom()); + assertEquals(display_size.x, galleryButton.getRight()); + } + else { + assertTrue(settingsButton.getRight() > (int)(0.8*display_size.x)); + assertEquals(0, settingsButton.getTop()); + assertEquals(display_size.x, galleryButton.getRight()); + assertEquals(0, galleryButton.getTop()); + } { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); @@ -8647,10 +8676,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Thu, 11 Feb 2021 23:38:55 +0000 Subject: [PATCH 163/430] Update year for 2021. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7ef9b77f..9a79f5706 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1015,7 +1015,7 @@ Open Source licences Open Camera -Open Camera is © 2013–2020 Mark Harman, released under the GPL v3 or later. Tap here for full licence text and terms of service. +Open Camera is © 2013–2021 Mark Harman, released under the GPL v3 or later. Tap here for full licence text and terms of service. AndroidX libraries Open Camera uses the AndroidX/Jetpack libraries under the Apache license version 2.0. Tap here for full licence text. -- GitLab From fea149aed4dd335c75ac76f398b57b5ed5e88bff Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Fri, 12 Feb 2021 23:17:25 +0000 Subject: [PATCH 164/430] Fix exiting immersive mode on Android 11. --- _docs/history.html | 1 + .../net/sourceforge/opencamera/MainActivity.java | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 737a828f1..969cb817f 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -50,6 +50,7 @@ Version 1.49 (Work in progress) +FIXED Couldn't exit immersive mode on Android 11. UPDATED Support longer exposure time (1/3s) on some Samsung Galaxy S devices. UPDATED Improvement to Noise Reduction photo mode to avoid overexposing in lower light scenes. diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 06a0ba028..6a5e6eee3 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -565,12 +565,16 @@ public class MainActivity extends Activity { String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_low_profile"); boolean hide_ui = immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything"); - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + // Note that Android example code says to test against SYSTEM_UI_FLAG_FULLSCREEN, + // but this stopped working on Android 11, as when calling setSystemUiVisibility(0) + // to exit immersive mode, when we arrive here the flag SYSTEM_UI_FLAG_FULLSCREEN + // is still set. Fixed by checking for SYSTEM_UI_FLAG_HIDE_NAVIGATION instead - + // which makes some sense since we run in fullscreen mode all the time anyway. + //if( (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 ) { + if( (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 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. + // change UI due to having exited immersive mode if( hide_ui ) mainUI.setImmersiveMode(false); setImmersiveTimer(); @@ -578,9 +582,7 @@ public class MainActivity extends Activity { 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. + // change UI due to having entered immersive mode if( hide_ui ) mainUI.setImmersiveMode(true); } -- GitLab From d6fbcd3f358ee254f909e79ee9f3f38f14734715 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sun, 14 Feb 2021 15:47:22 +0000 Subject: [PATCH 165/430] Add logging. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 0f961511a..9936e5239 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -12733,6 +12733,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sun, 14 Feb 2021 15:48:00 +0000 Subject: [PATCH 166/430] Fix logging. --- app/src/main/java/net/sourceforge/opencamera/StorageUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 13f76c77b..80e649550 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -1158,7 +1158,7 @@ public class StorageUtils { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private Media getLatestMediaSAF(Uri treeUri) { if (MyDebug.LOG) - Log.d(TAG, "getLatestMedia: " + treeUri); + Log.d(TAG, "getLatestMediaSAF: " + treeUri); Media media = null; -- GitLab From eaafa748917c42c52c034cdfb8a990f7a5e76023 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 14 Feb 2021 20:54:32 +0000 Subject: [PATCH 167/430] Fix some testLogProfile*() tests for Galaxy devices. --- .../opencamera/test/MainActivityTest.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 9936e5239..dac1bbf9f 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -16753,7 +16753,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sun, 14 Feb 2021 21:08:45 +0000 Subject: [PATCH 168/430] Fix tests that load images for scoped storage. --- .../opencamera/test/MainActivityTest.java | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index dac1bbf9f..ac6d12001 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -12315,10 +12315,41 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sun, 14 Feb 2021 21:09:30 +0000 Subject: [PATCH 169/430] Run testLogProfile*() tests earlier, due to now requiring user SAF permission with scoped storage. --- .../opencamera/test/VideoTests.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java index 93a4a5f0f..b3a6c29a1 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java @@ -8,6 +8,7 @@ public class VideoTests { public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideo")); + // put these tests first as they require various permissions be allowed, that can only be set by user action: if( !MainActivityTest.test_camera2 ) { suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoAudioControl")); } @@ -17,6 +18,15 @@ public class VideoTests { suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoSubtitlesSAF")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoSubtitlesGPS")); } + if( MainActivityTest.test_camera2 ) { + // tests for video log profile (but these don't actually record video) + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile1")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile2")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile3")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile1_extra_strong")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile2_extra_strong")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile3_extra_strong")); + } suite.addTest(TestSuite.createTest(MainActivityTest.class, "testIntentVideo")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testIntentVideoDurationLimit")); @@ -78,15 +88,6 @@ public class VideoTests { /*suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoBitrate")); suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideo4K"));*/ - // tests for video log profile (but these don't actually record video) - if( MainActivityTest.test_camera2 ) { - suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile1")); - suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile2")); - suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile3")); - suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile1_extra_strong")); - suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile2_extra_strong")); - suite.addTest(TestSuite.createTest(MainActivityTest.class, "testLogProfile3_extra_strong")); - } return suite; } } -- GitLab From 38c29c08c4e10ea34b7fc205a2cd1bdab860649d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 20 Feb 2021 19:12:52 +0000 Subject: [PATCH 170/430] Add comment for running tests on Android 10+ with scoped storage. --- .../java/net/sourceforge/opencamera/test/AvgTests.java | 1 + .../java/net/sourceforge/opencamera/test/HDRNTests.java | 1 + .../java/net/sourceforge/opencamera/test/HDRTests.java | 1 + .../java/net/sourceforge/opencamera/test/PanoramaTests.java | 1 + 4 files changed, 4 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/AvgTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/AvgTests.java index a502dd85c..6e5334ce5 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/AvgTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/AvgTests.java @@ -9,6 +9,7 @@ public class AvgTests { * 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 every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. */ public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRNTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRNTests.java index 7283de7a3..4c82cfd8a 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRNTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRNTests.java @@ -9,6 +9,7 @@ public class HDRNTests { * 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 every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. */ public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java index ec247386e..5c6b56224 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java @@ -9,6 +9,7 @@ public class HDRTests { * 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 every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. */ public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/PanoramaTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/PanoramaTests.java index 5e3c38d00..554ac0b79 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/PanoramaTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/PanoramaTests.java @@ -9,6 +9,7 @@ public class PanoramaTests { * 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 every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. */ public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); -- GitLab From 29d43bae3f4872f055121ecf2a16a64620ea759c Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 20 Feb 2021 19:15:33 +0000 Subject: [PATCH 171/430] Fix saving bitmaps on Android 10 with scoped storage. --- .../opencamera/test/MainActivityTest.java | 117 +++++++++++++++++- .../sourceforge/opencamera/StorageUtils.java | 2 +- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index ac6d12001..a09fc8262 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -1,6 +1,7 @@ package net.sourceforge.opencamera.test; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -36,6 +37,8 @@ import net.sourceforge.opencamera.ui.PopupView; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.ContentValues; import android.content.Intent; import android.content.SharedPreferences; //import android.content.res.AssetManager; @@ -58,6 +61,7 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; //import android.os.Environment; +import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.provider.MediaStore; @@ -12361,10 +12365,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 = Build.VERSION_CODES.Q ? + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) : + MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + // first try to delete pre-existing image + Uri old_uri = getUriFromName(folder, name); + if( old_uri != null ) { + Log.d(TAG, "delete: " + old_uri); + mActivity.getContentResolver().delete(old_uri, null, null); + } + + contentValues = new ContentValues(); + contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, name); + String extension = name.substring(name.lastIndexOf(".")); + String mime_type = mActivity.getStorageUtils().getImageMimeType(extension); + Log.d(TAG, "mime_type: " + mime_type); + contentValues.put(MediaStore.Images.Media.MIME_TYPE, mime_type); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + String relative_path = Environment.DIRECTORY_DCIM + File.separator; + Log.d(TAG, "relative_path: " + relative_path); + contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, relative_path); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 1); + } + + uri = mActivity.getContentResolver().insert(folder, contentValues); + Log.d(TAG, "saveUri: " + uri); + if( uri == null ) { + throw new IOException(); + } + outputStream = mActivity.getContentResolver().openOutputStream(uri); + } + else { + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator + name); + outputStream = new FileOutputStream(file); + } + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream); outputStream.close(); - mActivity.getStorageUtils().broadcastFile(file, true, false, true); + + if( MainActivity.useScopedStorage() ) { + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { + contentValues.clear(); + contentValues.put(MediaStore.Images.Media.IS_PENDING, 0); + mActivity.getContentResolver().update(uri, contentValues, null, null); + } + } + else { + mActivity.getStorageUtils().broadcastFile(file, true, false, true); + } } /** diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 80e649550..e6299ade5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -781,7 +781,7 @@ public class StorageUtils { /** Return the mime type corresponding to the supplied extension. Supports images only, not video. */ - String getImageMimeType(String extension) { + public String getImageMimeType(String extension) { String mimeType; switch (extension) { case "dng": -- GitLab From 9b807292a52509926289edcf36cd27601733f673 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 20 Feb 2021 19:25:34 +0000 Subject: [PATCH 172/430] Comment out debug code for saving debug csv files. --- .../main/java/net/sourceforge/opencamera/HDRProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index 831d1cfc5..5500952bc 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -262,7 +262,7 @@ public class HDRProcessor { Log.d(TAG, "parameter_B = " + parameter_B); } - if( MyDebug.LOG ) { + /*if( MyDebug.LOG ) { // log samples to a CSV file File file = new File(context.getExternalFilesDir(null).getPath() + "/net.sourceforge.opencamera.hdr_samples_" + id + ".csv"); if( file.exists() ) { @@ -300,7 +300,7 @@ public class HDRProcessor { } } MediaScannerConnection.scanFile(context, new String[] { file.getAbsolutePath() }, null, null); - } + }*/ } } -- GitLab From f33912280b98f5d9ccf963c8b779db8e69b9ca6a Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 20 Feb 2021 22:25:33 +0000 Subject: [PATCH 173/430] Comment out to avoid loads of logging when lots of files in folder. --- .../main/java/net/sourceforge/opencamera/StorageUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index e6299ade5..a5659e288 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -1247,9 +1247,9 @@ public class StorageUtils { } String this_filename = cursor.getString(column_name_c); - if( MyDebug.LOG ) { + /*if( MyDebug.LOG ) { Log.d(TAG, "Date: " + this_date + " doc_id: " + doc_id + " Name: " + this_filename + " Uri: " + this_uri); - } + }*/ if( latest_uri == null || this_date > latest_date ) { latest_uri = this_uri; -- GitLab From 9d70a5e634b98d4c902d9bbce31ca9d4c5354453 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 20 Feb 2021 22:59:10 +0000 Subject: [PATCH 174/430] Add logging. --- app/src/main/java/net/sourceforge/opencamera/StorageUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index a5659e288..66ac25ae9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -71,6 +71,8 @@ public class StorageUtils { } void clearLastMediaScanned() { + if( MyDebug.LOG ) + Log.d(TAG, "clearLastMediaScanned"); last_media_scanned = null; } -- GitLab From adf8931c05a61b4f6e89ae43d8802ea55be5a93e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 20 Feb 2021 23:37:45 +0000 Subject: [PATCH 175/430] Remove spurious logging. --- app/src/main/java/net/sourceforge/opencamera/SpeechControl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java b/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java index 7f02942f1..87c8283e7 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java +++ b/app/src/main/java/net/sourceforge/opencamera/SpeechControl.java @@ -43,8 +43,6 @@ class SpeechControl { } void showToast(boolean force) { - if( MyDebug.LOG ) - Log.d(TAG, "speechRecognizerStarted"); if( force || !shown_toast || System.currentTimeMillis() > last_toast_time_ms + 10000 ) { shown_toast = true; last_toast_time_ms = System.currentTimeMillis(); -- GitLab From 9724dcef5d8480bc1b5f1282954aa80ddf6ae2f7 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 28 Feb 2021 15:19:14 +0000 Subject: [PATCH 176/430] Correct pitch and compass line lengths for portrait vs landscape orientations. --- _docs/history.html | 1 + .../java/net/sourceforge/opencamera/ui/DrawPreview.java | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 969cb817f..49badb462 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -51,6 +51,7 @@ Version 1.49 (Work in progress) FIXED Couldn't exit immersive mode on Android 11. +FIXED Corrected pitch and compass line lengths for portrait vs landscape orientations. UPDATED Support longer exposure time (1/3s) on some Samsung Galaxy S devices. UPDATED Improvement to Noise Reduction photo mode to avoid overexposing in lower light scenes. diff --git a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java index 14fcb118f..222e7ead1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java @@ -2188,6 +2188,7 @@ public class DrawPreview { boolean has_geo_direction = preview.hasGeoDirection(); double geo_direction = preview.getGeoDirection(); // n.b., must draw this without the standard canvas rotation + // lines should be shorter in portrait int radius_dps = (device_ui_rotation == 90 || device_ui_rotation == 270) ? 60 : 80; int radius = (int) (radius_dps * scale + 0.5f); // convert dps to pixels double angle = - preview.getOrigLevelAngle(); @@ -2300,7 +2301,8 @@ public class DrawPreview { 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_pref ) { - int pitch_radius_dps = (device_ui_rotation == 90 || device_ui_rotation == 270) ? 100 : 80; + // lines should be shorter in portrait + int pitch_radius_dps = (device_ui_rotation == 90 || device_ui_rotation == 270) ? 80 : 100; int pitch_radius = (int) (pitch_radius_dps * scale + 0.5f); // convert dps to pixels int angle_step = 10; if( preview.getZoomRatio() >= 2.0f ) @@ -2342,7 +2344,9 @@ public class DrawPreview { } } if( has_geo_direction && has_pitch_angle && show_geo_direction_lines_pref ) { - int geo_radius_dps = (device_ui_rotation == 90 || device_ui_rotation == 270) ? 80 : 100; + // lines should be longer in portrait - n.b., this is opposite to behaviour of pitch lines, as + // geo lines are drawn perpendicularly + int geo_radius_dps = (device_ui_rotation == 90 || device_ui_rotation == 270) ? 100 : 80; 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; -- GitLab From ad524b12bb9fefdefedfc591467ad12348287134 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 28 Feb 2021 15:25:03 +0000 Subject: [PATCH 177/430] Update broken link. --- _docs/help.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/help.html b/_docs/help.html index d94c73905..fd57e356a 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -1040,7 +1040,7 @@ behaviour of these buttons). Video subtitles - This option is analogous to the "Stamp photos" option, but rather than embedding text into the video itself, it stores the text in a separate subtitles -(".SRT") file. Most decent video players should support +(".SRT") file. Most decent video players should support SRT files, and use them to display the information as subtitles. The subtitles will record the date and time. If "Store location data" is enabled (see "Location settings" below), then the current location latitude and longitude coordinates will also be recorded (if the location is known). Similarly for "Store compass direction". Note that you can control the formatting style for date, time and location using -- GitLab From 605dac1ac77adf43bb3e4dfe826cc8fdc2710a6b Mon Sep 17 00:00:00 2001 From: Mark Harman
API. Known issues are:Date: Sun, 28 Feb 2021 15:27:55 +0000 Subject: [PATCH 178/430] Updates. --- _docs/devices.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/_docs/devices.html b/_docs/devices.html index 1e8f6e38a..0f1029021 100644 --- a/_docs/devices.html +++ b/_docs/devices.html @@ -168,16 +168,15 @@ manual controls, RAW and 120fps video. -
- Slow motion and high speed frame rate video doesn't work (see below for more details).
-- The maximum manual shutter speed allowed for third party camera applications is 0.1s (most devices - seem to allow at least 0.5s).
- The "Image quality" setting has no effect for JPEGs (unless post-processing options such as auto-level or photo stamp are applied). This has also been reported for other Samsung devices; I also have the same issue with other third party camera applications on my S10e. See this thread for details.
Both rear physical cameras are available to Open Camera; also the two modes for the front camera ("cropped" and - "wide") are available to Open Camera.
+
Both rear physical cameras (standard and ultra wide) are available to Open Camera; also the two modes for the front camera + ("cropped" and "wide") are available to Open Camera. Note however that at least some Samsung devices do not expose all + of their cameras for third party applications.
More generally I have occasionally tested on various Samsung devices using their remote test labs - although useful, this is limited compared to owning a real device (especially when the test labs are dark!)
@@ -205,7 +204,7 @@ manual controls, RAW and 120fps video. Note 9 (these articles are for Filmic Pro, but the issues faced likely affect all third party camera applications, including Open Camera). -On a positive note, the Galaxy Note 4 and 5 were used with Open Camera to film +
On a related note, the Galaxy Note 4 and 5 were used with Open Camera to film the world's first 4K feature film shot on a phone.
-- GitLab From 6fda7c38aabbdee9690267cb1ceef603e3152edb Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sun, 28 Feb 2021 15:57:52 +0000 Subject: [PATCH 179/430] Tweaks for Samsung devices, to take advantage of the better results in auto mode. --- _docs/history.html | 1 + .../cameracontroller/CameraController2.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 49badb462..afa6ce0b6 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -54,6 +54,7 @@ FIXED Couldn't exit immersive mode on Android 11. FIXED Corrected pitch and compass line lengths for portrait vs landscape orientations. UPDATED Support longer exposure time (1/3s) on some Samsung Galaxy S devices. UPDATED Improvement to Noise Reduction photo mode to avoid overexposing in lower light scenes. +UPDATED Optimisations for DRO and NR photo modes on Samsung devices. Version 1.48.3 (2020/11/20) diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 193d50b79..095d9c142 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -3883,6 +3883,7 @@ public class CameraController2 extends CameraController { return 0; // total burst size is unknown return n_burst_total; } + @Override public void setOptimiseAEForDRO(boolean optimise_ae_for_dro) { if( MyDebug.LOG ) @@ -3895,6 +3896,13 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "don't modify ae for OnePlus"); } + else if( is_samsung ) { + // At least some Samsung devices (e.g., Galaxy S10e on Android 11) give better results in auto mode + // than manual mode, so we're better off staying in auto mode. + this.optimise_ae_for_dro = false; + if( MyDebug.LOG ) + Log.d(TAG, "don't modify ae for Samsung"); + } else { this.optimise_ae_for_dro = optimise_ae_for_dro; } @@ -6535,7 +6543,9 @@ public class CameraController2 extends CameraController { Log.d(TAG, "optimise for bright scene"); //n_burst = 2; n_burst = 3; - if( !camera_settings.has_iso ) { + // At least some Samsung devices (e.g., Galaxy S10e on Android 11) give better results in auto mode + // than manual mode, so we're better off staying in auto mode. + if( !camera_settings.has_iso && !is_samsung ) { double exposure_time_scale = getScaleForExposureTime(exposure_time, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale); exposure_time *= exposure_time_scale; if( MyDebug.LOG ) { -- GitLab From 3feb00458fe8e11cd8ecf20506404f062f481e81 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 28 Feb 2021 15:58:24 +0000 Subject: [PATCH 180/430] Refactor - store is_oneplus field. --- .../opencamera/cameracontroller/CameraController2.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index 095d9c142..e7aa20496 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -65,6 +65,7 @@ public class CameraController2 extends CameraController { private CameraDevice camera; private final String cameraIdS; + private final boolean is_oneplus; private final boolean is_samsung; private final boolean is_samsung_s7; // Galaxy S7 or Galaxy S7 Edge private final boolean is_samsung_galaxy_s; @@ -1760,10 +1761,12 @@ public class CameraController2 extends CameraController { this.preview_error_cb = preview_error_cb; this.camera_error_cb = camera_error_cb; + this.is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus"); this.is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung"); this.is_samsung_s7 = Build.MODEL.toLowerCase(Locale.US).contains("sm-g93"); this.is_samsung_galaxy_s = is_samsung && Build.MODEL.toLowerCase(Locale.US).contains("sm-g"); if( MyDebug.LOG ) { + Log.d(TAG, "is_oneplus: " + is_oneplus); Log.d(TAG, "is_samsung: " + is_samsung); Log.d(TAG, "is_samsung_s7: " + is_samsung_s7); Log.d(TAG, "is_samsung_galaxy_s: " + is_samsung_galaxy_s); @@ -3888,10 +3891,9 @@ public class CameraController2 extends CameraController { public void setOptimiseAEForDRO(boolean optimise_ae_for_dro) { if( MyDebug.LOG ) Log.d(TAG, "setOptimiseAEForDRO: " + optimise_ae_for_dro); - boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus"); if( is_oneplus ) { - // OnePlus 3T has preview corruption / camera freezing problems when using manual shutter speeds - // So best not to modify auto-exposure for DRO + // OnePlus 3T has preview corruption / camera freezing problems when using manual shutter speeds. + // So best not to modify auto-exposure for DRO. this.optimise_ae_for_dro = false; if( MyDebug.LOG ) Log.d(TAG, "don't modify ae for OnePlus"); @@ -6501,7 +6503,6 @@ public class CameraController2 extends CameraController { if( MyDebug.LOG ) Log.d(TAG, "optimise for dark scene"); n_burst = noise_reduction_low_light ? N_IMAGES_NR_DARK_LOW_LIGHT : N_IMAGES_NR_DARK; - boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus"); // OnePlus 3T at least has bug where manual ISO can't be set to above 800, so dark images end up too dark - // so no point enabling this code, which is meant to brighten the scene, not make it darker! if( !camera_settings.has_iso && !is_oneplus ) { -- GitLab From 4b5a28b4c79b1fb7757fab5848db3f495c21ab31 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 28 Feb 2021 16:05:35 +0000 Subject: [PATCH 181/430] Data folders become inaccessible in Android 11+, best not to mention this at all. --- _docs/help.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index fd57e356a..b9747b0d2 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -1225,11 +1225,7 @@ e.g., "extSdCard".
Can you implement disabling shutter sound for my phone? -
--
GitLab
From b67efcd31894624822474e53b5b266991533209e Mon Sep 17 00:00:00 2001
From: Mark Harman Show camera when locked - If you have a lock screen on your device (e.g., PIN to unlock), Open Camera by
-default will show above the lock screen - i.e., if locked, you won't have to enter the PIN to use Open Camera. The
-device still needs to be unlocked in order to go to the Settings or Gallery. If you would prefer Open Camera to always
-be unavailable when your device is locked, you can disable this option. Show camera when locked - If you have a lock screen on your device (e.g., PIN to unlock), if this option
+ is enabled Open Camera will show above the lock screen - i.e., if locked, you won't have to enter the PIN to
+ use Open Camera. The device still needs to be unlocked in order to go to the Settings or Gallery. If you would
+ prefer Open Camera to always be unavailable when your device is locked, this option should be disabled. Perform auto-focus on startup - Whether Open Camera should auto-focus when starting the camera. Some devices
have a bug where the flash turns on when this happens, so a workaround is to disable this option.
Show free memory - Whether to display the remaining storage space of the device.
Show ISO - If selected, the current ISO, exposure time and frame rate (FPS) will be displayed (only available if Camera2 - API is used). The text will be shown in red when the auto-exposure routine is scanning. In flash auto mode, a flash symbol will - also indicate when flash will fire. Note that this is not an absolute guarantee - when you take a photo, the camera will make an - updated decision on whether to fire the flash, which in some cases may disagree with whether the flash symbol was displayed.
+ API is used). The text will be shown in red when the auto-exposure routine is scanning.Show a histogram - Allows displaying an on-screen histogram (only available if Camera2 API is used). Note that the
histogram reflects the currently display on-screen preview, and will not necessarily be accurate for the final resultant photograph,
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 402ae68bf..1a8550761 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -138,7 +138,7 @@
Open Camera is developed by Mark Harman. If you have inquiries about my privacy policy, + please contact by email me at + mark.harman.apps@gmail.com.
+Open Camera accesses and records camera sensor and microphone data, which is used for the purpose of taking photos and recording videos, to fulfil its purpose as a camera. Microphone permission is also used for the optional "Audio control" options.
Open Camera requires permission (at least for Android 9 and earlier, or using versions of Open Camera older than 1.48.3) to "access photos, media and files on your devices" (storage permission), as this permission is required for Android to save resultant files such as photos and videos to your device.
-Location permission is required for the optional geotagging features (for photos and videos, including stamp and subtitles options). +
Location permission is requested in order to deliver the optional geotagging features (for photos and videos, including stamp and subtitles options). When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files. Location permission is also required to connect to Bluetooth remote control devices.
-Bluetooth permission is required for communicating with some supported Bluetooth remote control devices.
+Location permission is also requested in order to connect to Bluetooth remote control devices.
+ +Resultant data such as photos or videos can be shared with + other apps if you use the share option in Open Camera, or when Open Camera is called by + another app on your device.
+ +Since Open Camera does not transmit personal or sensitive information to me, I do not have + data handling procedures, data retention or deletion policies.
Since Open Camera also uses operating system APIs, you should review relevant privacy policies such as for your device, manufacturer, operating system and/or Google accounts. For example:
@@ -71,7 +82,7 @@ of taking photos and recording videos, to fulfil its purpose as a camera. MicropSave location - Select the folder to store the photos in.
+Save location - Select the folder to store the resultant photos or videos in.
Once you have specified a new save location, you can long press on the Gallery icon to quickly switch between recent save locations. If you want to save to an SD card, see "How can I save to my external SD card?" under the FAQ.
-Use Storage Access Framework - If selected, Open Camera will instead use the Android +
Storage Access Framework - If selected, Open Camera will instead use the Android Storage Access Framework. This has some advantages, such as using the standard Android file picker, and being the only way to save to SD cards on Android 5+. +In some cases it may allow you to save to cloud or local storage providers provided by other apps or services. Furthermore on Android 10+, it is the only way to save outside of the DCIM/ folder. (Requires Android 5.0 or higher.)
Open Camera is completely free, however if you wish you can show your appreciation to me by making an optional payment. - Please note that this is not required, and that doing so will not provide any additional features (or remove the ads I have on the - website). However for those who wish to make a contribution to me, this is greatly appreciated!
- -You can contribute through PayPal, simply click the "Buy Now" button, and enter the amount you wish to give me:
- - - -...or you can also set up a regular contribution:
- - - - -(PayPal account not required, supports debit or credit card). -Thanks!
- -Please note that these contributions do not constitute a charitable donation, and hence not eligible for tax-deduction. (If you have found my webpage because you are using a camera app with ads inside it, then you may have downloaded a third party clone that is not distributed by me. For my Open Camera app, please make sure you've downloaded from the links given on my Main Page.)
- -Privacy policy: Please note that some information such as name, email, amount will be shared to me by PayPal. All PayPal transactions are subject to the PayPal Privacy Policy (alternatively known as PayPal's Privacy Statement).
+I am not currently accepting donations. Thanks to those who have supported me in the past!
Open Camera is developed by Mark Harman. If you have inquiries about my privacy policy, - please contact by email me at + please contact me by email at mark.harman.apps@gmail.com.
Open Camera accesses and records camera sensor and microphone data, which is used for the purpose @@ -61,8 +61,7 @@ of taking photos and recording videos, to fulfil its purpose as a camera. Microp "access photos, media and files on your devices" (storage permission), as this permission is required for Android to save resultant files such as photos and videos to your device.
Location permission is requested in order to deliver the optional geotagging features (for photos and videos, including stamp and subtitles options). - When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files. - Location permission is also required to connect to Bluetooth remote control devices.
+ When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files.Location permission is also requested in order to connect to Bluetooth remote control devices.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f58c2ac50..b6362718e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -994,7 +994,7 @@ Open Camera is developed by Mark Harman. If you have inquiries about my privacy policy, - please contact by email me at + please contact me by email at <a href=\"mailto:mark.harman.apps@gmail.com?subject=Open%20Camera%20privacy%20policy\">mark.harman.apps@gmail.com</a>. <br/>Open Camera accesses camera sensor and microphone data to fulfil its purpose as a camera. Microphone is also used for the optional \"Audio control\". -- GitLab From aec7ef7cf03fea445b206b7e1f037a7bb5d12a04 Mon Sep 17 00:00:00 2001 From: Mark HarmanLocation permission is also requested in order to connect to Bluetooth remote control devices.
+Bluetooth permission is used to allow the optional feature to enable Bluetooth remote control devices.
+Resultant data such as photos or videos can be shared with
other apps if you use the share option in Open Camera, or when Open Camera is called by
another app on your device, or when you use the Storage Access Framework option to save
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b6362718e..fff8dbce9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1003,6 +1003,7 @@
including stamp and subtitles options).
When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files.
Location permission is also requested in order to connect to Bluetooth remote control devices.
+ Bluetooth permission is used to allow the optional feature to enable Bluetooth remote control devices.
<br/>Resultant data such as photos or videos can be shared with
other apps if you use the share option in Open Camera, or when Open Camera is called by
another app on your device, or when you use the Storage Access Framework option to save
--
GitLab
From 21b1b371d3b75d29d2b3963d053b2869bb1a4365 Mon Sep 17 00:00:00 2001
From: Mark Harman Open Camera is developed by Mark Harman. If you have inquiries about my privacy policy,
- please contact me by email at
- mark.harman.apps@gmail.com. Open Camera is developed by Mark Harman. Open Camera accesses and records camera sensor and microphone data, which is used for the purpose
of taking photos and recording videos, to fulfil its purpose as a camera. Microphone permission is also used for the optional "Audio control" options.
If you have inquiries about my privacy policy, please contact me by email at + mark.harman.apps@gmail.com.
+Although Open Camera is ad-free, the Open Camera website has ads via Google Adsense: Third party vendors, including Google, use cookies to
serve ads based on a user's previous visits to this website or other websites. Google's use of advertising cookies enables it and
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fff8dbce9..428d67147 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -993,9 +993,7 @@
-Version 1.49 (Work in progress)
+Version 1.49 (2021/09/07)
FIXED Crash when failing to save photos/videos with mediastore (Android 10+ if not using Storage
Access Framework).
--
GitLab
From 658c159c007cc0c10bf4e82fb652ecdbb08cdb81 Mon Sep 17 00:00:00 2001
From: Mark Harman
+Version 1.49.1 (Work in progress)
+
+FIXED Crop guides weren't drawn correctly in portrait orientation in 1.49.
+FIXED Diagonals grid wasn't drawn correctly in portrait orientation in 1.49.
+
Version 1.49 (2021/09/07)
FIXED Crash when failing to save photos/videos with mediastore (Android 10+ if not using Storage
--
GitLab
From 311ffd65b3dfec8ce03bd70af87e88368d463291 Mon Sep 17 00:00:00 2001
From: Mark Harman
-Version 1.49.1 (Work in progress)
+Version 1.49.1 (2021/09/20)
FIXED Crop guides weren't drawn correctly in portrait orientation in 1.49.
FIXED Diagonals grid wasn't drawn correctly in portrait orientation in 1.49.
--
GitLab
From 2b2a07af0d5da9360b0fa7bffa427d7b73a35734 Mon Sep 17 00:00:00 2001
From: Mark Harman
+Version 1.49.2 (Work in progress)
+
+UPDATED Switched to using AppCompat AppCompatActivity.
+
Version 1.49.1 (2021/09/20)
FIXED Crop guides weren't drawn correctly in portrait orientation in 1.49.
diff --git a/app/build.gradle b/app/build.gradle
index fd00a7016..bf47580b8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,6 +8,7 @@ android {
applicationId "net.sourceforge.opencamera"
minSdkVersion 15
targetSdkVersion 30
+ compileSdkVersion 31 // needed to support appcompat:1.4.0 (which we need for emoji policy support, and not yet ready to target SDK 30)
renderscriptTargetApi 21
//renderscriptSupportModeEnabled true // don't use support library as it bloats the APK, and we don't need pre-4.4 support
@@ -38,6 +39,9 @@ android {
}
dependencies {
+ // appcompat version must be 1.4.0 or later to satisfy emoji policy!
+ //implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
testImplementation 'junit:junit:4.13.1'
diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
index 839613a37..67c034882 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -53,7 +53,6 @@ import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
-import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
@@ -97,9 +96,11 @@ import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.ZoomControls;
+import androidx.appcompat.app.AppCompatActivity;
+
/** The main Activity for Open Camera.
*/
-public class MainActivity extends Activity {
+public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static int activity_count = 0;
diff --git a/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java b/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java
index a80f5a1ad..982977ed5 100644
--- a/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java
+++ b/app/src/main/java/net/sourceforge/opencamera/TakePhoto.java
@@ -1,15 +1,16 @@
package net.sourceforge.opencamera;
-import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
+import androidx.appcompat.app.AppCompatActivity;
+
/** 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 {
+public class TakePhoto extends AppCompatActivity {
private static final String TAG = "TakePhoto";
// Usually passing data via intent is preferred to using statics - however here a static is better for security,
diff --git a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java
index 9eb0a1ffd..30ce66887 100644
--- a/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java
+++ b/app/src/main/java/net/sourceforge/opencamera/remotecontrol/DeviceScanner.java
@@ -3,7 +3,7 @@ package net.sourceforge.opencamera.remotecontrol;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
-import android.app.ListActivity;
+//import android.app.ListActivity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
@@ -18,12 +18,14 @@ import android.os.Handler;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
@@ -37,7 +39,9 @@ import net.sourceforge.opencamera.R;
import java.util.ArrayList;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
-public class DeviceScanner extends ListActivity {
+//public class DeviceScanner extends ListActivity {
+//public class DeviceScanner extends Activity {
+public class DeviceScanner extends AppCompatActivity {
private static final String TAG = "OC-BLEScanner";
private LeDeviceListAdapter leDeviceListAdapter;
private BluetoothAdapter bluetoothAdapter;
@@ -83,7 +87,21 @@ public class DeviceScanner extends ListActivity {
TextView currentRemote = findViewById(R.id.currentRemote);
currentRemote.setText(getResources().getString(R.string.bluetooth_current_remote) + " " + remote_name);
+ }
+
+ @Override
+ public void onContentChanged() {
+ if( MyDebug.LOG )
+ Log.d(TAG, "onContentChanged");
+ super.onContentChanged();
+
+ ListView list = findViewById(R.id.list);
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ onListItemClick((ListView)parent, v, position, id);
+ }
+ });
}
private void startScanning() {
@@ -98,7 +116,9 @@ public class DeviceScanner extends ListActivity {
}
leDeviceListAdapter = new LeDeviceListAdapter();
- setListAdapter(leDeviceListAdapter);
+ //setListAdapter(leDeviceListAdapter);
+ ListView list = findViewById(R.id.list);
+ list.setAdapter(leDeviceListAdapter);
// In real life most of bluetooth LE devices associated with location, so without this
// permission the sample shows nothing in most cases
@@ -245,7 +265,7 @@ public class DeviceScanner extends ListActivity {
super.onDestroy();
}
- @Override
+ //@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
final BluetoothDevice device = leDeviceListAdapter.getDevice(position);
if( device == null )
diff --git a/app/src/main/res/layout/activity_device_select.xml b/app/src/main/res/layout/activity_device_select.xml
index 930c4b4c6..b8d79cf7b 100644
--- a/app/src/main/res/layout/activity_device_select.xml
+++ b/app/src/main/res/layout/activity_device_select.xml
@@ -28,8 +28,9 @@
android:layout_marginTop="0dp"
android:text="@string/bluetooth_scan" />
+
Distance unit - If "Stamp photos" is enabled, this controls whether to use metres (m) or
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 048d6987c..278f59fe1 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -895,10 +895,11 @@
an entfernte Server gesendet werden, um die Spracherkennung durchzuführen.
\n%s
+
-
-Version 1.49.2 (Work in progress)
+Version 1.49.2 (2022/01/13)
FIXED Dialog for "Save settings" shouldn't allow multiple lines.
FIXED Crash for NR photo mode on some devices since version 1.49.
--
GitLab
From abac963cf36e2feeef5cd85034a537771ec2367b Mon Sep 17 00:00:00 2001
From: Mark Harman
+Version 1.50 (Work in progress)
+
+FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12.
+
Version 1.49.2 (2022/01/13)
FIXED Dialog for "Save settings" shouldn't allow multiple lines.
diff --git a/app/src/main/rs/process_hdr.rs b/app/src/main/rs/process_hdr.rs
index 8a82d6653..3821de08d 100644
--- a/app/src/main/rs/process_hdr.rs
+++ b/app/src/main/rs/process_hdr.rs
@@ -194,8 +194,14 @@ uchar4 __attribute__((kernel)) hdr(uchar4 in, uint32_t x, uint32_t y) {
}
else {
pixels[0] = in;
- parameter_A[0] = parameter_A[mid_indx];
- parameter_B[0] = parameter_B[mid_indx];
+ // Reading from the parameter_A1, parameter_B1 instead of the local arrays should be equivalent -
+ // but doing it this way fixes odd bug on some Galaxy devices (e.g., S10e) with Android 12 - HDR photos
+ // would come out black due to corrupt values in the local parameter_A, parameter_B arrays
+ // n.b., doesn't affect hdr_n()
+ //parameter_A[0] = parameter_A[mid_indx];
+ //parameter_B[0] = parameter_B[mid_indx];
+ parameter_A[0] = parameter_A1;
+ parameter_B[0] = parameter_B1;
}
// middle image is not offset
@@ -206,8 +212,11 @@ uchar4 __attribute__((kernel)) hdr(uchar4 in, uint32_t x, uint32_t y) {
}
else {
pixels[2] = in;
- parameter_A[2] = parameter_A[mid_indx];
- parameter_B[2] = parameter_B[mid_indx];
+ // See note above about bug with some Galaxy devices and Android 12
+ //parameter_A[2] = parameter_A[mid_indx];
+ //parameter_B[2] = parameter_B[mid_indx];
+ parameter_A[2] = parameter_A1;
+ parameter_B[2] = parameter_B1;
}
float3 hdr = (float3){0.0f, 0.0f, 0.0f};
--
GitLab
From 2fe8917794d93e20e1b0dad43ea490d861309369 Mon Sep 17 00:00:00 2001
From: Mark Harman
Both rear physical cameras (standard and ultra wide) are available to Open Camera; also the two modes for the front camera ("cropped" and "wide") are available to Open Camera. Note however that at least some Samsung devices do not expose all diff --git a/_docs/help.html b/_docs/help.html index 1394db568..8490e0caf 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -949,6 +949,11 @@ Camera2 API. A common issue is poor flash behaviour (either flash doesn't fire, If so, enabling this option may help - this uses an alternative algorithm for flash (using the torch to simulate flash as a workaround). Note that this is enabled by default for Samsung devices.
+Enable dummy capture HDR/expo fix - (Camera2 API only.) Enable this option if your device has problems taking photos +in HDR or Exposure Bracketing photo modes, specifically if some expo images come out with the same exposures. This option +takes an additional "dummy" image which may resolve such problems. Note that "Enable fast HDR/expo burst" (below) must +be enabled for this option to have an effect.
+Enable fast HDR/expo burst - (Camera2 API only.) Disable this option if your device has problems taking photos in HDR or Exposure Bracketing photo modes (disabling this option will result in a longer delay between the photos being taken, but may give more stable behaviour if your device is having problems with this).
diff --git a/_docs/history.html b/_docs/history.html index 7549767ff..585ecb66c 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -50,6 +50,9 @@ Version 1.50 (Work in progress) FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12. +ADDED New debug option Settings/Photo settings/"Enable dummy capture HDR/expo fix". Please enable + this if you are having problems with HDR or expo bracketing mode on Samsung Galaxy devices + with Android 11+ (specifically if some expo images come out with the same exposures). Version 1.49.2 (2022/01/13) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index b3fa8ceb4..37baaca09 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -1751,6 +1751,11 @@ public class MyApplicationInterface extends BasicApplicationInterface { return sharedPreferences.getBoolean(PreferenceKeys.Camera2FakeFlashPreferenceKey, false); } + @Override + public boolean useCamera2DummyCaptureHack() { + return sharedPreferences.getBoolean(PreferenceKeys.Camera2DummyCaptureHackPreferenceKey, false); + } + @Override public boolean useCamera2FastBurst() { return sharedPreferences.getBoolean(PreferenceKeys.Camera2FastBurstPreferenceKey, true); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java index 3dfe2ff00..d565aaa7a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java @@ -698,6 +698,10 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared PreferenceGroup pg = (PreferenceGroup)this.findPreference("preference_category_photo_debugging"); pg.removePreference(pref); + pref = findPreference("preference_camera2_dummy_capture_hack"); + pg = (PreferenceGroup)this.findPreference("preference_category_photo_debugging"); + pg.removePreference(pref); + pref = findPreference("preference_camera2_fast_burst"); pg = (PreferenceGroup)this.findPreference("preference_category_photo_debugging"); pg.removePreference(pref); diff --git a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java index 178b87427..4227d7c98 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java +++ b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -170,6 +170,8 @@ public class PreferenceKeys { public static final String Camera2FakeFlashPreferenceKey = "preference_camera2_fake_flash"; + public static final String Camera2DummyCaptureHackPreferenceKey = "preference_camera2_dummy_capture_hack"; + public static final String Camera2FastBurstPreferenceKey = "preference_camera2_fast_burst"; public static final String Camera2PhotoVideoRecordingPreferenceKey = "preference_camera2_photo_video_recording"; diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java index 46cb3f70d..4de8e5107 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java @@ -405,6 +405,11 @@ public abstract class CameraController { */ public abstract void setExpoBracketingStops(double stops); public abstract void setUseExpoFastBurst(boolean use_expo_fast_burst); + /** Whether to enable a workaround hack for some Galaxy devices - take an additional dummy photo + * when taking an expo/HDR burst, to avoid problem where manual exposure is ignored for the + * first image. + */ + public abstract void setDummyCaptureHack(boolean dummy_capture_hack); public abstract boolean isBurstOrExpo(); /** If true, then the camera controller is currently capturing a burst of images. */ diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java index 8b8d58f22..934bad0ed 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java @@ -836,6 +836,11 @@ public class CameraController1 extends CameraController { this.expo_bracketing_stops = stops; } + @Override + public void setDummyCaptureHack(boolean dummy_capture_hack) { + // not supported for CameraController1 + } + @Override public void setUseExpoFastBurst(boolean use_expo_fast_burst) { // not supported for CameraController1 diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index a9502b1ca..3884c4845 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -187,12 +187,19 @@ public class CameraController2 extends CameraController { private boolean continuous_burst_in_progress; // whether we're currently taking a continuous burst private boolean continuous_burst_requested_last_capture; // whether we've requested the last capture + // Whether to enable a workaround hack for some Galaxy devices - take an additional dummy photo + // when taking an expo/HDR burst, to avoid problem where manual exposure is ignored for the + // first image. + private boolean dummy_capture_hack = false; + //private boolean dummy_capture_hack = true; // test + private boolean optimise_ae_for_dro = false; private boolean want_raw; //private boolean want_raw = true; private int max_raw_images; private android.util.Size raw_size; private ImageReader imageReaderRaw; + private OnImageAvailableListener onImageAvailableListener; private OnRawImageAvailableListener onRawImageAvailableListener; private PictureCallback picture_cb; private boolean jpeg_todo; // whether we are still waiting for JPEG images @@ -1312,6 +1319,8 @@ public class CameraController2 extends CameraController { } private class OnImageAvailableListener implements ImageReader.OnImageAvailableListener { + private boolean skip_next_image = false; // whether to ignore the next image (used for dummy_capture_hack) + @Override public void onImageAvailable(ImageReader reader) { if( MyDebug.LOG ) @@ -1324,6 +1333,14 @@ public class CameraController2 extends CameraController { image.close(); return; } + if( skip_next_image ) { + if( MyDebug.LOG ) + Log.d(TAG, "skipping image"); + skip_next_image = false; + Image image = reader.acquireNextImage(); + image.close(); + return; + } ListLocation permission is requested in order to deliver the optional geotagging features (for photos and videos, including stamp and subtitles options). When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files.
-Location permission is also requested in order to connect to Bluetooth remote control devices.
- -Bluetooth permission is used to allow the optional feature to enable Bluetooth remote control devices.
+Bluetooth permissions are used to allow the optional feature to discover and connect to Bluetooth LE remote control devices; + the Bluetooth remote control feature also requires location permission (on Android 11 or earlier) or + Nearby Devices permission (on Android 12 or later).
Resultant data such as photos or videos can be shared with
other apps if you use the share option in Open Camera, or when Open Camera is called by
diff --git a/app/build.gradle b/app/build.gradle
index fbd51016c..518fc641d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,14 +1,14 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 30
+ compileSdkVersion 31
compileOptions.encoding = 'UTF-8'
defaultConfig {
applicationId "net.sourceforge.opencamera"
minSdkVersion 15
- targetSdkVersion 30
- compileSdkVersion 31 // needed to support appcompat:1.4.0 (which we need for emoji policy support, and not yet ready to target SDK 30)
+ targetSdkVersion 31
+ //compileSdkVersion 31 // needed to support appcompat:1.4.0 (which we need for emoji policy support, and not yet ready to target SDK 30)
renderscriptTargetApi 21
//renderscriptSupportModeEnabled true // don't use support library as it bloats the APK, and we don't need pre-4.4 support
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a5286c56d..d913932e4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,14 +10,23 @@
X@E1Nv2aE>_KXB)8}~7jBv**EF;0?ahz_I8O8%A^9pzv@w9AnnVn8tA
zdx6edZm>VLrzRutJ5|0D2ay?$#)@SkuRNSeag2B34gA(R+S4c4jAv^>lM}p^u99^=
zBnqLY8ruCV3y}_3WsLL+O#el@GNOmaBB1NdIW=EI1C=8UzhPgKhq*4gaegQ8CBH>y
zgRAoG@{ByrzmX%a`o>)0HCw}h^74h(WZ5Iy@SwU%#e37uGVoJ4&`bo*pZ+N28^4tT
zk3z=(MBuQtGP|SjUTs;Q
z*4~}f%w(b{ilT&sj1@;ZEYfjgMK**pgwi@)(P_TT!C}pKMmtm_k3kxWHYC
z9?#>8R>gq|a`<4k;=&9HSa*S`iVrP}V%aE56fZVn*}IAxRrFxhcT_5V)Nu}req*AN
z!5j)$)6Xm=hxuH_k|7$DESed{ifc3}d9*Tu1y^ZNI%wuHuDeW=(nBNtxNMMzl`iIR
z4p*IKj?zaRJ-Dcg$#Mf#Y@~!sN_;?-TtW*4Tv1?|+`|kG;esEildEW>pEM1yR&HZ5
z9gLEO65E(23uxpRNjt=RS;A6|kd)&rmqje%AaU!WS(eejRtAW}5FIqg2V}^yhhd`f
zH(j(bUVfp5b!?}PvAD!;)-YK
Exposure lock - Click to lock or unlock the exposure. @@ -775,7 +777,8 @@ rather take photos by other methods (e.g., if your device has a hardware shutter
Show What's New dialog - When updating to a major new version, Open Camera displays a dialog explaining the new features and other options. You can disable this dialog from being displayed if you prefer.
-Multiple cameras icon - This option only shows on devices with more than one front and/or back cameras. When +
Multiple cameras icon - This option only shows on devices that allow explicitly switching between more than +one front and/or back cameras. When enabled (the default), such devices will have two on-screen icons: one to switch between front/back cameras, and another to switch between the multiple front or back cameras. If this option is disabled, only a single icon will show, which will cycle between all cameras.
@@ -1282,9 +1285,9 @@ and untick Auto-level. that are made available to third party applications. Usually this means front and back cameras, but some devices have multiple front and/or back-facing cameras. Use the switch multi-camera icon -to switch between multiple front or back cameras. In some cases the extra cameras aren't made available to third party -applications, so it isn't possible for Open Camera to support them. Even where they are, since there is no current standard -on what the extra cameras are used for, it can't do anything with them other than allowing you to switch between them. +to switch between multiple front or back cameras. Note that some devices do not expose the multiple cameras explicitly, +but instead will automatically switch cameras as required when zooming in or out. In some cases the extra cameras aren't +made available to third party applications, so it isn't possible for Open Camera to support them.Why doesn't Open Camera support the maximum video resolution on my device? - If you
are using Camera2 API, make sure that you're not in slow motion mode (see "Speed" under
--
GitLab
From e464a08b0cdd616caa4d2225e9175a19901198d5 Mon Sep 17 00:00:00 2001
From: Mark Harman In Noise Reduction photo mode, an additional "NR mode" option will appear on the popup mode. This defaults to Normal, but
diff --git a/_docs/history.html b/_docs/history.html
index fe3db14af..a18ad9823 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -55,6 +55,7 @@ FIXED Granting only approximate location permission on Android 12 would turn g
FIXED On-screen text looked strange on Android 12.
FIXED Gallery icon overlapped with navigation bar if using widescreen resolution with UI in left
or right handed mode.
+ advanced photo modes are now available (e.g., Night Sight on Pixel 6 Pro; Night, Bokeh and
ADDED Improved support for devices with multiple camera devices, where extra cameras are exposed
via zooming in and out (e.g., Pixel 5/6).
ADDED New debug option Settings/Photo settings/"Enable dummy capture HDR/expo fix". Please enable
diff --git a/_docs/index.html b/_docs/index.html
index 8d2b3da13..fa8ea302b 100644
--- a/_docs/index.html
+++ b/_docs/index.html
@@ -120,7 +120,7 @@ browsers -->
In general, Google Nexuses have worked well for Open Camera. Firstly because Google provide good support for their own
- Android camera API, both old and Camera2 (as you'd hope!) - and they use it for their own Google camera. Secondly because
- I've owned several Nexuses (Galaxy Nexus, Nexus 7, Nexus 6), and so have been able to test Open Camera against them. In general, Google Nexuses and Pixels have worked well for Open Camera. Camera2 on the Nexus 6 works well (there are some minor issues, e.g., manual exposure doesn't work well when recording
+ Camera2 API on the Nexus 6 works well (there are some minor issues, e.g., manual exposure doesn't work well when recording
video). It's hard to be sure about other Nexuses though. In theory all this should apply to the Pixels, but I haven't been able to test - though from what people tell me, things
-seem to work including Camera2 features. Open Camera also supports Google's HDR+ mode on the Pixels with Pixel Visual Core. Similarly Camera2 API works well on the Pixel 6 Pro, although some minor issues (manual white balance doesn't work, some
+ of the Video picture profiles don't work). Open Camera also supports Google's HDR+ mode on the Pixels with Pixel Visual Core
+ (including the Pixel 6 Pro). As of Open Camera 1.50, Night Sight on the Pixel 6 Pro is available via the photo mode X-Night.
+ As of Open Camera 1.50, all of the Pixel 6 Pro's cameras are available to use by zooming in or out. Color effects don't work on the Nexus 7.
+
+ Note many features are unavailable when using an extension mode, including flash, zoom and manual controls.
+ Google Nexuses/Pixels
-
Both rear physical cameras (standard and ultra wide) are available to Open Camera; also the two modes for the front camera - ("cropped" and "wide") are available to Open Camera. Note however that at least some Samsung devices do not expose all - of their cameras for third party applications.
+
Both rear physical cameras (standard and ultra wide) are available to Open Camera (as separate cameras); also the two modes for the front camera + ("cropped" and "wide") are available to Open Camera.
+ +
At least some Samsung Galaxy devices support the camera extension modes (X-Auto, X-Night, X-Bokeh, X-Bty) (including the Galaxy S10e; +in general this is likely available for the flagship S devices running Android 12+).
More generally I have occasionally tested on various Samsung devices using their remote test labs - although useful, this is limited compared to owning a real device (especially when the test labs are dark!)
-- GitLab From 5f6e080919a0f0b896a276c61cee01baccb71738 Mon Sep 17 00:00:00 2001 From: Mark HarmanAudio control options - If enabled, this allows taking a photo (or starting video recording, depending on the mode) by making a noise. An on-screen microphone button will appear, to start/stop listening. The "loud noise" option will listen for any noise (so you can remotely take a photo by saying "cheese", -whistling, or whatever you prefer). Note that leaving the listening turned on may use additional battery. The "voice command" option -listens specifically for saying "cheese" - so this has the advantage that it's less likely to be triggered unintentionally. +whistling, or whatever you prefer). Note that leaving the listening turned on may use additional battery. Note that this can't be used to stop video recording - if you want to have some remote control on video recording, see the "Max duration of video" option.
diff --git a/_docs/index.html b/_docs/index.html index fa8ea302b..d3bd47db0 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -112,7 +112,7 @@ browsers -->Since Open Camera also uses operating system APIs, you should review relevant privacy policies such as for your device, manufacturer, operating system and/or Google accounts. For example:
GPS stamp format - If "Stamp photos" is enabled, this allows extra control over the GPS formatting. Also used for Video settings/"Video subtitles".
-Use addresses - If "Stamp photos" and "Store location data" is enabled, this allows the location +
Distance unit - If "Stamp photos" is enabled, this controls whether to use metres (m) or feet (ft) when recording the GPS altitude. Also used for Video settings/"Video subtitles".
diff --git a/_docs/privacy_oc.html b/_docs/privacy_oc.html index b8a78d1cc..48728d312 100644 --- a/_docs/privacy_oc.html +++ b/_docs/privacy_oc.html @@ -84,10 +84,11 @@ of taking photos and recording videos, to fulfil its purpose as a camera. Microp https://privacy.google.com/businesses/gdprprocessorterms/ , as updated from time to time. This option is no longer available in version 1.50 onwards.-Version 1.50 (Work in progress) +Version 1.49.3 (Work in progress) FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12. FIXED Problems with flash on Camera2 API (Samsung Galaxy, OnePlus, Pixel 6 Pro). Galaxy and -- GitLab From fba9f33b7a232fff71f94aedc7a45e23ba940985 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 4 Jun 2022 13:53:15 +0100 Subject: [PATCH 422/430] Revert last release. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 1a961a870..fa3978c70 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -47,7 +47,7 @@ -Version 1.49.3 (Work in progress) +Version 1.50 (Work in progress) FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12. FIXED Problems with flash on Camera2 API (Samsung Galaxy, OnePlus, Pixel 6 Pro). Galaxy and -- GitLab From 0f9c8dfa84c35061f69edeedc30ac909b812c7c7 Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Sat, 4 Jun 2022 13:53:59 +0100 Subject: [PATCH 423/430] Bump version to 84 / 1.50. --- app/src/main/AndroidManifest.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6fe2d874b..f590945bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ @@ -31,7 +31,7 @@ - + Date: Sat, 4 Jun 2022 13:54:13 +0100 Subject: [PATCH 424/430] Add what's new for 1.50. --- .../java/net/sourceforge/opencamera/MainActivity.java | 2 +- app/src/main/res/values/strings.xml | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index edbef769b..d3079115a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -575,7 +575,7 @@ public class MainActivity extends AppCompatActivity { // E.g., we have a "What's New" for 1.44 (64), but then push out a quick fix for 1.44.1 (65). We don't want to // show the dialog again to people who already received 1.44 (64), but we still want to show the dialog to people // upgrading from earlier versions. - int whats_new_version = 83; // 1.49.2 + int whats_new_version = 84; // 1.50 whats_new_version = Math.min(whats_new_version, version_code); // whats_new_version should always be <= version_code, but just in case! if( MyDebug.LOG ) { Log.d(TAG, "whats_new_version: " + whats_new_version); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31bc288aa..05207476b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1056,10 +1056,14 @@ [This dialog is shown when Open Camera is updated. You can disable it under Settings/On screen GUI/Show What\'s New dialog.] - \n\nv1.49.2:\n + \n\nv1.50:\n -- GitLab From 6c0ba5d6320bea05fb72e126e368ea6f2359f1d2 Mon Sep 17 00:00:00 2001 From: Mark Harman-
- Fixed crash for NR photo mode on some devices since version 1.49.\n
-- Support for latest emoji (e.g. for custom text stamp).\n
+- Support for Camera Extensions. Selected Android 12 devices now have photo modes such as Night, Bokeh, Beauty. Requires Settings/\"Camera API\" to be set to Camera2 API.\n
+- Allow accessing ultra-wide cameras via zoom e.g. on Pixel 5/6 (when using Camera2 API).\n
+- Removed \"use addresses\" and \"say cheese\" options. Sorry about that, but this is due to new data privacy requirements.\n
+- Fixed HDR on Samsung devices since Android 12.\n
+- Fixes for flash on Camera2 API.\n
+- Various improvements for Android 12.\n
Date: Sat, 4 Jun 2022 13:54:57 +0100 Subject: [PATCH 425/430] Add date for release. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index fa3978c70..0a8a1f74c 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -47,7 +47,7 @@ -Version 1.50 (Work in progress) +Version 1.50 (2022/06/04) FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12. FIXED Problems with flash on Camera2 API (Samsung Galaxy, OnePlus, Pixel 6 Pro). Galaxy and -- GitLab From a64c9590e3a4ef592dae32c3c97bb19431f81f6d Mon Sep 17 00:00:00 2001 From: Mark HarmanDate: Wed, 8 Jun 2022 21:26:29 +0100 Subject: [PATCH 426/430] Fix crash on OPPO devices due to setting face detection listener to null. --- _docs/history.html | 4 ++++ .../cameracontroller/CameraController1.java | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index 0a8a1f74c..1e08a256c 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -47,6 +47,10 @@ +Version 1.50.1 (2022/06/08) + +FIXED Crash on OPPO devices for old camera API introduced in 1.50. + Version 1.50 (2022/06/04) FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12. diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java index d4b08f5d8..379da2c2b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java @@ -1611,17 +1611,22 @@ public class CameraController1 extends CameraController { } 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;iDate: Wed, 8 Jun 2022 21:26:41 +0100 Subject: [PATCH 427/430] Bump to 85 / 1.50.1. --- app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f590945bb..179d65b75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ -- GitLab From 0e6881fe034cb594a31174182d90f927c11332f4 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Wed, 17 Aug 2022 12:12:50 +0600 Subject: [PATCH 428/430] merge tag 1.50.1 with master --- .idea/inspectionProfiles/Project_Default.xml | 3 +- _docs/baseline_delete_white_48.png | Bin 0 -> 670 bytes _docs/baseline_switch_camera_white_48.png | Bin 0 -> 1143 bytes _docs/credits.html | 25 +- _docs/devices.html | 44 +- _docs/focus_mode_auto.png | Bin 1151 -> 1612 bytes _docs/focus_mode_continuous_picture.png | Bin 2040 -> 1964 bytes _docs/focus_mode_edof.png | Bin 1805 -> 2378 bytes _docs/focus_mode_fixed.png | Bin 995 -> 933 bytes _docs/focus_mode_infinity.png | Bin 2197 -> 2458 bytes _docs/focus_mode_manual.png | Bin 1566 -> 1614 bytes _docs/help.html | 195 +- _docs/history.html | 143 +- _docs/index.html | 58 +- _docs/info.html | 58 +- _docs/privacy_oc.html | 60 +- _docs/stylesheet.css | 1 + _docs/switch_camera.png | Bin 1126 -> 0 bytes _docs/trash.png | Bin 766 -> 0 bytes androidx_LICENSE-2.0.txt | 202 +++ app/build.gradle | 22 +- .../sourceforge/opencamera/test/AvgTests.java | 1 + .../opencamera/test/HDRNTests.java | 1 + .../sourceforge/opencamera/test/HDRTests.java | 5 + .../opencamera/test/MainActivityTest.java | 1565 +++++++++++++---- .../opencamera/test/MainTests.java | 10 +- .../opencamera/test/Nexus7Tests.java | 13 + .../opencamera/test/OldDeviceTests.java | 39 + .../opencamera/test/PanoramaTests.java | 1 + .../opencamera/test/PhotoTests.java | 11 +- .../opencamera/test/TempTests.java | 15 + .../opencamera/test/VideoTests.java | 25 +- app/src/main/AndroidManifest.xml | 57 +- app/src/main/assets/androidx_LICENSE-2.0.txt | 202 +++ .../sourceforge/opencamera/AudioListener.java | 3 + .../sourceforge/opencamera/HDRProcessor.java | 1120 +++++++----- .../sourceforge/opencamera/ImageSaver.java | 993 ++++++----- .../opencamera/LocationSupplier.java | 50 +- .../sourceforge/opencamera/MainActivity.java | 1564 +++++++++++++--- .../opencamera/MyApplicationInterface.java | 922 +++++++--- .../opencamera/MyPreferenceFragment.java | 149 +- .../opencamera/MyWidgetProvider.java | 7 +- .../opencamera/MyWidgetProviderTakePhoto.java | 7 +- .../opencamera/PanoramaProcessor.java | 13 +- .../PanoramaProcessorException.java | 1 + .../opencamera/PermissionHandler.java | 70 +- .../opencamera/PreferenceKeys.java | 9 +- .../opencamera/SaveLocationHistory.java | 15 + .../opencamera/SettingsManager.java | 12 +- .../sourceforge/opencamera/SpeechControl.java | 295 ---- .../sourceforge/opencamera/StorageUtils.java | 634 +++++-- .../net/sourceforge/opencamera/TakePhoto.java | 5 +- .../cameracontroller/CameraController.java | 39 +- .../cameracontroller/CameraController1.java | 86 +- .../cameracontroller/CameraController2.java | 1341 +++++++++++--- .../CameraControllerManager2.java | 9 + .../preview/ApplicationInterface.java | 38 +- .../preview/BasicApplicationInterface.java | 43 +- .../opencamera/preview/Preview.java | 1058 +++++++---- .../opencamera/preview/VideoProfile.java | 3 + .../remotecontrol/BluetoothLeService.java | 124 +- .../remotecontrol/BluetoothRemoteControl.java | 28 +- .../remotecontrol/DeviceScanner.java | 275 ++- .../opencamera/ui/DrawPreview.java | 458 +++-- .../opencamera/ui/FolderChooserDialog.java | 56 +- .../net/sourceforge/opencamera/ui/MainUI.java | 543 ++++-- .../opencamera/ui/ManualSeekbars.java | 6 +- .../opencamera/ui/MyEditTextPreference.java | 158 ++ .../sourceforge/opencamera/ui/PopupView.java | 163 +- .../baseline_bedtime_white_48.png | Bin 0 -> 597 bytes .../baseline_delete_white_48.png | Bin 0 -> 192 bytes ...eline_face_retouching_natural_white_48.png | Bin 0 -> 1098 bytes .../baseline_portrait_white_48.png | Bin 0 -> 361 bytes .../baseline_switch_camera_white_48.png | Bin 0 -> 355 bytes .../baseline_bedtime_white_48.png | Bin 0 -> 428 bytes .../baseline_delete_white_48.png | Bin 0 -> 151 bytes ...eline_face_retouching_natural_white_48.png | Bin 0 -> 723 bytes .../baseline_portrait_white_48.png | Bin 0 -> 266 bytes .../baseline_switch_camera_white_48.png | Bin 0 -> 244 bytes app/src/main/res/drawable-mdpi/expo_icon.png | Bin 502 -> 582 bytes .../res/drawable-mdpi/focus_bracket_icon.png | Bin 598 -> 0 bytes .../res/drawable-mdpi/focus_mode_auto.png | Bin 1467 -> 1084 bytes .../focus_mode_continuous_picture.png | Bin 1617 -> 936 bytes .../focus_mode_continuous_video.png | Bin 1617 -> 936 bytes .../res/drawable-mdpi/focus_mode_edof.png | Bin 1806 -> 1858 bytes .../res/drawable-mdpi/focus_mode_fixed.png | Bin 870 -> 686 bytes .../res/drawable-mdpi/focus_mode_infinity.png | Bin 2303 -> 1092 bytes .../res/drawable-mdpi/focus_mode_manual.png | Bin 1639 -> 1193 bytes .../drawable-mdpi/white_balance_locked.png | Bin 2777 -> 2617 bytes .../drawable-mdpi/white_balance_unlocked.png | Bin 1922 -> 1760 bytes .../baseline_bedtime_white_48.png | Bin 0 -> 778 bytes .../baseline_delete_white_48.png | Bin 0 -> 239 bytes ...eline_face_retouching_natural_white_48.png | Bin 0 -> 1473 bytes .../baseline_portrait_white_48.png | Bin 0 -> 461 bytes .../baseline_switch_camera_white_48.png | Bin 0 -> 449 bytes .../baseline_bedtime_white_48.png | Bin 0 -> 1130 bytes .../baseline_delete_white_48.png | Bin 0 -> 343 bytes ...eline_face_retouching_natural_white_48.png | Bin 0 -> 2383 bytes .../baseline_portrait_white_48.png | Bin 0 -> 715 bytes .../baseline_switch_camera_white_48.png | Bin 0 -> 667 bytes .../main/res/drawable-xxhdpi/expo_icon.png | Bin 1325 -> 1644 bytes .../drawable-xxhdpi/focus_bracket_icon.png | Bin 1596 -> 0 bytes .../baseline_bedtime_white_48.png | Bin 0 -> 1539 bytes .../baseline_delete_white_48.png | Bin 0 -> 451 bytes ...eline_face_retouching_natural_white_48.png | Bin 0 -> 3242 bytes .../baseline_portrait_white_48.png | Bin 0 -> 1038 bytes .../baseline_switch_camera_white_48.png | Bin 0 -> 841 bytes .../main/res/drawable-xxxhdpi/flash_auto.png | Bin 0 -> 1437 bytes .../main/res/drawable-xxxhdpi/flash_off.png | Bin 0 -> 957 bytes .../main/res/drawable-xxxhdpi/flash_on.png | Bin 0 -> 629 bytes .../res/layout/activity_device_select.xml | 3 +- .../main/res/layout/alertdialog_edittext.xml | 23 + .../main/res/layout/alertdialog_textview.xml | 12 + .../res/layout/arrayseekbarpreference.xml | 1 + .../main/res/layout/myedittextpreference.xml | 29 + .../res/layout/popupview_arrayoptions.xml | 30 + app/src/main/res/layout/popupview_button.xml | 11 + .../main/res/layout/popupview_radiobutton.xml | 11 + app/src/main/res/layout/popupview_switch.xml | 14 + .../main/res/layout/popupview_textview.xml | 20 + app/src/main/res/layout/stamp_image_text.xml | 19 + app/src/main/res/layout/toast_textview.xml | 13 + app/src/main/res/menu/main.xml | 5 +- app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-de/arrays.xml | 10 - app/src/main/res/values-de/strings.xml | 357 ++-- app/src/main/res/values-el/strings.xml | 6 +- app/src/main/res/values-es/arrays.xml | 10 - app/src/main/res/values-es/strings.xml | 339 +--- app/src/main/res/values-fr/arrays.xml | 10 - app/src/main/res/values-fr/strings.xml | 80 +- app/src/main/res/values-hu/arrays.xml | 10 - app/src/main/res/values-hu/strings.xml | 14 +- app/src/main/res/values-it/arrays.xml | 10 - app/src/main/res/values-it/strings.xml | 1343 +++++++------- app/src/main/res/values-ja/arrays.xml | 10 - app/src/main/res/values-ja/strings.xml | 18 +- app/src/main/res/values-nb/arrays.xml | 10 - app/src/main/res/values-nb/strings.xml | 11 +- app/src/main/res/values-pl/arrays.xml | 14 +- app/src/main/res/values-pl/strings.xml | 777 ++++++-- app/src/main/res/values-pt-rPT/arrays.xml | 10 - app/src/main/res/values-pt-rPT/strings.xml | 8 +- app/src/main/res/values-ru/arrays.xml | 10 - app/src/main/res/values-ru/strings.xml | 164 +- app/src/main/res/values-sl/arrays.xml | 41 +- app/src/main/res/values-sl/strings.xml | 647 +++++-- app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/arrays.xml | 10 - app/src/main/res/values-uk/strings.xml | 6 +- app/src/main/res/values-v11/styles.xml | 2 +- app/src/main/res/values-v14/styles.xml | 2 +- app/src/main/res/values-v21/styles.xml | 2 +- app/src/main/res/values-vi/arrays.xml | 10 - app/src/main/res/values-vi/strings.xml | 10 +- app/src/main/res/values-zh-rCN/arrays.xml | 10 - app/src/main/res/values-zh-rCN/strings.xml | 323 ++-- app/src/main/res/values-zh-rTW/arrays.xml | 66 +- app/src/main/res/values-zh-rTW/strings.xml | 130 +- app/src/main/res/values/arrays.xml | 42 +- app/src/main/res/values/strings.xml | 115 +- app/src/main/res/values/styles.xml | 6 +- app/src/main/res/xml/preferences.xml | 65 +- app/src/main/rs/create_mtb.rs | 2 +- app/src/main/rs/process_avg.rs | 50 +- app/src/main/rs/process_hdr.rs | 17 +- .../sourceforge/opencamera/test/UnitTest.java | 266 ++- build.gradle | 6 +- gradle/wrapper/gradle-wrapper.properties | 4 +- makesrcarchive.bat | 6 +- opencamera_source.txt | 8 +- 172 files changed, 12717 insertions(+), 5597 deletions(-) create mode 100644 _docs/baseline_delete_white_48.png create mode 100644 _docs/baseline_switch_camera_white_48.png delete mode 100644 _docs/switch_camera.png delete mode 100644 _docs/trash.png create mode 100644 androidx_LICENSE-2.0.txt create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/test/OldDeviceTests.java create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/test/TempTests.java create mode 100644 app/src/main/assets/androidx_LICENSE-2.0.txt delete mode 100644 app/src/main/java/net/sourceforge/opencamera/SpeechControl.java create mode 100644 app/src/main/java/net/sourceforge/opencamera/ui/MyEditTextPreference.java create mode 100644 app/src/main/res/drawable-hdpi/baseline_bedtime_white_48.png create mode 100644 app/src/main/res/drawable-hdpi/baseline_delete_white_48.png create mode 100644 app/src/main/res/drawable-hdpi/baseline_face_retouching_natural_white_48.png create mode 100644 app/src/main/res/drawable-hdpi/baseline_portrait_white_48.png create mode 100644 app/src/main/res/drawable-hdpi/baseline_switch_camera_white_48.png create mode 100644 app/src/main/res/drawable-mdpi/baseline_bedtime_white_48.png create mode 100644 app/src/main/res/drawable-mdpi/baseline_delete_white_48.png create mode 100644 app/src/main/res/drawable-mdpi/baseline_face_retouching_natural_white_48.png create mode 100644 app/src/main/res/drawable-mdpi/baseline_portrait_white_48.png create mode 100644 app/src/main/res/drawable-mdpi/baseline_switch_camera_white_48.png delete mode 100644 app/src/main/res/drawable-mdpi/focus_bracket_icon.png create mode 100644 app/src/main/res/drawable-xhdpi/baseline_bedtime_white_48.png create mode 100644 app/src/main/res/drawable-xhdpi/baseline_delete_white_48.png create mode 100644 app/src/main/res/drawable-xhdpi/baseline_face_retouching_natural_white_48.png create mode 100644 app/src/main/res/drawable-xhdpi/baseline_portrait_white_48.png create mode 100644 app/src/main/res/drawable-xhdpi/baseline_switch_camera_white_48.png create mode 100644 app/src/main/res/drawable-xxhdpi/baseline_bedtime_white_48.png create mode 100644 app/src/main/res/drawable-xxhdpi/baseline_delete_white_48.png create mode 100644 app/src/main/res/drawable-xxhdpi/baseline_face_retouching_natural_white_48.png create mode 100644 app/src/main/res/drawable-xxhdpi/baseline_portrait_white_48.png create mode 100644 app/src/main/res/drawable-xxhdpi/baseline_switch_camera_white_48.png delete mode 100644 app/src/main/res/drawable-xxhdpi/focus_bracket_icon.png create mode 100644 app/src/main/res/drawable-xxxhdpi/baseline_bedtime_white_48.png create mode 100644 app/src/main/res/drawable-xxxhdpi/baseline_delete_white_48.png create mode 100644 app/src/main/res/drawable-xxxhdpi/baseline_face_retouching_natural_white_48.png create mode 100644 app/src/main/res/drawable-xxxhdpi/baseline_portrait_white_48.png create mode 100644 app/src/main/res/drawable-xxxhdpi/baseline_switch_camera_white_48.png create mode 100644 app/src/main/res/drawable-xxxhdpi/flash_auto.png create mode 100644 app/src/main/res/drawable-xxxhdpi/flash_off.png create mode 100644 app/src/main/res/drawable-xxxhdpi/flash_on.png create mode 100644 app/src/main/res/layout/alertdialog_edittext.xml create mode 100644 app/src/main/res/layout/alertdialog_textview.xml create mode 100644 app/src/main/res/layout/myedittextpreference.xml create mode 100644 app/src/main/res/layout/popupview_arrayoptions.xml create mode 100644 app/src/main/res/layout/popupview_button.xml create mode 100644 app/src/main/res/layout/popupview_radiobutton.xml create mode 100644 app/src/main/res/layout/popupview_switch.xml create mode 100644 app/src/main/res/layout/popupview_textview.xml create mode 100644 app/src/main/res/layout/stamp_image_text.xml create mode 100644 app/src/main/res/layout/toast_textview.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 4c67987dd..739592789 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,11 +1,10 @@ - - + diff --git a/_docs/baseline_delete_white_48.png b/_docs/baseline_delete_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..318f8e78d1211e7931a498b6d49eb7dda4049166 GIT binary patch literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1wJE{-7;ac}SJ&3oh^;*gjwXfGQYA=l70 z>-+QyMlLOt78mZmmTR(X6LrjI UZ1i>vv3B5^5~Pzad7i`)1Cxu>aFH>2%-Cn;v%dgPBMg!wkj*wgyRt zYcp?OuzPVdcg4TnXaTSzH^npmstG(-c=4}Rb%UL~^r??;o@;McfU1XUqz&UHvyEJT z_OBb(xo76tI>cVzf9d$W%;axx7|%Wb?7e60p8TX|Gi-G?pOkx0wfEneRj+JytCiFG zZeBK;9j15a_~VUlF4sGrkuqMM8n%|D;-Bi-@4x@9 6;Lz0YsU zs iT+yIgN z8X+wDfBrv^|KKFR?Rw#~iBmZ@Yjk(n=XdiO5^nosrQPeSo;Um38CU;Lc5hRh4(G(g z$De0zJ+^F*mP!bNq^N@%>x4kY6fpDH>fa4(*RD-memT=4v$bx&eNA=s=3C1Aahxy8 zc7G0Ay;X6k=p&1H->Ym_+kSd*R-~k=>Q~(Q>ucGY E^L)S6Xm4&t6_m?q5pV{HEjs8z0O_IiZ=Y>%98vD}|pw z*>2yy{Waxn+| ({SetkKnUGUd{=#N;B*8+Y&a zPT~&N=DcvSYpGAif$z(gFaPoMsp^!6K0V79xGffpjf-nL`O5KdJcE&=*$IxwlLcyS zOp`R#UzJ!bWSh@x API. Known issues are: -
- Slow motion and high speed frame rate video doesn't work (see below for more details).
-- The maximum manual shutter speed allowed for third party camera applications is 0.1s (most devices - seem to allow at least 0.5s).
- The "Image quality" setting has no effect for JPEGs (unless post-processing options such as auto-level or photo stamp are applied). This has also been reported for other Samsung devices; I also have the same issue with other third party camera applications on my S10e. See this thread for details.
+- HDR and Expo bracketing does not work properly on Android 11+ (the image that is meant to be a darker + exposure does not come out dark). This can be resolved in Open Camera by enabling the option + Settings/Photo settings/"Enable dummy capture HDR/expo fix" (and making sure + "Enable fast HDR/expo burst" is also enabled).
Both rear physical cameras are available to Open Camera; also the two modes for the front camera ("cropped" and - "wide") are available to Open Camera.
+
Both rear physical cameras (standard and ultra wide) are available to Open Camera (as separate cameras); also the two modes for the front camera + ("cropped" and "wide") are available to Open Camera.
+ +
At least some Samsung Galaxy devices support the camera extension modes (X-Auto, X-Night, X-Bokeh, X-Bty) (including the Galaxy S10e; +in general this is likely available for the flagship S devices running Android 12+).
More generally I have occasionally tested on various Samsung devices using their remote test labs - although useful, this is limited compared to owning a real device (especially when the test labs are dark!)
@@ -205,7 +209,7 @@ manual controls, RAW and 120fps video. Note 9 (these articles are for Filmic Pro, but the issues faced likely affect all third party camera applications, including Open Camera). -On a positive note, the Galaxy Note 4 and 5 were used with Open Camera to film +
On a related note, the Galaxy Note 4 and 5 were used with Open Camera to film the world's first 4K feature film shot on a phone.
diff --git a/_docs/focus_mode_auto.png b/_docs/focus_mode_auto.png index 56b64ba353ee10c7756b2f35c367e0839ccd652e..7e48c33a32bccdcd0e3eefd092a610803a017922 100644 GIT binary patch literal 1612 zcmc`#;kQ82*mTt*MYooYYxjPMvbuX+tz*?yR{Ljf#om)=0@^Zpo*ZQi;iqBq_E` zCSh}HXmh_;7^g6rxmGsXSwEfs;G7?x_xU{U``i0`-sjD5b+&~lX(|B#0I@?`W28*{ z2?emUww&}AB%8-D7&$*<=zR;{K>Pmo$9FQ(Uv;s920X$bALJv5u4ru+S zZDtyfWXYqmScz5R#&C-R;eKltw-wS*_%bXEe*F0Cyg*=rk7w4^nND aPe&9`GuLEdm?i0xwCh5G Sa=G^u zLta;`MCYJ~mLO+q)6>%t`WoaRrRaQtY!D-obHtfZQ=?2Gkq*b`Hzd37v0Rw)Orgo% z-QoECc6p0$X_Bkn>NKwgm8Pk!9beU9xHeMXy5jXKaYD$t>>wE)9*$IFW-!lDbn+KD z*=2Ksu(urP^ofLSR8lj=<26Q`WTT-d-3`^%aC 7nj-BRh1NI(kKvwIzPxeC>XvXp JO2mbWW1XibU#o9 (dPUzEvL!zq99>`D--LRcdq;lJzlP`3kchbpletlkVyN7 zFGD|4OEBeBD%|Y#r3WC`ef!C|x#uq!f|XYw>|oXgTD<0Q-Qx56CXaoXC8fenU^3$S zfcNG+F~BhIgxZzXmKLi}dEvo)xr@O;;v5u)WDO13c#1pK+mM=(k#VP^qhnuR!aKTA zzN7BnW!PXxGWyeclc$*hvA&}vjE@7;i&t?opFTl_1>k!{MMd}c2`~(6wyA2gItwqi zDBgs`#Fk-^$HuaxH%BGi9P3F(gwdm#1%irS%p};JDih*|T%&&eYNk3-IKHzlToG}) z%EyH!)){N;&pUJ@J>96%>&uwUV}*zs7E4mQcQ=mcPEj~OA !lvI6;>1s;*b z3=DjGL736~_k^`TL8%hgh?3y^w370~qEv=}#LT=BJwMkFg)(D3Q$0figD*u385o#< zd%8G=RK&f#bJzE=gGk%MFAnS`4$L7A%oi9JG3xDj-oz##>maF_Q2nV$>8#eTZx$OT zOa8BY?{)sH+P8?}I0cr&dHe5|e{f}Z#&JM~afUKOF-t-p!x_N_OC|$%iUrMN54_)R zKmEkHt)a6y9M~hGw>=A6W5ghPValf0k;Y6r0?J~uT^Pbotjo>ic;I#O>sn66kO{Gm zqXZmOZ 7kj zc8{5CnLN`C1KF~)QySR{4T2>%W`1%qv99anNMMmIYhy6;V_(OS@VMkb7nu2oSCc8i zQudVu%Z3E2HDX>2y7wkzC2nSXk+95(!T3_*nMQ`R1 p3|Rn0CGNc4Fv# z*>*kXodQ=Ozr&t|!FTT6lWUXee_7bYAa&`ioOWAqo?y3PuPcM`<+FC`cB|U&n3k-$ z<0!Ik-#)i^W=4}m(zRYZ`)Y2Ti!qjo^%its-gR>Mk+;!SAAYPjcdnW<)*mcX`{mmE zSP5u4x!~XsIFBivpUk_yon3-2F|MA20=PO;Iyk$96C*pQqUy`lO zw8F>SqWDj3ZCT%}l3Z7YsV~{my%>UXcCA~>vB2o=10PNW>wRXi(Si=CyADb+?YN`H zq>=wU?fCU&P`$$YFWb8^?8<9n(7Md~PPqPsqV+zp*!BF3CJScI={(Q$(faSrkJm4h zo>OdKE1CTJ$nAqSKTf|;%nx?y{K<2ZHEgd(uw3ZK`yz0&(x~c=DaQiAyDye1HMEs% zUM^NH_42 (lBeBkI88~EhW&bak$ROrozHggEO3jt&57}$|+8ESK zp7mSsiRNphmWxa`{ra VGd00%)yL_t(|UhSOgPZ~`W#=%uA7!mM-w1sG;iN?gHG5-I4H%-$fO`BA$ z0YwoNg@{+=J&*4pQE`{cp4pvQ`F&w#+N|t(cFtv(o$%)7W-BzrH%9n#h))a!@R^|i zK4Y{1mSxqrX)MMHFg7+;Sz20(e{ YR~Y0(;96AkbLsn!hlAjad>oee>6g4>Bj-h z&_V#!Zl^5>&5m-hSd28A%_RSm@Axi&*FGO&oSvRu?(OY0$W6`iuy1Hzh)DyX-@ku9 z0nu~0T!V*w!*>Dj6n^{mt%4`hOIV25%F0S;W@e_$!=9tX4DeHgxw*MeKA%r7EG&5Y z$C*H+p--8bntC*BzqGHlf4jR|rnYAplU4$NendDvJ`Vl<{hNFLV_DWkGMOwV5{V35 zYqDQKAjGd 4|=3)}KFr4$sfem5ZQd z79<%HDEH6&{rh)fYip}|aBxuT<(d#`d3iZAIXQXAL(0%L4V4bee<;4Tx3}Zpzkfd} zm&?s=*M$hHtE(CMRGJ}aT?eShj_6wR !g3jkD!Kxt>@=g*&xB48HG&(DvmD%O+;pkm9QDiIouMo%K3SPO^4;!79e z0#JEh{qp5Y9H}G^e~F<|gl_H#A}lQ}HMuK>r~o4)BlTP^m!$M9ggo9P5#Wxx1!i(X zlhXGjDgb2+Ej%dzpQ=Tm)RISJKwzfg%l4wUCkn_h?usBL0Q@68A $Ic-PYyygKGhh-M}Z>L3y?EHFZ@u zeRte5rPn9i9T?XFBoc{JC;v}3VNCFSNwr#?bBY5bg6n#~wE&a>MY!1ke5gs;&r2v9 zN;t& dGcibEp*8 gc>xWNb28b?BnS>SsPzoxN zj+sb{;O4-%7Jvek{h~?0p#;PAFJwajTtJ1|6*mXQwE*-buDSW {lRHR6GBXJ^N9nguY{%F0S67K<4r!jB(6YS=x^Lt=;v0ROnXy^Xicd888 eDXC?|1pwrR(p0FB2Hiz4$sMUci2#5C^0-rR24F0>Bk90wk_gHK0DSNT_g)9a zLSSBAUT#ubWPJsgB7#x@$oOd-e9?;tf9Z7E`ug?jd_JGANiAELAc9f>08Smj!57^` z05I5x4 gNv zUg6NCZs1zT=aEwKkQR7)Tn!n&jI$T8&fbp@ZQ6o7rJrl*D1tBN1>x)ky#7Cke+X{T zO$6mJ;5Cq >!4+1HGDaHqZ z6Tl?ngFp#jn(;wk1TfL~AP@qWYJ6b40L^C8SmOiZ1;E*%rWzjvVFnoY`qNVa6NF7= hD1grl1@M(n=s(u(Sf58P^%npD002ovPDHLkV1kEbi68&~ literal 2040 zcmcIl`#;l*8~ nd^{yHW{50rgTsUyOjG#$7Pf(4js9D)saZ%GRwrAgXMndTb)BO zw3N$O=B1NSb8U#GZ*C2nTT?Eb_1pI!I6pkk^Lk#->v=t|=k>hb? >JoE`+N9pxpM| zKnJ?4hX6n>*A<8UIWc&-!plRQpw0es#7AQ+FsmC3ffYL4ank4Gl$>+XDtp;->Y<;j zvvqTObIyu-ixY=%o*g 9I(m@{*Gc79%x%$jy{U?M|9LkAfbyXanCUU%0GCoj2!%JK6~1VV{*dr@H_ zvKVk(-lu+tMr)^JWp(iINHv%V&iqOr@0ogw8p6VY@n(L#^|;d4OA8o9Wo10x)6;jA zIMYN19{JTn@6zdQ&9JR6{gpbhva)M@*98JP5k$?&p_rPQQkoBLkpZ3Q&Xgd;+8x9a z3izX)N~JEiLExnKdWy2n=V}Z4D4AFnP<(v+J?g!iwG9nfim9j58S8ty+YdPu6>ikL z>KqnhSPsO^#!j_a39qnpKCC|1$i{u>Fx9}w11~71js=2?t?{SFpG44xCMN0{tB%kA zRa-mQ>s5ZD=3U!w?la$HAQ*XpWcyrr219OVZI(MU{HNE0ygcRTa$}lLP*7RI$mpo+ zmA4H=2B(sc6}G=Yapy`)OEvP(Y&Yk^!nH!ICn;><3t5DqJz?2LPfw3yc4OzIa5E;Z zed_AU2HMib*7hsj*EhmUGl-*R|80H#&FtJ I_bHW2gB|Of^1CQ_J`&mp(O JiQs{lwgXO!V0RNs;S?s)5_b~^o6fmXU}0LXHAnq`;td1-uzq5_9nq|ZV~ z-$stbZ(@h6?Y8W$uWYQ+ZrLJ6)Aq^r@rOp>P;%!Bk;Z{X3e~!;Ux~urvCH*J*}>Ks zj(XPri#l<2uD>)#%i>HJ0ri1{Aub{ttE;OEmbb6ourY6>iG;%8hP`cx=w-;%9318z zvZ%xU8>=6;i4~_w?{WQ{c)+j+6>^N<;$g=b?FxOV+%;yX7)k(zh+@NbMV~$my^h;_ zaxi1LlK;w@et=Z}?X1ywSLK%{=WFs+U?z=^0RvjZtKIfn41<`_Zi5SYNDsqn%BtBo z=K4d=GKWZ|?w>FUyvfN&6}z{-6^|i2jF^CJRBN%ovm4o>>O`N_h*dL@q8c~%|18bp zfup87N}k>JfCZBJ+&GLK^ G56j9A|9%v4}O+5B_Wy&;VOH4EpshPlR 4))9F8PaqtE`h|B!ynvv#9_{_5tZ9axnU`U3+4 z8RAQI2iyjKQJ* }zGKjb5LJF{UYjl*kk{T3z$wC%RGHpMqG>w^(l %lKm=kpA&|?4yYUdi*CyFQCG_IuA-SaMl zQoX2mcwoTF%1TH`fKNY*y?CL8+hIA*jDO LKUlz$%d1;>1*J(+>`2`YkZ0!zCVP z#!)lefifGt5)Ge}6c#gOJE69&&Mi5Clxl8%G$)foT-+4Cb^HVsI`L{W(O4{}eQazD z+>)%?X_B@q!%NdK)PO>y3#EvAcx5)R%;uHwl7k!3lP&e16YX2RB%?{ukYbg7#TOw* zOI?ufqK>?Ypj;0N` 8n@IYvdU z=UKT*)|eyq $>g;g`kotScc3oUwJO$Ceg%+~>B#bJn`lY_ zDKXD8?GajR0tI(e^5{)oLhH>-C3%Z%dV)7d*mr!P?yaI6fN=!g%Ly4fi@a6;zlfq; zqqjg|@BOmU(!L3LLbUj!UT(Z>Ie}0z(BBVZ_7@fuXa@NC;i~I)jf|CnBUVjqZOH-X zv9CNHFByaRSx))9GW*y4EtRuq=c#7_v#~AgtSnGKmFGiGx^?IBa!hPgRFtBa);=vQ zQY*g;OuafaI~zho!46ao&80jZ&EfaSVX;_{gM$O*l9&h_lIw{2aI8nTw5%+zwzl># zCEWL^XLUvlyf%D|7;SZ%^3cN^;GYRGeW=< T2u-Zfy27nW@+o)cg;7%fhY(8qLfBnW>vwQ 9@N_qJZ-cCsosB15 zBSVfrjF0aj7CC8tAHe-PPqceRBH7Ne=+a5A1#P`SjhOUk1A7&dcD7nLd8?aS30*{c z^z#h33yLwi@8YbkwRe@IP+XRrY3C<7R?)Cu%+Qhjp$<8WGi5DT!s+$(Maa}aQz~&l z(P^icYP4_=?;e&AAMd7rriMKl$kT(;f*!9w^&9h=21eyl*A_ ^Z~;0y3mF%X{g4?IANm4rIMo%mqAl_osE27k~Jv# z*T07T`if-}Q2JIDYleRhcRM EuD=+RJBtv?N(?linc;AebeI!EA9bf3(ng1YC=yCWT^nhE|x39is2P~Ij^?5scH z(H>|Cd=QuFRtvGvq)>t(;IQgQ-8iYg_oCL^2TK|Nw~aV4#1HcIjtm5J63&;{kM^Zp zmb%wOgMB6jPyOLF8iVCk0pk069vm~lAyl-hDdt )wwGMOs97WFMHs!SFO z{MGv0a;*X-S1#oc5c2E63H8`DcML?WeQpMU#$^D`==y>Pi>oxne8|X%dX 10aJ#`ey!-`m%}|Cb5}!K)2Wq9X&qfk8nXtDbj!SR?NCMTEzT%8`Lx zNocdqBRi9E*X#H3@IYn<6qm|)X~s@g@%d$DT*CNb*Zy-exkvJF#o0a*b{Fn`N=ZXk z^DUrqys^3Ya#F(sr}d#B4<~dZ3Z{e=CKF1G0{UfSWUNx_Vs6~9(hwIpyh7*~I^VWp zeNzY6Gl >?mk{ z;;m|TRbNrdF&y05Gv)Oes!v#ACW}g?YCiE}4BeU0gr+4D7f<3rp(`fEj2zD1WvQs- z1eIO_=|%9U42E~~)M(kxR}i;xNhEcfZ!WbI`eo8UU=oG0_StF$gf&X~_U5Y4INdA4 zYA*MHE$yX3u|%AVi;dAW8ADrcqy*6@Oek(9xSnZ}`@U;KWV n%ntWrVGaY_4G@tm2XR5yzcH{X7W^ w=I_e5@-RSRkH$Ob7TU2Ppl&2y+l tRE)qKZRlnGg&yvA&v#$%J@;OYz=v+u->MG)z~->M zA(0wpeg#BFbDyFUMH&Jh#~#1}0KeQoov5w(^=NxZnE lPMp~TUb~q8^>u?IDvYv0uy_I3};}M2@|Fl zR0ehk{dV%du=eu{ztd?nTJ nXuHRuc!SqD9Kro;ZnDoSzli-w?dy+ z)v5MJkN{|zO|^FbL*07Z6{0Kj(|HyYZ&o{a(xo3iQZU@$^}g2~t#3PJ2ZPOS@$2ej zu~?PAwf6+(Zm9c5Mq*Or->t)+w_jKa7lR+@jT`QV=J*6fr>QY$mQm>*r&LA;YrOz<=Q}oWv<>wN#?8&Glf`8u5(orCnN0S!h5Bye*rJuKEuHOx-H%Z&yQeTe zg(FIKAf-|%mD_AZp1>Kqy1FtRpPh@z= )uKxE|ugYra$2Vc`HSs$sKdEXK-TrtQpa9w|WD-!Pv5S3LJ~Ek#-sLXWpL zHJgk(MS*EeZ|7 -55dC{iR6=XoDvvU`E(~` z)!y2AMMSkMIJ0qXeoTV+8?WX3+66efb-lsnpkC|N_0&L~CkaK(DU_904|&s4z%+8> z%le;xIjt@A!B9vbrunJ#bmO !G(6)9QE=N!tGnzyVaKM!BozIl zN!NLO$Wc;xUQ`t@qvk32Beiu5w =#zf*y)Y_JJzfRr#F z6Z?3G9fYEesh!fQLburl;qUUsA-_Y&=E@;ntVFrb0}RPTY~PNfTOXS|Bh4p;Cg|WI zA_3)r_{U H2*I5 z)TH-x4qT-WJMu~~Qiu7cEW;mBCyjbA&GXvDzslcf17=;hm8kp6N3JcbN>nNpCRz<2 zMHh(Pp78ZXRjx(@OQO4v8<6Eqo&}?xDL)B&&X+tncW#wo{G4+A!*6q+Cq=Am=kBS9 z>Q{NA6g`#F;woa+t!&N!1RIV}_{ZO9x}Xcaq-hT`K&-%xP4sJ_u1q^o?05l=z5afF zs~1|u6}ob2+?5U!fMAJlbx%cw^^tGQ?QZPL$96l}#V^*~FQ?{Z4_g-=?wyngJMu-G zq4EIvXj|s=hgxwoTO5`O>i#F_+m-MCGjMBR;ESJYo&S`zZ;58QfG`|B SpaXGQ9 zx$`b}TR`|ujn}3t%$?2PIp=EtxZG_4tE;PN*sEV`0oT{pX{*&*dU$xa50@w5u90=G z*MF;ayWLNZM?I_6YNy-V+h5}H$!~Lqo@Rx+M&^dkt6p7Qy_h@i3KPKo{{C_J0&K_S z#Igc7JUnc)+wHBmEF8B0I0Jw)05}7HGXOXPfGbP@3kwS~c`RXWC0;G0!n_Y~dwW~z zbUJaBEltyJgTbH{mrt_Z2N-wm+1XjM-+%Ad<1#l26TrBRj*d=Rt=5Z^K3M^btI=qj zHk-{?AOFALUISDrm6<%|FgKT51uB5y(|NDgd-Xa0EW^3oZuf0m=1O6 R&cdiysE?%y$uYU-0 ztMR7cAADYgxj*ji?*4fGpa^mofCHw>Et9bW8j}zMMw1W&78uY `Z5 N002ovPDHLkV1l~SPZR(E delta 748 zcmV 5&RZ@0e8X0)r?LKj)I$lpTN!0 zK^z1>fFK%UF@jR-&4kD0Q)P>0sw;UQ23B|D|^}ws-eD!*7YX4Znf+xyvPYq|t7- zGjWl@FMk|1faBxi+w=4DqwDMI+afM6FFU8Fr-!k9YTc^A1<>hqu40h)_xEqJEPMT2 zu|{hBs=)+sa&q!v6oc 32bz1`j2!-Io^N{3MQ0PaRT_(6?EBU@Ztyh{Av z%BXb!vFE1-H!22DVrOS(?_YB74+c S-U zOp{;(7bvb)0p$4+X#;qwRsr N~+p@y>YQE-PSuSwlDm5ijY$%lFC=t}G%)LrYEk|YQYZPW?3R>>W z365}^N}@S&q83^xj*s_zf5P|u@Z5Wzd+smy+~+yxNp^9z6A^+40RR9I2YWbz+fn~Q zfR9^eUfea}Hl7fKofV+GS9Xa@K)#kvmH >SoC{J=1u{M{&ZWCZ>(iBX8D*o{cW=_oVbR+LvqUdDIbOTNyb0BteDm(F ziq?${zh1t5_l*DDyk3KCYUr4gVcy({ANf8*`_FZ}M|On23p{G#b2`Rz>N=?Zv4n+z zEuiOrCTt~(=;(uz+THUpJNcmamP5D|H90va*<;5#dwP5S$w*J{O(B?^!O#~?p^fD& zG@2(Yt1DklLBX3-(?}T`Lk8Tuc{3D?)m6&ep>%YtM2m@*-*QpUsdgzII9nun#G(5n zSX=;fwgxj%e+&XisYJ0gzTo(khBab=3VS |0%_gIV`haBMdmbjy- 6508iEM|OOk9*;EG(R3!*}0_B{uunquox}RBDw2*%$k5 zk$#&mS%v1&N$|`dl)jd>mX^)u#Vu!&b6#FvIl60rcj8MVhrUixr7B2y``y1^xBPru zQP{Yz(7dqyV? #iQr@vrtA8^pQWytNPhtf|P5*W)e;NW}TQ`ss4=lWHbZSu&X* zZyuGu=pHFhm}7q;iT2b6A*E$4esMNBTkXRU)|B5_RaI3R`g7dX#5C}ffswwxer!ax ztQ2ea*ZjHVjg5Z8r0! N((qJ?cN7VBd`)uni*=<5*S#I|W8+vP{M z3ts$KTU#3#8F5FRxH@pcLO)j%yI6aJ>Zmtchyp^bc#64~Uoo&46Ngx<6Z8(&e>C|9 z*U=x_2Y_ii;xMTYS5c88@c8p7K9@<8_{g+tu;bbO#I&@u)yc_8!<9%Ufabd}1z3d9 zO7?Ms)>SW?Hs1kLNhME+Q0%0U_GQ1^WU45Vp^RK0QA2)h?@B-fm|lAymk}|{B;TI; zGy&McM8jPgWvj2s7==c+;9lq-5-!RjdN=Wn`$qu#6xt3H;5yf9B}>ZCp8fH~;$109 z4iL&!8l_mo%iCM5W{90o%BiZVs?Q2}FZDI9?v53Gq)rk5)wj9zHzp5786O|#_p6n7 zIxHa`ESXAz76Vxg++Tg{s7;Eu^$`KF?0oI-SMlM~Y`tz2o8aa`+8>P{fMfo7_DMt6 zfKYF0Y8tpDZdyrOJtZ@>4oqFR=-n88CvMv&ydh3L`o@hSu~bTo<81UU+cltq0JI&_ zwf
-t4A$BxDOo_@+r*0aAN<(u}vF4jU ?ul~^C zdsTN?`#90~XtGZqK0f&6EYca25B3`6jWO{5RV=-_hZT_Xv^>5jJf>Urxl(GAEhcY9 z5-F6vEu5}NHT6J`o;T{KV6Kv?Hx4aAOP#0r8x6FyABcuW xa-+k6OJ!HKM+%%(ahVN|p)61p>We)Z@e;!2Lk09a!(3M~T=>8kK41yWV zqc3Rkv+)yJ0aIcSsuh#cu>edhT?jifd}>+~(5POMK@RkA{mGuVf-0G+CLbE)adc1p z0M|fn!<7cME@0H =Nk4JM>juITnvNli^nGd?*K3Prm8hKI{fc0?QDdaMdK zfBf50eG@vb?c|5yhs^o;B4*wL(Z3=PGb0{ue|{cBH7@Xx?}S^fsl2o6Rog{gHoLTADIq@qSU8* z5HA_7lReqt5oyf_enEz`x?YKTDEefMgXMbVu_Q)SP{Kq9lCkJ5Xn|UKIoWr*_3_Uq zU7SDb&rOZ3I^z(Ixs;&Mn@W!4RPyUtHQrWzO&uN81qS1Lx!9kQT8Bl(BvN` pkE)Ha^kNv+fCE?7R6#eU63@|v5O(UXW!%3QE#nftc2=;>ep?jGn?a^3#^{wv2e zDaI1H#0Xse{-Se71?&@p-H>XMqmd)D&e8K9wZgk7%8H&>Y*%qxZhJ}(=ov%+te!U~ zzaJ-&N=r*i5G8XZTWPj$a*K*omRNYNmK|(iH>Q+uLus4qPM!jOgGK3xk%wXmgI;G~ zXe6FAQJygl%V7$TUKQV|yy(>&7d%EJ5*PON>m-+-+CIt=RbaN7?CtM0BxEqdK)53@ z{o#XqY*&=@ur_QbxmzOQ$cNq&rn% zryQ2h^vW`bsMyHNQjrtv7s6oCe!hCSBQB)c?nx(|)*Y1Ag@Yc5)x6orq4ccx>i>XI rE^Q-)kox%lQ}Xz4bUEw&;{fmm5p&e9cXyPVrvMH%&hT<8RNQ|6nDn7Y literal 2197 zcmcImdpy&N8~$!hVT8zr(#S1lLPxo_&0-e))|4#QxeH4+glKXp$t9)8rMW~@M 4A-^q1(GJd7Iex>FqIS<@dk62D`n~?o zSmaaO3vqE1uRyiZ=XgzZ$zmJXVm{@FyE-?qNzZFAwcH_nDlYy__}Ie1C||gid5uRz zgh|xcq#AoO!r#xoFv>q7f~S(3m!|@cHU!=E2J3V|i0Kl{ee!5iEk>9 =)uZQF6#rc z&=v(o@`aL#SUfE-a5hAz7m}=)MH_97szEEeKX^A=0Q5dQel@RzEB4u)Oy*nOUH7Zi z)CgP+xjd5%)YR0FRXhUJF~is_r%qEl@92Z=Y_^Yfnn5Xkd3AOG={t7^(jq&)y|t<5 zX3|RhDl4OE(QT!$hPgHgwulpKSf_Nty7O{d&XxUzV%6S`>^H@n235*#@;~Gndst`7 zPw^Ip6H;LYyVLiX8Xcq0^hKV^KgM042{+7gew@rLD=V{dZ;DuG>$_wcx`GnzqFi4s zTb(%+-q$Xom2cg;g`0Rqcs^GfdLC?fTP(klTb{AsoQI;@qq<|D^<^$)Il%hj4;RbG zMc1>tCT? kA9I@`mrg hOx#PR `B6Q5YH*ddHVVJ#r76j3KMlZuCtU>R5tT=PB#;yt@a(Yva-7Ri~at% zs}gT%%tKS@)2>Qd)FnY-V9;i;~po?I<;ZwAcv<^Abrnxw9&IsEMdtapq?gBJYTJ z2X-Kt*Kld7vmT##m#P3RCOUE%LC;vjd;&!d^qA> *LW@A(V4v!Si({=1OEI2s z37r8po9$5wV|8&RI<7Cda!DFPm#MP$2CxDs`dy#7Aay;^T*CMwBiDAutZ&8H{p7@=6hH zcyh49QFPVISI(qxT5KRy?R19VmG$y~V&>C%2V{XH`XW<(j(R(c7guCgm1x3xyAVi- zbCC;yo9$(gX-~n8k_k_(5#FrBeKsY}+4`rMRPgt;!+jONi26H7_x^1+c)AzVB*aGJ z*VOO% HNu5r#r>}nc0CF1@CC63 zrU%TF!1HsSw$M<)2Dd7<;qn`9wjOfpdJj0{epkV+jVKB8^Nfz`GV7C*QEU~caCLj1 zipg)%u4@F)pYAP HsT_-qS*tp7_e0Q%9OWhgH !NVcHZXbh4ab~TR!h#2VHc}QOW}+SO<>2lb zpw5>MTg)nf$;LTGw`F=`S+a1}$?ii!Ke{duGcAiy+*xsBZo}z*qGL>3k4-{twZex& zMt^_5?zd3)zO@aR0?GC|Bv~>oWPn;@G_&PU$84(Ad_6QYR8_R<7SRkKnl6#*l3u=4 zh~fX@WE5GDzFi=zT1G3x_GT`f6qGvr1fQu#uNX Ea*He8=s*1 zz4mtH bF|u2o7b6@T0Gtpx*U} z-D~)0RAJE-$V`(X*6Xmbnvb?ecN@=gIGh6{l>?;~6?r&}k9fbv#dH}rTbP1edn{_y zC&&c TnK&Z62gJQjX zko3V3lAJVjFAQ`$N*rev8+2tR4M2+-YKQ9O2lTPmBe2V 5>7~qRkKZB8a@ToSb{P5mQNE4z zd*I5aI%C`-twR(5z>S^3h$5q-PlBT#$XI4A9n mLSKlY<8 aAMdAyd`X8t;fY=d!0yB;+@Dy_YySWW5BUZF diff --git a/_docs/focus_mode_manual.png b/_docs/focus_mode_manual.png index 09171291ab76e80f3c019bc675aeae0b308be68b..c4341672b3ad641f5c9d11526af2583016a44a64 100644 GIT binary patch literal 1614 zcmc{WsGK82<`mq!e{CdE3-*L|iVe7@8t1%(-ld2vZcNh1zAN6gygymrNAqC1RDg z-3(ju(!_ARE=HH6HE+2?X5+5=N8I~4=kq-0^ZDU9&pFRIpNH&?I<2MxQ2_uz&D{;@ zE9;~mR0hfFTgQ<1vL=W3J?#R}xrYTZp@4Jtat44`nW~ZyMVSW2x?%7Du!s4Ba^2sD ze*pl+S$Cwf-;JQfq6uFJM)R&(n#GI-RV#}` Tolof){;1+)_(OdBRp!&*(g39^B@c4y>O+;BdH$dI+Maj;`*M z`z>#y>k{#rkSd(Ic;!m1Db#e_!@+Prxlnm!QZ~c4>R!_Kl{W+${5YB|5?z;8r}&8G zErA3sP~}%m$liF+e=U^ns0$vI7{}5<`9(z|&f;kPISV_x&EA5JSZ7kh;zVTX(QHl% zUZfwkl3|jWw$Zc`a=8(E^l5+c)Q1ly?rJ`hGW12%EApDcx&4(xf?5iNGRftlI4H0} z%0c$$*@`9RKJeA^smILBx|l<(puhRdghlD`2OmBKf)Hl=4VxH@ryM7^>K&IeO^C7^ z=S>b~yf60^+-$^3OQ7-N3Rn`22IFs!CfPtZXf)d9z9F?qh{xj{o~yQ2)EJ5Bl)D<% zBxE1mdLq)$!`UW=AB71B_`X0PlDnMBXMFxj1s*z9wp3wZSlC3fKeT(@bi(TEa&wM^ z7wba$?YXtkYXmX~;f?CJSwW;TXvsHLIRvF#{4TMG5up?lwq?*6-0^t3-0h;+rf{EA z#X{xOB`28ue)0ll>?bV=d^DOyMdn4f=v}advFGOI8m6rtcpf#cnMk8+wg_0fXx(%8 zXQ^*P;LGYDcT&`Rc M*m>O=2X4Ep>*TKxdD8567e#>WOZ!k%XW_!((WaCSIW;? zPJ=!C$BI_%Yv8AG_Ngu<)i&a_z%2c%{F2hrzW)ATCI{C%bY)?y-B?aC5S(a|Tkz_t z5*4zCNeiwT6T-~5k-bh*Zjpu)OXFh^s7$@EAwr3|gO;s3uc>mUdsGK3)+z%Qv*U7X z;UuC#M+_vNN);d(NFvW%%D~n>Iq>HAB`XMS?%JkjPtH*2s{jq3v&cVSRd##AwoE+w zTN52w$ppf$FC(-RQ7E}Os4XKx+iUINs(r$GUS6KKO=Pfkl3TFRE*2Bkhbqs`YHMq2 z2(A+>vpJr)dLFN@uMb`)EqFgi94fET(LGeC=~OCwo&Xt0-yuc?m@Ectxt`3-{nGMi zX4!ta?e$Ikpu3zXt9sfY#s5W6Yi9Q>h9xqWABJ^0u(yD{oX$?ykpi8vl`ULc+*<#V zD5i6|)qe+?pKZ*>$c1^IZ5blzL8Ndv{B7duytcq{yBF9?_8Ur)*}j|8U$QJrxGKeN zpm9cpd4WL~IKK4=+x8f#1|)7XAE!hp_i2tEaa1@(VkS-pfe^$ItAR+Xwt(#2Y{Bkg zx701iz` eNZe+|EgwiqzWJgot_Ujm-1j)S*1@+7i^x1h zLpNf*^zs{Qv^wN5?Ox*sZGk~SsGfMEdBl<9O%zb_N&v z{O|M@B7}_89TTptY|L(~uiu}m2Yof@_W} q)Gg*GsjT(A*qj$kXCBulpBzy*>}G&*%Ml`ss5GAM6i<>OuhkfCUEl zhR8itp(^|2_8BJTrQ9hecm;U@KwIenSqwxTtEL8o{{jF9pC}aMzfEhACv`}EVWiNM zcoI1#Jq}2X`I$(vBgT;oo$c)HoQgh`;{gD2C(zgH)WvfP)$CxCK&|}D{hB`-JD#Ij zU<2+A!f6PaqiSov^%9hphP*zM8V FbgccWvP `?)rX(SH^g0dD$jC@fNQ{(c`D$!z42?1s>@F|g z;K2mf-$Waa=JOt--OMd&Bw;lSM&ILz78Z~A*4Y?%^`@jRh1)Uby=6ny$Kh~j%#}rC zXBvg#AuXg{8&)p)PCMH`dLklk4&vtNnKNFok$rZVf`+N9tLNnFyqun%F5pEWd` rL(-m~3WANc$)jgd>)qZ!ps@VX7VHH;Vgu1bTKGuXz)#xyiEbn*xAokK w60(QNng=VUL2tE+2qB(zS`@QFb}4grJeeXkz V zjD4bo1=|UMAXpoTq;GC+3gVtQczEy;s-CzMy$&1?$L4d+3)CeNNzPB`H5jL@ql4bu z+?>tMo*f{}&(E8D|Afu99uKKXRMycxfhvr&TQ#%?RiGGZRrZ|)X;_yAoa$df>zVm^ zLVHh-)p#gf;!S=1I;ibobbS1M@^;w1)@f0gS+?06^3HOu`8nO|diyaVwr+BA^3^q- zX%fgjjVrB0lluJG-6iaGVbKv+u9$w*|I+a2%|+9tcSA#d2PC(vs#b1B6A0EDR`5r_ zETPxBTcU*cAd+95(6aTr`{vyeCCBELmNl2m3zsPxTQ}ZujUzy*U+j=a-b~eho=?4W zX)CU*+T+$2sT;?`x^18d@vq(}qq99xqaFw|Ux5sVGf(CbySB;Wel0@Hvc6BTHoBz* zN{s_Eiib<`&qYD{ZELK1K51Ue%G!>V7tOdbBIuI-*vW6lSYEg1l>6Ju(OXQ>>eG$F zs}^*k{<|s%_r;N(!#c^AgH)*nZo$BU(y*s<9A6FQ5?#vyExoi1Kcf^I43OU!bOg_L zk@Yd)?%AW$IteQi>d;XO;@d+XnQb?k*_-Lc`BqO$0h&Fp8hZ*n)4E#=jUunivUY1Z zC&@ly-$Ak2BPY9)szMgqsCa#XqEr-_lyFu&K3E12m2{+)A`@(Yh9wo=wOrW=5Y3>+ z4nhs{z^7u51M4ydScVJ*cjpdm*Jn600Y4NK4rLZe20kKD_x2KXT86v D%fW FFaHOrCCF|7 diff --git a/_docs/help.html b/_docs/help.html index 0ebedc42b..534d9d337 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -31,16 +31,15 @@ - + + + @@ -124,7 +123,7 @@ a photo. In some cases, you can also hold (long press) for a continuous burst: smaller video icon will switch to video mode. The photo and video icons will then swap: click the larger video icon to start/stop video recording, and click the smaller photo icon to switch back to photo mode. Switch camera - Switches between front and back camera (if your +
Switch camera - Switches between front and back camera (if your device has both front and back cameras). If your device has more than one front and/or back camera, then this will switch between the first front and back camera.
@@ -134,7 +133,9 @@ between the first front and back camera. the standard and ultra-wide camera. If Settings/On screen GUI/"Multiple cameras icon" is disabled, then this icon will not show; instead the "Switch camera" icon can by used to cycle through all the cameras. - Note that some devices do not allow third party applications to access their multiple cameras, in which case Open Camera isn't + Note that some devices do not expose the multiple cameras explicitly, but instead will automatically switch cameras as required + when zooming in or out (requires Camera2 API). + Note that some other devices do not allow third party applications to access their multiple cameras at all, in which case Open Camera isn't able to use them.Exposure lock - Click to lock or unlock the exposure. @@ -195,7 +196,8 @@ will be available which instead work by making the screen light up (note, front
- Manual - A slider appears allowing you to manually control the focus distance (only available if Camera2 - API is used). + API is used). Also see the options "Focus assist" and "Focus peaking" under Settings/Camera preview/ + which may be useful when using manual focus.
- Fixed - The focus remains fixed. @@ -226,7 +228,20 @@ will be available which instead work by making the screen light up (note, front taken, at infinite focus distance. Focus bracketing is typically used with Focus stacking software to merge the images into a single photo. Note that whilst taking a set of focus bracketed photos, you can cancel the set by pressing the "take photo" - button again.
+ button again. Also see the options "Focus assist" and "Focus peaking" under Settings/Camera preview/ + which may be useful when adjusting the focus distances. +- X- modes - These extension modes enable device specific algorithms or effects that manufacturers have exposed to + third party applications (via Android's camera extensions API) (requires Android 12; only available on some devices, + and if Camera2 API is used). +
+
+ Note many features are unavailable when using an extension mode, including flash, zoom and manual controls. +- X-Auto: Allows the device to choose which algorithm to use based on the current scene. Note this differs to + "STD" mode in that it allows the use of the other camera extensions e.g. Night mode for low light scenes.
+- X-Night: Improves image quality under low light conditions.
+- X-Bokeh: Blurs the background of photos. This is typically intended when taking portraits of people.
+- X-Bty: Face retouch or "beauty", applies cosmetic effects to people's faces.
+
Pause video - When recording @@ -296,8 +311,8 @@ cached location). If the location isn't available, a dash will be shown through photos look right whether you hold the device in "portrait" or "landscape" mode. But Open Camera has the option to rotate the photos so they are perfectly level, so your shots come out looking perfectly level every time!
-
-
+
The above shows a rather exaggerated example - in practice, you can probably take better photos, but this feature ensures they
@@ -338,6 +353,7 @@ algorithm is in reducing noise and enhancing detail.
In Noise Reduction photo mode, an additional "NR mode" option will appear on the popup mode. This defaults to Normal, but
@@ -345,8 +361,6 @@ you can change to "Low Light" mode, which further improves results in dark scene
mode, it will take a burst of images for a duration of around 5 seconds. For best results, use a tripod, or try to hold the
camera as steady as possible. See my blog post for more details. Dynamic Range Optimisation (DRO) is a technique that optimises the dynamic range available in the image. In particular, dark
@@ -360,13 +374,13 @@ see DRO vs HDR. High Dynamic Range Imaging (HDR) is a technique where the camera takes multiple shots at different exposures, and combines them
into a single image. A typical problem in photography is that a scene may contain a brightness range that is wider than what can be
-captured in a single shot. Varying the exposure (whether by touching on the screen, or exposure compentation or manual exposure) might
-make darker regions brighter, but leave other areas over-exposured. Or reducing the exposure to prevent over-exposure may result in
+captured in a single shot. Varying the exposure (whether by touching on the screen, or exposure compensation or manual exposure) might
+make darker regions brighter, but leave other areas over-exposed. Or reducing the exposure to prevent over-exposure may result in
the rest of the scene being too dark. HDR uses an algorithm to combine the best parts of each image, and adjusts the colors so that
the full range of brightness values are captured in the scene: The set of images below shows: Standard photo on the left, Noise Reduction in the middle, HDR on the right. There is less overexposure
-in the sky in the Noise Reduction result, but the HDR mode does even better. In summary: NR is better if you just want a "works best in most cases" option. HDR may be a better choice specifically in
scenes with high dynamic range, that also don't have movement in the scene.
Pause after taking photo - If ticked, after taking a photo the display will pause, with options to share or delete - the + the photo. To keep the photo and continue, touch the screen, press back, or take another photo. Note that this isn't supported when holding the shutter button to take a continuous burst of photos.
@@ -522,17 +529,18 @@ key to focus, then both to take a photo.Audio control options - If enabled, this allows taking a photo (or starting video recording, depending on the mode) by making a noise. An on-screen microphone button will appear, to start/stop listening. The "loud noise" option will listen for any noise (so you can remotely take a photo by saying "cheese", -whistling, or whatever you prefer). Note that leaving the listening turned on may use additional battery. The "voice command" option -listens specifically for saying "cheese" - so this has the advantage that it's less likely to be triggered unintentionally. +whistling, or whatever you prefer). Note that leaving the listening turned on may use additional battery. Note that this can't be used to stop video recording - if you want to have some remote control on video recording, see the "Max duration of video" option.
Audio control sensitivity - This controls how sensitive Open Camera is to noises, if "Audio control" is set to "Loud noise". If you find it's taking photos too often unintentionally, or isn't responding to your sounds, try adjusting this option.
-Bluetooth LE remote control - Open Camera supports connecting to the Kraken Smart Housing via the options in -these settings. Once connected via Bluetooth, it should be possible to control Open Camera from the device. The on-screen -display of Open Camera will also display information from the Kraken (temperature and depth).
+Bluetooth LE remote control - Open Camera supports connecting to some specific "smart housing" cases via the + options in these settings. See "Remote device type" for supported types. At the time of writing, only one make/model + is supported. Once connected via Bluetooth, it should be possible to control Open Camera from the device. + The on-screen display of Open Camera will also display information from the housing (temperature and depth).
Lock photo/video orientation - Normally the orientation of the photo/video will be rotated by some multiple of 90 degree such that the orientation looks right - e.g. if your device is held in portrait, the resultant image/video will @@ -540,19 +548,28 @@ be in portrait. This option allows fixing the camera to either be in portrait or auto-level is also enabled, it will have the effect of aligning photos to the nearest 90 degrees.
-Save location - Select the folder to store the photos in. Click on a folder -(or "Parent Folder") to navigate through the filesystem. Select "New Folder" to create a new folder in the currently -displayed folder. Select "Use Folder" to choose the currently displayed folder. Note that on Android, there are some -folders that cannot be written to - Open Camera will display a message if you try to use one of these folders. Once -you have specified a new save location, you can long press on the Gallery icon to quickly switch between recent save -locations. Note that if "Use Storage Access Framework" is selected, this option will instead show up the Android standard -file chooser - navigate to the desired folder, and click "SELECT". If you want to save to an SD card, see "How can I -save to my external SD card?" under the FAQ.
+Save location - Select the folder to store the resultant photos or videos in.
+Once you have specified a new save location, you can long press on the Gallery icon to quickly switch between recent save + locations. If you want to save to an SD card, see "How can I save to my external SD card?" under the FAQ.
-Use Storage Access Framework - If selected, Open Camera will instead use the Android +
Storage Access Framework - If selected, Open Camera will instead use the Android Storage Access Framework. This -has some advantages, such as using the standard Android file picker, and being the only way to save to SD cards on Android 5 -- though it may be slightly slower to take photos. (Requires Android 5.0 or higher.)
+has some advantages, such as using the standard Android file picker, and being the only way to save to SD cards on Android 5+. +In some cases it may allow you to save to cloud or local storage providers provided by other apps or services. +Furthermore on Android 10+, it is the only way to save outside of the DCIM/ folder. (Requires Android 5.0 or higher.)Distance unit - If "Stamp photos" is enabled, this controls whether to use metres (m) or feet (ft) when recording the GPS altitude. Also used for Video settings/"Video subtitles".
@@ -942,6 +965,11 @@ Camera2 API. A common issue is poor flash behaviour (either flash doesn't fire, If so, enabling this option may help - this uses an alternative algorithm for flash (using the torch to simulate flash as a workaround). Note that this is enabled by default for Samsung devices. +Enable dummy capture HDR/expo fix - (Camera2 API only.) Enable this option if your device has problems taking photos +in HDR or Exposure Bracketing photo modes, specifically if some expo images come out with the same exposures. This option +takes an additional "dummy" image which may resolve such problems. Note that "Enable fast HDR/expo burst" (below) must +be enabled for this option to have an effect.
+Enable fast HDR/expo burst - (Camera2 API only.) Disable this option if your device has problems taking photos in HDR or Exposure Bracketing photo modes (disabling this option will result in a longer delay between the photos being taken, but may give more stable behaviour if your device is having problems with this).
@@ -963,7 +991,7 @@ your device supports optical image stabilization (OIS).Video format - Allows choice of various video file formats and codecs. Please test before using, as some may not work properly on all devices! Also note:
Video subtitles - This option is analogous to the "Stamp photos" option, but rather than embedding text into the video itself, it stores the text in a separate subtitles -(".SRT") file. Most decent video players should support +(".SRT") file. Most decent video players should support SRT files, and use them to display the information as subtitles. The subtitles will record the date and time. If "Store location data" is enabled (see "Location settings" below), then the current location latitude and longitude coordinates will also be recorded (if the location is known). Similarly for "Store compass direction". Note that you can control the formatting style for date, time and location using -the options under the "Photo settings" menu (Datestamp format, Timestamp format, GPS stamp format, Distance unit).
+ the options under the "Photo settings" menu (Datestamp format, Timestamp format, GPS stamp format, Distance unit). +Video bitrate (approx) - If set to a value other than "default", the default video bitrate is overridden. Higher values mean better quality video, but the files take up more disk space. Note that some values may be unsupported by your device, and may @@ -1083,7 +1117,7 @@ location. Location data will also be stored in videos (though only for devices t
Store compass direction - If selected, then photos will be tagged with the compass direction. Only supported for JPEG format. Not supported for RAW photos (DNG format) or videos.
-Store compass direction - If selected, then photos will be tagged with the device's yaw, pitch and roll. +
Store yaw, pitch and roll - If selected, then photos will be tagged with the device's yaw, pitch and roll. Note that Exif data does not have direct support for this, instead it will be written as a string in the Exif data's User Comment for the image. Only supported for JPEG format. Not supported for RAW photos (DNG format) or videos.
@@ -1115,9 +1149,6 @@ specified by using this option.Online help - Load this web page.
-Donate to support development - Loads the page for -my donation app.
-Camera API - If set to "Camera2 API", this enables support for the Camera2 API that was introduced in Android 5. Changing this setting will cause Open Camera to restart. Camera2 API enables more advanced features (including manual ISO/exposure, manual focus, HDR, exposure bracketing). @@ -1151,7 +1182,8 @@ this information to the clipboard.
is the risk that settings on some devices may be incompatible with other devices. Also if the saved settings file specified a save location, this may not be valid on the new device (or if using "Storage Access Framework", you may have to reselect the folder in Open Camera, to grant - write permission for the folder). + write permission for the folder). Note that on Android 10+, the file dialog will only let you + select a file inside Android/data/net.sourceforge.opencamera/files/.Reset settings - Resets all Open Camera settings to their default. Selecting this option will cause Open Camera to restart. Note that this will not delete any saved settings (see above options).
@@ -1211,21 +1243,16 @@ e.g., "extSdCard".Can you implement disabling shutter sound for my phone? - @@ -1244,7 +1271,7 @@ with Android 4.4).
Photos or videos fail to save! - Firstly, if you're trying to save to an external SD card, see "How can I save to my external SD card?" above. Otherwise:
I switched to a new phone, and now something doesn't work! - Google's auto-backup will typically transfer settings to a new phone, but this may mean a camera-specific setting is no longer relevant. In particular, if you set a non-default save location, it may be that the path is -not valid on the new device, or if using Settings/More camera controls/"Use Storage Access Framework", +not valid on the new device, or if using Settings/More camera controls/"Storage Access Framework", you may need to rechoose the save location (from Settings/More camera controls/"Save location") to grant permission to the new device. You can use Settings/Settings manager/"Reset settings" to reset Open Camera to its original state, to rule out any issues from an Android backup from another @@ -1271,9 +1298,9 @@ and untick Auto-level.
that are made available to third party applications. Usually this means front and back cameras, but some devices have multiple front and/or back-facing cameras. Use the switch multi-camera icon -to switch between multiple front or back cameras. In some cases the extra cameras aren't made available to third party -applications, so it isn't possible for Open Camera to support them. Even where they are, since there is no current standard -on what the extra cameras are used for, it can't do anything with them other than allowing you to switch between them. +to switch between multiple front or back cameras. Note that some devices do not expose the multiple cameras explicitly, +but instead will automatically switch cameras as required when zooming in or out. In some cases the extra cameras aren't +made available to third party applications, so it isn't possible for Open Camera to support them.Why doesn't Open Camera support the maximum video resolution on my device? - If you are using Camera2 API, make sure that you're not in slow motion mode (see "Speed" under @@ -1467,6 +1494,8 @@ can paste the information into your web browser, email or whatever.
forums. For some enquiries you may prefer to use email. Please contact me at mark.harman.apps@gmail.com. + Please note that I get a lot of emails for Open Camera these days - I try to reply as many as I can, but this is not always feasible. I do however read every email and forum post. diff --git a/_docs/history.html b/_docs/history.html index 2ff449e9d..1e08a256c 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -24,16 +24,15 @@ - + + + @@ -48,6 +47,132 @@+Version 1.50.1 (2022/06/08) + +FIXED Crash on OPPO devices for old camera API introduced in 1.50. + +Version 1.50 (2022/06/04) + +FIXED HDR photos came out black on some Samsung Galaxy devices with Android 12. +FIXED Problems with flash on Camera2 API (Samsung Galaxy, OnePlus, Pixel 6 Pro). Galaxy and + OnePlus devices therefore no longer default to using the "alternative flash method". +FIXED Problems with expo, HDR and long manual exposures on some devices (e.g., Pixel 6 Pro). +FIXED Granting only approximate location permission on Android 12 would turn geotagging option + back off. +FIXED On-screen text looked strange on Android 12. +FIXED Gallery icon overlapped with navigation bar if using widescreen resolution with UI in left + or right handed mode. +ADDED Support for Android 12's camera extensions API. When using Camera2 API, on selected devices + advanced photo modes are now available (e.g., Night on Pixel 6 Pro; Night, Bokeh and Beauty + on some Galaxy devices). +ADDED Improved support for devices with multiple camera devices, where extra cameras are exposed + via zooming in and out (e.g., Pixel 5/6). +ADDED New debug option Settings/Photo settings/"Enable dummy capture HDR/expo fix". Please enable + this if you are having problems with HDR or expo bracketing mode on Samsung Galaxy devices + with Android 11+ (specifically if some expo images come out with the same exposures). +UPDATED Removed "use addresses" and "say cheese" options. Sorry about that, but + this is due to new data privacy requirements on Google Play: although + these used standard Android APIs, information was not available for these APIs to satisfy + data privacy requirements. +UPDATED Now targetting Android 12. For remote control device options, new bluetooth permissions are + used instead of requiring location permission. +UPDATED Move gallery icon slightly to avoid overlapping with Android 12 camera privacy icon. +UPDATED Made pinch zoom smoother, to allow finer control. + +Version 1.49.2 (2022/01/13) + +FIXED Dialog for "Save settings" shouldn't allow multiple lines. +FIXED Crash for NR photo mode on some devices since version 1.49. +UPDATED Switched to using AppCompat AppCompatActivity. +UPDATED Photo stamp custom text now uses AppCompat libraries to support latest emoji. +UPDATED Made appearance of info "toasts" more consistent. + +Version 1.49.1 (2021/09/20) + +FIXED Crop guides weren't drawn correctly in portrait orientation in 1.49. +FIXED Diagonals grid wasn't drawn correctly in portrait orientation in 1.49. + +Version 1.49 (2021/09/07) + +FIXED Crash when failing to save photos/videos with mediastore (Android 10+ if not using Storage + Access Framework). +FIXED Crash related to original camera API. +FIXED Crash when using photo stamp with auto-level when angle is zero. +FIXED Couldn't exit immersive mode on Android 11. +FIXED Behaviour where widescreen preview aspect ratios show under on-screen navigation bar wasn't + working properly on Android 11. +FIXED Manual white balance had inverted effect. +FIXED Video subtitles file didn't work properly when video file restarted due to max filesize. +FIXED Taking a photo in RAW only, then clicking on the gallery thumbnail would sometimes + incorrectly open an earlier non-RAW photo or video. +FIXED Corrected pitch and compass line lengths for portrait vs landscape orientations. +FIXED Single and double tap options to take photo weren't working correctly in panorama mode. +FIXED When using manual ISO seekbar, sometimes incorrect ISO button would be highlighted. +UPDATED Now supports portrait and landscape system orientations, rather than being locked to + landscape system orientation. +UPDATED Double tap to take photo option no longer performs a touch to focus, this now only happens + from a single tap. +UPDATED Improved performance when opening camera and clicking on gallery icon (mainly relevant for + using Storage Access Framework with Android 10+ when save folder has large number of files). +UPDATED Set max preview exposure time to be 1/5s instead of 1/12s, for when using manual exposure. +UPDATED Support longer exposure time (1/5s) on some Samsung Galaxy S devices. +UPDATED Improvements to brightness levels for Noise Reduction, DRO and HDR photo modes (images + coming out too dark in some cases). +UPDATED Improvement to Noise Reduction photo mode quality (improved ability to distinguish noise + from ghosting effects). +UPDATED Improvement to Noise Reduction photo mode to avoid overexposing in lower light scenes. +UPDATED Improved choosing when to use 8 images for Noise Reduction photo mode. +UPDATED Optimisations for DRO and NR photo modes on Samsung devices. +UPDATED Accessibility improvement, set hints for EditTexts. +UPDATED Now targetting Android 11. Due to changes in Android 11 this means "video subtitles" option + is now only available when saving with + Settings/More camera controls/"Storage Access Framework" enabled. +UPDATED Updated some user interface icons. + +Version 1.48.3 (2020/11/20) + +FIXED Possible crash for panorama if failing to crop due to poor transformations; now fails + gracefully. +FIXED Crash on EXTERNAL devices with Camera2 API that didn't support querying the view angles. +FIXED Photos would sometimes fail to save on some devices with Storage Access Framework, when some + options were enabled (options like DRO, HDR, auto-level, photostamp that require + post-processing; custom Exif tags like artist or copyright; or when using geotagging with + Camera2 API). +FIXED Fix for HDR scenes with both very bright and very dark regions, result would be over + exposed. +FIXED Fixed possible misalignment for HDR scenes with very bright or very dark images. +FIXED Corrupt videos could be left over if video failed to start. +FIXED Possible problem taking photos on some devices with LIMITED Camera2 API support. +FIXED Possible problem with default edge mode and noise reduction mode behaviours on some devices + with LIMITED Camera2 API support. +FIXED UI would become sluggish if camera or storage permission denied with "Don't ask again". +UPDATED Now supporting "scoped storage" on Android 10+. This means storage permission is no longer + required on Android 10+. However this means the following changes: + * Saving outside of DCIM/ is no longer possible unless using the Storage Access Framework + option. If you had set up a custom save folder outside of DCIM/ and are on Android 10+, + it will be reset to the default DCIM/OpenCamera/ folder. If you want to continue saving + outside of DCIM/, you can enable + Settings/More camera controls/"Use Storage Access Framework" and choose a new folder. + * If using Video subtitles option, then the .SRT files will show up in gallery + applications, unless Settings/More camera controls/"Use Storage Access Framework" is + enabled. + Note that these changes are required due to changes being made in Android that applications + are required to support. +UPDATED Use seekbar for more settings (audio control sensitivity, image quality, photo stamp font + size). +UPDATED Debug XML files for panorama now saved in Android/data/net.sourceforge.opencamera/files/. +UPDATED Camera now closed when in settings or preview otherwise in background. + +Version 1.48.2 (2020/07/12) + +FIXED Manual focus and focus bracketing seekbars weren't being hidden when in immersive mode. +FIXED Video subtitles would stop before end of video on some devices when using Storage Access + Framework. +UPDATED Switched to AndroidX support library. +UPDATED Artist, Copyright exif tags option now supported for devices running Android 6 or earlier. +UPDATED Selecting remote device type for Bluetooth remote control now calls Open Camera's + DeviceScanner directly; DeviceScanner activity no longer exported. + Version 1.48.1 (2020/05/02) FIXED Crash on devices with Camera2 API where camera reports no picture, video or preview diff --git a/_docs/index.html b/_docs/index.html index 6f5829bd1..d3bd47db0 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -16,6 +16,13 @@ } --> + + @@ -106,7 +112,7 @@ browsers -->
(Some features may not be available on all devices, as they may depend on hardware features, or the Android version.)
- - -Also see alternative download sites.
+ -Open Camera is completely free, however if you wish you can show your appreciation by supporting me.
-Open Camera Blog ~ @@ -142,7 +143,6 @@ browsers -->
Open Camera uses the AndroidX/Jetpack libraries, under Apache license version 2.0.
+The following files are used in Open Camera:
Open Camera is completely free, however if you wish you can show your appreciation to me by making an optional payment. - Please note that this is not required, and that doing so will not provide any additional features (or remove the ads I have on the - website). However for those who wish to make a contribution to me, this is greatly appreciated!
- -You can contribute through PayPal, simply click the "Buy Now" button, and enter the amount you wish to give me:
- - - -...or you can also set up a regular contribution:
- - - - -(PayPal account not required, supports debit or credit card). -Thanks!
- -Please note that these contributions do not constitute a charitable donation, and hence not eligible for tax-deduction. (If you have found my webpage because you are using a camera app with ads inside it, then you may have downloaded a third party clone that is not distributed by me. For my Open Camera app, please make sure you've downloaded from the links given on my Main Page.)
- -Privacy policy: Please note that some information such as name, email, amount will be shared to me by PayPal. All PayPal transactions are subject to the PayPal Privacy Policy (alternatively known as PayPal's Privacy Statement).
+I am not currently accepting donations. Thanks to those who have supported me in the past!
Open Camera is developed by Mark Harman.
+Open Camera accesses and records camera sensor and microphone data, which is used for the purpose of taking photos and recording videos, to fulfil its purpose as a camera. Microphone permission is also used for the optional "Audio control" options.
-Open Camera requires permission to "access photos, media and files on your devices", as this permission is required for Android to -save resultant files such as photos and videos to your device.
+Open Camera requires permission (at least for Android 9 and earlier, or using versions of Open Camera older than 1.48.3) to + "access photos, media and files on your devices" (storage permission), as this permission is required for Android to save resultant files such as photos and videos to your device.
+ +Location permission is requested in order to deliver the optional geotagging features (for photos and videos, including stamp and subtitles options). + When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files.
-Location permission is required for the optional geotagging features (for photos and videos, including stamp and subtitles options). - When relevant option(s) are enabled, your device location will be stored in photo/video/subtitle files. - Location permission is also required to connect to Bluetooth remote control devices.
+Bluetooth permissions are used to allow the optional feature to discover and connect to Bluetooth LE remote control devices; + the Bluetooth remote control feature also requires location permission (on Android 11 or earlier) or + Nearby Devices permission (on Android 12 or later).
-Bluetooth permission is required for communicating with some supported Bluetooth remote control devices.
+Resultant data such as photos or videos can be shared with + other apps if you use the share option in Open Camera, or when Open Camera is called by + another app on your device, or when you use the Storage Access Framework option to save + to another app or service.
+ +Data handling procedures, data retention and deletion policies: Open Camera + does not transmit personal or sensitive information to me.
Since Open Camera also uses operating system APIs, you should review relevant privacy policies such as for your device, manufacturer, operating system and/or Google accounts. For example:
If you have inquiries about my privacy policy, please contact me by email at + mark.harman.apps@gmail.com.
+Although Open Camera is ad-free, the Open Camera website has ads via Google Adsense: Third party vendors, including Google, use cookies to serve ads based on a user's previous visits to this website or other websites. Google's use of advertising cookies enables it and its partners to serve ads based on people's visit to this sites and/or other sites on the Internet. You may opt out of personalised -advertising by visiting Google's Ads Settings. Alternatively, you can -opt out of some third-party vendors' uses of cookies for personalised advertising by visiting +advertising by visiting Google's Ads Settings. The cookies of other third-party +vendors or ad networks may also be used to serve ads. You can opt out of some third-party vendors' uses of cookies for personalised advertising by visiting www.aboutads.info.
Update: I have instructed Google to not display personalised ads to users in the EEA.
+Note that cookies are still used for serving even non-personalised ads.
+The Open Camera website also uses Google Analytics which uses cookies, please see their Privacy Policy for more details.
diff --git a/_docs/stylesheet.css b/_docs/stylesheet.css index fbf030952..804a107de 100644 --- a/_docs/stylesheet.css +++ b/_docs/stylesheet.css @@ -1,4 +1,5 @@ body { color: #000000; background-color: rgb(245,236,220); + font-family: Tahoma, Geneva, sans-serif; } diff --git a/_docs/switch_camera.png b/_docs/switch_camera.png deleted file mode 100644 index c5f8a1860822bb8f3a039406330616def0f5df0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1126 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85o30K$!7fntTONP^!c=q9iy!t)x7$D3u`~F*C13&(AePq0Cs% zRL{`B;7id$1_tJX@E1Nv2aE>_KXB)8}~7jBv**EF;0?ahz_I8O8%A^9pzv@w9AnnVn8tA
zdx6edZm>VLrzRutJ5|0D2ay?$#)@SkuRNSeag2B34gA(R+S4c4jAv^>lM}p^u99^=
zBnqLY8ruCV3y}_3WsLL+O#el@GNOmaBB1NdIW=EI1C=8UzhPgKhq*4gaegQ8CBH>y
zgRAoG@{ByrzmX%a`o>)0HCw}h^74h(WZ5Iy@SwU%#e37uGVoJ4&`bo*pZ+N28^4tT
zk3z CQ`wMktUc(8Hn0k`%x{MCj9_S4>~GnD
zWnh`4Nk0emsdO-*CA+BR=q^!a!1&^|uqvZS#Pq(9sw*lEmz(%xYs1_ITO^!-{x8Lf
z^F$nS8(n|GEkk1N_B6u%YiQ!R&2&LDB)DK(!QFzRW)3S66OuPOgH9ua&^Xtemk+Ry
zi>*uzi6Y2?rt|Dbm_8SkLi#X_W2Y~OH#@!jz=!&&v3arjcuF!ZvsVRyU-(hErktks
zriA%$l&L8xqm`AF{cUZ+M-2_B7!2mi_wT(8TIz$IW0RBi$wU1e9R)>~FDI+wNkLni
zdV4Q9lF13*=({OE)O&ZAv01HWYisMhG|QmS(CPi?W#rtv8|4FVsbYtz&?>u878)Wb
zsX1KT_eG;Iz#0#xfQ{en?ChSUR9CUlcmW^ISA%o1jq8NOj>Cqv3tUx2k5ZY>wAE_0
z&jxXVIQ0YTSvradSN8Or`+Gjwx)_J2)E;6Y(_%JkTBdcHD`MjKi?<643lDC#Tnt#n
zfH`5p&5>TBBV?f-Quh%e*gL(Ay5X{mMN!z|pIKtTXp&Fr+xAD1AVAz+aq6q7alqGw
z8Bl%W=JRlZY=*qs7Rd{46j}8(<1Nlq&a$!sVX|q?R3g#dtxB}s3iu3_4o|!meVo7s
z;VX>Wpts##Y35_^^86v1eJ2*2+$*HsykHkaIj(nLfR$wm6nA>skw}s*y`j3X<^!vJ
z^4+Adh{8Q;fAsLXGct$3YSF`!4h7l~8xMxh>CVp1YmH%~H~SA&_c7HLa=Bc&cP0T(
z=(MBuQtGP|SjUTs;Q
z*4~}f%w(b{ilT&sj1@;ZEYfjgMK**p
#3w*|VtSoSus72*~2YAOclnr+9_8C#;Si*a$tZ
#3w*|VtSoSus72*~2YAOclnr+9_8C#;Si*a$tZ