From d31ef4c9beff7631280bdc173bbd18b15976a1aa Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 22 Jul 2022 21:58:54 +0100 Subject: [PATCH 001/117] Make it easier to zoom back to 1x on devices that have min zoom less than 1. --- _docs/history.html | 5 +++++ .../opencamera/cameracontroller/CameraController2.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 1e08a256c..a12306a89 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -47,6 +47,11 @@

< Main Page.

+Version 1.51 (Work in progress)
+
+UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
+        zoom out to ultra-wide camera.
+
 Version 1.50.1 (2022/06/08)
 
 FIXED   Crash on OPPO devices for old camera API introduced in 1.50.
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 3ff23c220..76659946e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -2584,7 +2584,7 @@ public class CameraController2 extends CameraController {
                 int n_steps_below_one = Math.max(1, n_steps/5);
                 // if the min zoom is < 1.0, we add multiple entries for 1x zoom, when using the zoom
                 // seekbar it's easy for the user to zoom to exactly 1x
-                int n_steps_one = Math.max(1, n_steps/20);
+                int n_steps_one = Math.max(1, n_steps/10);
                 if( MyDebug.LOG ) {
                     Log.d(TAG, "n_steps_below_one: " + n_steps_below_one);
                     Log.d(TAG, "n_steps_one: " + n_steps_one);
-- 
GitLab


From 8a8b966c0d6b8585d3723ea067391352a421ad85 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Fri, 22 Jul 2022 22:05:24 +0100
Subject: [PATCH 002/117] Fix bug since 1.50, focus bracketing should still use
 TEMPLATE_STILL_CAPTURE.

---
 _docs/history.html                                            | 2 ++
 .../opencamera/cameracontroller/CameraController2.java        | 4 +++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/_docs/history.html b/_docs/history.html
index a12306a89..70aaedc73 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -49,6 +49,8 @@
 
 Version 1.51 (Work in progress)
 
+FIXED   Focus bracketing images came out underexposed on some devices since
+        version 1.50 (e.g. Pixel 6 Pro).
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
 
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 76659946e..2128e3ae6 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -6730,9 +6730,11 @@ public class CameraController2 extends CameraController {
                 }
                 int n_dummy_requests = 0;
 
-                CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_MANUAL);
+                CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(burst_type == BurstType.BURSTTYPE_EXPO ? CameraDevice.TEMPLATE_MANUAL : CameraDevice.TEMPLATE_STILL_CAPTURE);
                 // Needs to be TEMPLATE_MANUAL! Otherwise first image in burst may come out incorrectly (on Pixel 6 Pro,
                 // the first image incorrectly had HDR+ applied, which we don't want here).
+                // update: but only when doing burst for expo bracketing, not focus bracketing! (Only manual exposure
+                // should use TEMPLATE_MANUAL, otherwise focus bracketing images come out underexposed on Pixel 6 Pro)
                 // n.b., don't set RequestTagType.CAPTURE here - we only do it for the last of the burst captures (see below)
                 camera_settings.setupBuilder(stillBuilder, true);
 
-- 
GitLab


From 264eafacf432979d7de5d086d4fa620bb7a3dbd2 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 24 Jul 2022 13:40:29 +0100
Subject: [PATCH 003/117] Apply a timeout of 1 second for focusing with Camera2
 API.

---
 _docs/history.html                            |  1 +
 .../cameracontroller/CameraController2.java   | 21 +++++++++++++++----
 2 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index 70aaedc73..e70ce31dd 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -51,6 +51,7 @@ Version 1.51 (Work in progress)
 
 FIXED   Focus bracketing images came out underexposed on some devices since
         version 1.50 (e.g. Pixel 6 Pro).
+UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
 
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 2128e3ae6..8f66c3a70 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -176,7 +176,10 @@ public class CameraController2 extends CameraController {
     private CaptureRequest.Builder previewBuilder;
     private boolean previewIsVideoMode;
     private AutoFocusCallback autofocus_cb;
+    private long autofocus_time_ms = -1; // time we set autofocus_cb to non-null
+    private static final long autofocus_timeout_c = 1000; // timeout for calling autofocus_cb (applies for both auto and continuous focus)
     private boolean capture_follows_autofocus_hint;
+    private boolean ready_for_capture;
     private FaceDetectionListener face_detection_listener;
     private int last_faces_detected = -1;
     private final Object open_camera_lock = new Object(); // lock to wait for camera to be opened from CameraDevice.StateCallback
@@ -262,7 +265,6 @@ public class CameraController2 extends CameraController {
     private long precapture_state_change_time_ms = -1; // time we changed state for precapture modes
     private static final long precapture_start_timeout_c = 2000;
     private static final long precapture_done_timeout_c = 3000;
-    private boolean ready_for_capture;
 
     private boolean use_fake_precapture; // see CameraController.setUseCamera2FakeFlash() for details - this is the user/application setting, see use_fake_precapture_mode for whether fake precapture is enabled (as we may do this for other purposes, e.g., front screen flash)
     private boolean use_fake_precapture_mode; // true if either use_fake_precapture is true, or we're temporarily using fake precapture mode (e.g., for front screen flash or exposure bracketing)
@@ -6189,6 +6191,7 @@ public class CameraController2 extends CameraController {
                     Log.d(TAG, "skip af trigger due to continuous mode");
                 this.capture_follows_autofocus_hint = capture_follows_autofocus_hint;
                 this.autofocus_cb = cb;
+                this.autofocus_time_ms = System.currentTimeMillis();
                 return;
             }
             else if( is_video_high_speed ) {
@@ -6202,6 +6205,7 @@ public class CameraController2 extends CameraController {
                 // need to update the callback!
                 this.capture_follows_autofocus_hint = capture_follows_autofocus_hint;
                 this.autofocus_cb = cb;
+                this.autofocus_time_ms = System.currentTimeMillis();
                 return;
             }*/
             CaptureRequest.Builder afBuilder = previewBuilder;
@@ -6225,6 +6229,7 @@ public class CameraController2 extends CameraController {
             precapture_state_change_time_ms = -1;
             this.capture_follows_autofocus_hint = capture_follows_autofocus_hint;
             this.autofocus_cb = cb;
+            this.autofocus_time_ms = System.currentTimeMillis();
             try {
                 if( use_fake_precapture_mode ) {
                     boolean want_flash = false;
@@ -6284,6 +6289,7 @@ public class CameraController2 extends CameraController {
                 precapture_state_change_time_ms = -1;
                 push_autofocus_cb = autofocus_cb;
                 autofocus_cb = null;
+                this.autofocus_time_ms = -1;
                 this.capture_follows_autofocus_hint = false;
             }
             afBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); // ensure set back to idle
@@ -6345,6 +6351,7 @@ public class CameraController2 extends CameraController {
             }
             previewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
             this.autofocus_cb = null;
+            this.autofocus_time_ms = -1;
             this.capture_follows_autofocus_hint = false;
             state = STATE_NORMAL;
             precapture_state_change_time_ms = -1;
@@ -8226,7 +8233,10 @@ public class CameraController2 extends CameraController {
                     Log.d(TAG, "CONTROL_AWB_STATE = " + awb_state);
             }*/
 
-            if( af_state != null && af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN ) {
+            boolean autofocus_timeout = autofocus_time_ms != -1 && System.currentTimeMillis() > autofocus_time_ms + autofocus_timeout_c;
+            if( MyDebug.LOG && autofocus_timeout )
+                Log.d(TAG, "autofocus timeout!");
+            if( af_state != null && af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN && !autofocus_timeout ) {
                 /*if( MyDebug.LOG )
                     Log.d(TAG, "not ready for capture: " + af_state);*/
                 ready_for_capture = false;
@@ -8257,6 +8267,7 @@ public class CameraController2 extends CameraController {
                         }
                         autofocus_cb.onAutoFocus(focus_success);
                         autofocus_cb = null;
+                        autofocus_time_ms = -1;
                         capture_follows_autofocus_hint = false;
                     }
                 }
@@ -8292,11 +8303,12 @@ public class CameraController2 extends CameraController {
                         autofocus_cb.onAutoFocus(false);
                         autofocus_cb = null;
                     }
+                    autofocus_time_ms = -1;
                     capture_follows_autofocus_hint = false;
                 }
-                else if( af_state != last_af_state ) {
+                else if( af_state != last_af_state || autofocus_timeout ) {
                     // check for autofocus completing
-                    if( af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af_state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED /*||
+                    if( autofocus_timeout || af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af_state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED /*||
                             af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED || af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED*/
                     ) {
                         boolean focus_success = af_state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af_state == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED;
@@ -8360,6 +8372,7 @@ public class CameraController2 extends CameraController {
                             autofocus_cb.onAutoFocus(focus_success);
                             autofocus_cb = null;
                         }
+                        autofocus_time_ms = -1;
                         capture_follows_autofocus_hint = false;
                     }
                 }
-- 
GitLab


From 36b96106608fae43decd550ad3b61b771096e84b Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 24 Jul 2022 14:20:42 +0100
Subject: [PATCH 004/117] Shading for auto-level and crop guides.

---
 _docs/history.html                            |  2 +
 .../opencamera/ui/DrawPreview.java            | 42 ++++++++++++++++---
 2 files changed, 39 insertions(+), 5 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index e70ce31dd..b5a876893 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -51,6 +51,8 @@ Version 1.51 (Work in progress)
 
 FIXED   Focus bracketing images came out underexposed on some devices since
         version 1.50 (e.g. Pixel 6 Pro).
+ADDED   Shading for auto-level and crop guides, to darken the preview outside of the region of
+        interest.
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
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 31f38eff8..9faf9bb62 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
@@ -238,6 +238,8 @@ public class DrawPreview {
     private final static int histogram_width_dp = 100;
     private final static int histogram_height_dp = 60;
 
+    private final static int crop_shading_alpha_c = 160; // alpha to use for shading areas not of interest
+
     public DrawPreview(MainActivity main_activity, MyApplicationInterface applicationInterface) {
         if( MyDebug.LOG )
             Log.d(TAG, "DrawPreview");
@@ -989,9 +991,6 @@ public class DrawPreview {
         if( preview.isVideo() || preview_size_wysiwyg_pref ) {
             String preference_crop_guide = sharedPreferences.getString(PreferenceKeys.ShowCropGuidePreferenceKey, "crop_guide_none");
             if( camera_controller != null && preview.getTargetRatio() > 0.0 && !preference_crop_guide.equals("crop_guide_none") ) {
-                p.setStyle(Paint.Style.STROKE);
-                p.setStrokeWidth(stroke_width);
-                p.setColor(Color.rgb(255, 235, 59)); // Yellow 500
                 double crop_ratio = -1.0;
                 switch(preference_crop_guide) {
                     case "crop_guide_1":
@@ -1047,23 +1046,36 @@ public class DrawPreview {
                             Log.d(TAG, "canvas width: " + canvas.getWidth());
                             Log.d(TAG, "canvas height: " + canvas.getHeight());
                         }*/
+                        p.setStyle(Paint.Style.FILL);
+                        p.setColor(Color.rgb(0, 0, 0));
+                        p.setAlpha(crop_shading_alpha_c);
                         int left = 1, top = 1, right = canvas.getWidth()-1, bottom = canvas.getHeight()-1;
                         if( crop_ratio > preview_aspect_ratio ) {
                             // crop ratio is wider, so we have to crop top/bottom
                             double new_hheight = ((double)canvas.getWidth()) / (2.0f*crop_ratio);
                             top = (canvas.getHeight()/2 - (int)new_hheight);
                             bottom = (canvas.getHeight()/2 + (int)new_hheight);
+                            // draw shaded area
+                            canvas.drawRect(0, 0, canvas.getWidth(), top, p);
+                            canvas.drawRect(0, bottom, canvas.getWidth(), canvas.getHeight(), p);
                         }
                         else {
                             // crop ratio is taller, so we have to crop left/right
                             double new_hwidth = (((double)canvas.getHeight()) * crop_ratio) / 2.0f;
                             left = (canvas.getWidth()/2 - (int)new_hwidth);
                             right = (canvas.getWidth()/2 + (int)new_hwidth);
+                            // draw shaded area
+                            canvas.drawRect(0, 0, left, canvas.getHeight(), p);
+                            canvas.drawRect(right, 0, canvas.getWidth(), canvas.getHeight(), p);
                         }
+                        p.setStyle(Paint.Style.STROKE);
+                        p.setStrokeWidth(stroke_width);
+                        p.setColor(Color.rgb(255, 235, 59)); // Yellow 500
                         canvas.drawRect(left, top, right, bottom, p);
+                        p.setStyle(Paint.Style.FILL); // reset
+                        p.setAlpha(255); // reset
                     }
                 }
-                p.setStyle(Paint.Style.FILL); // reset
             }
         }
     }
@@ -2456,9 +2468,28 @@ public class DrawPreview {
                 int cx = canvas.getWidth()/2;
                 int cy = canvas.getHeight()/2;
 
+                float left = (canvas.getWidth() - w2)/2.0f;
+                float top = (canvas.getHeight() - h2)/2.0f;
+                float right = (canvas.getWidth() + w2)/2.0f;
+                float bottom = (canvas.getHeight() + h2)/2.0f;
+
                 canvas.save();
                 canvas.rotate((float)-level_angle, cx, cy);
 
+                // draw shaded area
+                float o_dist = (float)Math.sqrt(canvas.getWidth()*canvas.getWidth() + canvas.getHeight()*canvas.getHeight());
+                float o_left = (canvas.getWidth() - o_dist)/2.0f;
+                float o_top = (canvas.getHeight() - o_dist)/2.0f;
+                float o_right = (canvas.getWidth() + o_dist)/2.0f;
+                float o_bottom = (canvas.getHeight() + o_dist)/2.0f;
+                p.setStyle(Paint.Style.FILL);
+                p.setColor(Color.rgb(0, 0, 0));
+                p.setAlpha(crop_shading_alpha_c);
+                canvas.drawRect(o_left, o_top, left, o_bottom, p);
+                canvas.drawRect(right, o_top, o_right, o_bottom, p);
+                canvas.drawRect(left, o_top, right, top, p); // top
+                canvas.drawRect(left, bottom, right, o_bottom, p); // bottom
+
                 if( has_level_angle && Math.abs(level_angle) <= close_level_angle ) { // n.b., use level_angle, not angle or orig_level_angle
                     p.setColor(angle_highlight_color_pref);
                 }
@@ -2468,11 +2499,12 @@ public class DrawPreview {
                 p.setStyle(Paint.Style.STROKE);
                 p.setStrokeWidth(stroke_width);
 
-                canvas.drawRect((canvas.getWidth() - w2)/2.0f, (canvas.getHeight() - h2)/2.0f, (canvas.getWidth() + w2)/2.0f, (canvas.getHeight() + h2)/2.0f, p);
+                canvas.drawRect(left, top, right, bottom, p);
 
                 canvas.restore();
 
                 p.setStyle(Paint.Style.FILL); // reset
+                p.setAlpha(255); // reset
             }
         }
     }
-- 
GitLab


From c437a275bea1133898cb3be72c08bef64dda0972 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 24 Jul 2022 14:29:11 +0100
Subject: [PATCH 005/117] Treat DRO, HDR, NR modes as "simple" for the info
 toast.

---
 app/src/main/java/net/sourceforge/opencamera/MainActivity.java | 3 ++-
 1 file changed, 2 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 d3079115a..ff3322e5c 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -5623,7 +5623,8 @@ public class MainActivity extends AppCompatActivity {
             String photo_mode_string = getPhotoModeString(photo_mode, false);
             if( photo_mode_string != null ) {
                 toast_string += (toast_string.length()==0 ? "" : "\n") + getResources().getString(R.string.photo_mode) + ": " + photo_mode_string;
-                simple = false;
+                if( photo_mode != MyApplicationInterface.PhotoMode.DRO && photo_mode != MyApplicationInterface.PhotoMode.HDR && photo_mode != MyApplicationInterface.PhotoMode.NoiseReduction )
+                    simple = false;
             }
 
             if( preview.supportsFocus() && preview.getSupportedFocusValues().size() > 1 && photo_mode != MyApplicationInterface.PhotoMode.FocusBracketing ) {
-- 
GitLab


From f58a284067afdc805a0c70417953d481d522c3eb Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 24 Jul 2022 14:29:26 +0100
Subject: [PATCH 006/117] Update history for last change.

---
 _docs/history.html | 1 +
 1 file changed, 1 insertion(+)

diff --git a/_docs/history.html b/_docs/history.html
index b5a876893..068cc62c2 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -56,6 +56,7 @@ ADDED   Shading for auto-level and crop guides, to darken the preview outside of
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
+UPDATE  DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 
 Version 1.50.1 (2022/06/08)
 
-- 
GitLab


From eb1d1329ffc0dfb6602766a85d2a761e9fe2eea7 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 24 Jul 2022 14:37:26 +0100
Subject: [PATCH 007/117] Shouldn't show face detection icon for camera
 extensions.

---
 _docs/history.html                                          | 2 ++
 app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java | 4 ++++
 2 files changed, 6 insertions(+)

diff --git a/_docs/history.html b/_docs/history.html
index 068cc62c2..c04355f35 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -51,6 +51,8 @@ Version 1.51 (Work in progress)
 
 FIXED   Focus bracketing images came out underexposed on some devices since
         version 1.50 (e.g. Pixel 6 Pro).
+FIXED   Face detection on-screen icon shouldn't show in camera vendor extension modes (as not
+        supported).
 ADDED   Shading for auto-level and crop guides, to darken the preview outside of the region of
         interest.
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
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 1d2a20899..a2f89f4b6 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java
@@ -1287,6 +1287,10 @@ public class MainUI {
     public boolean showFaceDetectionIcon() {
         if( !main_activity.getPreview().supportsFaceDetection() )
             return false;
+        if( main_activity.getApplicationInterface().isCameraExtensionPref() ) {
+            // not supported for camera extensions
+            return false;
+        }
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(main_activity);
         return sharedPreferences.getBoolean(PreferenceKeys.ShowFaceDetectionPreferenceKey, false);
     }
-- 
GitLab


From 20d64104b66d87b763acb0a24afa3a87249a98a0 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Mon, 25 Jul 2022 23:02:35 +0100
Subject: [PATCH 008/117] Fix typo.

---
 _docs/history.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/_docs/history.html b/_docs/history.html
index c04355f35..6e1c99278 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -58,7 +58,7 @@ ADDED   Shading for auto-level and crop guides, to darken the preview outside of
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
-UPDATE  DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
+UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 
 Version 1.50.1 (2022/06/08)
 
-- 
GitLab


From 5dd0b79333f00623cd0a5f8aa307cbb3b8df7777 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 30 Jul 2022 19:40:27 +0100
Subject: [PATCH 009/117] Fix gallery thumbnail orientation for Android 10+.

---
 _docs/history.html                            |  1 +
 .../sourceforge/opencamera/MainActivity.java  | 19 +++++++++++--------
 2 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index 6e1c99278..c653c17d4 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -49,6 +49,7 @@
 
 Version 1.51 (Work in progress)
 
+FIXED   Gallery thumbnail had incorrect orientation on some Android 10+ devices.
 FIXED   Focus bracketing images came out underexposed on some devices since
         version 1.50 (e.g. Pixel 6 Pro).
 FIXED   Face detection on-screen icon shouldn't show in camera vendor extension modes (as not
diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
index ff3322e5c..c0126351e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -3615,7 +3615,7 @@ public class MainActivity extends AppCompatActivity {
      *  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.
+     * @param exif_rotate Whether the rotate the bitmap due to exif orientation.
      */
     private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor, boolean mediastore) {
         Bitmap thumbnail = null;
@@ -3674,10 +3674,7 @@ public class MainActivity extends AppCompatActivity {
             }
             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.
+            if( exif_rotate ) {
                 thumbnail = rotateForExif(thumbnail, uri);
             }
         }
@@ -3760,6 +3757,7 @@ public class MainActivity extends AppCompatActivity {
                     Log.d(TAG, "doInBackground");
                 StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia();
                 Bitmap thumbnail = null;
+                boolean rotate_for_orientation = true; // whether we should apply the media.orientation rotation
                 KeyguardManager keyguard_manager = (KeyguardManager)MainActivity.this.getSystemService(Context.KEYGUARD_SERVICE);
                 boolean is_locked = keyguard_manager != null && keyguard_manager.inKeyguardRestrictedInputMode();
                 if( MyDebug.LOG )
@@ -3774,7 +3772,8 @@ public class MainActivity extends AppCompatActivity {
                     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, media.mediastore);
+                        // if media.mediastore, we'll rotate below using the media.orientation
+                        thumbnail = loadThumbnailFromUri(media.uri, 1, !media.mediastore);
                     }
                     if( thumbnail == null ) {
                         try {
@@ -3813,7 +3812,7 @@ public class MainActivity extends AppCompatActivity {
                                 else {
                                     if( MyDebug.LOG )
                                         Log.d(TAG, "load thumbnail for photo from SAF uri");
-                                    thumbnail = loadThumbnailFromUri(media.uri, 4, media.mediastore);
+                                    thumbnail = loadThumbnailFromUri(media.uri, 4, true);
                                 }
                             }
                             else if( media.video ) {
@@ -3825,6 +3824,10 @@ public class MainActivity extends AppCompatActivity {
                                 if( MyDebug.LOG )
                                     Log.d(TAG, "load thumbnail for photo");
                                 thumbnail = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Images.Thumbnails.MINI_KIND, null);
+                                if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) {
+                                    // MediaStore.Images.Thumbnails.getThumbnail() is documented that as of Android Q, we no longer need to apply the orientation
+                                    rotate_for_orientation = false;
+                                }
                             }
                         }
                         catch(Throwable exception) {
@@ -3835,7 +3838,7 @@ public class MainActivity extends AppCompatActivity {
                             exception.printStackTrace();
                         }
                     }
-                    if( thumbnail != null ) {
+                    if( thumbnail != null && rotate_for_orientation ) {
                         if( MyDebug.LOG )
                             Log.d(TAG, "thumbnail orientation is " + media.orientation);
                         if( media.orientation != 0 ) {
-- 
GitLab


From 8115b0cd0fca730fd1b8e11ca0c3e9876cfdc3b8 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 30 Jul 2022 19:41:05 +0100
Subject: [PATCH 010/117] Fix last release.

---
 app/src/main/java/net/sourceforge/opencamera/MainActivity.java | 2 +-
 1 file changed, 1 insertion(+), 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 c0126351e..dca43db5e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -3617,7 +3617,7 @@ public class MainActivity extends AppCompatActivity {
      *  sample_factor is greater than 1, it will be scaled down to a lower resolution.
      * @param exif_rotate Whether the rotate the bitmap due to exif orientation.
      */
-    private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor, boolean mediastore) {
+    private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor, boolean exif_rotate) {
         Bitmap thumbnail = null;
         try {
             //thumbnail = MediaStore.Images.Media.getBitmap(getContentResolver(), media.uri);
-- 
GitLab


From e695fb0aa0f3c762829a15ee3f7480ed583b75ce Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 30 Jul 2022 20:47:06 +0100
Subject: [PATCH 011/117] Improve look of on-screen level line; use shadow
 layers.

---
 _docs/history.html                            |  1 +
 .../opencamera/ui/DrawPreview.java            | 71 ++++++++++---------
 2 files changed, 38 insertions(+), 34 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index c653c17d4..bab9b184a 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -59,6 +59,7 @@ ADDED   Shading for auto-level and crop guides, to darken the preview outside of
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
+UPDATED Improved look of on-screen level line.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 
 Version 1.50.1 (2022/06/08)
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 9faf9bb62..381bcca62 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
@@ -2240,6 +2240,7 @@ public class DrawPreview {
             // 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
+            int o_radius = (int) (10 * 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.getDisplayRotation();
@@ -2273,28 +2274,41 @@ public class DrawPreview {
                 is_level = true;
             }
 
-            if( is_level ) {
-                radius = (int)(radius * 1.2);
+            final int line_alpha = 160;
+            float hthickness = (0.5f * scale + 0.5f); // convert dps to pixels
+            float shadow_radius = hthickness;
+            shadow_radius = Math.max(shadow_radius, 1.0f);
+            p.setStyle(Paint.Style.FILL);
+
+            if( actual_show_angle_line_pref && preview.hasLevelAngleStable() ) {
+                // draw the non-rotated part of the level
+                // only show the angle line if level angle "stable" (i.e., not pointing near vertically up or down)
+
+                p.setShadowLayer(shadow_radius, 0.0f, 0.0f, Color.BLACK);
+
+                if( is_level ) {
+                    p.setColor(angle_highlight_color_pref);
+                }
+                else {
+                    p.setColor(Color.WHITE);
+                }
+                p.setAlpha(line_alpha);
+                draw_rect.set(cx - radius - o_radius, cy - hthickness, cx - radius, cy + hthickness);
+                canvas.drawRoundRect(draw_rect, hthickness, hthickness, p);
+                draw_rect.set(cx + radius, cy - hthickness, cx + radius + o_radius, cy + hthickness);
+                canvas.drawRoundRect(draw_rect, hthickness, hthickness, p);
+
+                p.clearShadowLayer();
             }
 
             canvas.save();
             canvas.rotate((float)angle, cx, cy);
 
-            final int line_alpha = 160;
-            float hthickness = (0.5f * scale + 0.5f); // convert dps to pixels
-            p.setStyle(Paint.Style.FILL);
             if( actual_show_angle_line_pref && preview.hasLevelAngleStable() ) {
                 // only show the angle line if level angle "stable" (i.e., not pointing near vertically up or down)
-                // draw outline
-                p.setColor(Color.BLACK);
-                p.setAlpha(64);
-                // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21
-                draw_rect.set(cx - radius - hthickness, cy - 2 * hthickness, cx + radius + hthickness, cy + 2 * hthickness);
-                canvas.drawRoundRect(draw_rect, 2 * hthickness, 2 * hthickness, p);
-                // draw the vertical crossbar
-                draw_rect.set(cx - 2 * hthickness, cy - radius / 2.0f - hthickness, cx + 2 * hthickness, cy + radius / 2.0f + hthickness);
-                canvas.drawRoundRect(draw_rect, hthickness, hthickness, p);
-                // draw inner portion
+
+                p.setShadowLayer(shadow_radius, 0.0f, 0.0f, Color.BLACK);
+
                 if( is_level ) {
                     p.setColor(angle_highlight_color_pref);
                 }
@@ -2312,16 +2326,13 @@ public class DrawPreview {
                 if( is_level ) {
                     // draw a second line
 
-                    p.setColor(Color.BLACK);
-                    p.setAlpha(64);
-                    draw_rect.set(cx - radius - hthickness, cy - 7 * hthickness, cx + radius + hthickness, cy - 3 * hthickness);
-                    canvas.drawRoundRect(draw_rect, 2 * hthickness, 2 * hthickness, p);
-
                     p.setColor(angle_highlight_color_pref);
                     p.setAlpha(line_alpha);
                     draw_rect.set(cx - radius, cy - 6 * hthickness, cx + radius, cy - 4 * hthickness);
                     canvas.drawRoundRect(draw_rect, hthickness, hthickness, p);
                 }
+
+                p.clearShadowLayer();
             }
             updateCachedViewAngles(time_ms); // ensure view_angle_x_preview, view_angle_y_preview are computed and up to date
             float camera_angle_x, camera_angle_y;
@@ -2364,13 +2375,6 @@ public class DrawPreview {
 							Log.d(TAG, "pitch_angle: " + pitch_angle);
 							Log.d(TAG, "pitch_distance_dp: " + pitch_distance_dp);
 						}*/
-                        // draw outline
-                        p.setColor(Color.BLACK);
-                        p.setAlpha(64);
-                        // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21
-                        draw_rect.set(cx - pitch_radius - hthickness, cy + pitch_distance - 2*hthickness, cx + pitch_radius + hthickness, cy + pitch_distance + 2*hthickness);
-                        canvas.drawRoundRect(draw_rect, 2*hthickness, 2*hthickness, p);
-                        // draw inner portion
                         p.setColor(Color.WHITE);
                         p.setTextAlign(Paint.Align.LEFT);
                         if( latitude_angle == 0 && Math.abs(pitch_angle) < 1.0 ) {
@@ -2385,8 +2389,11 @@ public class DrawPreview {
                         else {
                             p.setAlpha(line_alpha);
                         }
+                        p.setShadowLayer(shadow_radius, 0.0f, 0.0f, Color.BLACK);
+                        // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21
                         draw_rect.set(cx - pitch_radius, cy + pitch_distance - hthickness, cx + pitch_radius, cy + pitch_distance + hthickness);
                         canvas.drawRoundRect(draw_rect, hthickness, hthickness, p);
+                        p.clearShadowLayer();
                         // draw pitch angle indicator
                         applicationInterface.drawTextWithBackground(canvas, p, "" + latitude_angle + "\u00B0", p.getColor(), Color.BLACK, (int)(cx + pitch_radius + 4*hthickness), (int)(cy + pitch_distance - 2*hthickness), MyApplicationInterface.Alignment.ALIGNMENT_CENTRE);
                     }
@@ -2421,18 +2428,14 @@ public class DrawPreview {
 							Log.d(TAG, "this_angle is now: " + this_angle);
 						}*/
                         float geo_distance = angle_scale * (float)Math.tan( Math.toRadians(this_angle) ); // angle_scale is already in pixels rather than dps
-                        // draw outline
-                        p.setColor(Color.BLACK);
-                        p.setAlpha(64);
-                        // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21
-                        draw_rect.set(cx + geo_distance - 2*hthickness, cy - geo_radius - hthickness, cx + geo_distance + 2*hthickness, cy + geo_radius + hthickness);
-                        canvas.drawRoundRect(draw_rect, 2*hthickness, 2*hthickness, p);
-                        // draw inner portion
                         p.setColor(Color.WHITE);
                         p.setTextAlign(Paint.Align.CENTER);
                         p.setAlpha(line_alpha);
+                        p.setShadowLayer(shadow_radius, 0.0f, 0.0f, Color.BLACK);
+                        // can't use drawRoundRect(left, top, right, bottom, ...) as that requires API 21
                         draw_rect.set(cx + geo_distance - hthickness, cy - geo_radius, cx + geo_distance + hthickness, cy + geo_radius);
                         canvas.drawRoundRect(draw_rect, hthickness, hthickness, p);
+                        p.clearShadowLayer();
                         // draw geo direction angle indicator
                         applicationInterface.drawTextWithBackground(canvas, p, "" + longitude_angle + "\u00B0", p.getColor(), Color.BLACK, (int)(cx + geo_distance), (int)(cy - geo_radius - 4*hthickness), MyApplicationInterface.Alignment.ALIGNMENT_BOTTOM);
                     }
-- 
GitLab


From 0314cd8c520b5c0b0966774475a5be0b9302072b Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 30 Jul 2022 21:48:18 +0100
Subject: [PATCH 012/117] Fix test failures on Pixel 6 Pro.

---
 .../opencamera/test/MainActivityTest.java     | 25 +++++++++++++------
 1 file changed, 18 insertions(+), 7 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 7ce5ea3c5..ee4de2f0b 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -905,7 +905,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 (int)(0.8*display_size.y));
             assertEquals(display_size.x, settingsButton.getRight());
             // position may be 1 coordinate different on some devices, e.g., Galaxy Nexus
-            assertEquals((double)(display_size.y-1-expected_gap), (double)(galleryButton.getBottom()), 1.0+1.0e-5);
+            // have 14 pixel gap on Pixel 6 Pro
+            assertEquals((double)(display_size.y-1-expected_gap), (double)(galleryButton.getBottom()), 14.0+1.0e-5);
             assertEquals(display_size.x-expected_gap, galleryButton.getRight());
         }
         else {
@@ -8780,7 +8790,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sat, 30 Jul 2022 23:09:56 +0100
Subject: [PATCH 013/117] Add comments for testing with on Pixel 6 Pro.

---
 .../net/sourceforge/opencamera/test/MainActivityTest.java     | 4 +++-
 1 file changed, 3 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 ee4de2f0b..2b20bf401 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -7342,7 +7342,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 31 Jul 2022 14:07:51 +0100
Subject: [PATCH 014/117] Apply a dimmed effect when reopening camera or
 switching modes (for Camera2 API).

---
 _docs/history.html                            |  1 +
 .../sourceforge/opencamera/MainActivity.java  | 41 ++++++++++---
 .../cameracontroller/CameraController.java    | 10 ++-
 .../cameracontroller/CameraController2.java   |  4 ++
 .../opencamera/preview/Preview.java           |  4 ++
 .../opencamera/ui/DrawPreview.java            | 61 ++++++++++++++++++-
 .../sourceforge/opencamera/ui/PopupView.java  | 10 +--
 7 files changed, 114 insertions(+), 17 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index bab9b184a..1083f8cf3 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -59,6 +59,7 @@ ADDED   Shading for auto-level and crop guides, to darken the preview outside of
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
+UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API).
 UPDATED Improved look of on-screen level line.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 
diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
index dca43db5e..a31b2af15 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -6,6 +6,7 @@ import net.sourceforge.opencamera.cameracontroller.CameraControllerManager2;
 import net.sourceforge.opencamera.preview.Preview;
 import net.sourceforge.opencamera.preview.VideoProfile;
 import net.sourceforge.opencamera.remotecontrol.BluetoothRemoteControl;
+import net.sourceforge.opencamera.ui.DrawPreview;
 import net.sourceforge.opencamera.ui.FolderChooserDialog;
 import net.sourceforge.opencamera.ui.MainUI;
 import net.sourceforge.opencamera.ui.ManualSeekbars;
@@ -1368,6 +1369,8 @@ public class MainActivity extends AppCompatActivity {
         // (this should already have been set from the call in onPause(), but we set it here again just in case)
         applicationInterface.getDrawPreview().setCoverPreview(true);
 
+        applicationInterface.getDrawPreview().clearDimPreview(); // shouldn't be needed, but just in case the dim preview flag got set somewhere
+
         cancelImageSavingNotification();
 
         // Set black window background; also needed if we hide the virtual buttons in immersive mode
@@ -2133,6 +2136,7 @@ public class MainActivity extends AppCompatActivity {
         switchCameraButton.setEnabled(false);
         switchMultiCameraButton.setEnabled(false);
         applicationInterface.reset(true);
+        this.getApplicationInterface().getDrawPreview().setDimPreview(true);
         this.preview.setCamera(cameraId);
         switchCameraButton.setEnabled(true);
         switchMultiCameraButton.setEnabled(true);
@@ -2210,6 +2214,7 @@ public class MainActivity extends AppCompatActivity {
         View switchVideoButton = findViewById(R.id.switch_video);
         switchVideoButton.setEnabled(false); // prevent slowdown if user repeatedly clicks
         applicationInterface.reset(false);
+        this.getApplicationInterface().getDrawPreview().setDimPreview(true);
         this.preview.switchVideo(false, true);
         switchVideoButton.setEnabled(true);
 
@@ -2735,11 +2740,11 @@ public class MainActivity extends AppCompatActivity {
     }
 
     public void updateForSettings(boolean update_camera) {
-        updateForSettings(update_camera, null, false);
+        updateForSettings(update_camera, null);
     }
 
     public void updateForSettings(boolean update_camera, String toast_message) {
-        updateForSettings(update_camera, toast_message, false);
+        updateForSettings(update_camera, toast_message, false, false);
     }
 
     /** Must be called when an settings (as stored in SharedPreferences) are made, so we can update the
@@ -2749,10 +2754,12 @@ public class MainActivity extends AppCompatActivity {
      * @param toast_message If non-null, display this toast instead of the usual camera "startup" toast
      *                      that's shown in showPhotoVideoToast(). If non-null but an empty string, then
      *                      this means no toast is shown at all.
-     * @param keep_popup If false, the popup will be closed and destroyed. Set to true if you're sure
-     *                   that the changed setting isn't one that requires the PopupView to be recreated
+     * @param keep_popup    If false, the popup will be closed and destroyed. Set to true if you're sure
+     *                      that the changed setting isn't one that requires the PopupView to be recreated
+     * @param allow_dim     If true, for Camera2 API a dimming effect will be applied if updating the
+     *                      camera.
      */
-    public void updateForSettings(boolean update_camera, String toast_message, boolean keep_popup) {
+    public void updateForSettings(boolean update_camera, String toast_message, boolean keep_popup, boolean allow_dim) {
         if( MyDebug.LOG ) {
             Log.d(TAG, "updateForSettings()");
             if( toast_message != null ) {
@@ -2898,6 +2905,8 @@ public class MainActivity extends AppCompatActivity {
             // don't try to update camera
         }
         else if( need_reopen || preview.getCameraController() == null ) { // if camera couldn't be opened before, might as well try again
+            if( allow_dim )
+                applicationInterface.getDrawPreview().setDimPreview(true);
             preview.reopenCamera();
             if( MyDebug.LOG ) {
                 Log.d(TAG, "updateForSettings: time after reopen: " + (System.currentTimeMillis() - debug_time));
@@ -2908,14 +2917,21 @@ public class MainActivity extends AppCompatActivity {
             if( MyDebug.LOG ) {
                 Log.d(TAG, "updateForSettings: time after set display orientation: " + (System.currentTimeMillis() - debug_time));
             }
+            if( allow_dim )
+                applicationInterface.getDrawPreview().setDimPreview(true);
             preview.pausePreview(true);
             if( MyDebug.LOG ) {
                 Log.d(TAG, "updateForSettings: time after pause: " + (System.currentTimeMillis() - debug_time));
             }
-            preview.setupCamera(false);
-            if( MyDebug.LOG ) {
-                Log.d(TAG, "updateForSettings: time after setup: " + (System.currentTimeMillis() - debug_time));
-            }
+
+            Handler handler = new Handler();
+            // We run setupCamera on the UI thread, but we do it on a post-delayed so that the dimming effect (for Camera2 API) has a chance to run.
+            // Even if allow_dim==false, still run as a postDelayed (a) for consistency, (b) to allow UI to run for a bit (to avoid risk of slow frames).
+            handler.postDelayed(new Runnable() {
+                public void run() {
+                    preview.setupCamera(false);
+                }
+            }, DrawPreview.dim_effect_time_c+16); // +16 to allow time for a frame update to run
         }
         // don't set block_startup_toast to false yet, as camera might be closing/opening on background thread
         if( toast_message != null && toast_message.length() > 0 )
@@ -3032,6 +3048,11 @@ public class MainActivity extends AppCompatActivity {
         // to avoid reading the preferences in every single frame).
         applicationInterface.getDrawPreview().updateSettings();
 
+        // Set the flag to cover the preview until the camera is open and receiving frames again
+        // (for Camera2 API) - avoids showing a flash of the preview from before the user went to
+        // the settings.
+        applicationInterface.getDrawPreview().setCoverPreview(true);
+
         if( preferencesListener.anyChange() ) {
             mainUI.updateOnScreenIcons();
         }
@@ -5159,6 +5180,8 @@ public class MainActivity extends AppCompatActivity {
         block_startup_toast = false;
         if( MyDebug.LOG )
             Log.d(TAG, "cameraSetup: total time for cameraSetup: " + (System.currentTimeMillis() - debug_time));
+
+        this.getApplicationInterface().getDrawPreview().setDimPreview(false);
     }
 
     private void setManualFocusSeekbar(final boolean is_target_distance) {
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 c8f6e12c7..4b3f99357 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java
@@ -325,14 +325,20 @@ public abstract class CameraController {
         return cameraId;
     }
 
-    /** For CameraController2 only. Applications should cover the preview textureview if either camera_controller==null, or if this
-     *  method returns true. Otherwise there is a risk when opening the camera that the textureview still shows an image from when
+    /** For CameraController2 only. Applications should cover the preview textureview if since last resuming, camera_controller
+     *  has never been non-null or this method has never returned false.
+     *  Otherwise there is a risk when opening the camera that the textureview still shows an image from when
      *  the camera was previously opened (e.g., from pausing and resuming the application). This returns false (for CameraController2)
      *  when the camera has received its first frame.
      */
     public boolean shouldCoverPreview() {
         return false;
     }
+    /** For CameraController2 only. After calling this, shouldCoverPreview() will return true, until a new
+     *  frame from the camera has been received.
+     */
+    public void resetCoverPreview() {
+    }
     public abstract SupportedValues setSceneMode(String value);
     /**
      * @return The current scene mode. Will be null if scene mode not supported.
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 8f66c3a70..4b97c7495 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -3200,6 +3200,10 @@ public class CameraController2 extends CameraController {
         return !has_received_frame;
     }
 
+    public void resetCoverPreview() {
+        this.has_received_frame = false;
+    }
+
     private String convertSceneMode(int value2) {
         String value;
         switch( value2 ) {
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 b99890065..b86d902a4 100644
--- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
@@ -8546,6 +8546,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
     	   Camera2 API. Testing suggests this does not seem to adversely affect battery life.
     	   This is limited to Android 7+, to avoid causing problems on older devices (which don't
     	   contribute to Google Analytics anyway).
+    	   If we ever are able to use lower frame rates in future, remember we'll still need a high
+    	   frame rate when applying the dimming effect when reopening or updating the camera (see
+    	   DrawPreview.setDimPreview()) (especially for MainActivity.updateForSettings() when we
+    	   pause/unpause the preview instead of reopening the camera).
     	 */
         //
         if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N )
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 381bcca62..d808c9f1f 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
@@ -56,7 +56,25 @@ public class DrawPreview {
     private final MainActivity main_activity;
     private final MyApplicationInterface applicationInterface;
 
+    // In some cases when reopening the camera or pausing preview, we apply a dimming effect (only
+    // supported when using Camera2 API, since we need to know when frames have been received).
+    enum DimPreview {
+        DIM_PREVIEW_OFF, // don't dim the preview
+        DIM_PREVIEW_ON, // do dim the preview
+        DIM_PREVIEW_UNTIL // dim the preview until the camera_controller is non-null and has received frames, then switch to DIM_PREVIEW_OFF
+    }
+    private DimPreview dim_preview = DimPreview.DIM_PREVIEW_OFF;
+
+    // Time for the dimming effect. This should be quick, because we call Preview.setupCamera() on
+    // the UI thread, which will block redraws:
+    // - When reopening the camera, we want the dimming to have occurred whilst reopening the
+    //   camera, before we call setupCamera() on the UI thread.
+    // - When pausing the preview in MainActivity.updateForSettings(), we call setupCamera() after
+    //   this delay - so we don't want to keep the user waiting too long.
+    public final static long dim_effect_time_c = 50;
+
     private boolean cover_preview; // whether to cover the preview for Camera2 API
+    private long camera_inactive_time_ms = -1; // if != -1, the time when the camera became inactive
 
     // store to avoid calling PreferenceManager.getDefaultSharedPreferences() repeatedly
     private final SharedPreferences sharedPreferences;
@@ -2668,6 +2686,21 @@ public class DrawPreview {
         this.cover_preview = cover_preview;
     }
 
+    public void setDimPreview(boolean on) {
+        if( MyDebug.LOG )
+            Log.d(TAG, "setDimPreview: " + on);
+        if( on ) {
+            this.dim_preview = DimPreview.DIM_PREVIEW_ON;
+        }
+        else if( this.dim_preview == DimPreview.DIM_PREVIEW_ON ) {
+            this.dim_preview = DimPreview.DIM_PREVIEW_UNTIL;
+        }
+    }
+
+    public void clearDimPreview() {
+        this.dim_preview = DimPreview.DIM_PREVIEW_OFF;
+    }
+
     public void onDrawPreview(Canvas canvas) {
 		/*if( MyDebug.LOG )
 			Log.d(TAG, "onDrawPreview");*/
@@ -2715,19 +2748,45 @@ public class DrawPreview {
         // cover up the camera when the application is pausing, and to keep it covered up until
         // after we've resumed, and the camera has been reopened and we've received frames.
         if( preview.usingCamera2API() ) {
+            boolean camera_is_active = camera_controller != null && !camera_controller.shouldCoverPreview();
             if( cover_preview ) {
                 // see if we have received a frame yet
-                if( camera_controller != null && !camera_controller.shouldCoverPreview() ) {
+                if( camera_is_active ) {
                     if( MyDebug.LOG )
                         Log.d(TAG, "no longer need to cover preview");
                     cover_preview = false;
                 }
             }
             if( cover_preview ) {
+                // camera has never been active since last resuming
                 p.setColor(Color.BLACK);
                 //p.setColor(Color.RED); // test
                 canvas.drawRect(0.0f, 0.0f, canvas.getWidth(), canvas.getHeight(), p);
             }
+            else if( dim_preview == DimPreview.DIM_PREVIEW_ON || ( !camera_is_active && dim_preview == DimPreview.DIM_PREVIEW_UNTIL ) ) {
+                long time_now = System.currentTimeMillis();
+                if( camera_inactive_time_ms == -1 ) {
+                    camera_inactive_time_ms = time_now;
+                }
+                float frac = ((time_now - camera_inactive_time_ms) / (float)dim_effect_time_c);
+                frac = Math.min(frac, 1.0f);
+                int alpha = (int)(frac * 127);
+                if( MyDebug.LOG ) {
+                    Log.d(TAG, "time diff: " + (time_now - camera_inactive_time_ms));
+                    Log.d(TAG, "    frac: " + frac);
+                    Log.d(TAG, "    alpha: " + alpha);
+                }
+                p.setColor(Color.BLACK);
+                p.setAlpha(alpha);
+                canvas.drawRect(0.0f, 0.0f, canvas.getWidth(), canvas.getHeight(), p);
+                p.setAlpha(255);
+            }
+            else {
+                camera_inactive_time_ms = -1;
+                if( dim_preview == DimPreview.DIM_PREVIEW_UNTIL && camera_is_active ) {
+                    dim_preview = DimPreview.DIM_PREVIEW_OFF;
+                }
+            }
         }
 
         if( camera_controller!= null && front_screen_flash ) {
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 edf1ec5e7..a17ec2d9e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java
@@ -409,7 +409,7 @@ public class PopupView extends LinearLayout {
                         public void run() {
                             if (MyDebug.LOG)
                                 Log.d(TAG, "update settings due to resolution change");
-                            main_activity.updateForSettings(true, "", true); // keep the popupview open
+                            main_activity.updateForSettings(true, "", true, false); // keep the popupview open
                         }
                     };
 
@@ -493,7 +493,7 @@ public class PopupView extends LinearLayout {
                         public void run() {
                             if( MyDebug.LOG )
                                 Log.d(TAG, "update settings due to video resolution change");
-                            main_activity.updateForSettings(true, "", true); // keep the popupview open
+                            main_activity.updateForSettings(true, "", true, false); // keep the popupview open
                         }
                     };
 
@@ -805,7 +805,7 @@ public class PopupView extends LinearLayout {
                             public void run() {
                                 if (MyDebug.LOG)
                                     Log.d(TAG, "update settings due to video capture rate change");
-                                main_activity.updateForSettings(true, "", true); // keep the popupview open
+                                main_activity.updateForSettings(true, "", true, false); // keep the popupview open
                             }
                         };
 
@@ -850,7 +850,7 @@ public class PopupView extends LinearLayout {
                                 handler.postDelayed(update_runnable, 400);
                             }
                             else {
-                                main_activity.updateForSettings(true, toast_message, keep_popup);
+                                main_activity.updateForSettings(true, toast_message, keep_popup, false);
                             }
                         }
                         @Override
@@ -1222,7 +1222,7 @@ public class PopupView extends LinearLayout {
             }
 
             main_activity.getApplicationInterface().getDrawPreview().updateSettings(); // because we cache the photomode
-            main_activity.updateForSettings(true, toast_message); // need to setup the camera again, as options may change (e.g., required burst mode, or whether RAW is allowed in this mode)
+            main_activity.updateForSettings(true, toast_message, false, true); // need to setup the camera again, as options may change (e.g., required burst mode, or whether RAW is allowed in this mode)
             main_activity.getMainUI().destroyPopup(); // need to recreate popup for new selection
         }
     }
-- 
GitLab


From 5a75be76c0993a341fb66853e0a1ece4641707c8 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 31 Jul 2022 14:08:19 +0100
Subject: [PATCH 015/117] No longer run testTakeVideoFPS.

---
 .../java/net/sourceforge/opencamera/test/VideoTests.java        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 4787ac2ba..38b1f38cd 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/VideoTests.java
@@ -79,7 +79,7 @@ public class VideoTests {
             suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoEdgeModeNoiseReductionMode"));
         }
         // put tests which change bitrate, fps or test 4K at end
-        suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoFPS"));
+        //suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoFPS")); // disabled as unreliable
         if( MainActivityTest.test_camera2 ) {
             suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoFPSHighSpeedManual"));
             suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideoSlowMotion"));
-- 
GitLab


From 08e722e4e1c1d9eb0d892ec3ab891906548b3360 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Tue, 2 Aug 2022 21:26:34 +0100
Subject: [PATCH 016/117] Make setting of CONTROL_CAPTURE_INTENT consistent
 with the template.

---
 .../opencamera/cameracontroller/CameraController2.java         | 3 ++-
 1 file changed, 2 insertions(+), 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 4b97c7495..7243eba2e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -7107,8 +7107,9 @@ public class CameraController2 extends CameraController {
                     Log.d(TAG, "imageReader surface: " + imageReader.getSurface().toString());
                 }
 
+                // n.b., takePictureBurst() not currently called if previewIsVideoMode==true, but have put this code here for possible future use
                 CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(previewIsVideoMode ? CameraDevice.TEMPLATE_VIDEO_SNAPSHOT : CameraDevice.TEMPLATE_STILL_CAPTURE);
-                stillBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
+                stillBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, previewIsVideoMode ? CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT : CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
                 // n.b., don't set RequestTagType.CAPTURE here - we only do it for the last of the burst captures (see below)
                 camera_settings.setupBuilder(stillBuilder, true);
                 if( use_fake_precapture_mode && fake_precapture_torch_performed ) {
-- 
GitLab


From d0242c1fa6ef3eb39a537bf3f5bef929a6ee3adc Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Tue, 2 Aug 2022 21:34:54 +0100
Subject: [PATCH 017/117] Make zoom seekbar snap to powers of two (for Camera2
 API).

---
 _docs/history.html                            |  1 +
 .../cameracontroller/CameraController2.java   | 56 +++++++++++--------
 2 files changed, 34 insertions(+), 23 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index 1083f8cf3..7493e0017 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -59,6 +59,7 @@ ADDED   Shading for auto-level and crop guides, to darken the preview outside of
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
+UPDATED Make zoom seekbar snap to powers of two (for Camera2 API).
 UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API).
 UPDATED Improved look of on-screen level line.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
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 7243eba2e..844006af5 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -2563,14 +2563,27 @@ public class CameraController2 extends CameraController {
             Log.d(TAG, "max_zoom: " + max_zoom);
         }
         if( camera_features.is_zoom_supported ) {
-            float zoom_max_min_ratio = max_zoom / min_zoom;
+            // prepare zoom rations > 1x
             // set 20 steps per 2x factor
-            final int steps_per_2x_factor = 20;
-            int n_steps = (int)( (steps_per_2x_factor * Math.log(zoom_max_min_ratio + 1.0e-11)) / Math.log(2.0));
+            final double scale_factor_c = 1.0352649238413775043477881942112;
+            List zoom_ratios_above_one = new ArrayList<>();
+            double zoom = scale_factor_c;
+            while( zoom < max_zoom - 1.0e-5f ) {
+                int zoom_ratio = (int)(zoom*100);
+                zoom_ratios_above_one.add(zoom_ratio);
+                zoom *= scale_factor_c;
+            }
+            int max_zoom_ratio = (int)(max_zoom*100);
+            if( zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) != max_zoom_ratio ) {
+                zoom_ratios_above_one.add(max_zoom_ratio);
+            }
+            int n_steps_above_one = zoom_ratios_above_one.size();
             if( MyDebug.LOG ) {
-                Log.d(TAG, "n_steps: " + n_steps);
+                Log.d(TAG, "n_steps_above_one: " + n_steps_above_one);
             }
 
+            // now populate full zoom ratios
+
             camera_features.zoom_ratios = new ArrayList<>();
 
             // add minimum zoom
@@ -2583,17 +2596,17 @@ public class CameraController2 extends CameraController {
             }
 
             if( camera_features.zoom_ratios.get(0) < 100 ) {
-                int n_steps_below_one = Math.max(1, n_steps/5);
+                int n_steps_below_one = Math.max(1, n_steps_above_one/5);
                 // if the min zoom is < 1.0, we add multiple entries for 1x zoom, when using the zoom
                 // seekbar it's easy for the user to zoom to exactly 1x
-                int n_steps_one = Math.max(1, n_steps/10);
+                int n_steps_one = Math.max(1, n_steps_above_one/10);
                 if( MyDebug.LOG ) {
                     Log.d(TAG, "n_steps_below_one: " + n_steps_below_one);
                     Log.d(TAG, "n_steps_one: " + n_steps_one);
                 }
 
                 // add rest of zoom values < 1.0f
-                double zoom = min_zoom;
+                zoom = min_zoom;
                 final double scale_factor = Math.pow(1.0f / min_zoom, 1.0/(double)n_steps_below_one);
                 if( MyDebug.LOG ) {
                     Log.d(TAG, "scale_factor for below 1.0x: " + scale_factor);
@@ -2607,7 +2620,7 @@ public class CameraController2 extends CameraController {
                     }
                 }
 
-                // add values for 1.0f
+                // add values for 1.0f (we add repeated values so for cameras with min_zoom < 1x, the zoom seekbar will snap to 1x)
                 zoom_value_1x = camera_features.zoom_ratios.size();
                 for(int i=0;i 1.0f
-            double zoom = 1.0f;
-            final double scale_factor = Math.pow(max_zoom, 1.0/(double)n_steps_above_one);
+            int n_steps_power_two = Math.max(1, (int)(0.5f+n_steps_above_one/15.0f));
             if( MyDebug.LOG ) {
-                Log.d(TAG, "scale_factor for above 1.0x: " + scale_factor);
+                Log.d(TAG, "n_steps_power_two: " + n_steps_power_two);
             }
-            for(int i=0;i
Date: Tue, 2 Aug 2022 22:14:26 +0100
Subject: [PATCH 018/117] Fix testTakePhotoExposureCompensation() for Pixel 6
 Pro (we don't add a button for ISO 10000).

---
 .../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 2b20bf401..0b284e1a7 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -2717,7 +2717,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sat, 6 Aug 2022 12:15:03 +0100
Subject: [PATCH 019/117] Fixes for various PhotoTests with camera2 API on
 Pixel 6 Pro.

---
 .../opencamera/test/MainActivityTest.java            | 12 +++++++++---
 1 file changed, 9 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 0b284e1a7..760eaa088 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -3806,6 +3806,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Wed, 24 Aug 2022 22:00:03 +0100
Subject: [PATCH 020/117] Fix crash for
 testTakePhotoAutoFocusReleaseDuringPhoto on Pixel 6 Pro.

---
 .../cameracontroller/CameraController2.java   | 21 +++++++++++++++----
 1 file changed, 17 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 844006af5..cd92d48f7 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -1463,7 +1463,8 @@ public class CameraController2 extends CameraController {
                 // or strange behaviour where an old image appears when the user next takes a photo
                 Log.e(TAG, "no picture callback available");
                 Image image = reader.acquireNextImage();
-                image.close();
+                if( image != null )
+                    image.close();
                 return;
             }
             if( skip_next_image ) {
@@ -1471,7 +1472,8 @@ public class CameraController2 extends CameraController {
                     Log.d(TAG, "skipping image");
                 skip_next_image = false;
                 Image image = reader.acquireNextImage();
-                image.close();
+                if( image != null )
+                    image.close();
                 return;
             }
 
@@ -1480,6 +1482,11 @@ public class CameraController2 extends CameraController {
             boolean call_takePhotoCompleted = false;
 
             Image image = reader.acquireNextImage();
+            if( image == null ) {
+                // can happen if camera closed whilst taking photo - this happens in testTakePhotoAutoFocusReleaseDuringPhoto() on Pixel 6 Pro
+                Log.e(TAG, "onImageAvailable: image is null");
+                return;
+            }
             if( MyDebug.LOG )
                 Log.d(TAG, "image timestamp: " + image.getTimestamp());
             ByteBuffer buffer = image.getPlanes()[0].getBuffer();
@@ -1895,7 +1902,8 @@ public class CameraController2 extends CameraController {
                 // or strange behaviour where an old image appears when the user next takes a photo
                 Log.e(TAG, "no picture callback available");
                 Image this_image = reader.acquireNextImage();
-                this_image.close();
+                if( this_image != null )
+                    this_image.close();
                 return;
             }
             if( skip_next_image ) {
@@ -1903,12 +1911,17 @@ public class CameraController2 extends CameraController {
                     Log.d(TAG, "skipping image");
                 skip_next_image = false;
                 Image image = reader.acquireNextImage();
-                image.close();
+                if( image != null )
+                    image.close();
                 return;
             }
             synchronized( background_camera_lock ) {
                 // see comment above in setCaptureResult() for why we synchronize
                 Image image = reader.acquireNextImage();
+                if( image == null ) {
+                    Log.e(TAG, "RAW onImageAvailable: image is null");
+                    return;
+                }
                 images.add(image);
             }
             processImage();
-- 
GitLab


From afda44053b19eed56a767a81c0c189d6593de0df Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 24 Aug 2022 22:01:49 +0100
Subject: [PATCH 021/117] Fixes for now that we restart preview on postDelayed
 in MainActivity.updateForSettings().

---
 .../opencamera/test/MainActivityTest.java     | 27 +++++++++++++++----
 1 file changed, 22 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 760eaa088..792893b02 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -32,6 +32,7 @@ import net.sourceforge.opencamera.preview.VideoProfile;
 import net.sourceforge.opencamera.SaveLocationHistory;
 import net.sourceforge.opencamera.cameracontroller.CameraController;
 import net.sourceforge.opencamera.preview.Preview;
+import net.sourceforge.opencamera.ui.DrawPreview;
 import net.sourceforge.opencamera.ui.FolderChooserDialog;
 import net.sourceforge.opencamera.ui.MainUI;
 import net.sourceforge.opencamera.ui.PopupView;
@@ -252,6 +253,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Wed, 24 Aug 2022 22:02:00 +0100
Subject: [PATCH 022/117] Fix for Pixel 6 Pro.

---
 .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 3 ++-
 1 file changed, 2 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 792893b02..b062b996e 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -6233,6 +6233,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sat, 27 Aug 2022 20:28:51 +0100
Subject: [PATCH 023/117] Various updates.

---
 opencamera_source.txt | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/opencamera_source.txt b/opencamera_source.txt
index 967a8a992..a3a7cc236 100644
--- a/opencamera_source.txt
+++ b/opencamera_source.txt
@@ -19,6 +19,11 @@ Preview: At a higher level, you might want to take the classes in Preview/ as we
 
 Application: You might prefer to use Open Camera in its entirety, as an Activity within your application (or possibly converting the MainActivity to a fragment). Consider whether Open Camera's Settings should be unified with the settings in your application. Accesses to SharedPreferences use the PreferencesKeys class to find the keys.
 
+If you are using the Open Camera source code, there are some obvious things to change:
+* Package name
+* Homepage (see MainActivity.getOnlineHelpUrl())
+* Email contact (see "preference_privacy_policy_text" in strings.xml)
+
 Android inspection warnings/errors
 ==================================
 
@@ -44,7 +49,7 @@ The following are enabled, but with modifications:
 Licence
 =======
 
-This source is released under the GPL v3 or later. Please see the licence in gpl-3.0.txt (or https://www.gnu.org/copyleft/gpl.html ). If you would like to use the Open Camera in a closed source app, please contact me for a closed source commercial licence, with details on your company and app.
+This source is released under the GPL v3 or later. Please see the licence in gpl-3.0.txt (or https://www.gnu.org/copyleft/gpl.html ). In particular, if you are using the Open Camera source code in your own application, please see the requirements in the licence for making source code available.
 
 Please contact me if you have suggestions, bug fixes, or whatever: mark DOT harman DOT apps AT gmail DOT com .
 
-- 
GitLab


From eb1eef5e1eae9e8b5a7b6a37c8ba05c65e825ef7 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 27 Aug 2022 20:29:01 +0100
Subject: [PATCH 024/117] Update comment.

---
 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 a31b2af15..f145a92e2 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -1194,6 +1194,8 @@ public class MainActivity extends AppCompatActivity {
         if( MyDebug.LOG )
             Log.d(TAG, "getOnlineHelpUrl: " + append);
         // if we change this, remember that any page linked to must abide by Google Play developer policies!
+        // also if we change this method name or where it's located, remember to update the mention in
+        // opencamera_source.txt
         //return "https://opencamera.sourceforge.io/" + append;
         return "https://opencamera.org.uk/" + append;
     }
-- 
GitLab


From 7ea523feb40eda9e0edb94360d6f00436dee3beb Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 27 Aug 2022 20:31:50 +0100
Subject: [PATCH 025/117] Refactor.

---
 .../cameracontroller/CameraController2.java   | 23 ++++++++++---------
 1 file changed, 12 insertions(+), 11 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 cd92d48f7..6e22fd46d 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -2597,18 +2597,18 @@ public class CameraController2 extends CameraController {
 
             // now populate full zoom ratios
 
-            camera_features.zoom_ratios = new ArrayList<>();
+            List ratios = new ArrayList<>();
 
             // add minimum zoom
-            camera_features.zoom_ratios.add((int)(min_zoom*100));
-            if( camera_features.zoom_ratios.get(0)/100.0f < min_zoom ) {
+            ratios.add((int)(min_zoom*100));
+            if( ratios.get(0)/100.0f < min_zoom ) {
                 // fix for rounding down to less than the min_zoom
                 // e.g. if min_zoom = 0.666, we'd have stored a zoom ratio of 66 which then would
                 // convert back to 0.66
-                camera_features.zoom_ratios.set(0, camera_features.zoom_ratios.get(0) + 1);
+                ratios.set(0, ratios.get(0) + 1);
             }
 
-            if( camera_features.zoom_ratios.get(0) < 100 ) {
+            if( ratios.get(0) < 100 ) {
                 int n_steps_below_one = Math.max(1, n_steps_above_one/5);
                 // if the min zoom is < 1.0, we add multiple entries for 1x zoom, when using the zoom
                 // seekbar it's easy for the user to zoom to exactly 1x
@@ -2627,16 +2627,16 @@ public class CameraController2 extends CameraController {
                 for(int i=0;i camera_features.zoom_ratios.get(0) ) {
+                    if( zoom_ratio > ratios.get(0) ) {
                         // on some devices (e.g., Pixel 6 Pro), the second entry would equal the first entry, due to the rounding fix above
-                        camera_features.zoom_ratios.add(zoom_ratio);
+                        ratios.add(zoom_ratio);
                     }
                 }
 
                 // add values for 1.0f (we add repeated values so for cameras with min_zoom < 1x, the zoom seekbar will snap to 1x)
-                zoom_value_1x = camera_features.zoom_ratios.size();
+                zoom_value_1x = ratios.size();
                 for(int i=0;i
Date: Sat, 27 Aug 2022 20:48:00 +0100
Subject: [PATCH 026/117] New unit test testCameraController2ZoomRatios().

---
 .../cameracontroller/CameraController2.java   | 178 ++++++++++--------
 .../sourceforge/opencamera/test/UnitTest.java |  37 ++++
 2 files changed, 133 insertions(+), 82 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 6e22fd46d..fe7c466a4 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -2497,7 +2497,101 @@ public class CameraController2 extends CameraController {
     public String getAPI() {
         return "Camera2 (Android L)";
     }
-    
+
+    /** Computes the zoom ratios to use, for devices that support zoom.
+     * @param ratios   List to be filled with zoom ratios.
+     * @param min_zoom Minimum zoom supported.
+     * @param max_zoom Maximum zoom supported.
+     * @return         Index of ratios list that is for 1x zoom.
+     */
+    public static int computeZoomRatios(List ratios, float min_zoom, float max_zoom) {
+        int zoom_value_1x = 0;
+
+        // prepare zoom rations > 1x
+        // set 20 steps per 2x factor
+        final double scale_factor_c = 1.0352649238413775043477881942112;
+        List zoom_ratios_above_one = new ArrayList<>();
+        double zoom = scale_factor_c;
+        while( zoom < max_zoom - 1.0e-5f ) {
+            int zoom_ratio = (int)(zoom*100);
+            zoom_ratios_above_one.add(zoom_ratio);
+            zoom *= scale_factor_c;
+        }
+        int max_zoom_ratio = (int)(max_zoom*100);
+        if( zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) != max_zoom_ratio ) {
+            zoom_ratios_above_one.add(max_zoom_ratio);
+        }
+        int n_steps_above_one = zoom_ratios_above_one.size();
+        if( MyDebug.LOG ) {
+            Log.d(TAG, "n_steps_above_one: " + n_steps_above_one);
+        }
+
+        // now populate full zoom ratios
+
+        // add minimum zoom
+        ratios.add((int)(min_zoom*100));
+        if( ratios.get(0)/100.0f < min_zoom ) {
+            // fix for rounding down to less than the min_zoom
+            // e.g. if min_zoom = 0.666, we'd have stored a zoom ratio of 66 which then would
+            // convert back to 0.66
+            ratios.set(0, ratios.get(0) + 1);
+        }
+
+        if( ratios.get(0) < 100 ) {
+            int n_steps_below_one = Math.max(1, n_steps_above_one/5);
+            // if the min zoom is < 1.0, we add multiple entries for 1x zoom, when using the zoom
+            // seekbar it's easy for the user to zoom to exactly 1x
+            int n_steps_one = Math.max(1, n_steps_above_one/10);
+            if( MyDebug.LOG ) {
+                Log.d(TAG, "n_steps_below_one: " + n_steps_below_one);
+                Log.d(TAG, "n_steps_one: " + n_steps_one);
+            }
+
+            // add rest of zoom values < 1.0f
+            zoom = min_zoom;
+            final double scale_factor = Math.pow(1.0f / min_zoom, 1.0/(double)n_steps_below_one);
+            if( MyDebug.LOG ) {
+                Log.d(TAG, "scale_factor for below 1.0x: " + scale_factor);
+            }
+            for(int i=0;i ratios.get(0) ) {
+                    // on some devices (e.g., Pixel 6 Pro), the second entry would equal the first entry, due to the rounding fix above
+                    ratios.add(zoom_ratio);
+                }
+            }
+
+            // add values for 1.0f (we add repeated values so for cameras with min_zoom < 1x, the zoom seekbar will snap to 1x)
+            zoom_value_1x = ratios.size();
+            for(int i=0;i 1.0f
+        int n_steps_power_two = Math.max(1, (int)(0.5f+n_steps_above_one/15.0f));
+        if( MyDebug.LOG ) {
+            Log.d(TAG, "n_steps_power_two: " + n_steps_power_two);
+        }
+        for(int zoom_ratio : zoom_ratios_above_one) {
+            ratios.add(zoom_ratio);
+
+            if( zoom_ratio != zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) && zoom_ratio % 100 == 0 ) {
+                int zoom_ratio_int = zoom_ratio/100;
+                if( zoom_ratio_int != 0 && (zoom_ratio_int & (zoom_ratio_int-1)) == 0 ) {
+                    // is power of 2 that isn't the max zoom
+                    for(int i=0;i 1x
-            // set 20 steps per 2x factor
-            final double scale_factor_c = 1.0352649238413775043477881942112;
-            List zoom_ratios_above_one = new ArrayList<>();
-            double zoom = scale_factor_c;
-            while( zoom < max_zoom - 1.0e-5f ) {
-                int zoom_ratio = (int)(zoom*100);
-                zoom_ratios_above_one.add(zoom_ratio);
-                zoom *= scale_factor_c;
-            }
-            int max_zoom_ratio = (int)(max_zoom*100);
-            if( zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) != max_zoom_ratio ) {
-                zoom_ratios_above_one.add(max_zoom_ratio);
-            }
-            int n_steps_above_one = zoom_ratios_above_one.size();
-            if( MyDebug.LOG ) {
-                Log.d(TAG, "n_steps_above_one: " + n_steps_above_one);
-            }
-
-            // now populate full zoom ratios
 
             List ratios = new ArrayList<>();
-
-            // add minimum zoom
-            ratios.add((int)(min_zoom*100));
-            if( ratios.get(0)/100.0f < min_zoom ) {
-                // fix for rounding down to less than the min_zoom
-                // e.g. if min_zoom = 0.666, we'd have stored a zoom ratio of 66 which then would
-                // convert back to 0.66
-                ratios.set(0, ratios.get(0) + 1);
-            }
-
-            if( ratios.get(0) < 100 ) {
-                int n_steps_below_one = Math.max(1, n_steps_above_one/5);
-                // if the min zoom is < 1.0, we add multiple entries for 1x zoom, when using the zoom
-                // seekbar it's easy for the user to zoom to exactly 1x
-                int n_steps_one = Math.max(1, n_steps_above_one/10);
-                if( MyDebug.LOG ) {
-                    Log.d(TAG, "n_steps_below_one: " + n_steps_below_one);
-                    Log.d(TAG, "n_steps_one: " + n_steps_one);
-                }
-
-                // add rest of zoom values < 1.0f
-                zoom = min_zoom;
-                final double scale_factor = Math.pow(1.0f / min_zoom, 1.0/(double)n_steps_below_one);
-                if( MyDebug.LOG ) {
-                    Log.d(TAG, "scale_factor for below 1.0x: " + scale_factor);
-                }
-                for(int i=0;i ratios.get(0) ) {
-                        // on some devices (e.g., Pixel 6 Pro), the second entry would equal the first entry, due to the rounding fix above
-                        ratios.add(zoom_ratio);
-                    }
-                }
-
-                // add values for 1.0f (we add repeated values so for cameras with min_zoom < 1x, the zoom seekbar will snap to 1x)
-                zoom_value_1x = ratios.size();
-                for(int i=0;i 1.0f
-            int n_steps_power_two = Math.max(1, (int)(0.5f+n_steps_above_one/15.0f));
-            if( MyDebug.LOG ) {
-                Log.d(TAG, "n_steps_power_two: " + n_steps_power_two);
-            }
-            for(int zoom_ratio : zoom_ratios_above_one) {
-                ratios.add(zoom_ratio);
-
-                if( zoom_ratio != zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) && zoom_ratio % 100 == 0 ) {
-                    int zoom_ratio_int = zoom_ratio/100;
-                    if( zoom_ratio_int != 0 && (zoom_ratio_int & (zoom_ratio_int-1)) == 0 ) {
-                        // is power of 2 that isn't the max zoom
-                        for(int i=0;i ratios = new ArrayList<>();
+
+        int zoom_value_1x = CameraController2.computeZoomRatios(ratios, min_zoom, max_zoom);
+        assertEquals(100, (int)ratios.get(zoom_value_1x));
+        if( min_zoom == 1.0f ) {
+            assertEquals(0, zoom_value_1x);
+        }
+        else {
+            assertTrue(zoom_value_1x > 0);
+        }
+
+        int zoom_ratio = 100;
+        while( zoom_ratio <= (int)(100*max_zoom + 0.5) ) {
+            assertTrue(ratios.contains(zoom_ratio));
+            zoom_ratio *= 2;
+        }
+    }
+
+    @Test
+    public void testCameraController2ZoomRatios() {
+        Log.d(TAG, "testCameraController2ZoomRatios");
+
+        checkCameraController2ZoomRatios(1.0f, 2.0f);
+        checkCameraController2ZoomRatios(1.0f, 4.0f);
+        checkCameraController2ZoomRatios(1.0f, 8.0f);
+        checkCameraController2ZoomRatios(1.0f, 10.0f);
+        checkCameraController2ZoomRatios(1.0f, 16.0f);
+        checkCameraController2ZoomRatios(1.0f, 20.0f);
+
+        checkCameraController2ZoomRatios(0.7f, 4.0f);
+        checkCameraController2ZoomRatios(0.7f, 8.0f);
+        checkCameraController2ZoomRatios(0.7f, 10.0f);
+        checkCameraController2ZoomRatios(0.7f, 16.0f);
+        checkCameraController2ZoomRatios(0.7f, 20.0f);
+    }
 }
-- 
GitLab


From c4d932e87d8ff8bb7ed182b7926690d908c4ce3d Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 27 Aug 2022 21:40:25 +0100
Subject: [PATCH 027/117] New test suite for multicam devices.

---
 .../opencamera/test/MultiCameraTests.java      | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/test/MultiCameraTests.java

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MultiCameraTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MultiCameraTests.java
new file mode 100644
index 000000000..830e58348
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MultiCameraTests.java
@@ -0,0 +1,18 @@
+package net.sourceforge.opencamera.test;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+public class MultiCameraTests {
+    // Tests to run specifically on devices where MainActivity.isMultiCamEnabled() returns true.
+    public static Test suite() {
+        TestSuite suite = new TestSuite(MainTests.class.getName());
+
+        suite.addTest(TestSuite.createTest(MainActivityTest.class, "testIconsAgainstCameras"));
+        suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFrontCameraAll"));
+        suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFrontCamera"));
+        suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakePhotoFrontCameraMulti"));
+
+        return suite;
+    }
+}
-- 
GitLab


From 24414a4e08ee8d9ed8e7b392c8e32b9ac810de5a Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 4 Sep 2022 14:44:00 +0100
Subject: [PATCH 028/117] Make some things static.

---
 .../opencamera/test/MainActivityTest.java            | 12 ++++++------
 1 file changed, 6 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 b062b996e..59e94d261 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -12526,7 +12526,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 14:47:12 +0100
Subject: [PATCH 029/117] Refactor code to new setDefaultIntent().

---
 .../net/sourceforge/opencamera/test/MainActivityTest.java | 8 ++++++--
 1 file changed, 6 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 59e94d261..2e552bc94 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -96,9 +96,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 14:51:45 +0100
Subject: [PATCH 030/117] Refactor code to new initTest().

---
 .../opencamera/test/MainActivityTest.java     | 28 +++++++++++--------
 1 file changed, 17 insertions(+), 11 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 2e552bc94..028fcb1a2 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -41,6 +41,7 @@ import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.content.ContentUris;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 //import android.content.res.AssetManager;
@@ -106,21 +107,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 14:52:55 +0100
Subject: [PATCH 031/117] Spacing.

---
 .../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 028fcb1a2..6cc3248a3 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -432,6 +432,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 14:54:33 +0100
Subject: [PATCH 032/117] Move logging outside of if condition.

---
 .../java/net/sourceforge/opencamera/preview/Preview.java     | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

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 b86d902a4..21f16b9a2 100644
--- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
@@ -438,8 +438,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
         if( activity.getIntent() != null && activity.getIntent().getExtras() != null ) {
             // whether called from testing
             is_test = activity.getIntent().getExtras().getBoolean("test_project");
-            if( MyDebug.LOG )
-                Log.d(TAG, "is_test: " + is_test);
+        }
+        if( MyDebug.LOG ) {
+            Log.d(TAG, "is_test: " + is_test);
         }
 
         this.using_android_l = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && applicationInterface.useCamera2();
-- 
GitLab


From 92292a5596960135b93262f1fd2af36600098683 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 4 Sep 2022 15:14:58 +0100
Subject: [PATCH 033/117] Minor fixes to logging and variable names for
 consistency.

---
 .../net/sourceforge/opencamera/test/MainActivityTest.java | 8 ++++----
 1 file changed, 4 insertions(+), 4 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 6cc3248a3..33f890441 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -305,10 +305,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 20:40:01 +0100
Subject: [PATCH 034/117] Move code to new openPopupMenu(), closePopupMenu().

---
 .../opencamera/test/MainActivityTest.java     | 162 ++++++------------
 1 file changed, 48 insertions(+), 114 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 33f890441..7633baf71 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -304,19 +304,35 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) {
@@ -1961,11 +1958,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) {
@@ -2120,10 +2109,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 21:16:55 +0100
Subject: [PATCH 035/117] Refactor code to new TestUtils class.

---
 .../net/sourceforge/opencamera/TestUtils.java | 39 +++++++++++++++++++
 .../opencamera/test/MainActivityTest.java     | 29 ++------------
 2 files changed, 42 insertions(+), 26 deletions(-)
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
new file mode 100644
index 000000000..4e515ab57
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -0,0 +1,39 @@
+package net.sourceforge.opencamera;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/** Helper class for testing. This method should not include any code specific to any test framework
+ *  (e.g., shouldn't be specific to ActivityInstrumentationTestCase2).
+ */
+public class TestUtils {
+    private static final String TAG = "TestUtils";
+
+    public static void setDefaultIntent(Intent intent) {
+        intent.putExtra("test_project", true);
+    }
+
+    public static void initTest(Context context, boolean test_camera2) {
+        Log.d(TAG, "initTest: " + test_camera2);
+        // initialise test statics (to avoid the persisting between tests in a test suite run!)
+        MainActivity.test_preview_want_no_limits = false;
+        MainActivity.test_preview_want_no_limits_value = false;
+        ImageSaver.test_small_queue_size = false;
+
+        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+        SharedPreferences.Editor editor = settings.edit();
+        editor.clear();
+        if( test_camera2 ) {
+            MainActivity.test_force_supports_camera2 = true;
+            //editor.putBoolean(PreferenceKeys.UseCamera2PreferenceKey, true);
+            editor.putString(PreferenceKeys.CameraAPIPreferenceKey, "preference_camera_api_camera2");
+        }
+        editor.apply();
+
+        Log.d(TAG, "initTest: done");
+    }
+
+}
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 7633baf71..f6c068653 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -20,6 +20,7 @@ import java.util.Set;
 import net.sourceforge.opencamera.LocationSupplier;
 import net.sourceforge.opencamera.MyPreferenceFragment;
 import net.sourceforge.opencamera.PanoramaProcessorException;
+import net.sourceforge.opencamera.TestUtils;
 import net.sourceforge.opencamera.cameracontroller.CameraController2;
 import net.sourceforge.opencamera.HDRProcessor;
 import net.sourceforge.opencamera.HDRProcessorException;
@@ -97,36 +98,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 4 Sep 2022 21:28:06 +0100
Subject: [PATCH 036/117] Refactor code to TestUtils.

---
 .../net/sourceforge/opencamera/TestUtils.java | 164 ++++++++++++++++++
 .../opencamera/test/MainActivityTest.java     | 153 +---------------
 2 files changed, 170 insertions(+), 147 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 4e515ab57..0b01a5754 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -3,15 +3,31 @@ package net.sourceforge.opencamera;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
 import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
 import android.util.Log;
 
+import androidx.exifinterface.media.ExifInterface;
+
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
 /** Helper class for testing. This method should not include any code specific to any test framework
  *  (e.g., shouldn't be specific to ActivityInstrumentationTestCase2).
  */
 public class TestUtils {
     private static final String TAG = "TestUtils";
 
+    final public static String images_base_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
+
     public static void setDefaultIntent(Intent intent) {
         intent.putExtra("test_project", true);
     }
@@ -36,4 +52,152 @@ public class TestUtils {
         Log.d(TAG, "initTest: done");
     }
 
+    /** Converts a path to a Uri for com.android.providers.media.documents.
+     */
+    private static Uri getDocumentUri(String filename) throws FileNotFoundException {
+        Log.d(TAG, "getDocumentUri: " + filename);
+
+        // convert from File path format to Storage Access Framework form
+        Uri treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADCIM%2FtestOpenCamera");
+        Log.d(TAG, "treeUri: " + treeUri);
+        if( !filename.startsWith(images_base_path) ) {
+            Log.e(TAG, "unknown base for: " + filename);
+            throw new FileNotFoundException();
+        }
+        String stem = filename.substring(images_base_path.length());
+        Uri stemUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADCIM" + stem.replace("/", "%2F"));
+        Log.d(TAG, "stem: " + stem);
+        Log.d(TAG, "stemUri: " + stemUri);
+        //String docID = "primary:DCIM" + stem;
+        String docID = DocumentsContract.getTreeDocumentId(stemUri);
+        Log.d(TAG, "docID: " + docID);
+        Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docID);
+
+        if( uri == null ) {
+            throw new FileNotFoundException();
+        }
+        return uri;
+    }
+
+    public static Bitmap getBitmapFromFile(MainActivity activity, String filename) throws FileNotFoundException {
+        return getBitmapFromFile(activity, filename, 1);
+    }
+
+    /** Loads bitmap from supplied filename.
+     *  Note that on Android 10+ (with scoped storage), this uses Storage Access Framework, which
+     *  means Open Camera must have SAF permission to the folder DCIM/testOpenCamera.
+     */
+    public static Bitmap getBitmapFromFile(MainActivity activity, String filename, int inSampleSize) throws FileNotFoundException {
+        Log.d(TAG, "getBitmapFromFile: " + filename);
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inMutable = true;
+        //options.inSampleSize = inSampleSize;
+        if( inSampleSize > 1 ) {
+            // use inDensity for better quality, as inSampleSize uses nearest neighbour
+            // see same code in ImageSaver.setBitmapOptionsSampleSize()
+            options.inDensity = inSampleSize;
+            options.inTargetDensity = 1;
+        }
+
+        Uri uri = null;
+        Bitmap bitmap;
+
+        if( MainActivity.useScopedStorage() ) {
+            uri = getDocumentUri(filename);
+            Log.d(TAG, "uri: " + uri);
+            InputStream is = activity.getContentResolver().openInputStream(uri);
+            bitmap = BitmapFactory.decodeStream(is, null, options);
+            try {
+                is.close();
+            }
+            catch(IOException e) {
+                e.printStackTrace();
+            }
+        }
+        else {
+            bitmap = BitmapFactory.decodeFile(filename, options);
+        }
+        if( bitmap == null )
+            throw new FileNotFoundException();
+        Log.d(TAG, "    done: " + bitmap);
+
+        // 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
+        ParcelFileDescriptor parcelFileDescriptor = null;
+        FileDescriptor fileDescriptor;
+        try {
+            ExifInterface exif = null;
+            if( uri != null ) {
+                parcelFileDescriptor = activity.getContentResolver().openFileDescriptor(uri, "r");
+                if( parcelFileDescriptor != null ) {
+                    fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+                    exif = new ExifInterface(fileDescriptor);
+                }
+            }
+            else {
+                exif = new ExifInterface(filename);
+            }
+            if( exif != null ) {
+                int exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+                boolean needs_tf = false;
+                int exif_orientation = 0;
+                // from http://jpegclub.org/exif_orientation.html
+                // and http://stackoverflow.com/questions/20478765/how-to-get-the-correct-orientation-of-the-image-selected-from-the-default-image
+                if( exif_orientation_s == ExifInterface.ORIENTATION_UNDEFINED || exif_orientation_s == ExifInterface.ORIENTATION_NORMAL ) {
+                    // leave unchanged
+                }
+                else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_180 ) {
+                    needs_tf = true;
+                    exif_orientation = 180;
+                }
+                else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_90 ) {
+                    needs_tf = true;
+                    exif_orientation = 90;
+                }
+                else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_270 ) {
+                    needs_tf = true;
+                    exif_orientation = 270;
+                }
+                else {
+                    // just leave unchanged for now
+                    Log.e(TAG, "    unsupported exif orientation: " + exif_orientation_s);
+                }
+                Log.d(TAG, "    exif orientation: " + exif_orientation);
+
+                if( needs_tf ) {
+                    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;
+                    }
+                }
+            }
+        }
+        catch(IOException e) {
+            e.printStackTrace();
+        }
+        finally {
+            if( parcelFileDescriptor != null ) {
+                try {
+                    parcelFileDescriptor.close();
+                }
+                catch(IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        /*{
+            for(int y=0;y 1 ) {
-            // use inDensity for better quality, as inSampleSize uses nearest neighbour
-            // see same code in ImageSaver.setBitmapOptionsSampleSize()
-            options.inDensity = inSampleSize;
-            options.inTargetDensity = 1;
-        }
-
-        Uri uri = null;
-        Bitmap bitmap;
-
-        if( MainActivity.useScopedStorage() ) {
-            uri = getDocumentUri(filename);
-            Log.d(TAG, "uri: " + uri);
-            InputStream is = mActivity.getContentResolver().openInputStream(uri);
-            bitmap = BitmapFactory.decodeStream(is, null, options);
-            try {
-                is.close();
-            }
-            catch(IOException e) {
-                e.printStackTrace();
-            }
-        }
-        else {
-            bitmap = BitmapFactory.decodeFile(filename, options);
-        }
-        if( bitmap == null )
-            throw new FileNotFoundException();
-        Log.d(TAG, "    done: " + bitmap);
-
-        // 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
-        ParcelFileDescriptor parcelFileDescriptor = null;
-        FileDescriptor fileDescriptor;
-        try {
-            ExifInterface exif = null;
-            if( uri != null ) {
-                parcelFileDescriptor = mActivity.getContentResolver().openFileDescriptor(uri, "r");
-                if( parcelFileDescriptor != null ) {
-                    fileDescriptor = parcelFileDescriptor.getFileDescriptor();
-                    exif = new ExifInterface(fileDescriptor);
-                }
-            }
-            else {
-                exif = new ExifInterface(filename);
-            }
-            if( exif != null ) {
-                int exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
-                boolean needs_tf = false;
-                int exif_orientation = 0;
-                // from http://jpegclub.org/exif_orientation.html
-                // and http://stackoverflow.com/questions/20478765/how-to-get-the-correct-orientation-of-the-image-selected-from-the-default-image
-                if( exif_orientation_s == ExifInterface.ORIENTATION_UNDEFINED || exif_orientation_s == ExifInterface.ORIENTATION_NORMAL ) {
-                    // leave unchanged
-                }
-                else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_180 ) {
-                    needs_tf = true;
-                    exif_orientation = 180;
-                }
-                else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_90 ) {
-                    needs_tf = true;
-                    exif_orientation = 90;
-                }
-                else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_270 ) {
-                    needs_tf = true;
-                    exif_orientation = 270;
-                }
-                else {
-                    // just leave unchanged for now
-                    Log.e(TAG, "    unsupported exif orientation: " + exif_orientation_s);
-                }
-                Log.d(TAG, "    exif orientation: " + exif_orientation);
-
-                if( needs_tf ) {
-                    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;
-                    }
-                }
-            }
-        }
-        catch(IOException e) {
-            e.printStackTrace();
-        }
-        finally {
-            if( parcelFileDescriptor != null ) {
-                try {
-                    parcelFileDescriptor.close();
-                }
-                catch(IOException e) {
-                    e.printStackTrace();
-                }
-            }
-        }
-        /*{
-            for(int y=0;y
Date: Sun, 4 Sep 2022 22:02:48 +0100
Subject: [PATCH 037/117] Refactor _images_path fields to TestUtils.

---
 .../net/sourceforge/opencamera/TestUtils.java |    6 +-
 .../opencamera/test/MainActivityTest.java     | 2067 ++++++++---------
 2 files changed, 1036 insertions(+), 1037 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 0b01a5754..4ea183530 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -26,7 +26,11 @@ import java.io.InputStream;
 public class TestUtils {
     private static final String TAG = "TestUtils";
 
-    final public static String images_base_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
+    final private static String images_base_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
+    final public static String hdr_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/hdrsamples/";
+    final public static String avg_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/avgsamples/";
+    final public static String logprofile_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/logprofilesamples/";
+    final public static String panorama_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/panoramasamples/";
 
     public static void setDefaultIntent(Intent intent) {
         intent.putExtra("test_project", true);
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 ebba1cd4e..4a361f2fa 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -12806,11 +12806,6 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input3.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input4.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input4.jpg") );
 
         // actual ISO unknown, so guessing
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR1_output.jpg", false, 1600, 1000000000L);
@@ -12847,11 +12842,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input3.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input4.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "saintpaul/input5.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input4.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "saintpaul/input5.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR1_exp5_output.jpg", false, -1, -1);
 
@@ -12872,9 +12867,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "stlouis/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "stlouis/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "stlouis/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "stlouis/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "stlouis/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "stlouis/input3.jpg") );
 
         // actual ISO unknown, so guessing
         subTestHDR(inputs, "testHDR2_output.jpg", false, 1600, (long)(1000000000L*2.5));
@@ -12893,9 +12888,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR3/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR3/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR3/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR3/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR3/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR3/input2.jpg") );
         
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR3_output.jpg", false, 40, 1000000000L/680);
 
@@ -12917,9 +12912,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR4/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR4/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR4/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR4/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR4/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR4/input2.jpg") );
         
         subTestHDR(inputs, "testHDR4_output.jpg", true, 102, 1000000000L/60);
 
@@ -12937,9 +12932,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR5/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR5/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR5/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR5/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR5/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR5/input2.jpg") );
         
         subTestHDR(inputs, "testHDR5_output.jpg", false, 40, 1000000000L/398);
 
@@ -12961,9 +12956,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR6/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR6/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR6/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR6/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR6/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR6/input2.jpg") );
         
         subTestHDR(inputs, "testHDR6_output.jpg", false, 40, 1000000000L/2458);
 
@@ -12981,9 +12976,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR7/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR7/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR7/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR7/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR7/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR7/input2.jpg") );
         
         subTestHDR(inputs, "testHDR7_output.jpg", false, 40, 1000000000L/538);
 
@@ -13001,9 +12996,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR8/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR8/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR8/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR8/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR8/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR8/input2.jpg") );
         
         subTestHDR(inputs, "testHDR8_output.jpg", false, 40, 1000000000L/148);
 
@@ -13021,9 +13016,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR9/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR9/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR9/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR9/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR9/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR9/input2.jpg") );
         
         subTestHDR(inputs, "testHDR9_output.jpg", false, 40, 1000000000L/1313);
 
@@ -13041,9 +13036,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR10/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR10/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR10/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR10/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR10/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR10/input2.jpg") );
         
         subTestHDR(inputs, "testHDR10_output.jpg", false, 107, 1000000000L/120);
 
@@ -13061,9 +13056,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR11/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR11/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR11/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR11/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR11/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR11/input2.jpg") );
         
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR11_output.jpg", true, 40, 1000000000L/2662);
 
@@ -13085,9 +13080,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR12/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR12/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR12/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR12/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR12/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR12/input2.jpg") );
         
         subTestHDR(inputs, "testHDR12_output.jpg", true, 1196, 1000000000L/12);
 
@@ -13105,9 +13100,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR13/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR13/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR13/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR13/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR13/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR13/input2.jpg") );
         
         subTestHDR(inputs, "testHDR13_output.jpg", false, 323, 1000000000L/24);
 
@@ -13125,9 +13120,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR14/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR14/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR14/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR14/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR14/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR14/input2.jpg") );
         
         subTestHDR(inputs, "testHDR14_output.jpg", false, 40, 1000000000L/1229);
 
@@ -13145,9 +13140,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR15/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR15/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR15/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR15/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR15/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR15/input2.jpg") );
         
         subTestHDR(inputs, "testHDR15_output.jpg", false, 40, 1000000000L/767);
 
@@ -13165,9 +13160,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR16/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR16/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR16/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR16/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR16/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR16/input2.jpg") );
         
         subTestHDR(inputs, "testHDR16_output.jpg", false, 52, 1000000000L/120);
 
@@ -13185,9 +13180,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR17/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR17/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR17/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR17/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR17/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR17/input2.jpg") );
         
         subTestHDR(inputs, "testHDR17_output.jpg", true, 557, 1000000000L/12);
 
@@ -13209,9 +13204,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR18/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR18/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR18/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR18/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR18/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR18/input2.jpg") );
         
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR18_output.jpg", true, 100, 1000000000L/800);
 
@@ -13234,9 +13229,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR19/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR19/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR19/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR19/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR19/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR19/input2.jpg") );
         
         subTestHDR(inputs, "testHDR19_output.jpg", true, 100, 1000000000L/160);
 
@@ -13254,9 +13249,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR20/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR20/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR20/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR20/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR20/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR20/input2.jpg") );
         
         subTestHDR(inputs, "testHDR20_output.jpg", true, 100, 1000000000L*2);
 
@@ -13274,9 +13269,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR21/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR21/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR21/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR21/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR21/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR21/input2.jpg") );
 
         // ISO and exposure unknown, so guessing
         subTestHDR(inputs, "testHDR21_output.jpg", true, 800, 1000000000L/12);
@@ -13295,9 +13290,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR22/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR22/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR22/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR22/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR22/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR22/input2.jpg") );
         
         subTestHDR(inputs, "testHDR22_output.jpg", true, 391, 1000000000L/12);
 
@@ -13319,8 +13314,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_exp2_output.jpg", false, -1, -1);
 
@@ -13341,8 +13336,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0070.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0062.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_exp2b_output.jpg", false, -1, -1);
 
@@ -13360,9 +13355,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0066.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0066.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
 
         // ISO unknown, so guessing
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_output.jpg", false, 1600, 1000000000L);
@@ -13385,10 +13380,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0070.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0062.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_exp4_output.jpg", false, -1, -1);
 
@@ -13409,11 +13404,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0070.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0066.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0062.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0066.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_exp5_output.jpg", false, -1, -1);
 
@@ -13435,12 +13430,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0072.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0070.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0062.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0061.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0072.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0061.png") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_exp6_output.jpg", false, -1, -1);
 
@@ -13461,13 +13456,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0072.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0070.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0068.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0066.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0064.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0062.png") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR23/memorial0061.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0072.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0066.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR23/memorial0061.png") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR23_exp7_output.jpg", false, -1, -1);
 
@@ -13489,9 +13484,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR24/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR24/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR24/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR24/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR24/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR24/input2.jpg") );
         
         subTestHDR(inputs, "testHDR24_output.jpg", true, 40, 1000000000L/422);
 
@@ -13509,9 +13504,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR25/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR25/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR25/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR25/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR25/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR25/input2.jpg") );
         
         subTestHDR(inputs, "testHDR25_output.jpg", true, 40, 1000000000L/1917);
 
@@ -13529,9 +13524,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR26/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR26/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR26/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR26/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR26/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR26/input2.jpg") );
         
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR26_output.jpg", true, 40, 1000000000L/5325);
 
@@ -13552,9 +13547,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR27/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR27/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR27/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR27/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR27/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR27/input2.jpg") );
         
         subTestHDR(inputs, "testHDR27_output.jpg", true, 40, 1000000000L/949);
 
@@ -13572,9 +13567,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR28/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR28/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR28/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR28/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR28/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR28/input2.jpg") );
         
         subTestHDR(inputs, "testHDR28_output.jpg", true, 294, 1000000000L/20);
 
@@ -13592,9 +13587,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR29/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR29/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR29/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR29/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR29/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR29/input2.jpg") );
         
         subTestHDR(inputs, "testHDR29_output.jpg", false, 40, 1000000000L/978);
 
@@ -13612,9 +13607,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR30/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR30/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR30/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR30/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR30/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR30/input2.jpg") );
 
         subTestHDR(inputs, "testHDR30_output.jpg", false, 40, 1000000000L/978);
 
@@ -13636,9 +13631,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR31/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR31/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR31/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR31/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR31/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR31/input2.jpg") );
 
         subTestHDR(inputs, "testHDR31_output.jpg", false, 40, 1000000000L/422);
 
@@ -13660,9 +13655,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR32/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR32/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR32/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR32/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR32/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR32/input2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR32_output.jpg", true, 40, 1000000000L/1331);
 
@@ -13685,9 +13680,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR33/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR33/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR33/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR33/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR33/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR33/input2.jpg") );
 
         subTestHDR(inputs, "testHDR33_output.jpg", true, 40, 1000000000L/354);
 
@@ -13705,9 +13700,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR34/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR34/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR34/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR34/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR34/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR34/input2.jpg") );
 
         subTestHDR(inputs, "testHDR34_output.jpg", true, 40, 1000000000L/4792);
 
@@ -13725,9 +13720,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR35/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR35/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR35/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR35/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR35/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR35/input2.jpg") );
 
         subTestHDR(inputs, "testHDR35_output.jpg", true, 40, 1000000000L/792);
 
@@ -13745,9 +13740,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR36/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR36/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR36/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR36/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR36/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR36/input2.jpg") );
 
         subTestHDR(inputs, "testHDR36_output.jpg", false, 100, 1000000000L/1148);
 
@@ -13765,9 +13760,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR37/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR37/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR37/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR37/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR37/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR37/input2.jpg") );
 
         subTestHDR(inputs, "testHDR37_output.jpg", false, 46, 1000000000L/120);
 
@@ -13786,9 +13781,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR38/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR38/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR38/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR38/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR38/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR38/input2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR38_filmic_output.jpg", false, 125, 1000000000L/2965, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FILMIC);
 
@@ -13809,9 +13804,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR39/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR39/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR39/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR39/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR39/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR39/input2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR39_output.jpg", false, 125, 1000000000L/2135);
 
@@ -13831,9 +13826,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR40_output.jpg", false, 50, 1000000000L/262);
 
@@ -13853,9 +13848,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR40_exponential_output.jpg", false, 50, 1000000000L/262, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_EXPONENTIAL);
 
@@ -13875,9 +13870,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR40/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR40/input2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR40_filmic_output.jpg", false, 50, 1000000000L/262, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FILMIC);
 
@@ -13897,9 +13892,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR41/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR41/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR41/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR41/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR41/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR41/input2.jpg") );
 
         subTestHDR(inputs, "testHDR41_output.jpg", false, 925, 1000000000L/25);
     }
@@ -13913,9 +13908,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR42/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR42/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR42/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR42/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR42/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR42/input2.jpg") );
 
         subTestHDR(inputs, "testHDR42_output.jpg", false, 112, 1000000000L/679);
     }
@@ -13929,9 +13924,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR43/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR43/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR43/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR43/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR43/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR43/input2.jpg") );
 
         subTestHDR(inputs, "testHDR43_output.jpg", false, 1196, 1000000000L/12);
     }
@@ -13945,9 +13940,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR44/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR44/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR44/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR44/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR44/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR44/input2.jpg") );
 
         subTestHDR(inputs, "testHDR44_output.jpg", false, 100, 1000000000L/1016);
     }
@@ -13961,13 +13956,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6314.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6312.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6310.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6309.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6311.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6313.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6315.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6314.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6312.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6310.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6309.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6311.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6313.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6315.jpg") );
 
         // ISO 100, exposure time 2s, but pass in -1 since these are HDRNTests
         subTestHDR(inputs, "testHDR45_output.jpg", false, -1, -1);
@@ -13982,13 +13977,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6314.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6312.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6310.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6309.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6311.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6313.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6315.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6314.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6312.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6310.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6309.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6311.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6313.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6315.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR45_exp5_output.jpg", false, -1, -1);
     }
@@ -14002,13 +13997,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6314.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6312.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6310.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6309.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6311.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6313.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR45/IMG_6315.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6314.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6312.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6310.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6309.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6311.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6313.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR45/IMG_6315.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR45_exp7_output.jpg", false, -1, -1);
     }
@@ -14022,12 +14017,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 06.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 05.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 04.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 03.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 02.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 01.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 06.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 05.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 04.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 03.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 02.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 01.jpg") );
 
         // ISO 100, exposure time 1/60s, but pass in -1 since these are HDRNTests
         subTestHDR(inputs, "testHDR46_output.jpg", false, -1, -1);
@@ -14042,12 +14037,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 06.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 05.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 04.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 03.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 02.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR46/Izmir Harbor - ppw - 01.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 06.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 05.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 04.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 03.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 02.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 01.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR46_exp5_output.jpg", false, -1, -1);
     }
@@ -14062,8 +14057,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
 
         subTestHDR(inputs, "testHDR47_exp2_output.jpg", false, -1, -1);
     }
@@ -14078,14 +14073,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
 
         // ISO 400, exposure time 1/60s, but pass in -1 since these are HDRNTests
         subTestHDR(inputs, "testHDR47_output.jpg", false, -1, -1);
@@ -14100,14 +14095,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR47_exp5_output.jpg", false, -1, -1);
 
@@ -14123,14 +14118,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR47_exp7_output.jpg", false, -1, -1);
 
@@ -14147,11 +14142,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input3.jpg") );
-        //inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input4.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input3.jpg") );
+        //inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input4.jpg") );
 
         // ISO 100, exposure time 1/716s, but pass in -1 since these are HDRNTests
         subTestHDR(inputs, "testHDR48_output.jpg", false, -1, -1);
@@ -14167,11 +14162,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input3.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR48/input4.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR48/input4.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR48_exp5_output.jpg", false, -1, -1);
 
@@ -14187,8 +14182,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR49_exp2_output.jpg", false, -1, -1);
 
@@ -14204,9 +14199,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
 
         // ISO 100, exposure time 1/417s, but pass in -1 since these are HDRNTests
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR49_output.jpg", false, -1, -1);
@@ -14224,10 +14219,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input3.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input4.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input4.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR49_exp4_output.jpg", false, -1, -1);
 
@@ -14244,11 +14239,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input2.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input3.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR49/input4.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR49/input4.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR49_exp5_output.jpg", false, -1, -1);
 
@@ -14265,9 +14260,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR50/IMG_20180626_221357_0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR50/IMG_20180626_221357_1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR50/IMG_20180626_221357_2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR50/IMG_20180626_221357_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR50/IMG_20180626_221357_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR50/IMG_20180626_221357_2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR50_output.jpg", false, 867, 1000000000L/14);
 
@@ -14283,9 +14278,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR51/IMG_20180323_104702_0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR51/IMG_20180323_104702_1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR51/IMG_20180323_104702_2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR51/IMG_20180323_104702_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR51/IMG_20180323_104702_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR51/IMG_20180323_104702_2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR51_output.jpg", true, 1600, 1000000000L/11);
 
@@ -14301,9 +14296,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR52/IMG_20181023_143633_EXP0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR52/IMG_20181023_143633_EXP1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR52/IMG_20181023_143633_EXP2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR52/IMG_20181023_143633_EXP0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR52/IMG_20181023_143633_EXP1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR52/IMG_20181023_143633_EXP2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR52_output.jpg", false, 100, 1000000000L/2105);
 
@@ -14319,9 +14314,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR53/IMG_20181106_135411_EXP0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR53/IMG_20181106_135411_EXP1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR53/IMG_20181106_135411_EXP2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR53/IMG_20181106_135411_EXP0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR53/IMG_20181106_135411_EXP1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR53/IMG_20181106_135411_EXP2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR53_output.jpg", false, 103, 1000000000L/5381);
 
@@ -14338,9 +14333,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR54/IMG_20181107_115508_EXP0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR54/IMG_20181107_115508_EXP1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR54/IMG_20181107_115508_EXP2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR54/IMG_20181107_115508_EXP0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR54/IMG_20181107_115508_EXP1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR54/IMG_20181107_115508_EXP2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR54_output.jpg", false, 752, 1000000000L/14);
 
@@ -14356,9 +14351,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR55/IMG_20181107_115608_EXP0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR55/IMG_20181107_115608_EXP1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR55/IMG_20181107_115608_EXP2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR55/IMG_20181107_115608_EXP0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR55/IMG_20181107_115608_EXP1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR55/IMG_20181107_115608_EXP2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR55_output.jpg", false, 1505, 1000000000L/10);
 
@@ -14374,9 +14369,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR56/180502_141722_OC_0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR56/180502_141722_OC_1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR56/180502_141722_OC_2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR56/180502_141722_OC_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR56/180502_141722_OC_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR56/180502_141722_OC_2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR56_output.jpg", false, 50, 1000000000L/40);
 
@@ -14392,9 +14387,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR57/IMG_20181119_145313_EXP0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR57/IMG_20181119_145313_EXP1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDR57/IMG_20181119_145313_EXP2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR57/IMG_20181119_145313_EXP0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR57/IMG_20181119_145313_EXP1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR57/IMG_20181119_145313_EXP2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR57_output.jpg", true, 100, 1000000000L/204);
 
@@ -14410,9 +14405,9 @@ 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") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR58/IMG_20190911_210146_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR58/IMG_20190911_210146_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.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);
@@ -14429,9 +14424,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 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") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR59/IMG_20190911_210154_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR59/IMG_20190911_210154_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.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);
@@ -14448,9 +14443,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 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") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR60/IMG_20200507_020319_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR60/IMG_20200507_020319_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.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);
@@ -14467,9 +14462,9 @@ 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") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR61/IMG_20191111_145230_0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR61/IMG_20191111_145230_1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDR61/IMG_20191111_145230_2.jpg") );
 
         HistogramDetails hdrHistogramDetails = subTestHDR(inputs, "testHDR61_output.jpg", false, 50, 1000000000L/5025);
 
@@ -14491,9 +14486,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDRtemp/input0.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDRtemp/input1.jpg") );
-        inputs.add( getBitmapFromFile(hdr_images_path + "testHDRtemp/input2.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDRtemp/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDRtemp/input1.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.hdr_images_path + "testHDRtemp/input2.jpg") );
         
         subTestHDR(inputs, "testHDRtemp_output.jpg", true, 100, 1000000000L/100);
     }
@@ -14508,7 +14503,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(avg_images_path + "testAvg3/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.avg_images_path + "testAvg3/input0.jpg") );
 
         subTestHDR(inputs, "testDRODark0_output.jpg", true, -1, -1);
     }
@@ -14523,7 +14518,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add( getBitmapFromFile(avg_images_path + "testAvg8/input0.jpg") );
+        inputs.add( getBitmapFromFile(TestUtils.avg_images_path + "testAvg8/input0.jpg") );
 
         subTestHDR(inputs, "testDRODark1_output.jpg", true, -1, -1);
     }
@@ -14541,7 +14536,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg1/input0.jpg");
-        inputs.add(avg_images_path + "testAvg1/input1.jpg");
-        inputs.add(avg_images_path + "testAvg1/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg1/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg1/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg1/input2.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -14763,9 +14758,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg2/input0.jpg");
-        inputs.add(avg_images_path + "testAvg2/input1.jpg");
-        inputs.add(avg_images_path + "testAvg2/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg2/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg2/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg2/input2.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -14814,11 +14809,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg3/input0.jpg");
-        inputs.add(avg_images_path + "testAvg3/input1.jpg");
-        inputs.add(avg_images_path + "testAvg3/input2.jpg");
-        inputs.add(avg_images_path + "testAvg3/input3.jpg");
-        inputs.add(avg_images_path + "testAvg3/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg3/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg3/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg3/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg3/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg3/input4.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -14905,11 +14900,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg4/input0.jpg");
-        inputs.add(avg_images_path + "testAvg4/input1.jpg");
-        inputs.add(avg_images_path + "testAvg4/input2.jpg");
-        inputs.add(avg_images_path + "testAvg4/input3.jpg");
-        inputs.add(avg_images_path + "testAvg4/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg4/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg4/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg4/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg4/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg4/input4.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -14970,11 +14965,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg5/input0.jpg");
-        inputs.add(avg_images_path + "testAvg5/input1.jpg");
-        inputs.add(avg_images_path + "testAvg5/input2.jpg");
-        inputs.add(avg_images_path + "testAvg5/input3.jpg");
-        inputs.add(avg_images_path + "testAvg5/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg5/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg5/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg5/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg5/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg5/input4.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -15037,14 +15032,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg6/input0.jpg");
-        inputs.add(avg_images_path + "testAvg6/input1.jpg");
-        inputs.add(avg_images_path + "testAvg6/input2.jpg");
-        inputs.add(avg_images_path + "testAvg6/input3.jpg");
-        inputs.add(avg_images_path + "testAvg6/input4.jpg");
-        inputs.add(avg_images_path + "testAvg6/input5.jpg");
-        inputs.add(avg_images_path + "testAvg6/input6.jpg");
-        inputs.add(avg_images_path + "testAvg6/input7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg6/input7.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -15121,14 +15116,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg7/input0.jpg");
-        inputs.add(avg_images_path + "testAvg7/input1.jpg");
-        inputs.add(avg_images_path + "testAvg7/input2.jpg");
-        inputs.add(avg_images_path + "testAvg7/input3.jpg");
-        inputs.add(avg_images_path + "testAvg7/input4.jpg");
-        inputs.add(avg_images_path + "testAvg7/input5.jpg");
-        inputs.add(avg_images_path + "testAvg7/input6.jpg");
-        inputs.add(avg_images_path + "testAvg7/input7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg7/input7.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -15169,14 +15164,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg8/input0.jpg");
-        inputs.add(avg_images_path + "testAvg8/input1.jpg");
-        inputs.add(avg_images_path + "testAvg8/input2.jpg");
-        inputs.add(avg_images_path + "testAvg8/input3.jpg");
-        inputs.add(avg_images_path + "testAvg8/input4.jpg");
-        inputs.add(avg_images_path + "testAvg8/input5.jpg");
-        inputs.add(avg_images_path + "testAvg8/input6.jpg");
-        inputs.add(avg_images_path + "testAvg8/input7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg8/input7.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -15211,24 +15206,24 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
         // note, we don't actually use 8 images for a bright scene like this, but it serves as a good test for
         // misalignment/ghosting anyway
-        inputs.add(avg_images_path + "testAvg11/input0.jpg");
-        inputs.add(avg_images_path + "testAvg11/input1.jpg");
-        inputs.add(avg_images_path + "testAvg11/input2.jpg");
-        inputs.add(avg_images_path + "testAvg11/input3.jpg");
-        inputs.add(avg_images_path + "testAvg11/input4.jpg");
-        inputs.add(avg_images_path + "testAvg11/input5.jpg");
-        inputs.add(avg_images_path + "testAvg11/input6.jpg");
-        inputs.add(avg_images_path + "testAvg11/input7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg11/input7.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg11_output.jpg", 100, 1000000000L/338, 1.0f, new TestAvgCallback() {
             @Override
@@ -15417,8 +15412,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg12/input0.jpg");
-        inputs.add(avg_images_path + "testAvg12/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg12/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg12/input1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg12_output.jpg", 100, 1000000000L/1617, 1.0f, new TestAvgCallback() {
             @Override
@@ -15446,8 +15441,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg13/input0.jpg");
-        inputs.add(avg_images_path + "testAvg13/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg13/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg13/input1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg13_output.jpg", 100, 1000000000L/2482, 1.0f, new TestAvgCallback() {
             @Override
@@ -15471,14 +15466,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg14/input0.jpg");
-        inputs.add(avg_images_path + "testAvg14/input1.jpg");
-        inputs.add(avg_images_path + "testAvg14/input2.jpg");
-        inputs.add(avg_images_path + "testAvg14/input3.jpg");
-        inputs.add(avg_images_path + "testAvg14/input4.jpg");
-        inputs.add(avg_images_path + "testAvg14/input5.jpg");
-        inputs.add(avg_images_path + "testAvg14/input6.jpg");
-        inputs.add(avg_images_path + "testAvg14/input7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg14/input7.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -15514,8 +15509,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg15/input0.jpg");
-        inputs.add(avg_images_path + "testAvg15/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg15/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg15/input1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg15_output.jpg", 100, 1000000000L/1525, 1.0f, new TestAvgCallback() {
             @Override
@@ -15539,8 +15534,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg16/input0.jpg");
-        inputs.add(avg_images_path + "testAvg16/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg16/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg16/input1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg16_output.jpg", 100, 1000000000L/293, 1.0f, new TestAvgCallback() {
             @Override
@@ -15564,14 +15559,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg17/input0.jpg");
-        inputs.add(avg_images_path + "testAvg17/input1.jpg");
-        inputs.add(avg_images_path + "testAvg17/input2.jpg");
-        inputs.add(avg_images_path + "testAvg17/input3.jpg");
-        inputs.add(avg_images_path + "testAvg17/input4.jpg");
-        inputs.add(avg_images_path + "testAvg17/input5.jpg");
-        inputs.add(avg_images_path + "testAvg17/input6.jpg");
-        inputs.add(avg_images_path + "testAvg17/input7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg17/input7.jpg");
 
         // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
         // of 800; in reality for a scene this dark, it was probably more like ISO 1600
@@ -15610,8 +15605,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg18/input0.jpg");
-        inputs.add(avg_images_path + "testAvg18/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg18/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg18/input1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg18_output.jpg", 100, 1000000000L/591, 1.0f, new TestAvgCallback() {
             @Override
@@ -15636,8 +15631,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
         // repeat same image twice
-        inputs.add(avg_images_path + "testAvg19/input0.jpg");
-        inputs.add(avg_images_path + "testAvg19/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg19/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg19/input0.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg19_output.jpg", 100, 1000000000L/2483, 1.0f, new TestAvgCallback() {
             @Override
@@ -15666,8 +15661,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
         // repeat same image twice
-        inputs.add(avg_images_path + "testAvg20/input0.jpg");
-        inputs.add(avg_images_path + "testAvg20/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg20/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg20/input0.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg20_output.jpg", 100, 1000000000L/3124, 1.0f, new TestAvgCallback() {
             @Override
@@ -15692,8 +15687,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
         // repeat same image twice
-        inputs.add(avg_images_path + "testAvg21/input0.jpg");
-        inputs.add(avg_images_path + "testAvg21/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg21/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg21/input0.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg21_output.jpg", 102, 1000000000L/6918, 1.0f, new TestAvgCallback() {
             @Override
@@ -15718,8 +15713,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
         // repeat same image twice
-        inputs.add(avg_images_path + "testAvg22/input0.jpg");
-        inputs.add(avg_images_path + "testAvg22/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg22/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg22/input0.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg22_output.jpg", 100, 1000000000L/3459, 1.0f, new TestAvgCallback() {
             @Override
@@ -15743,15 +15738,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_0.jpg");
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_1.jpg");
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_2.jpg");
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_3.jpg");
         // only test 4 images, to reflect latest behaviour that we take 4 images for this ISO
-        /*inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_4.jpg");
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_5.jpg");
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_6.jpg");
-        inputs.add(avg_images_path + "testAvg23/IMG_20180520_111250_7.jpg");*/
+        /*inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_7.jpg");*/
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg23_output.jpg", 1044, 1000000000L/10, 1.0f, new TestAvgCallback() {
             @Override
@@ -15812,8 +15807,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg24/input0.jpg");
-        inputs.add(avg_images_path + "testAvg24/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg24/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg24/input1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg24_output.jpg", 100, 1000000000L/2421, 1.0f, new TestAvgCallback() {
             @Override
@@ -15839,10 +15834,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg25/input0.jpg");
-        inputs.add(avg_images_path + "testAvg25/input1.jpg");
-        inputs.add(avg_images_path + "testAvg25/input2.jpg");
-        inputs.add(avg_images_path + "testAvg25/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg25/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg25/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg25/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg25/input3.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg25_output.jpg", 512, 1000000000L/20, 1.0f, new TestAvgCallback() {
             @Override
@@ -15865,10 +15860,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
         // note we now take only 3 images for bright scenes, but still test with 4 images as this serves as a good test
         // against ghosting
-        inputs.add(avg_images_path + "testAvg26/input0.jpg");
-        inputs.add(avg_images_path + "testAvg26/input1.jpg");
-        inputs.add(avg_images_path + "testAvg26/input2.jpg");
-        inputs.add(avg_images_path + "testAvg26/input3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg26/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg26/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg26/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg26/input3.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg26_output.jpg", 100, 1000000000L/365, 1.0f, new TestAvgCallback() {
             @Override
@@ -15909,8 +15904,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg27/IMG_20180610_205929_0.jpg");
-        inputs.add(avg_images_path + "testAvg27/IMG_20180610_205929_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg27/IMG_20180610_205929_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg27/IMG_20180610_205929_1.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg27_output.jpg", 100, 1000000000L/482, 1.0f, new TestAvgCallback() {
             @Override
@@ -15934,14 +15929,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg34/IMG_20180627_121959_0.jpg");
-        inputs.add(avg_images_path + "testAvg34/IMG_20180627_121959_1.jpg");
-        inputs.add(avg_images_path + "testAvg34/IMG_20180627_121959_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg34/IMG_20180627_121959_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg34/IMG_20180627_121959_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg34/IMG_20180627_121959_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg34_output.jpg", 100, 1000000000L/289, 1.0f, new TestAvgCallback() {
             @Override
@@ -16172,9 +16167,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg35/IMG_20180711_144453_0.jpg");
-        inputs.add(avg_images_path + "testAvg35/IMG_20180711_144453_1.jpg");
-        inputs.add(avg_images_path + "testAvg35/IMG_20180711_144453_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg35/IMG_20180711_144453_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg35/IMG_20180711_144453_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg35/IMG_20180711_144453_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg35_output.jpg", 100, 1000000000L/2549, 1.0f, new TestAvgCallback() {
             @Override
@@ -16196,15 +16191,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_0.jpg");
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_1.jpg");
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_2.jpg");
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_3.jpg");
         // only test 4 images, to reflect latest behaviour that we take 4 images for this ISO/exposure time
-        /*inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_4.jpg");
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_5.jpg");
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_6.jpg");
-        inputs.add(avg_images_path + "testAvg36/IMG_20180709_114831_7.jpg");*/
+        /*inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_7.jpg");*/
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg36_output.jpg", 752, 1000000000L/10, 1.0f, new TestAvgCallback() {
             @Override
@@ -16235,10 +16230,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg37/IMG_20180715_173155_0.jpg");
-        inputs.add(avg_images_path + "testAvg37/IMG_20180715_173155_1.jpg");
-        inputs.add(avg_images_path + "testAvg37/IMG_20180715_173155_2.jpg");
-        inputs.add(avg_images_path + "testAvg37/IMG_20180715_173155_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_3.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg37_output.jpg", 131, 1000000000L/50, 1.0f, new TestAvgCallback() {
             @Override
@@ -16264,14 +16259,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_0.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_1.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_2.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_3.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_4.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_5.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_6.jpg");
-        inputs.add(avg_images_path + "testAvg38/IMG_20180716_232102_7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_7.jpg");
 
         // n.b., this was a zoomed in photo, but can't quite remember the exact zoom level!
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg38_output.jpg", 1505, 1000000000L/10, 3.95f, new TestAvgCallback() {
@@ -16294,16 +16289,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg42/IMG_20180822_145152_0.jpg");
-        inputs.add(avg_images_path + "testAvg42/IMG_20180822_145152_1.jpg");
-        inputs.add(avg_images_path + "testAvg42/IMG_20180822_145152_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg42/IMG_20180822_145152_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg42/IMG_20180822_145152_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg42/IMG_20180822_145152_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg42_output.jpg", 100, 1000000000L/2061, 1.0f, new TestAvgCallback() {
             @Override
@@ -16419,9 +16414,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg43/IMG_20180831_143226_0.jpg");
-        inputs.add(avg_images_path + "testAvg43/IMG_20180831_143226_1.jpg");
-        inputs.add(avg_images_path + "testAvg43/IMG_20180831_143226_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg43/IMG_20180831_143226_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg43/IMG_20180831_143226_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg43/IMG_20180831_143226_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg43_output.jpg", 100, 1000000000L/2152, 1.0f, new TestAvgCallback() {
             @Override
@@ -16442,9 +16437,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg44/IMG_20180830_133917_0.jpg");
-        inputs.add(avg_images_path + "testAvg44/IMG_20180830_133917_1.jpg");
-        inputs.add(avg_images_path + "testAvg44/IMG_20180830_133917_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg44/IMG_20180830_133917_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg44/IMG_20180830_133917_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg44/IMG_20180830_133917_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg44_output.jpg", 40, 1000000000L/2130, 1.0f, new TestAvgCallback() {
             @Override
@@ -16465,9 +16460,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg45/IMG_20180719_133947_0.jpg");
-        inputs.add(avg_images_path + "testAvg45/IMG_20180719_133947_1.jpg");
-        inputs.add(avg_images_path + "testAvg45/IMG_20180719_133947_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg45/IMG_20180719_133947_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg45/IMG_20180719_133947_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg45/IMG_20180719_133947_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg45_output.jpg", 100, 1000000000L/865, 1.0f, new TestAvgCallback() {
             @Override
@@ -16488,14 +16483,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_0.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_1.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_2.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_3.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_4.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_5.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_6.jpg");
-        inputs.add(avg_images_path + "testAvg46/IMG_20180903_203141_7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_7.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg46_output.jpg", 1505, 1000000000L/10, 1.0f, new TestAvgCallback() {
             @Override
@@ -16516,10 +16511,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg47/IMG_20180911_114752_0.jpg");
-        inputs.add(avg_images_path + "testAvg47/IMG_20180911_114752_1.jpg");
-        inputs.add(avg_images_path + "testAvg47/IMG_20180911_114752_2.jpg");
-        inputs.add(avg_images_path + "testAvg47/IMG_20180911_114752_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_3.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg47_output.jpg", 749, 1000000000L/12, 1.0f, new TestAvgCallback() {
             @Override
@@ -16540,14 +16535,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_0.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_1.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_2.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_3.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_4.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_5.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_6.jpg");
-        inputs.add(avg_images_path + "testAvg48/IMG_20180911_110520_7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_7.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg48_output.jpg", 1196, 1000000000L/10, 1.0f, new TestAvgCallback() {
             @Override
@@ -16568,14 +16563,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_0.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_1.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_2.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_3.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_4.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_5.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_6.jpg");
-        inputs.add(avg_images_path + "testAvg49/IMG_20180911_120200_7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_7.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg49_output.jpg", 1505, 1000000000L/10, 1.0f, new TestAvgCallback() {
             @Override
@@ -16596,10 +16591,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg50/IMG_20181015_144335_0.jpg");
-        inputs.add(avg_images_path + "testAvg50/IMG_20181015_144335_1.jpg");
-        inputs.add(avg_images_path + "testAvg50/IMG_20181015_144335_2.jpg");
-        inputs.add(avg_images_path + "testAvg50/IMG_20181015_144335_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_3.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg50_output.jpg", 114, 1000000000L/33, 1.0f, new TestAvgCallback() {
             @Override
@@ -16620,14 +16615,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_0.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_1.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_2.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_3.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_4.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_5.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_6.jpg");
-        inputs.add(avg_images_path + "testAvg51/IMG_20181025_182917_7.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_3.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_7.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg51_output.jpg", 1600, 1000000000L/3, 1.0f, new TestAvgCallback() {
             @Override
@@ -16659,9 +16654,9 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvg52/IMG_20181119_144836_0.jpg");
-        inputs.add(avg_images_path + "testAvg52/IMG_20181119_144836_1.jpg");
-        inputs.add(avg_images_path + "testAvg52/IMG_20181119_144836_2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg52/IMG_20181119_144836_0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg52/IMG_20181119_144836_1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg52/IMG_20181119_144836_2.jpg");
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvg52_output.jpg", 100, 1000000000L/297, 1.0f, new TestAvgCallback() {
             @Override
@@ -16684,15 +16679,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
-        inputs.add(avg_images_path + "testAvgtemp/input0.png");
-        /*inputs.add(avg_images_path + "testAvgtemp/input0.jpg");
-        inputs.add(avg_images_path + "testAvgtemp/input1.jpg");
-        inputs.add(avg_images_path + "testAvgtemp/input2.jpg");
-        inputs.add(avg_images_path + "testAvgtemp/input3.jpg");*/
-        /*inputs.add(avg_images_path + "testAvgtemp/input4.jpg");
-        inputs.add(avg_images_path + "testAvgtemp/input5.jpg");
-        inputs.add(avg_images_path + "testAvgtemp/input6.jpg");
-        inputs.add(avg_images_path + "testAvgtemp/input7.jpg");*/
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input0.png");
+        /*inputs.add(TestUtils.avg_images_path + "testAvgtemp/input0.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input1.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input2.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input3.jpg");*/
+        /*inputs.add(TestUtils.avg_images_path + "testAvgtemp/input4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvgtemp/input7.jpg");*/
 
         HistogramDetails hdrHistogramDetails = subTestAvg(inputs, "testAvgtemp_output.jpg", 250, 1000000000L/33, 1.0f, new TestAvgCallback() {
             @Override
@@ -16801,7 +16796,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
-        inputs.add(panorama_images_path + "testPanoramaWhite/input0.jpg");
-        inputs.add(panorama_images_path + "testPanoramaWhite/input0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanoramaWhite/input0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanoramaWhite/input0.jpg");
         float camera_angle_x = 66.3177f;
         float camera_angle_y = 50.04736f;
         float panorama_pics_per_screen = 2.0f;
@@ -17204,10 +17199,10 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
-        inputs.add(panorama_images_path + "testPanorama1/input0.jpg");
-        inputs.add(panorama_images_path + "testPanorama1/input1.jpg");
-        inputs.add(panorama_images_path + "testPanorama1/input2.jpg");
-        inputs.add(panorama_images_path + "testPanorama1/input3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input3.jpg");
         float camera_angle_x = 62.93796f;
         float camera_angle_y = 47.44656f;
         float panorama_pics_per_screen = 2.0f;
@@ -17229,22 +17224,22 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         /*final float panorama_pics_per_screen = 1.0f;
-        //inputs.add(panorama_images_path + "testPanorama2xxx/input0.jpg");
-        inputs.add(panorama_images_path + "testPanorama2xxx/input1.jpg");
-        inputs.add(panorama_images_path + "testPanorama2xxx/input2.jpg");*/
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama2xxx/input0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2xxx/input1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2xxx/input2.jpg");*/
         /*final float panorama_pics_per_screen = 2.0f;
-        //inputs.add(panorama_images_path + "testPanorama1/input0.jpg");
-        inputs.add(panorama_images_path + "testPanorama1/input1.jpg");
-        inputs.add(panorama_images_path + "testPanorama1/input2.jpg");
-        inputs.add(panorama_images_path + "testPanorama1/input3.jpg");
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama1/input0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama1/input3.jpg");
         String output_name = "testPanorama1_output.jpg";*/
         float panorama_pics_per_screen = 4.0f;
-        inputs.add(panorama_images_path + "testPanorama2/input0.jpg");
-        inputs.add(panorama_images_path + "testPanorama2/input1.jpg");
-        inputs.add(panorama_images_path + "testPanorama2/input2.jpg");
-        inputs.add(panorama_images_path + "testPanorama2/input3.jpg");
-        inputs.add(panorama_images_path + "testPanorama2/input4.jpg");
-        inputs.add(panorama_images_path + "testPanorama2/input5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2/input0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2/input1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2/input2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2/input3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2/input4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama2/input5.jpg");
         String output_name = "testPanorama2_output.jpg";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17265,17 +17260,17 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 4.0f;
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131249.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131252.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131255.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131258.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131301.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131303.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131305.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131307.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131315.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131317.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131320.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131249.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131252.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131255.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131258.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131301.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131303.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131305.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131307.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131315.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131317.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131320.jpg");
         String output_name = "testPanorama3_output.jpg";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17297,17 +17292,17 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 2.0f;
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131249.jpg");
-        //inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131252.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131255.jpg");
-        //inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131258.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131301.jpg");
-        //inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131303.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131305.jpg");
-        //inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131307.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131315.jpg");
-        //inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131317.jpg");
-        inputs.add(panorama_images_path + "testPanorama3/IMG_20190214_131320.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131249.jpg");
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131252.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131255.jpg");
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131258.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131301.jpg");
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131303.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131305.jpg");
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131307.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131315.jpg");
+        //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131317.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131320.jpg");
         String output_name = "testPanorama3_picsperscreen2_output.jpg";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17328,16 +17323,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 4.0f;
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama4/IMG_20190222_225317_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_7.jpg");
         String output_name = "testPanorama4_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama4/IMG_20190222_225317.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17357,16 +17352,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 4.0f;
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama5/IMG_20190223_220524_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_7.jpg");
         String output_name = "testPanorama5_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama5/IMG_20190223_220524.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17386,16 +17381,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 4.0f;
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama6/IMG_20190225_154232_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_7.jpg");
         String output_name = "testPanorama6_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama6/IMG_20190225_154232.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17415,17 +17410,17 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 4.0f;
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama7/IMG_20190225_155510_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_8.jpg");
         String output_name = "testPanorama7_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama7/IMG_20190225_155510.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17445,12 +17440,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 2.0f;
-        inputs.add(panorama_images_path + "testPanorama8/IMG_20190227_001431_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama8/IMG_20190227_001431_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama8/IMG_20190227_001431_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama8/IMG_20190227_001431_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_3.jpg");
         String output_name = "testPanorama8_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama8/IMG_20190227_001431.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17470,15 +17465,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.0f;
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama9/IMG_20190301_145213_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_6.jpg");
         String output_name = "testPanorama9_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama9/IMG_20190301_145213.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17500,21 +17495,21 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.0f;
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_9.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_10.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_11.jpg");
-        inputs.add(panorama_images_path + "testPanorama10/IMG_20190301_144948_12.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_10.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_11.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_12.jpg");
         String output_name = "testPanorama10_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama10/IMG_20190301_144948.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17535,15 +17530,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.0f;
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama11/IMG_20190306_143652_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_6.jpg");
         String output_name = "testPanorama11_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama11/IMG_20190306_143652.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17563,18 +17558,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.0f;
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama12/IMG_20190308_152008_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_9.jpg");
         String output_name = "testPanorama12_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama12/IMG_20190308_152008.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
         // these images were taken with incorrect camera view angles, so we compensate in the test:
@@ -17594,18 +17589,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.0f;
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama13/IMG_20190512_014152_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_9.jpg");
         String output_name = "testPanorama13_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama13/IMG_20190512_014152.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152.xml";
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
 
@@ -17623,18 +17618,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama14/IMG_20190513_151249_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_9.jpg");
         String output_name = "testPanorama14_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama14/IMG_20190513_151249.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17653,18 +17648,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama15/IMG_20190513_151624_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_9.jpg");
         String output_name = "testPanorama15_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama15/IMG_20190513_151624.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17683,18 +17678,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama16/IMG_20190624_151731_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_9.jpg");
         String output_name = "testPanorama16_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama16/IMG_20190624_151731.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17713,18 +17708,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama17/IMG_20190625_135423_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_9.jpg");
         String output_name = "testPanorama17_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama17/IMG_20190625_135423.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17743,18 +17738,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama18/IMG_20190626_152559_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_9.jpg");
         String output_name = "testPanorama18_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama18/IMG_20190626_152559.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17773,18 +17768,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama19/IMG_20190627_134059_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_9.jpg");
         String output_name = "testPanorama19_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama19/IMG_20190627_134059.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17803,18 +17798,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama20/IMG_20190628_145027_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_9.jpg");
         String output_name = "testPanorama20_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama20/IMG_20190628_145027.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17833,18 +17828,18 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama21/IMG_20190628_145552_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_9.jpg");
         String output_name = "testPanorama21_output.jpg";
-        String gyro_name = panorama_images_path + "testPanorama21/IMG_20190628_145552.xml";
+        String gyro_name = TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552.xml";
         //gyro_name = null;
         float camera_angle_x = 66.708595f;
         float camera_angle_y = 50.282097f;
@@ -17863,14 +17858,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama22/IMG_20190629_165627_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_7.jpg");
         String output_name = "testPanorama22_output.jpg";
         String gyro_name = null;
         float camera_angle_x = 66.708595f;
@@ -17890,11 +17885,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama23/IMG_20190702_145916_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama23/IMG_20190702_145916_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama23/IMG_20190702_145916_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama23/IMG_20190702_145916_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama23/IMG_20190702_145916_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_4.jpg");
         String output_name = "testPanorama23_output.jpg";
         String gyro_name = null;
         float camera_angle_x = 66.708595f;
@@ -17914,16 +17909,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama24/IMG_20190703_154333_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_9.jpg");
         String output_name = "testPanorama24_output.jpg";
         String gyro_name = null;
         // taken with OnePlus 3T, Camera2 API:
@@ -17945,13 +17940,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama26/IMG_20190706_214842_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_6.jpg");
         String output_name = "testPanorama26_output.jpg";
         String gyro_name = null;
         // taken with Nokia 8, Camera2 API:
@@ -17999,13 +17994,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama27/IMG_20190706_192120_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_6.jpg");
         String output_name = "testPanorama27_output.jpg";
         String gyro_name = null;
         // taken with Nokia 8, Camera2 API:
@@ -18027,27 +18022,27 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        /*inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_9.jpg");*/
+        /*inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_9.jpg");*/
         // converted from original JPEGs to PNG using Nokia 8:
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_0.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_1.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_2.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_3.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_4.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_5.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_6.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_7.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_8.png");
-        inputs.add(panorama_images_path + "testPanorama30/nokia8_input_bitmap_9.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_0.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_1.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_2.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_3.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_4.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_5.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_6.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_7.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_8.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_9.png");
         String output_name = "testPanorama30_output.jpg";
         String gyro_name = null;
         // taken with Samsung Galaxy S10e, old API, standard rear camera:
@@ -18189,27 +18184,27 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        /*inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama30/IMG_20190723_142934_9.jpg");*/
+        /*inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_9.jpg");*/
         // converted from original JPEGs to PNG using Samsung Galaxy S10e:
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_0.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_1.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_2.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_3.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_4.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_5.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_6.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_7.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_8.png");
-        inputs.add(panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_9.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_0.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_1.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_2.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_3.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_4.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_5.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_6.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_7.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_8.png");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_9.png");
         String output_name = "testPanorama30_galaxys10e_output.jpg";
         String gyro_name = null;
         // taken with Samsung Galaxy S10e, old API, standard rear camera:
@@ -18231,13 +18226,13 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama31/IMG_20190704_135633_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_6.jpg");
         String output_name = "testPanorama31_output.jpg";
         String gyro_name = null;
         // taken with OnePlus 3T, Camera2 API:
@@ -18258,15 +18253,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama32/IMG_20190705_145938_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_8.jpg");
         String output_name = "testPanorama32_output.jpg";
         String gyro_name = null;
         // taken with OnePlus 3T, old API:
@@ -18287,12 +18282,12 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama33/IMG_20190713_013437_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama33/IMG_20190713_013437_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama33/IMG_20190713_013437_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama33/IMG_20190713_013437_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama33/IMG_20190713_013437_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama33/IMG_20190713_013437_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_5.jpg");
         String output_name = "testPanorama33_output.jpg";
         String gyro_name = null;
         // taken with Nokia 8, old API:
@@ -18314,16 +18309,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama35/IMG_20190717_145114_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_9.jpg");
         String output_name = "testPanorama35_output.jpg";
         String gyro_name = null;
         // taken with Nexus 7, old API:
@@ -18374,14 +18369,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama36/IMG_20190722_201331_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_7.jpg");
         String output_name = "testPanorama36_output.jpg";
         String gyro_name = null;
         // taken with Samsung Galaxy S10e, Camera2 API, ultra wide rear camera:
@@ -18402,15 +18397,15 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama37/IMG_20190723_203441_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_8.jpg");
         String output_name = "testPanorama37_output.jpg";
         String gyro_name = null;
         // taken with Samsung Galaxy S10e, old API, standard rear camera:
@@ -18432,16 +18427,16 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs = new ArrayList<>();
 
         float panorama_pics_per_screen = 3.33333f;
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_0.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_1.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_2.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_3.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_4.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_5.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_6.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_7.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_8.jpg");
-        inputs.add(panorama_images_path + "testPanorama38/IMG_20190722_141148_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_9.jpg");
         String output_name = "testPanorama38_output.jpg";
         String gyro_name = null;
         // taken with Samsung Galaxy S10e, Camera2 API, standard rear camera:
-- 
GitLab


From df0b6239e89896af50d43c12cf1fffb91e1352b6 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 7 Sep 2022 22:17:56 +0100
Subject: [PATCH 038/117] Add comments.

---
 .../androidTest/java/net/sourceforge/opencamera/TestUtils.java | 2 ++
 .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 3 ++-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 4ea183530..11f3f669f 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -36,6 +36,8 @@ public class TestUtils {
         intent.putExtra("test_project", true);
     }
 
+    /** Code to call before running each test.
+     */
     public static void initTest(Context context, boolean test_camera2) {
         Log.d(TAG, "initTest: " + test_camera2);
         // initialise test statics (to avoid the persisting between tests in a test suite run!)
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 4a361f2fa..b9453a8b9 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -144,7 +144,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2TestUtils.initTest(), it's useful
+        // to leave the application in a default state after running tests)
         SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity);
         SharedPreferences.Editor editor = settings.edit();
         editor.clear();
-- 
GitLab


From b09727ccedf3f50c92333bd23c7e65bfe028b0bf Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 7 Sep 2022 22:35:18 +0100
Subject: [PATCH 039/117] Remove redundant TestUtils reference.

---
 .../java/net/sourceforge/opencamera/TestUtils.java        | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 11f3f669f..13f9b2fe2 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -27,10 +27,10 @@ public class TestUtils {
     private static final String TAG = "TestUtils";
 
     final private static String images_base_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
-    final public static String hdr_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/hdrsamples/";
-    final public static String avg_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/avgsamples/";
-    final public static String logprofile_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/logprofilesamples/";
-    final public static String panorama_images_path = TestUtils.images_base_path + "/testOpenCamera/testdata/panoramasamples/";
+    final public static String hdr_images_path = images_base_path + "/testOpenCamera/testdata/hdrsamples/";
+    final public static String avg_images_path = images_base_path + "/testOpenCamera/testdata/avgsamples/";
+    final public static String logprofile_images_path = images_base_path + "/testOpenCamera/testdata/logprofilesamples/";
+    final public static String panorama_images_path = images_base_path + "/testOpenCamera/testdata/panoramasamples/";
 
     public static void setDefaultIntent(Intent intent) {
         intent.putExtra("test_project", true);
-- 
GitLab


From a37f25accafd9e04ac4f370c88c269747324017f Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 7 Sep 2022 22:35:45 +0100
Subject: [PATCH 040/117] Remove imports no longer needed.

---
 .../net/sourceforge/opencamera/test/MainActivityTest.java    | 5 +----
 1 file changed, 1 insertion(+), 4 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 b9453a8b9..ba632a9b3 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -1,8 +1,7 @@
 package net.sourceforge.opencamera.test;
 
 import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
+//import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.InputStream;
@@ -48,7 +47,6 @@ import android.content.SharedPreferences;
 //import android.content.res.AssetManager;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Point;
@@ -63,7 +61,6 @@ import android.media.MediaScannerConnection;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Environment;
-import android.os.ParcelFileDescriptor;
 import android.preference.PreferenceManager;
 import android.provider.DocumentsContract;
 import android.provider.MediaStore;
-- 
GitLab


From 450c7c76f1fa5e78b522c9258424d6c38ab991b3 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 7 Sep 2022 22:58:37 +0100
Subject: [PATCH 041/117] Refactor HDR test code to TestUtils.

---
 .../net/sourceforge/opencamera/TestUtils.java | 243 ++++++++++
 .../opencamera/test/MainActivityTest.java     | 439 +++++-------------
 2 files changed, 357 insertions(+), 325 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 13f9b2fe2..7bc40277f 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -1,24 +1,37 @@
 package net.sourceforge.opencamera;
 
+import static org.junit.Assert.*;
+
+import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Matrix;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.preference.PreferenceManager;
 import android.provider.DocumentsContract;
+import android.provider.MediaStore;
 import android.util.Log;
 
+import androidx.annotation.RequiresApi;
 import androidx.exifinterface.media.ExifInterface;
 
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
 
 /** Helper class for testing. This method should not include any code specific to any test framework
  *  (e.g., shouldn't be specific to ActivityInstrumentationTestCase2).
@@ -206,4 +219,234 @@ public class TestUtils {
         return bitmap;
     }
 
+    /** Returns the mediastore Uri for the supplied filename inside the supplied baseUri, or null
+     *  if an entry can't be found.
+     */
+    private static Uri getUriFromName(MainActivity activity, Uri baseUri, String name) {
+        Uri uri = null;
+        String [] projection = new String[]{MediaStore.Images.ImageColumns._ID};
+        Cursor cursor = null;
+        try {
+            cursor = activity.getContentResolver().query(baseUri, projection, MediaStore.Images.ImageColumns.DISPLAY_NAME + " LIKE ?", new String[]{name}, null);
+            if( cursor != null && cursor.moveToFirst() ) {
+                Log.d(TAG, "found: " + cursor.getCount());
+                long id = cursor.getLong(0);
+                uri = ContentUris.withAppendedId(baseUri, id);
+                Log.d(TAG, "id: " + id);
+                Log.d(TAG, "uri: " + uri);
+            }
+        }
+        catch(Exception e) {
+            Log.e(TAG, "Exception trying to find uri from filename");
+            e.printStackTrace();
+        }
+        finally {
+            if( cursor != null ) {
+                cursor.close();
+            }
+        }
+        return uri;
+    }
+
+    public static void saveBitmap(MainActivity activity, Bitmap bitmap, String name) throws IOException {
+        Log.d(TAG, "saveBitmap: " + name);
+
+        File file = null;
+        ContentValues contentValues = null;
+        Uri uri = null;
+        OutputStream outputStream;
+        if( MainActivity.useScopedStorage() ) {
+            Uri folder = Build.VERSION.SDK_INT >= 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(activity, folder, name);
+            if( old_uri != null ) {
+                Log.d(TAG, "delete: " + old_uri);
+                activity.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 = activity.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 = activity.getContentResolver().insert(folder, contentValues);
+            Log.d(TAG, "saveUri: " + uri);
+            if( uri == null ) {
+                throw new IOException();
+            }
+            outputStream = activity.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();
+
+        if( MainActivity.useScopedStorage() ) {
+            if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) {
+                contentValues.clear();
+                contentValues.put(MediaStore.Images.Media.IS_PENDING, 0);
+                activity.getContentResolver().update(uri, contentValues, null, null);
+            }
+        }
+        else {
+            activity.getStorageUtils().broadcastFile(file, true, false, true);
+        }
+    }
+
+    public static class HistogramDetails {
+        public final int min_value;
+        public final int median_value;
+        public final int max_value;
+
+        HistogramDetails(int min_value, int median_value, int max_value) {
+            this.min_value = min_value;
+            this.median_value = median_value;
+            this.max_value = max_value;
+        }
+    }
+
+    /** Checks for the resultant histogram.
+     *  We check that we have a single range of non-zero values.
+     * @param bitmap The bitmap to compute and check a histogram for.
+     */
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public static HistogramDetails checkHistogram(MainActivity activity, Bitmap bitmap) {
+        int [] histogram = activity.getApplicationInterface().getHDRProcessor().computeHistogram(bitmap, true);
+        assertEquals(256, histogram.length);
+        int total = 0;
+        for(int i=0;i= middle && median_value == -1 )
+                    median_value = i;
+            }
+        }
+        Log.d(TAG, "min_value: " + min_value);
+        Log.d(TAG, "median_value: " + median_value);
+        Log.d(TAG, "max_value: " + max_value);
+        return new HistogramDetails(min_value, median_value, max_value);
+    }
+
+    public static HistogramDetails subTestHDR(MainActivity activity, List inputs, String output_name, boolean test_dro, int iso, long exposure_time) throws IOException, InterruptedException {
+        return subTestHDR(activity, inputs, output_name, test_dro, iso, exposure_time, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD);
+    }
+
+    /** The testHDRX tests test the HDR algorithm on a given set of input images.
+     *  By testing on a fixed sample, this makes it easier to finetune the HDR algorithm for quality and performance.
+     *  To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/
+     *  folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes
+     *  time to transfer to the device everytime we run the tests.
+     * @param iso The ISO of the middle image (for testing Open Camera's "smart" contrast enhancement). If set to -1, then use "always" contrast enhancement.
+     * @param exposure_time The exposure time of the middle image (for testing Open Camera's "smart" contrast enhancement)
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public static HistogramDetails subTestHDR(MainActivity activity, List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm/*, HDRTestCallback test_callback*/) throws IOException, InterruptedException {
+        Log.d(TAG, "subTestHDR");
+
+        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
+            Log.d(TAG, "renderscript requires Android Lollipop or better");
+            return null;
+        }
+
+        Thread.sleep(1000); // wait for camera to open
+
+        Bitmap dro_bitmap_in = null;
+        if( test_dro ) {
+            // save copy of input bitmap to also test DRO (since the HDR routine will free the inputs)
+            int mid = (inputs.size()-1)/2;
+            dro_bitmap_in = inputs.get(mid);
+            dro_bitmap_in = dro_bitmap_in.copy(dro_bitmap_in.getConfig(), true);
+        }
+
+        HistogramDetails hdrHistogramDetails = null;
+        if( inputs.size() > 1 ) {
+            String preference_hdr_contrast_enhancement = (iso==-1) ? "preference_hdr_contrast_enhancement_always" : "preference_hdr_contrast_enhancement_smart";
+            float hdr_alpha = ImageSaver.getHDRAlpha(preference_hdr_contrast_enhancement, exposure_time, inputs.size());
+            long time_s = System.currentTimeMillis();
+            try {
+                activity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true, null, hdr_alpha, 4, true, tonemapping_algorithm, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_GAINGAMMA);
+                //test_callback.doHDR(inputs, tonemapping_algorithm, hdr_alpha);
+            }
+            catch(HDRProcessorException e) {
+                e.printStackTrace();
+                throw new RuntimeException();
+            }
+            Log.d(TAG, "HDR time: " + (System.currentTimeMillis() - time_s));
+
+            saveBitmap(activity, inputs.get(0), output_name);
+            hdrHistogramDetails = checkHistogram(activity, inputs.get(0));
+        }
+        inputs.get(0).recycle();
+        inputs.clear();
+
+        if( test_dro ) {
+            inputs.add(dro_bitmap_in);
+            long time_s = System.currentTimeMillis();
+            try {
+                activity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true, null, 0.5f, 4, true, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_GAINGAMMA);
+                //test_callback.doHDR(inputs, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD, 0.5f);
+            }
+            catch(HDRProcessorException e) {
+                e.printStackTrace();
+                throw new RuntimeException();
+            }
+            Log.d(TAG, "DRO time: " + (System.currentTimeMillis() - time_s));
+
+            saveBitmap(activity, inputs.get(0), "dro" + output_name);
+            checkHistogram(activity, inputs.get(0));
+            inputs.get(0).recycle();
+            inputs.clear();
+        }
+        Thread.sleep(500);
+
+        return hdrHistogramDetails;
+    }
+
+    public static void checkHDROffsets(MainActivity activity, int [] exp_offsets_x, int [] exp_offsets_y) {
+        checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, 1);
+    }
+
+    /** Checks that the HDR offsets used for auto-alignment are as expected.
+     */
+    public static void checkHDROffsets(MainActivity activity, int [] exp_offsets_x, int [] exp_offsets_y, int scale) {
+        int [] offsets_x = activity.getApplicationInterface().getHDRProcessor().offsets_x;
+        int [] offsets_y = activity.getApplicationInterface().getHDRProcessor().offsets_y;
+        for(int i=0;i= middle && median_value == -1 )
-                    median_value = i;
-            }
-        }
-        Log.d(TAG, "min_value: " + min_value);
-        Log.d(TAG, "median_value: " + median_value);
-        Log.d(TAG, "max_value: " + max_value);
-        return new HistogramDetails(min_value, median_value, max_value);
+    private TestUtils.HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time) throws IOException, InterruptedException {
+        return TestUtils.subTestHDR(mActivity, inputs, output_name, test_dro, iso, exposure_time);
     }
 
-    private HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time) throws IOException, InterruptedException {
-        return subTestHDR(inputs, output_name, test_dro, iso, exposure_time, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD);
-    }
-
-    /** The following testHDRX tests test the HDR algorithm on a given set of input images.
-     *  By testing on a fixed sample, this makes it easier to finetune the HDR algorithm for quality and performance.
-     *  To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/
-     *  folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes
-     *  time to transfer to the device everytime we run the tests.
-     * @param iso The ISO of the middle image (for testing Open Camera's "smart" contrast enhancement). If set to -1, then use "always" contrast enhancement.
-     * @param exposure_time The exposure time of the middle image (for testing Open Camera's "smart" contrast enhancement)
-     */
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm) throws IOException, InterruptedException {
-        Log.d(TAG, "subTestHDR");
-
-        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
-            Log.d(TAG, "renderscript requires Android Lollipop or better");
-            return null;
-        }
-
-        Thread.sleep(1000); // wait for camera to open
-
-        Bitmap dro_bitmap_in = null;
-        if( test_dro ) {
-            // save copy of input bitmap to also test DRO (since the HDR routine will free the inputs)
-            int mid = (inputs.size()-1)/2;
-            dro_bitmap_in = inputs.get(mid);
-            dro_bitmap_in = dro_bitmap_in.copy(dro_bitmap_in.getConfig(), true);
-        }
-
-        HistogramDetails hdrHistogramDetails = null;
-        if( inputs.size() > 1 ) {
-            String preference_hdr_contrast_enhancement = (iso==-1) ? "preference_hdr_contrast_enhancement_always" : "preference_hdr_contrast_enhancement_smart";
-    		float hdr_alpha = ImageSaver.getHDRAlpha(preference_hdr_contrast_enhancement, exposure_time, inputs.size());
-            long time_s = System.currentTimeMillis();
-            try {
-                mActivity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true, null, hdr_alpha, 4, true, tonemapping_algorithm, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_GAINGAMMA);
-            }
-            catch(HDRProcessorException e) {
-                e.printStackTrace();
-                throw new RuntimeException();
-            }
-            Log.d(TAG, "HDR time: " + (System.currentTimeMillis() - time_s));
-
-            saveBitmap(inputs.get(0), output_name);
-            hdrHistogramDetails = checkHistogram(inputs.get(0));
-        }
-        inputs.get(0).recycle();
-        inputs.clear();
-
-        if( test_dro ) {
-            inputs.add(dro_bitmap_in);
-            long time_s = System.currentTimeMillis();
-            try {
-                mActivity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true, null, 0.5f, 4, true, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_GAINGAMMA);
-            }
-            catch(HDRProcessorException e) {
-                e.printStackTrace();
-                throw new RuntimeException();
-            }
-            Log.d(TAG, "DRO time: " + (System.currentTimeMillis() - time_s));
-
-            saveBitmap(inputs.get(0), "dro" + output_name);
-            checkHistogram(inputs.get(0));
-            inputs.get(0).recycle();
-            inputs.clear();
-        }
-        Thread.sleep(500);
-
-        return hdrHistogramDetails;
+    private TestUtils.HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm/*, HDRTestCallback test_callback*/) throws IOException, InterruptedException {
+        return TestUtils.subTestHDR(mActivity, inputs, output_name, test_dro, iso, exposure_time, tonemapping_algorithm);
     }
 
     private void checkHDROffsets(int [] exp_offsets_x, int [] exp_offsets_y) {
-        checkHDROffsets(exp_offsets_x, exp_offsets_y, 1);
+        TestUtils.checkHDROffsets(mActivity, exp_offsets_x, exp_offsets_y);
     }
 
-    /** Checks that the HDR offsets used for auto-alignment are as expected.
-     */
     private void checkHDROffsets(int [] exp_offsets_x, int [] exp_offsets_y, int scale) {
-        int [] offsets_x = mActivity.getApplicationInterface().getHDRProcessor().offsets_x;
-        int [] offsets_y = mActivity.getApplicationInterface().getHDRProcessor().offsets_y;
-        for(int i=0;i inputs, String output_name, int iso, long exposure_time, float zoom_factor, TestAvgCallback cb) throws IOException, InterruptedException {
+    private TestUtils.HistogramDetails subTestAvg(List inputs, String output_name, int iso, long exposure_time, float zoom_factor, TestAvgCallback cb) throws IOException, InterruptedException {
         Log.d(TAG, "subTestAvg");
 
         if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
@@ -14684,7 +14557,7 @@ 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();
-
-        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);
-        }
+        TestUtils.saveBitmap(mActivity, bitmap, name);
     }
 
     /**
-- 
GitLab


From e1c33661eafce5a552115334e41b80a6e07ac3a0 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 10 Sep 2022 15:35:04 +0100
Subject: [PATCH 042/117] Refactor common code - any change to exif can go via
 modifyExif()

---
 .../sourceforge/opencamera/ImageSaver.java    | 25 +------------------
 1 file changed, 1 insertion(+), 24 deletions(-)

diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index b5defeac0..3fd7bfe8e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3532,7 +3532,7 @@ public class ImageSaver extends Thread {
     private void updateExif(Request request, File picFile, Uri saveUri) throws IOException {
         if( MyDebug.LOG )
             Log.d(TAG, "updateExif: " + picFile);
-        if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) || request.using_camera_extensions ) {
+        if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) || request.using_camera_extensions || needGPSTimestampHack(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
             long time_s = System.currentTimeMillis();
             if( MyDebug.LOG )
                 Log.d(TAG, "add additional exif info");
@@ -3558,29 +3558,6 @@ public class ImageSaver extends Thread {
             if( MyDebug.LOG )
                 Log.d(TAG, "*** time to add additional exif info: " + (System.currentTimeMillis() - time_s));
         }
-        else if( needGPSTimestampHack(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
-            if( MyDebug.LOG )
-                Log.d(TAG, "remove GPS timestamp hack");
-            try {
-                ExifInterfaceHolder exif_holder = createExifInterface(picFile, saveUri);
-                try {
-                    ExifInterface exif = exif_holder.getExif();
-                    if( exif != null ) {
-                        fixGPSTimestamp(exif, request.current_date);
-                        exif.saveAttributes();
-                    }
-                }
-                finally {
-                    exif_holder.close();
-                }
-            }
-            catch(NoClassDefFoundError exception) {
-                // have had Google Play crashes from new ExifInterface() elsewhere for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn), so also catch here just in case
-                if( MyDebug.LOG )
-                    Log.e(TAG, "exif orientation NoClassDefFoundError");
-                exception.printStackTrace();
-            }
-        }
         else {
             if( MyDebug.LOG )
                 Log.d(TAG, "no exif data to update for: " + picFile);
-- 
GitLab


From f7a49da05b45cf68274188ad6ae91b8d852bd795 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 18 Sep 2022 18:44:56 +0100
Subject: [PATCH 043/117] Move checkHistogramDetails() to TestUtils.

---
 .../java/net/sourceforge/opencamera/TestUtils.java    | 11 +++++++++++
 .../sourceforge/opencamera/test/MainActivityTest.java | 10 +---------
 2 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 7bc40277f..ca7099080 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -449,4 +449,15 @@ public class TestUtils {
         }
     }
 
+    public static void checkHistogramDetails(HistogramDetails hdrHistogramDetails, int exp_min_value, int exp_median_value, int exp_max_value) {
+        Log.d(TAG, "checkHistogramDetails");
+        Log.d(TAG, "compare min value " + hdrHistogramDetails.min_value + " to expected " + exp_min_value);
+        Log.d(TAG, "compare median value " + hdrHistogramDetails.median_value + " to expected " + exp_median_value);
+        Log.d(TAG, "compare max value " + hdrHistogramDetails.max_value + " to expected " + exp_max_value);
+        // we allow some tolerance as different devices can produce different results (e.g., Nexus 6 vs OnePlus 3T; see testHDR18 on Nexus 6 which needs a tolerance of 2)
+        // interestingly it's testHDR18 that also needs a higher tolerance for Nokia 8 vs Galaxy S10e
+        assertTrue(Math.abs(exp_min_value - hdrHistogramDetails.min_value) <= 3);
+        assertTrue(Math.abs(exp_median_value - hdrHistogramDetails.median_value) <= 3);
+        assertTrue(Math.abs(exp_max_value - hdrHistogramDetails.max_value) <= 3);
+    }
 }
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 23623bbce..089fe930f 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -12666,15 +12666,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Mon, 26 Sep 2022 21:55:43 +0100
Subject: [PATCH 044/117] Add support for AndroidJUnit4 InstrumentedTest tests.

---
 app/build.gradle | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/app/build.gradle b/app/build.gradle
index 9d494ca84..6408426d8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,7 +16,8 @@ android {
         // also need build tools at least 21 to avoid Google Play 64-bit warning (required from 1 August 2019)
 
         testApplicationId "net.sourceforge.opencamera.test"
-        testInstrumentationRunner "android.test.InstrumentationTestRunner"
+        //testInstrumentationRunner "android.test.InstrumentationTestRunner"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
 
     buildTypes {
@@ -49,4 +50,9 @@ dependencies {
     implementation 'androidx.exifinterface:exifinterface:1.3.3'
 
     testImplementation 'junit:junit:4.13.1'
+
+    // newer AndroidJUnit4 InstrumentedTest
+    androidTestImplementation "androidx.test:runner:1.4.0"
+    androidTestImplementation "androidx.test:rules:1.4.0"
+    androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
 }
-- 
GitLab


From d884cc5f2c9a9a92603786e7146f02e5e91f4b93 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 28 Sep 2022 22:24:01 +0100
Subject: [PATCH 045/117] Check CONTROL_AE_AVAILABLE_MODES for red eye flash.

---
 _docs/history.html                               |  2 ++
 .../cameracontroller/CameraController2.java      | 16 +++++++++++++++-
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/_docs/history.html b/_docs/history.html
index 7493e0017..6f8e1840e 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -54,6 +54,8 @@ FIXED   Focus bracketing images came out underexposed on some devices since
         version 1.50 (e.g. Pixel 6 Pro).
 FIXED   Face detection on-screen icon shouldn't show in camera vendor extension modes (as not
         supported).
+FIXED   For Camera2 API, red eye flash was incorrectly being shown even on devices that didn't
+        support it.
 ADDED   Shading for auto-level and crop guides, to darken the preview outside of the region of
         interest.
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
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 fe7c466a4..add00abfa 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -3054,13 +3054,27 @@ public class CameraController2 extends CameraController {
         }
 
         if( characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ) {
+            int [] supported_flash_modes_arr = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); // Android format
+            List supported_flash_modes = new ArrayList<>();
+            for(Integer supported_flash_mode : supported_flash_modes_arr)
+                supported_flash_modes.add(supported_flash_mode);
+
             camera_features.supported_flash_values = new ArrayList<>();
+            // also resort as well as converting
+
+            // documentation for CONTROL_AE_AVAILABLE_MODES says the following modes are always supported:
             camera_features.supported_flash_values.add("flash_off");
             camera_features.supported_flash_values.add("flash_auto");
             camera_features.supported_flash_values.add("flash_on");
             camera_features.supported_flash_values.add("flash_torch");
+
             if( !use_fake_precapture ) {
-                camera_features.supported_flash_values.add("flash_red_eye");
+                if( supported_flash_modes.contains(CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) ) {
+                    camera_features.supported_flash_values.add("flash_red_eye");
+                    if( MyDebug.LOG ) {
+                        Log.d(TAG, " supports flash_red_eye");
+                    }
+                }
             }
         }
         else if( (getFacing() == Facing.FACING_FRONT) ) {
-- 
GitLab


From 9b63d856e057952e15867382a7ee206124e0c9e7 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 1 Oct 2022 20:19:41 +0100
Subject: [PATCH 046/117] No longer use custom views for system toasts; switch
 more toasts to being "fake".

---
 _docs/history.html                            |  1 +
 .../sourceforge/opencamera/MainActivity.java  | 16 ++--
 .../opencamera/MyApplicationInterface.java    | 10 +--
 .../opencamera/preview/Preview.java           | 76 ++++++++++++-------
 .../net/sourceforge/opencamera/ui/MainUI.java |  6 +-
 .../sourceforge/opencamera/ui/PopupView.java  |  2 +-
 6 files changed, 65 insertions(+), 46 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index 6f8e1840e..7073751dc 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -65,6 +65,7 @@ UPDATED Make zoom seekbar snap to powers of two (for Camera2 API).
 UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API).
 UPDATED Improved look of on-screen level line.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
+UPDATED Use system toasts without custom views when appropriate.
 
 Version 1.50.1 (2022/06/08)
 
diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
index f145a92e2..4ff010df2 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -1854,7 +1854,7 @@ public class MainActivity extends AppCompatActivity {
         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);
+        preview.showToast(store_location_toast, message, true);
     }
 
     public void clickedTextStamp(View view) {
@@ -1919,7 +1919,7 @@ public class MainActivity extends AppCompatActivity {
 
         mainUI.updateStampIcon();
         applicationInterface.getDrawPreview().updateSettings();
-        preview.showToast(stamp_toast, value ? R.string.stamp_enabled : R.string.stamp_disabled);
+        preview.showToast(stamp_toast, value ? R.string.stamp_enabled : R.string.stamp_disabled, true);
     }
 
     public void clickedAutoLevel(View view) {
@@ -1948,7 +1948,7 @@ public class MainActivity extends AppCompatActivity {
 
         if( !done_dialog ) {
             String message = getResources().getString(R.string.preference_auto_stabilise) + ": " + getResources().getString(value ? R.string.on : R.string.off);
-            preview.showToast(this.getChangedAutoStabiliseToastBoxer(), message);
+            preview.showToast(this.getChangedAutoStabiliseToastBoxer(), message, true);
         }
 
         mainUI.updateAutoLevelIcon();
@@ -1978,7 +1978,7 @@ public class MainActivity extends AppCompatActivity {
         editor.apply();
 
         mainUI.updateFaceDetectionIcon();
-        preview.showToast(stamp_toast, value ? R.string.face_detection_enabled : R.string.face_detection_disabled);
+        preview.showToast(stamp_toast, value ? R.string.face_detection_enabled : R.string.face_detection_disabled, true);
         block_startup_toast = true; // so the toast from reopening camera is suppressed, otherwise it conflicts with the face detection toast
         preview.reopenCamera();
     }
@@ -2237,7 +2237,7 @@ public class MainActivity extends AppCompatActivity {
             Log.d(TAG, "clickedWhiteBalanceLock");
         this.preview.toggleWhiteBalanceLock();
         mainUI.updateWhiteBalanceLockIcon();
-        preview.showToast(white_balance_lock_toast, preview.isWhiteBalanceLocked() ? R.string.white_balance_locked : R.string.white_balance_unlocked);
+        preview.showToast(white_balance_lock_toast, preview.isWhiteBalanceLocked() ? R.string.white_balance_locked : R.string.white_balance_unlocked, true);
     }
 
     public void clickedExposureLock(View view) {
@@ -2245,7 +2245,7 @@ public class MainActivity extends AppCompatActivity {
             Log.d(TAG, "clickedExposureLock");
         this.preview.toggleExposureLock();
         mainUI.updateExposureLockIcon();
-        preview.showToast(exposure_lock_toast, preview.isExposureLocked() ? R.string.exposure_locked : R.string.exposure_unlocked);
+        preview.showToast(exposure_lock_toast, preview.isExposureLocked() ? R.string.exposure_locked : R.string.exposure_unlocked, true);
     }
 
     public void clickedExposure(View view) {
@@ -2937,7 +2937,7 @@ public class MainActivity extends AppCompatActivity {
         }
         // don't set block_startup_toast to false yet, as camera might be closing/opening on background thread
         if( toast_message != null && toast_message.length() > 0 )
-            preview.showToast(null, toast_message);
+            preview.showToast(null, toast_message, true);
 
         // don't need to reset to saved_focus_value, as we'll have done this when setting up the camera (or will do so when the camera is reopened, if need_reopen)
     	/*if( saved_focus_value != null ) {
@@ -5801,7 +5801,7 @@ public class MainActivity extends AppCompatActivity {
         MyAudioTriggerListenerCallback callback = new MyAudioTriggerListenerCallback(this);
         audio_listener = new AudioListener(callback);
         if( audio_listener.status() ) {
-            preview.showToast(audio_control_toast, R.string.audio_listener_started);
+            preview.showToast(audio_control_toast, R.string.audio_listener_started, true);
 
             audio_listener.start();
             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
index 45f81d8a1..18653fb8e 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
@@ -1988,7 +1988,7 @@ public class MyApplicationInterface extends BasicApplicationInterface {
                     Log.d(TAG, "TargetCallback.onTooFar");
 
                 if( !main_activity.is_test ) {
-                    main_activity.getPreview().showToast(null, R.string.panorama_cancelled);
+                    main_activity.getPreview().showToast(null, R.string.panorama_cancelled, true);
                     MyApplicationInterface.this.stopPanorama(true);
                 }
             }
@@ -2600,13 +2600,13 @@ public class MyApplicationInterface extends BasicApplicationInterface {
             if( MyDebug.LOG )
                 Log.d(TAG, "next output file started");
             int message_id = R.string.video_max_filesize;
-            main_activity.getPreview().showToast(null, message_id);
+            main_activity.getPreview().showToast(null, message_id, true);
         }
         else if( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED ) {
             if( MyDebug.LOG )
                 Log.d(TAG, "max filesize reached");
             int message_id = R.string.video_max_filesize;
-            main_activity.getPreview().showToast(null, message_id);
+            main_activity.getPreview().showToast(null, message_id, true);
         }
         // in versions 1.24 and 1.24, there was a bug where we had "info_" for onVideoError and "error_" for onVideoInfo!
         // fixed in 1.25; also was correct for 1.23 and earlier
@@ -3655,7 +3655,7 @@ public class MyApplicationInterface extends BasicApplicationInterface {
                     if( MyDebug.LOG )
                         Log.d(TAG, "successfully deleted " + image_uri);
                     if( from_user )
-                        preview.showToast(null, R.string.photo_deleted);
+                        preview.showToast(null, R.string.photo_deleted, true);
                     if( file != null ) {
                         // SAF doesn't broadcast when deleting them
                         storageUtils.broadcastFile(file, false, false, true);
@@ -3687,7 +3687,7 @@ public class MyApplicationInterface extends BasicApplicationInterface {
                 if( MyDebug.LOG )
                     Log.d(TAG, "successfully deleted " + image_name);
                 if( from_user )
-                    preview.showToast(photo_delete_toast, R.string.photo_deleted);
+                    preview.showToast(photo_delete_toast, R.string.photo_deleted, true);
                 storageUtils.broadcastFile(file, false, false, true);
             }
         }
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 21f16b9a2..8c48d732c 100644
--- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
@@ -1222,7 +1222,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                         toast = remaining_restart_video + " " + getContext().getResources().getString(R.string.repeats_to_go);
                     takePicture(due_to_max_filesize, false, false);
                     if( !due_to_max_filesize ) {
-                        showToast(null, toast); // show the toast afterwards, as we're hogging the UI thread here, and media recorder takes time to start up
+                        showToast(null, toast, true); // show the toast afterwards, as we're hogging the UI thread here, and media recorder takes time to start up
                         // must decrement after calling takePicture(), so that takePicture() doesn't reset the value of remaining_restart_video
                         remaining_restart_video--;
                     }
@@ -4263,7 +4263,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             if( camera_controller.setExposureCompensation(new_exposure) ) {
                 // now save
                 applicationInterface.setExposureCompensationPref(new_exposure);
-                showToast(getExposureCompensationString(new_exposure), 0, true);
+                showToast(null, getExposureCompensationString(new_exposure), 0, true);
             }
         }
     }
@@ -4278,7 +4278,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             if( camera_controller.setWhiteBalanceTemperature(new_temperature) ) {
                 // now save
                 applicationInterface.setWhiteBalanceTemperaturePref(new_temperature);
-                showToast(getResources().getString(R.string.white_balance) + " " + new_temperature, 0, true);
+                showToast(null, getResources().getString(R.string.white_balance) + " " + new_temperature, 0, true);
             }
         }
     }
@@ -4314,7 +4314,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             if( camera_controller.setISO(new_iso) ) {
                 // now save
                 applicationInterface.setISOPref("" + new_iso);
-                showToast(getISOString(new_iso), 0, true);
+                showToast(null, getISOString(new_iso), 0, true);
             }
         }
     }
@@ -4330,7 +4330,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             if( camera_controller.setExposureTime(new_exposure_time) ) {
                 // now save
                 applicationInterface.setExposureTimePref(new_exposure_time);
-                showToast(getExposureTimeString(new_exposure_time), 0, true);
+                showToast(null, getExposureTimeString(new_exposure_time), 0, true);
             }
         }
     }
@@ -4923,7 +4923,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                     if( MyDebug.LOG )
                         Log.d(TAG, "    found entry: " + i);
                     if( !initial ) {
-                        showToast(focus_flash_toast, flash_entries[i]);
+                        showToast(focus_flash_toast, flash_entries[i], true);
                     }
                     break;
                 }
@@ -5038,7 +5038,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             if( !quiet ) {
                 String focus_entry = findFocusEntryForValue(focus_value);
                 if( focus_entry != null ) {
-                    showToast(focus_flash_toast, focus_entry);
+                    showToast(focus_flash_toast, focus_entry, true);
                 }
             }
             this.setFocusValue(focus_value, auto_focus);
@@ -5185,7 +5185,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
         }
         if( this.isOnTimer() ) {
             cancelTimer();
-            showToast(take_photo_toast, R.string.cancelled_timer);
+            showToast(take_photo_toast, R.string.cancelled_timer, true);
             return;
         }
         //if( !photo_snapshot && this.phase == PHASE_TAKING_PHOTO ) {
@@ -5210,11 +5210,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                 Log.d(TAG, "already taking a photo");
             if( remaining_repeat_photos != 0 ) {
                 cancelRepeat();
-                showToast(take_photo_toast, R.string.cancelled_repeat_mode);
+                showToast(take_photo_toast, R.string.cancelled_repeat_mode, true);
             }
             else if( !is_video && camera_controller.getBurstType() == CameraController.BurstType.BURSTTYPE_FOCUS && camera_controller.isCapturingBurst() ) {
                 camera_controller.stopFocusBracketingBurst();
-                showToast(take_photo_toast, R.string.cancelled_focus_bracketing);
+                showToast(take_photo_toast, R.string.cancelled_focus_bracketing, true);
             }
             return;
         }
@@ -5540,7 +5540,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                 else {
                     if( MyDebug.LOG )
                         Log.d(TAG, "location data required, but not available");
-                    showToast(null, R.string.location_not_available);
+                    showToast(null, R.string.location_not_available, true);
                     if( !is_video || photo_snapshot )
                         this.phase = PHASE_NORMAL;
                     applicationInterface.cameraInOperation(false, false);
@@ -5690,7 +5690,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
 
             camera_controller.initVideoRecorderPrePrepare(local_video_recorder);
             if( profile.no_audio_permission ) {
-                showToast(null, R.string.permission_record_audio_not_available);
+                showToast(null, R.string.permission_record_audio_not_available, true);
             }
 
             boolean store_location = applicationInterface.getGeotaggingPref();
@@ -6023,7 +6023,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                 video_recorder.resume();
                 video_recorder_is_paused = false;
                 video_start_time = System.currentTimeMillis();
-                this.showToast(pause_video_toast, R.string.video_resume);
+                this.showToast(pause_video_toast, R.string.video_resume, true);
             }
             else {
                 if( MyDebug.LOG )
@@ -6036,7 +6036,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                     Log.d(TAG, "last_time: " + last_time);
                     Log.d(TAG, "video_accumulated_time is now: " + video_accumulated_time);
                 }
-                this.showToast(pause_video_toast, R.string.video_pause);
+                this.showToast(pause_video_toast, R.string.video_pause, true);
             }
         }
         else {
@@ -6240,7 +6240,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                 applicationInterface.onCaptureStarted();
                 if( applicationInterface.getBurstForNoiseReduction() && applicationInterface.getNRModePref() == ApplicationInterface.NRModePref.NRMODE_LOW_LIGHT ) {
                     if( camera_controller.getBurstTotal() >= CameraController.N_IMAGES_NR_DARK_LOW_LIGHT ) {
-                        showToast(null, R.string.preference_nr_mode_low_light_message);
+                        showToast(null, R.string.preference_nr_mode_low_light_message, true);
                     }
                 }
             }
@@ -7656,6 +7656,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
         showToast(clear_toast, getResources().getString(message_id), false);
     }
 
+    public void showToast(final ToastBoxer clear_toast, final int message_id, final boolean use_fake_toast) {
+        showToast(clear_toast, getResources().getString(message_id), use_fake_toast);
+    }
+
     public void showToast(final ToastBoxer clear_toast, final String message) {
         showToast(clear_toast, message, false);
     }
@@ -7669,9 +7673,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
         showToast(clear_toast, message, 32, use_fake_toast);
     }
 
-    public void showToast(final String message, final int offset_y_dp, final boolean use_fake_toast) {
+    /*public void showToast(final String message, final int offset_y_dp, final boolean use_fake_toast) {
         showToast(null, message, offset_y_dp, use_fake_toast);
-    }
+    }*/
 
     /** Displays a "toast", but has several advantages over calling Android's Toast API directly.
      *  We use a custom view, to rotate the toast to account for the device orientation (since
@@ -7680,15 +7684,27 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
      *                       with the same clear_toast value will overwrite the previous ones rather than
      *                       being queued. Note that toasts no longer seem to be queued anyway on
      *                       Android 9+.
+     *                       (N.B., some callers with use_fake_toast==true still supply a use_fake_toast
+     *                       for historical reasons, from when previously those calls weren't using a fake
+     *                       toast.)
      * @param message        The message to display.
-     * @param offset_y_dp    The y-offset from the centre of the screen.
-     * @param use_fake_toast If true, don't use Android's Toast system at all. This is due to problems on
-     *                       Android 9+ where rapidly displaying toasts (e.g., to display values from a
-     *                       seekbar being modified) cause problems where toast sometimes disappear (this
-     *                       happens whether using clear_toast or not). Note that using use_fake_toast
-     *                       means that the toasts don't have the fade out effect.
+     * @param offset_y_dp    The y-offset from the centre of the screen. Only relevant if use_fake_toast is
+     *                       true.
+     * @param use_fake_toast If true, don't use Android's Toast system at all, and instead display a message
+     *                       on the Preview.
+     *                       This is due to problems on Android 9+ where rapidly displaying toasts (e.g., to
+     *                       display values from a seekbar being modified) cause problems where toast sometimes
+     *                       disappear (this happens whether using clear_toast or not). Note that using
+     *                       use_fake_toast means that the toasts don't have the fade out effect.
+     *                       Update: Toasts with custom views (Toast.setView()) are now deprecated. So
+     *                       use_fake_toast==false no longer uses a custom view. So we should now only set
+     *                       use_fake_toast==false for when we really want to use the system toast (e.g.,
+     *                       anything that isn't when the Preview is showing such as from Settings, or when
+     *                       we want the Android toast look such as for an error message).
+     *                       Usages where we want to display info on the Preview should always set
+     *                       use_fake_toast==true for a consistent look.
      */
-    private void showToast(final ToastBoxer clear_toast, final String message, final int offset_y_dp, final boolean use_fake_toast) {
+    public void showToast(final ToastBoxer clear_toast, final String message, final int offset_y_dp, final boolean use_fake_toast) {
         //final boolean use_fake_toast = true;
         //final boolean use_fake_toast = old_use_fake_toast;
         if( !applicationInterface.getShowToastsPref() ) {
@@ -7782,6 +7798,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                 // However should only do this if the previous toast was the most recent toast (to avoid messing up ordering)
                 Toast toast;
                 long time_now = System.currentTimeMillis();
+                /*
                 // We recreate a toast every 2s, to workaround Android toast bug that calling show() no longer seems to extend the toast duration!
                 // (E.g., see bug where toasts for sliders disappear after a while if continually moving the slider.)
                 if( clear_toast != null && clear_toast.toast != null && clear_toast.toast == last_toast && time_now < last_toast_time_ms+2000) {
@@ -7795,28 +7812,29 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
                     view.invalidate(); // make sure the toast is redrawn
                     toast.setView(view);
                 }
-                else {
+                else*/ {
                     if( clear_toast != null && clear_toast.toast != null ) {
                         if( MyDebug.LOG )
                             Log.d(TAG, "cancel last toast: " + clear_toast.toast);
                         clear_toast.toast.cancel();
                     }
-                    toast = new Toast(activity);
+                    //toast = new Toast(activity);
+                    toast = Toast.makeText(activity, message, Toast.LENGTH_SHORT);
                     if( MyDebug.LOG )
                         Log.d(TAG, "created new toast: " + toast);
                     if( clear_toast != null )
                         clear_toast.toast = toast;
-                    @SuppressLint("InflateParams") // we add the view to the toast
+                    /*@SuppressLint("InflateParams") // we add the view to the toast
                     final View view = LayoutInflater.from(activity).inflate(R.layout.toast_textview, null);
                     TextView text = view.findViewById(R.id.text_view);
                     text.setShadowLayer(shadow_radius, 0.0f, 0.0f, Color.BLACK);
                     text.setText(message);
                     view.setPadding(0, offset_y, 0, 0);
                     toast.setView(text);
-                    toast.setGravity(Gravity.CENTER, 0, 0);
+                    toast.setGravity(Gravity.CENTER, 0, 0);*/
                     last_toast_time_ms = time_now;
                 }
-                toast.setDuration(Toast.LENGTH_SHORT);
+                //toast.setDuration(Toast.LENGTH_SHORT);
                 if( !((Activity)getContext()).isFinishing() ) {
                     // Workaround for crash due to bug in Android 7.1 when activity is closing whilst toast shows.
                     // This was fixed in Android 8, but still good to fix the crash on Android 7.1! See
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 a2f89f4b6..b1b7b9a14 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java
@@ -2097,7 +2097,7 @@ public class MainUI {
                         // also reset exposure time when changing from manual to auto from the popup menu:
                         editor.putLong(PreferenceKeys.ExposureTimePreferenceKey, CameraController.EXPOSURE_TIME_DEFAULT);
                         editor.apply();
-                        preview.showToast("ISO: " + toast_option, 0, true); // supply offset_y_dp to be consistent with preview.setExposure(), preview.setISO()
+                        preview.showToast(null, "ISO: " + toast_option, 0, true); // supply offset_y_dp to be consistent with preview.setExposure(), preview.setISO()
                         main_activity.updateForSettings(true, ""); // already showed the toast, so block from showing again
                     }
                     else if( old_iso.equals(CameraController.ISO_DEFAULT) ) {
@@ -2135,7 +2135,7 @@ public class MainUI {
                         }
 
                         editor.apply();
-                        preview.showToast("ISO: " + toast_option, 0, true); // supply offset_y_dp to be consistent with preview.setExposure(), preview.setISO()
+                        preview.showToast(null, "ISO: " + toast_option, 0, true); // supply offset_y_dp to be consistent with preview.setExposure(), preview.setISO()
                         main_activity.updateForSettings(true, ""); // already showed the toast, so block from showing again
                     }
                     else {
@@ -2837,7 +2837,7 @@ public class MainUI {
                             editor.putBoolean(PreferenceKeys.AutoStabilisePreferenceKey, auto_stabilise);
                             editor.apply();
                             String message = main_activity.getResources().getString(R.string.preference_auto_stabilise) + ": " + main_activity.getResources().getString(auto_stabilise ? R.string.on : R.string.off);
-                            main_activity.getPreview().showToast(main_activity.getChangedAutoStabiliseToastBoxer(), message);
+                            main_activity.getPreview().showToast(main_activity.getChangedAutoStabiliseToastBoxer(), message, true);
                             main_activity.getApplicationInterface().getDrawPreview().updateSettings(); // because we cache the auto-stabilise setting
                             this.destroyPopup(); // need to recreate popup in order to update the auto-level checkbox
                         }
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 a17ec2d9e..6c5df2b2f 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/PopupView.java
@@ -577,7 +577,7 @@ public class PopupView extends LinearLayout {
                             float new_aperture = apertures.get(index);
                             if( MyDebug.LOG )
                                 Log.d(TAG, "new_aperture: " + new_aperture);
-                            preview.showToast(null, getResources().getString(R.string.aperture) + ": " + option);
+                            preview.showToast(null, getResources().getString(R.string.aperture) + ": " + option, true);
                             main_activity.getApplicationInterface().setAperture(new_aperture);
                             if( preview.getCameraController() != null ) {
                                 preview.getCameraController().setAperture(new_aperture);
-- 
GitLab


From f0f44cc8c8759f523d2f6a32929928114bccbd81 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 1 Oct 2022 20:21:23 +0100
Subject: [PATCH 047/117] Should show toast when deleting image when using
 mediastore.

---
 .../net/sourceforge/opencamera/MyApplicationInterface.java   | 5 ++++-
 1 file changed, 4 insertions(+), 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 18653fb8e..c5a204c0b 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
@@ -3673,7 +3673,10 @@ public class MyApplicationInterface extends BasicApplicationInterface {
         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);
+            if( main_activity.getContentResolver().delete(image_uri, null, null) > 0 ) {
+                if( from_user )
+                    preview.showToast(photo_delete_toast, R.string.photo_deleted, true);
+            }
         }
         else if( image_name != null ) {
             if( MyDebug.LOG )
-- 
GitLab


From baa341f2c1cdc508d949493d5afae152997f2b72 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 2 Oct 2022 18:58:40 +0100
Subject: [PATCH 048/117] Fix typo in 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 8c48d732c..61775d425 100644
--- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
@@ -1164,7 +1164,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             // 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");
+                Log.d(TAG, "delete unused next video file");
             nextVideoFileInfo.close();
             applicationInterface.deleteUnusedVideo(nextVideoFileInfo.video_method, nextVideoFileInfo.video_uri, nextVideoFileInfo.video_filename);
         }
-- 
GitLab


From e5c3dbcfcbee0c17f9822cbdfe1f70d2741d294f Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 2 Oct 2022 18:59:31 +0100
Subject: [PATCH 049/117] Refactor to new allow_seamless_restart variable.

---
 .../java/net/sourceforge/opencamera/preview/Preview.java     | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

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 61775d425..e9c669010 100644
--- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
@@ -5354,7 +5354,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
     private void onVideoInfo(int what, int extra) {
         if( MyDebug.LOG )
             Log.d(TAG, "onVideoInfo: " + what + " extra: " + extra);
-        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING && video_restart_on_max_filesize ) {
+        boolean allow_seamless_restart = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+        if( allow_seamless_restart && what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING && video_restart_on_max_filesize ) {
             if( MyDebug.LOG )
                 Log.d(TAG, "seamless restart due to max filesize approaching - try setNextOutputFile");
             if( video_recorder == null ) {
@@ -5426,7 +5427,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
             // no need to explicitly stop if createVideoFile() or setNextOutputFile() fails - just let video reach max filesize
             // normally
         }
-        else if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && what == MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED && video_restart_on_max_filesize ) {
+        else if( allow_seamless_restart && what == MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED && video_restart_on_max_filesize ) {
             if( MyDebug.LOG )
                 Log.d(TAG, "seamless restart with setNextOutputFile has now occurred");
             if( nextVideoFileInfo == null ) {
-- 
GitLab


From 1ef1f0ed90259098181dd54112429818d554312b Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Mon, 3 Oct 2022 21:33:25 +0100
Subject: [PATCH 050/117] Update comment for failing
 testTakeVideoMaxFileSize4SAF().

---
 .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 3 ++-
 1 file changed, 2 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 089fe930f..b1d1f8bf0 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -7320,7 +7320,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Mon, 3 Oct 2022 22:00:55 +0100
Subject: [PATCH 051/117] Display current value for photo stamp font size and
 colour in preference summary.

---
 _docs/history.html                         | 1 +
 app/src/main/res/values-az/strings.xml     | 2 +-
 app/src/main/res/values-be/strings.xml     | 4 ++--
 app/src/main/res/values-cs/strings.xml     | 4 ++--
 app/src/main/res/values-de/strings.xml     | 4 ++--
 app/src/main/res/values-el/strings.xml     | 4 ++--
 app/src/main/res/values-es/strings.xml     | 4 ++--
 app/src/main/res/values-fr/strings.xml     | 4 ++--
 app/src/main/res/values-hu/strings.xml     | 4 ++--
 app/src/main/res/values-it/strings.xml     | 4 ++--
 app/src/main/res/values-ja/strings.xml     | 4 ++--
 app/src/main/res/values-ko/strings.xml     | 2 +-
 app/src/main/res/values-nb/strings.xml     | 4 ++--
 app/src/main/res/values-pl/strings.xml     | 4 ++--
 app/src/main/res/values-pt-rBR/strings.xml | 2 +-
 app/src/main/res/values-pt-rPT/strings.xml | 4 ++--
 app/src/main/res/values-ru/strings.xml     | 4 ++--
 app/src/main/res/values-sl/strings.xml     | 4 ++--
 app/src/main/res/values-tr/strings.xml     | 4 ++--
 app/src/main/res/values-uk/strings.xml     | 4 ++--
 app/src/main/res/values-vi/strings.xml     | 4 ++--
 app/src/main/res/values-zh-rCN/strings.xml | 4 ++--
 app/src/main/res/values-zh-rTW/strings.xml | 4 ++--
 app/src/main/res/values/strings.xml        | 4 ++--
 24 files changed, 44 insertions(+), 43 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index 7073751dc..2426f8265 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -66,6 +66,7 @@ UPDATED Apply a dimmed effect when reopening camera or switching modes (for Came
 UPDATED Improved look of on-screen level line.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 UPDATED Use system toasts without custom views when appropriate.
+UPDATED Display current value for photo stamp font size and colour in preference summary.
 
 Version 1.50.1 (2022/06/08)
 
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index c7b24b3d3..339ef20a2 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -157,7 +157,7 @@
     Mətn möhürü
     Şəkil üzərində xüsusi mətn yaz
     Şrift ölçüsü
-    Şəkil üzərində mətn yazısı üçün şriftin ölçüsü
+    Şəkil üzərində mətn yazısı üçün şriftin ölçüsü\n%s
     Video keyfiyyəti
     Video keyfiyyəti 4K UHD (eksperimental)
     Arxa kamera üçün 3840х2160 ölçüsü seçiləcək və yalnız cihaz verilən formatı dəstəklədiyi zaman işləyəcək!
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 97449a141..6fce86468 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -177,9 +177,9 @@
     Прызначыць тэкст
     Друкаваць на фотаздымках дадзены тэкст
     Памер шрыфту
-    Памер шрыфту для друку на фотаздымках
+    Памер шрыфту для друку на фотаздымках\n%s
     Колер шрыфту
-    Колер шрыфту для друку на фотаздымках
+    Колер шрыфту для друку на фотаздымках\n%s
     Стыль тэксту
     Стыль для друку на фотаздымках\n%s
     Раздзяляльная здольнасць відэа
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 67a67e418..e82149ee2 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -180,9 +180,9 @@
     Vlastní text
     Orazítkovat fotografie vlastním textem
     Velikost písma
-    Velikost písma pro oražení fotografií textem
+    Velikost písma pro oražení fotografií textem\n%s
     Barva písma
-    Barva písma pro oražení fotografií textem
+    Barva písma pro oražení fotografií textem\n%s
     Styl textu
     Styl textu pro oražení fotografií textem\n%s
     Rozlišení videa
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index a513c676b..b2b5b1679 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -188,9 +188,9 @@
     Textstempel
     Definition eines eigenen Textstempels
     Stempel-Schriftgröße
-    Einstellung der Stempel-Schriftgröße
+    Einstellung der Stempel-Schriftgröße\n%s
     Stempel-Schriftfarbe
-    Einstellung der Stempel-Schriftfarbe
+    Einstellung der Stempel-Schriftfarbe\n%s
     Stempelstil
     Einstellung der Stempelart\n%s
     Benutze Hintergrund-Thread
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 173f9677d..ad84b5ec0 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -187,9 +187,9 @@
     Προσαρμοσμένο κείμενο
     Προσθήκη σφραγίδας με προσαρμοσμένο κείμενο
     Μέγεθος γραματοσειράς
-    Μεγέθος γραμματοσειράς για χρήση στις σφραγίδες
+    Μεγέθος γραμματοσειράς για χρήση στις σφραγίδες\n%s
     Χρώμα γραμματοσειράς
-    Χρώμα γραμματοσειράς για χρήση στις σφραγίδες
+    Χρώμα γραμματοσειράς για χρήση στις σφραγίδες\n%s
     Στυλ κειμένου
     Στυλ κειμένου για χρήση στις σφραγίδες\n%s
     Χρήση νήματος φόντου
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 62bde2666..23a79e7d2 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -188,9 +188,9 @@
     Texto personalizado
     Estampe fotos con un texto personalizado
     Tamaño de fuente
-    Tamaño de fuente a utilizar cuando se estampe texto en las fotos
+    Tamaño de fuente a utilizar cuando se estampe texto en las fotos\n%s
     Color de fuente
-    Color de la fuente a utilizar cuando se estampe texto en las fotos
+    Color de la fuente a utilizar cuando se estampe texto en las fotos\n%s
     Estilo de texto
     Estilo a utilizar cuando se estampe texto en las fotos\n%s
     Utilizar subproceso en segundo plano
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 4b0a5410d..44602feb3 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -185,9 +185,9 @@
     Texte personnalisé
     Incruste un texte personnalisé sur la photo
     Taille de la police
-    Taille de la utilisée lors de l\'incrustation de texte sur les photos
+    Taille de la utilisée lors de l\'incrustation de texte sur les photos\n%s
     Couleur de la police
-    Couleur de la police utilisée pour l\'incrustation de texte sur les photos
+    Couleur de la police utilisée pour l\'incrustation de texte sur les photos\n%s
     Format du texte
     Format du texte utilisé pour l\'incrustation de texte sur les photos\n%s
     Résolution vidéo
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index a57d0b935..666ea50ff 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -188,9 +188,9 @@
     Egyéni szöveg
     Képek feliratozása egyéni szöveggel
     Betűméret
-    A feliratozás szövegének betűmérete
+    A feliratozás szövegének betűmérete\n%s
     Betűszín
-    A feliratozás szövegének betűszíne
+    A feliratozás szövegének betűszíne\n%s
     Szövegstílus
     A feliratozás szövegének stílusa\n%s
     Háttérszál használata
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index a3b7afdc7..9ff159fe7 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -189,9 +189,9 @@
     Testo personalizzato
     Applica il timbro sulle foto con testo personalizzato
     Dimensione carattere
-    Dimensione del carattere da utilizzare per il testo del timbro sulle foto
+    Dimensione del carattere da utilizzare per il testo del timbro sulle foto\n%s
     Colore carattere
-    Colore del carattere da utilizzare per il testo del timbro sulle foto
+    Colore del carattere da utilizzare per il testo del timbro sulle foto\n%s
     Stile testo
     Stile da utilizzare per il testo del timbro sulle foto\n%s
     Usa processo in background
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index fb48ee72f..e9565fc89 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -189,9 +189,9 @@
     文字の内容
     写真に貼り付けるスタンプ文字の内容を決めます
     文字のサイズ
-    写真に貼り付けるスタンプ文字の大きさを決めます
+    写真に貼り付けるスタンプ文字の大きさを決めます\n%s
     文字の色
-    写真に貼り付けるスタンプ文字の色を決めます
+    写真に貼り付けるスタンプ文字の色を決めます\n%s
     文字の装飾
     写真に貼り付けるスタンプ文字の装飾方法を決めます\n%s
     ビデオの解像度
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index d28009eae..2352d116c 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -166,7 +166,7 @@
     임의 텍스트
     사진에 임의의 텍스트를 새깁니다
     글자 크기
-    사진에 글자를 새길때 사용할 글자 크기를 선택합니다
+    사진에 글자를 새길때 사용할 글자 크기를 선택합니다\n%s
     동영상 해상도
     강제 4K UHD 비디오 (실험적)
     후면 카메라로 동영상 촬영시 3840x2160 해상도를 사용합니다 - 기기가 지원하는 경우에만 작동하며 아닐 경우 충돌이 발생할 수 있습니다
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index dcf0b0111..a55078c13 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -188,9 +188,9 @@
     Tilpasset tekst
     Stemple bilder med tilpasset tekst.
     Skrifttypestørrelse
-    Skrifttypestørrelsen som skal brukes når det stemples tekst oppå bilder.
+    Skrifttypestørrelsen som skal brukes når det stemples tekst oppå bilder.\n%s
     Skrifttypefarge
-    Skrifttypefargen som skal brukes når det stemples tekst oppå bilder.
+    Skrifttypefargen som skal brukes når det stemples tekst oppå bilder.\n%s
     Tekststil
     Stilen som skal brukes når det stemples tekst oppå bilder.\n%s
     Bruk bakgrunnstråd
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 0986a5601..c432d78df 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -189,9 +189,9 @@
     Własny tekst
     Określa własny tekst na stemplu
     Rozmiar czcionki
-    Rozmiar czcionki używanej do stemplowania zdjęcia
+    Rozmiar czcionki używanej do stemplowania zdjęcia\n%s
     Kolor czcionki
-    Kolor czcionki używanej do stemplowania zdjęcia
+    Kolor czcionki używanej do stemplowania zdjęcia\n%s
     Styl tekstu
     Określa styl tekstu stempla\n%s
     Użyj wątku w tle
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 96073784a..5784f0cd8 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -167,7 +167,7 @@
     Texto customizado
     Use uma marca d\'água personalizada para fotos
     Tamanho da fonte
-    Tamanho da fonte usada na marca d\'água sobre as fotos
+    Tamanho da fonte usada na marca d\'água sobre as fotos\n%s
     Resolução do vídeo
     Forçar resolução 4K UHD (experimental)
     Habilita a resolução 3840x2160 para gravação de vídeos na câmera traseira - isso funcionará se seu dispositivo suportar e pode falhar se não suportar!
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 880bccb8e..25ab52ba9 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -188,9 +188,9 @@
     Texto personalizado
     Carimbar fotografias com um texto personalizado
     Tamanho do tipo de letra
-    Tamanho do tipo de letra usado para carimbar texto nas fotografias
+    Tamanho do tipo de letra usado para carimbar texto nas fotografias\n%s
     Cor do tipo de letra
-    Cor do tipo de letra usado para carimbar texto nas fotografias
+    Cor do tipo de letra usado para carimbar texto nas fotografias\n%s
     Estilo do texto
     Estilo usado para carimbar texto nas fotografias\n%s
     Usar recurso em segundo plano
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index b6bed1a30..5fe9e4ab5 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -189,9 +189,9 @@
     Подписывать фото
     Добавлять текст на фотографию
     Размер шрифта
-    Размер шрифта для добавляемого на фотографию текста
+    Размер шрифта для добавляемого на фотографию текста\n%s
     Цвет
-    Цвет текста добавляемого на фотографию
+    Цвет текста добавляемого на фотографию\n%s
     Стиль
     Стиль текста добавляемого на фотографию\n%s
     Использовать фоновый поток
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index c06a29b32..2e1e9ff73 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -189,9 +189,9 @@
     Besedilo po meri
     Na fotografije dodaj žig z besedilom po meri
     Velikost pisave
-    Velikost pisave žiga z besedilom
+    Velikost pisave žiga z besedilom\n%s
     Barva pisave
-    Barva pisave žiga z besedilom
+    Barva pisave žiga z besedilom\n%s
     Slog besedila
     Slog besedila žiga\n%s
     Uporabi nit v ozadju
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index c32d8382b..583453dcd 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -179,9 +179,9 @@
     Özel yazı ekleme
     Özel bir yazı ekleyerek fotoğrafları işaretle
     Font(yazı) boyutu
-    Fotoğrafların üzerine eklendiğinde kullanılacak font(yazı)boyutu
+    Fotoğrafların üzerine eklendiğinde kullanılacak font(yazı)boyutu\n%s
     Yazı rengi
-    Fotoğrafların üzerine eklendiğinde kullanılacak yazı rengi
+    Fotoğrafların üzerine eklendiğinde kullanılacak yazı rengi\n%s
     Yazı stili
     Fotoğrafların üzerine eklendiğinde kullanılacak yazı stili\n%s
     Vidyo çözünürlüğü
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 6bd84b540..5fd2d9899 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -189,9 +189,9 @@
     Підпис фото
     Додавання тексту на фотографію
     Розмір шрифту
-    Розмір шрифту для тексту, що додаватиметься на фотографію
+    Розмір шрифту для тексту, що додаватиметься на фотографію\n%s
     Колір
-    Колір для тексту, що додаватиметься на фотографію
+    Колір для тексту, що додаватиметься на фотографію\n%s
     Стиль
     Стиль для тексту, що додаватиметься на фотографію\n%s
     Використовувати фоновий потік
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index adb44a642..df847eb3e 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -188,9 +188,9 @@
     Văn bản tùy chỉnh
     Gắn vào ảnh một văn bản tùy chỉnh
     Kích thước phông
-    Kích thước phông chữ được sử dụng khi gắn văn bản vào ảnh
+    Kích thước phông chữ được sử dụng khi gắn văn bản vào ảnh\n%s
     Màu phông
-    Màu phông chữ được sử dụng khi gắn văn bản vào ảnh
+    Màu phông chữ được sử dụng khi gắn văn bản vào ảnh\n%s
     Kiểu văn bản
     Kiểu văn bản được sử dụng khi gắn vào ảnh\n%s
     Chạy dưới nền
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 0fa9a0319..4246c5205 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -183,9 +183,9 @@
     自定义水印
     使用自定义的文本水印
     字体大小
-    水印文本的字体大小
+    水印文本的字体大小\n%s
     字体颜色
-    水印文本的字体颜色
+    水印文本的字体颜色\n%s
     文本样式
     水印文本的样式\n%s
     视频分辨率
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 624a8eed3..667e9c3c8 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -189,9 +189,9 @@
     自訂文字
     用自訂文字標記照片
     字型大小
-    當在照片上標記文字時使用的字型大小
+    當在照片上標記文字時使用的字型大小\n%s
     字型顏色
-    當在照片上標記文字時使用的字型顏色
+    當在照片上標記文字時使用的字型顏色\n%s
     文字樣式
     當在照片上標記文字時使用的樣式\n%s
     使用背景執行緒
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 05207476b..32c41e225 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -190,9 +190,9 @@
     Custom text
     Stamp photos with a custom text
     Font size
-    Font size to use when stamping text onto photos
+    Font size to use when stamping text onto photos\n%s
     Font color
-    Font color to use when stamping text onto photos
+    Font color to use when stamping text onto photos\n%s
     Text style
     Style to use when stamping text onto photos\n%s
     Use background thread
-- 
GitLab


From be68e79257f307c4008ce359a11338034d1a7389 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Mon, 3 Oct 2022 22:29:10 +0100
Subject: [PATCH 052/117] Camera2 extension night mode now adds "_Night" to
 filename.

---
 _docs/history.html                             |  1 +
 .../net/sourceforge/opencamera/ImageSaver.java | 18 +++++++++++-------
 .../opencamera/MyApplicationInterface.java     | 10 ++++++++--
 3 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index 2426f8265..ca9823525 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -64,6 +64,7 @@ UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
 UPDATED Make zoom seekbar snap to powers of two (for Camera2 API).
 UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API).
 UPDATED Improved look of on-screen level line.
+UPDATED Camera2 extension night mode now adds "_Night" to filename.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 UPDATED Use system toasts without custom views when appropriate.
 UPDATED Display current value for photo stamp font size and colour in preference summary.
diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index 3fd7bfe8e..6cdf15c85 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -108,9 +108,10 @@ public class ImageSaver extends Thread {
         final Type type;
         enum ProcessType {
             NORMAL,
-            HDR,
+            HDR, // also covers DRO, if only 1 image in the request
             AVERAGE,
-            PANORAMA
+            PANORAMA,
+            X_NIGHT
         }
         final ProcessType process_type; // for type==JPEG
         final boolean force_suffix; // affects filename suffixes for saving jpeg_images: if true, filenames will always be appended with a suffix like _0, even if there's only 1 image in jpeg_images
@@ -565,7 +566,7 @@ public class ImageSaver extends Thread {
      *  successfully.
      */
     boolean saveImageJpeg(boolean do_in_background,
-                          boolean is_hdr,
+                          Request.ProcessType processType,
                           boolean force_suffix,
                           int suffix_offset,
                           boolean save_expo,
@@ -597,7 +598,7 @@ public class ImageSaver extends Thread {
         }
         return saveImage(do_in_background,
                 false,
-                is_hdr,
+                processType,
                 force_suffix,
                 suffix_offset,
                 save_expo,
@@ -641,7 +642,7 @@ public class ImageSaver extends Thread {
         }
         return saveImage(do_in_background,
                 true,
-                false,
+                Request.ProcessType.NORMAL,
                 force_suffix,
                 suffix_offset,
                 false,
@@ -780,7 +781,7 @@ public class ImageSaver extends Thread {
      */
     private boolean saveImage(boolean do_in_background,
                               boolean is_raw,
-                              boolean is_hdr,
+                              Request.ProcessType processType,
                               boolean force_suffix,
                               int suffix_offset,
                               boolean save_expo,
@@ -815,7 +816,7 @@ public class ImageSaver extends Thread {
         //do_in_background = false;
 
         Request request = new Request(is_raw ? Request.Type.RAW : Request.Type.JPEG,
-                is_hdr ? Request.ProcessType.HDR : Request.ProcessType.NORMAL,
+                processType,
                 force_suffix,
                 suffix_offset,
                 save_expo ? Request.SaveBase.SAVEBASE_ALL : Request.SaveBase.SAVEBASE_NONE,
@@ -1817,6 +1818,9 @@ public class ImageSaver extends Thread {
             byte [] image = request.jpeg_images.get(i);
             boolean multiple_jpegs = request.jpeg_images.size() > 1 && !first_only;
             String filename_suffix = (multiple_jpegs || request.force_suffix) ? suffix + (i + request.suffix_offset) : "";
+            if( request.process_type == Request.ProcessType.X_NIGHT ) {
+                filename_suffix = "_Night" + filename_suffix;
+            }
             boolean share_image = share && (i == mid_image);
             if( !saveSingleImageNow(request, image, null, filename_suffix, update_thumbnail, share_image, false, false) ) {
                 if( MyDebug.LOG )
diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
index c5a204c0b..1e2987a90 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
@@ -3421,9 +3421,15 @@ public class MyApplicationInterface extends BasicApplicationInterface {
             success = true;
         }
         else {
-            boolean is_hdr = photo_mode == PhotoMode.DRO || photo_mode == PhotoMode.HDR;
+            ImageSaver.Request.ProcessType processType;
+            if( photo_mode == PhotoMode.DRO || photo_mode == PhotoMode.HDR )
+                processType = ImageSaver.Request.ProcessType.HDR;
+            else if( photo_mode == PhotoMode.X_Night )
+                processType = ImageSaver.Request.ProcessType.X_NIGHT;
+            else
+                processType = ImageSaver.Request.ProcessType.NORMAL;
             boolean force_suffix = forceSuffix(photo_mode);
-            success = imageSaver.saveImageJpeg(do_in_background, is_hdr,
+            success = imageSaver.saveImageJpeg(do_in_background, processType,
                     force_suffix,
                     // N.B., n_capture_images will be 1 for first image, not 0, so subtract 1 so we start off from _0.
                     // (It wouldn't be a huge problem if we did start from _1, but it would be inconsistent with the naming
-- 
GitLab


From 620e3f141b775628d6e7b47a317b289f804b0944 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Tue, 4 Oct 2022 22:37:36 +0100
Subject: [PATCH 053/117] Should only display auto_stabilise_not_supported
 toast if auto-level isn't supported at all.

---
 _docs/history.html                                         | 2 ++
 .../main/java/net/sourceforge/opencamera/MainActivity.java | 7 +++++++
 .../main/java/net/sourceforge/opencamera/ui/MainUI.java    | 3 ++-
 3 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/_docs/history.html b/_docs/history.html
index ca9823525..d9931fec1 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -56,6 +56,8 @@ FIXED   Face detection on-screen icon shouldn't show in camera vendor extension
         supported).
 FIXED   For Camera2 API, red eye flash was incorrectly being shown even on devices that didn't
         support it.
+FIXED   Doesn't display error message if using volume keys to turn auto-level on or off in RAW or
+        Panorama mode (unless device doesn't support auto-level at all).
 ADDED   Shading for auto-level and crop guides, to darken the preview outside of the region of
         interest.
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
index 4ff010df2..0b8512c37 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -5300,6 +5300,13 @@ public class MainActivity extends AppCompatActivity {
         return this.supports_auto_stabilise;
     }
 
+    /** Returns whether the device supports auto-level at all. Most callers probably want to use
+     *  supportsAutoStabilise() which also checks whether auto-level is allowed with current options.
+     */
+    public boolean deviceSupportsAutoStabilise() {
+        return this.supports_auto_stabilise;
+    }
+
     public boolean supportsDRO() {
         if( applicationInterface.isRawOnly(MyApplicationInterface.PhotoMode.DRO) )
             return false; // if not saving JPEGs, no point having DRO mode, as it won't affect the RAW images
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 b1b7b9a14..1c2393b90 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java
@@ -2841,7 +2841,8 @@ public class MainUI {
                             main_activity.getApplicationInterface().getDrawPreview().updateSettings(); // because we cache the auto-stabilise setting
                             this.destroyPopup(); // need to recreate popup in order to update the auto-level checkbox
                         }
-                        else {
+                        else if( !main_activity.deviceSupportsAutoStabilise() ) {
+                            // n.b., need to check deviceSupportsAutoStabilise() - if we're in e.g. Panorama mode, we shouldn't display a toast (as then supportsAutoStabilise() returns false even if auto-level is supported on the device)
                             main_activity.getPreview().showToast(main_activity.getChangedAutoStabiliseToastBoxer(), R.string.auto_stabilise_not_supported);
                         }
                         return true;
-- 
GitLab


From 468d38718657d1ecb76e04f481e13ae37aad6dff Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 5 Oct 2022 22:29:32 +0100
Subject: [PATCH 054/117] Add logging.

---
 .../java/net/sourceforge/opencamera/ImageSaver.java   | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index 6cdf15c85..44b7ae75f 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3542,6 +3542,8 @@ public class ImageSaver extends Thread {
                 Log.d(TAG, "add additional exif info");
             try {
                 ExifInterfaceHolder exif_holder = createExifInterface(picFile, saveUri);
+                if( MyDebug.LOG )
+                    Log.d(TAG, "*** time after create exif: " + (System.currentTimeMillis() - time_s));
                 try {
                     ExifInterface exif = exif_holder.getExif();
                     if( exif != null ) {
@@ -3656,10 +3658,17 @@ public class ImageSaver extends Thread {
             Log.d(TAG, "setDateTimeExif");
         String exif_datetime = exif.getAttribute(ExifInterface.TAG_DATETIME);
         if( exif_datetime != null ) {
-            if( MyDebug.LOG )
+            if( MyDebug.LOG ) {
                 Log.d(TAG, "write datetime tags: " + exif_datetime);
+                Log.d(TAG, "TAG_DATETIME_ORIGINAL was: " + exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+                Log.d(TAG, "TAG_DATETIME_DIGITIZED was: " + exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED));
+            }
             exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, exif_datetime);
             exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, exif_datetime);
+            if( MyDebug.LOG ) {
+                Log.d(TAG, "TAG_DATETIME_ORIGINAL is now: " + exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+                Log.d(TAG, "TAG_DATETIME_DIGITIZED is now: " + exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED));
+            }
         }
     }
 
-- 
GitLab


From 2bb4eb555fcc32894512f9eb58be650ef04626eb Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 5 Oct 2022 22:33:11 +0100
Subject: [PATCH 055/117] Fix ordering of logging.

---
 app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index 44b7ae75f..36c5e72d4 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3640,9 +3640,9 @@ public class ImageSaver extends Thread {
             exif.setAttribute(ExifInterface.TAG_ARTIST, custom_tag_artist);
         }
         if( custom_tag_copyright != null && custom_tag_copyright.length() > 0 ) {
-            exif.setAttribute(ExifInterface.TAG_COPYRIGHT, custom_tag_copyright);
             if( MyDebug.LOG )
                 Log.d(TAG, "apply TAG_COPYRIGHT: " + custom_tag_copyright);
+            exif.setAttribute(ExifInterface.TAG_COPYRIGHT, custom_tag_copyright);
         }
     }
 
-- 
GitLab


From 7e8b92f6e4f30547a41520540e45e81d76f3c183 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Wed, 5 Oct 2022 22:40:31 +0100
Subject: [PATCH 056/117] Add logging.

---
 app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index 36c5e72d4..a7b21b03a 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3548,7 +3548,12 @@ public class ImageSaver extends Thread {
                     ExifInterface exif = exif_holder.getExif();
                     if( exif != null ) {
                         modifyExif(exif, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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);
+
+                        if( MyDebug.LOG )
+                            Log.d(TAG, "*** time after modifyExif: " + (System.currentTimeMillis() - time_s));
                         exif.saveAttributes();
+                        if( MyDebug.LOG )
+                            Log.d(TAG, "*** time after saveAttributes: " + (System.currentTimeMillis() - time_s));
                     }
                 }
                 finally {
-- 
GitLab


From b7801e9c8b6e9fd22c71870550b24c186b61e0df Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Thu, 6 Oct 2022 21:09:52 +0100
Subject: [PATCH 057/117] Add comment.

---
 app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index a7b21b03a..33f5f2187 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3536,7 +3536,9 @@ public class ImageSaver extends Thread {
     private void updateExif(Request request, File picFile, Uri saveUri) throws IOException {
         if( MyDebug.LOG )
             Log.d(TAG, "updateExif: " + picFile);
-        if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) || request.using_camera_extensions || needGPSTimestampHack(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
+        if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) ||
+                request.using_camera_extensions || // when using camera extensions, we need to call modifyExif() to fix up various missing tags
+                needGPSExifFix(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
             long time_s = System.currentTimeMillis();
             if( MyDebug.LOG )
                 Log.d(TAG, "add additional exif info");
-- 
GitLab


From 4bee8e707d224430a05aeadece822fb18f4acb60 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Thu, 6 Oct 2022 21:11:06 +0100
Subject: [PATCH 058/117] Fix last release.

---
 app/src/main/java/net/sourceforge/opencamera/ImageSaver.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index 33f5f2187..b2a914c78 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3538,7 +3538,7 @@ public class ImageSaver extends Thread {
             Log.d(TAG, "updateExif: " + picFile);
         if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) ||
                 request.using_camera_extensions || // when using camera extensions, we need to call modifyExif() to fix up various missing tags
-                needGPSExifFix(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
+                needGPSTimestampHack(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
             long time_s = System.currentTimeMillis();
             if( MyDebug.LOG )
                 Log.d(TAG, "add additional exif info");
-- 
GitLab


From 10b2b812bad20e06e1bee6477c416220a5d525d3 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Thu, 6 Oct 2022 21:20:40 +0100
Subject: [PATCH 059/117] Workaround for Pixel 6 Pro on camera2 not saving
 location.

---
 _docs/history.html                            |  1 +
 .../sourceforge/opencamera/ImageSaver.java    | 24 +++++++++++++------
 2 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index d9931fec1..b02282b3c 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -56,6 +56,7 @@ FIXED   Face detection on-screen icon shouldn't show in camera vendor extension
         supported).
 FIXED   For Camera2 API, red eye flash was incorrectly being shown even on devices that didn't
         support it.
+FIXED   Not saving location exif information for Camera2 API on some devices (e.g., Pixel 6 Pro).
 FIXED   Doesn't display error message if using volume keys to turn auto-level on or off in RAW or
         Panorama mode (unless device doesn't support auto-level at all).
 ADDED   Shading for auto-level and crop guides, to darken the preview outside of the region of
diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
index b2a914c78..85c80971a 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java
@@ -3538,7 +3538,7 @@ public class ImageSaver extends Thread {
             Log.d(TAG, "updateExif: " + picFile);
         if( request.store_geo_direction || request.store_ypr || hasCustomExif(request.custom_tag_artist, request.custom_tag_copyright) ||
                 request.using_camera_extensions || // when using camera extensions, we need to call modifyExif() to fix up various missing tags
-                needGPSTimestampHack(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
+                needGPSExifFix(request.type == Request.Type.JPEG, request.using_camera2, request.store_location) ) {
             long time_s = System.currentTimeMillis();
             if( MyDebug.LOG )
                 Log.d(TAG, "add additional exif info");
@@ -3595,14 +3595,19 @@ public class ImageSaver extends Thread {
                 Log.d(TAG, "UserComment: " + exif.getAttribute(ExifInterface.TAG_USER_COMMENT));
         }
         setCustomExif(exif, custom_tag_artist, custom_tag_copyright);
+
+        if( store_location && ( !exif.hasAttribute(ExifInterface.TAG_GPS_LATITUDE) || !exif.hasAttribute(ExifInterface.TAG_GPS_LATITUDE) ) ) {
+            // We need this when using camera extensions (since Camera API doesn't support location for camera extensions).
+            // But some devices (e.g., Pixel 6 Pro with Camera2 API) seem to not store location data, so we always check if we need to add it.
+            if( MyDebug.LOG )
+                Log.d(TAG, "store location"); // don't log location for privacy reasons!
+            exif.setGpsInfo(location);
+        }
+
         if( using_camera_extensions ) {
             addDateTimeExif(exif, current_date);
-            if( store_location ) {
-                // also need to store geotagging, since Camera API doesn't support doing this for camera extensions
-                exif.setGpsInfo(location);
-            }
         }
-        else if( needGPSTimestampHack(is_jpeg, using_camera2, store_location) ) {
+        else if( needGPSExifFix(is_jpeg, using_camera2, store_location) ) {
             fixGPSTimestamp(exif, current_date);
         }
     }
@@ -3750,7 +3755,12 @@ public class ImageSaver extends Thread {
             Log.d(TAG, "fixGPSTimestamp exit");
     }
 
-    private boolean needGPSTimestampHack(boolean is_jpeg, boolean using_camera2, boolean store_location) {
+    /** Whether we need to fix up issues with location.
+     *  See comments in fixGPSTimestamp(), where some devices with Camera2 need fixes for TAG_GPS_DATESTAMP and TAG_GPS_TIMESTAMP.
+     *  Also some devices (e.g. Pixel 6 Pro) have problem that location is not stored in images with Camera2 API, so we need to
+     *  enter modifyExif() to add it if not present.
+     */
+    private boolean needGPSExifFix(boolean is_jpeg, boolean using_camera2, boolean store_location) {
         if( is_jpeg && using_camera2 ) {
             return store_location;
         }
-- 
GitLab


From 666b707d6634a1897c3b43aaf8a0d32458981391 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Fri, 7 Oct 2022 21:38:43 +0100
Subject: [PATCH 060/117] Display message to hold device steady when using
 X-Night photo mode.

---
 _docs/history.html                                            | 1 +
 .../net/sourceforge/opencamera/MyApplicationInterface.java    | 4 ++++
 2 files changed, 5 insertions(+)

diff --git a/_docs/history.html b/_docs/history.html
index b02282b3c..d6eef9c12 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -61,6 +61,7 @@ FIXED   Doesn't display error message if using volume keys to turn auto-level on
         Panorama mode (unless device doesn't support auto-level at all).
 ADDED   Shading for auto-level and crop guides, to darken the preview outside of the region of
         interest.
+ADDED   Display message to hold device steady when using X-Night photo mode.
 UPDATED Applied a timeout of 1 second for focusing with Camera2 API.
 UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
         zoom out to ultra-wide camera.
diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
index 1e2987a90..30c71d6d3 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java
@@ -2736,6 +2736,10 @@ public class MyApplicationInterface extends BasicApplicationInterface {
         n_capture_images = 0;
         n_capture_images_raw = 0;
         drawPreview.onCaptureStarted();
+
+        if( getPhotoMode() == PhotoMode.X_Night ) {
+            main_activity.getPreview().showToast(null, R.string.preference_nr_mode_low_light_message, true);
+        }
     }
 
     @Override
-- 
GitLab


From 7e3bae5741ec3e829c1172a93631ca7c52544056 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Fri, 7 Oct 2022 22:34:58 +0100
Subject: [PATCH 061/117] Catch some exceptions and fail instead.

---
 .../net/sourceforge/opencamera/TestUtils.java | 49 +++++++++++++++----
 .../opencamera/test/MainActivityTest.java     |  8 +--
 2 files changed, 44 insertions(+), 13 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index ca7099080..d9e92b0d7 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -98,16 +98,27 @@ public class TestUtils {
         return uri;
     }
 
-    public static Bitmap getBitmapFromFile(MainActivity activity, String filename) throws FileNotFoundException {
+    public static Bitmap getBitmapFromFile(MainActivity activity, String filename) {
         return getBitmapFromFile(activity, filename, 1);
     }
 
+    public static Bitmap getBitmapFromFile(MainActivity activity, String filename, int inSampleSize) {
+        try {
+            return getBitmapFromFileCore(activity, filename, inSampleSize);
+        }
+        catch(FileNotFoundException e) {
+            e.printStackTrace();
+            fail("FileNotFoundException loading: " + filename);
+            return null;
+        }
+    }
+
     /** Loads bitmap from supplied filename.
      *  Note that on Android 10+ (with scoped storage), this uses Storage Access Framework, which
      *  means Open Camera must have SAF permission to the folder DCIM/testOpenCamera.
      */
-    public static Bitmap getBitmapFromFile(MainActivity activity, String filename, int inSampleSize) throws FileNotFoundException {
-        Log.d(TAG, "getBitmapFromFile: " + filename);
+    private static Bitmap getBitmapFromFileCore(MainActivity activity, String filename, int inSampleSize) throws FileNotFoundException {
+        Log.d(TAG, "getBitmapFromFileCore: " + filename);
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inMutable = true;
         //options.inSampleSize = inSampleSize;
@@ -248,8 +259,18 @@ public class TestUtils {
         return uri;
     }
 
-    public static void saveBitmap(MainActivity activity, Bitmap bitmap, String name) throws IOException {
-        Log.d(TAG, "saveBitmap: " + name);
+    public static void saveBitmap(MainActivity activity, Bitmap bitmap, String name) {
+        try {
+            saveBitmapCore(activity, bitmap, name);
+        }
+        catch(IOException e) {
+            e.printStackTrace();
+            fail("IOException saving: " + name);
+        }
+    }
+
+    private static void saveBitmapCore(MainActivity activity, Bitmap bitmap, String name) throws IOException {
+        Log.d(TAG, "saveBitmapCore: " + name);
 
         File file = null;
         ContentValues contentValues = null;
@@ -357,7 +378,7 @@ public class TestUtils {
         return new HistogramDetails(min_value, median_value, max_value);
     }
 
-    public static HistogramDetails subTestHDR(MainActivity activity, List inputs, String output_name, boolean test_dro, int iso, long exposure_time) throws IOException, InterruptedException {
+    public static HistogramDetails subTestHDR(MainActivity activity, List inputs, String output_name, boolean test_dro, int iso, long exposure_time) {
         return subTestHDR(activity, inputs, output_name, test_dro, iso, exposure_time, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD);
     }
 
@@ -370,7 +391,7 @@ public class TestUtils {
      * @param exposure_time The exposure time of the middle image (for testing Open Camera's "smart" contrast enhancement)
      */
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    public static HistogramDetails subTestHDR(MainActivity activity, List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm/*, HDRTestCallback test_callback*/) throws IOException, InterruptedException {
+    public static HistogramDetails subTestHDR(MainActivity activity, List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm/*, HDRTestCallback test_callback*/) {
         Log.d(TAG, "subTestHDR");
 
         if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
@@ -378,7 +399,12 @@ public class TestUtils {
             return null;
         }
 
-        Thread.sleep(1000); // wait for camera to open
+        try {
+            Thread.sleep(1000); // wait for camera to open
+        }
+        catch(InterruptedException e) {
+            e.printStackTrace();
+        }
 
         Bitmap dro_bitmap_in = null;
         if( test_dro ) {
@@ -427,7 +453,12 @@ public class TestUtils {
             inputs.get(0).recycle();
             inputs.clear();
         }
-        Thread.sleep(500);
+        try {
+            Thread.sleep(500);
+        }
+        catch(InterruptedException e) {
+            e.printStackTrace();
+        }
 
         return hdrHistogramDetails;
     }
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 b1d1f8bf0..7b9f8fbf3 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -2,7 +2,7 @@ package net.sourceforge.opencamera.test;
 
 import java.io.File;
 //import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+//import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
@@ -12438,11 +12438,11 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sat, 8 Oct 2022 17:46:40 +0100
Subject: [PATCH 062/117] Fix typo in comment.

---
 .../opencamera/cameracontroller/CameraController2.java          | 2 +-
 1 file changed, 1 insertion(+), 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 add00abfa..12a2b64d7 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -7082,7 +7082,7 @@ public class CameraController2 extends CameraController {
                 try {
                     modified_from_camera_settings = true;
                     //setRepeatingRequest(requests.get(0));
-                    if( use_expo_fast_burst && burst_type == BurstType.BURSTTYPE_EXPO ) { // alway use slow burst for focus bracketing
+                    if( use_expo_fast_burst && burst_type == BurstType.BURSTTYPE_EXPO ) { // always use slow burst for focus bracketing
                         if( MyDebug.LOG )
                             Log.d(TAG, "using fast burst");
                         int sequenceId = captureSession.captureBurst(requests, previewCaptureCallback, handler);
-- 
GitLab


From 3276d8fc9828dadb1147dae442efdde10af0aae4 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 8 Oct 2022 21:59:34 +0100
Subject: [PATCH 063/117] Refactor subTestAvg to TestUtils.

---
 .../net/sourceforge/opencamera/TestUtils.java | 126 ++++++++++
 .../opencamera/test/MainActivityTest.java     | 225 +++++-------------
 2 files changed, 183 insertions(+), 168 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index d9e92b0d7..9938868ee 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -19,6 +19,7 @@ import android.os.ParcelFileDescriptor;
 import android.preference.PreferenceManager;
 import android.provider.DocumentsContract;
 import android.provider.MediaStore;
+import android.renderscript.Allocation;
 import android.util.Log;
 
 import androidx.annotation.RequiresApi;
@@ -31,6 +32,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.List;
 
 /** Helper class for testing. This method should not include any code specific to any test framework
@@ -491,4 +493,128 @@ public class TestUtils {
         assertTrue(Math.abs(exp_median_value - hdrHistogramDetails.median_value) <= 3);
         assertTrue(Math.abs(exp_max_value - hdrHistogramDetails.max_value) <= 3);
     }
+
+    public interface TestAvgCallback {
+        void doneProcessAvg(int index); // called after every call to HDRProcessor.processAvg()
+    }
+
+    /** The following testAvgX tests test the Avg noise reduction algorithm on a given set of input images.
+     *  By testing on a fixed sample, this makes it easier to finetune the algorithm for quality and performance.
+     *  To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/
+     *  folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes
+     *  time to transfer to the device everytime we run the tests.
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public static HistogramDetails subTestAvg(MainActivity activity, List inputs, String output_name, int iso, long exposure_time, float zoom_factor, TestAvgCallback cb) {
+        Log.d(TAG, "subTestAvg");
+
+        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
+            Log.d(TAG, "renderscript requires Android Lollipop or better");
+            return null;
+        }
+
+        try {
+            Thread.sleep(1000); // wait for camera to open
+        }
+        catch(InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        /*Bitmap nr_bitmap = getBitmapFromFile(activity, inputs.get(0));
+        long time_s = System.currentTimeMillis();
+        try {
+            for(int i=1;i times = new ArrayList<>();
+            long time_s = System.currentTimeMillis();
+            HDRProcessor.AvgData avg_data = activity.getApplicationInterface().getHDRProcessor().processAvg(bitmap0, bitmap1, avg_factor, iso, exposure_time, zoom_factor);
+            Allocation allocation = avg_data.allocation_out;
+            times.add(System.currentTimeMillis() - time_s);
+            // processAvg recycles both bitmaps
+            if( cb != null ) {
+                cb.doneProcessAvg(1);
+            }
+
+            for(int i=2;i inputs, String output_name, boolean test_dro, int iso, long exposure_time) throws IOException, InterruptedException {
+    private TestUtils.HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time) {
         return TestUtils.subTestHDR(mActivity, inputs, output_name, test_dro, iso, exposure_time);
     }
 
-    private TestUtils.HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm/*, HDRTestCallback test_callback*/) throws IOException, InterruptedException {
+    private TestUtils.HistogramDetails subTestHDR(List inputs, String output_name, boolean test_dro, int iso, long exposure_time, HDRProcessor.TonemappingAlgorithm tonemapping_algorithm/*, HDRTestCallback test_callback*/) {
         return TestUtils.subTestHDR(mActivity, inputs, output_name, test_dro, iso, exposure_time, tonemapping_algorithm);
     }
 
@@ -14446,118 +14445,8 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 inputs, String output_name, int iso, long exposure_time, float zoom_factor, TestAvgCallback cb) throws IOException, InterruptedException {
-        Log.d(TAG, "subTestAvg");
-
-        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
-            Log.d(TAG, "renderscript requires Android Lollipop or better");
-            return null;
-        }
-
-        Thread.sleep(1000); // wait for camera to open
-
-        /*Bitmap nr_bitmap = getBitmapFromFile(inputs.get(0));
-        long time_s = System.currentTimeMillis();
-        try {
-            for(int i=1;i times = new ArrayList<>();
-            long time_s = System.currentTimeMillis();
-            HDRProcessor.AvgData avg_data = mActivity.getApplicationInterface().getHDRProcessor().processAvg(bitmap0, bitmap1, avg_factor, iso, exposure_time, zoom_factor);
-            Allocation allocation = avg_data.allocation_out;
-            times.add(System.currentTimeMillis() - time_s);
-            // processAvg recycles both bitmaps
-            if( cb != null ) {
-                cb.doneProcessAvg(1);
-            }
-
-            for(int i=2;i inputs, String output_name, int iso, long exposure_time, float zoom_factor, TestUtils.TestAvgCallback cb) {
+        return TestUtils.subTestAvg(mActivity, inputs, output_name, iso, exposure_time, zoom_factor, cb);
     }
 
     /** Tests Avg algorithm on test samples "testAvg1".
@@ -14575,7 +14464,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2
Date: Sun, 9 Oct 2022 19:23:40 +0100
Subject: [PATCH 064/117] Refactor subTestPanorama() to TestUtils.

---
 .../net/sourceforge/opencamera/TestUtils.java | 148 ++++++++++++++++++
 .../opencamera/test/MainActivityTest.java     | 143 +----------------
 2 files changed, 150 insertions(+), 141 deletions(-)

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
index 9938868ee..1c4f75e8b 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java
@@ -617,4 +617,152 @@ public class TestUtils {
 
         return hdrHistogramDetails;
     }
+
+    /**
+     * @param panorama_pics_per_screen The value of panorama_pics_per_screen used when taking the input photos.
+     * @param camera_angle_x The value of preview.getViewAngleX(for_preview=false) (in degrees) when taking the input photos (on the device used).
+     * @param camera_angle_y The value of preview.getViewAngleY(for_preview=false) (in degrees) when taking the input photos (on the device used).
+     */
+    public static void subTestPanorama(MainActivity activity, List 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) {
+        Log.d(TAG, "subTestPanorama");
+
+        // we set panorama_pics_per_screen in the test rather than using MyApplicationInterface.panorama_pics_per_screen,
+        // in case the latter value is changed
+
+        boolean first = true;
+        Matrix scale_matrix = null;
+        int bitmap_width = 0;
+        int bitmap_height = 0;
+        List bitmaps = new ArrayList<>();
+        for(String input : inputs) {
+            Bitmap bitmap = getBitmapFromFile(activity, input);
+
+            if( first ) {
+                bitmap_width = bitmap.getWidth();
+                bitmap_height = bitmap.getHeight();
+                Log.d(TAG, "bitmap_width: " + bitmap_width);
+                Log.d(TAG, "bitmap_height: " + bitmap_height);
+
+                final int max_height = 2080;
+                //final int max_height = 2079; // test non power of 2
+                if( bitmap_height > max_height ) {
+                    float scale = ((float)max_height) / ((float)bitmap_height);
+                    Log.d(TAG, "scale: " + scale);
+                    scale_matrix = new Matrix();
+                    scale_matrix.postScale(scale, scale);
+                }
+
+                first = false;
+            }
+
+            // downscale
+            if( scale_matrix != null ) {
+                Bitmap new_bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap_width, bitmap_height, scale_matrix, true);
+                bitmap.recycle();
+                bitmap = new_bitmap;
+            }
+
+            bitmaps.add(bitmap);
+        }
+
+        bitmap_width = bitmaps.get(0).getWidth();
+        bitmap_height = bitmaps.get(0).getHeight();
+        Log.d(TAG, "bitmap_width is now: " + bitmap_width);
+        Log.d(TAG, "bitmap_height is now: " + bitmap_height);
+
+
+        /*ImageSaver.GyroDebugInfo gyro_debug_info = null;
+        if( gyro_debug_info_filename != null ) {
+            InputStream inputStream;
+            try {
+                inputStream = new FileInputStream(gyro_debug_info_filename);
+            }
+            catch(FileNotFoundException e) {
+                Log.e(TAG, "failed to load gyro debug info file: " + gyro_debug_info_filename);
+                e.printStackTrace();
+                throw new RuntimeException();
+            }
+
+            gyro_debug_info = new ImageSaver.GyroDebugInfo();
+            if( !ImageSaver.readGyroDebugXml(inputStream, gyro_debug_info) ) {
+                Log.e(TAG, "failed to read gyro debug xml");
+                throw new RuntimeException();
+            }
+            else if( gyro_debug_info.image_info.size() != bitmaps.size() ) {
+                Log.e(TAG, "gyro debug xml has unexpected number of images: " + gyro_debug_info.image_info.size());
+                throw new RuntimeException();
+            }
+        }*/
+        //bitmaps.subList(2,bitmaps.size()).clear(); // test
+
+        Bitmap panorama = null;
+        try {
+            final boolean crop = true;
+            //final boolean crop = false; // test
+            panorama = activity.getApplicationInterface().getPanoramaProcessor().panorama(bitmaps, panorama_pics_per_screen, camera_angle_y, crop);
+        }
+        catch(PanoramaProcessorException e) {
+            e.printStackTrace();
+            fail();
+        }
+
+        saveBitmap(activity, panorama, output_name);
+        try {
+            Thread.sleep(500);
+        }
+        catch(InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        // check we've cropped correctly:
+        final float black_factor = 0.9f;
+        // top:
+        int n_black = 0;
+        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
+                n_black++;
+            }
+        }
+        if( n_black >= panorama.getWidth()*black_factor ) {
+            Log.e(TAG, "too many black pixels on top border: " + n_black);
+            fail();
+        }
+        // bottom:
+        n_black = 0;
+        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
+                n_black++;
+            }
+        }
+        if( n_black >= panorama.getWidth()*black_factor ) {
+            Log.e(TAG, "too many black pixels on bottom border: " + n_black);
+            fail();
+        }
+        // left:
+        n_black = 0;
+        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
+                n_black++;
+            }
+        }
+        if( n_black >= panorama.getHeight()*black_factor ) {
+            Log.e(TAG, "too many black pixels on left border: " + n_black);
+            fail();
+        }
+        // right:
+        n_black = 0;
+        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
+                n_black++;
+            }
+        }
+        if( n_black >= panorama.getHeight()*black_factor ) {
+            Log.e(TAG, "too many black pixels on right border: " + n_black);
+            fail();
+        }
+    }
 }
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 c486afa6d..681d3de72 100644
--- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java
@@ -16694,147 +16694,8 @@ 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");
-
-        // we set panorama_pics_per_screen in the test rather than using MyApplicationInterface.panorama_pics_per_screen,
-        // in case the latter value is changed
-
-        boolean first = true;
-        Matrix scale_matrix = null;
-        int bitmap_width = 0;
-        int bitmap_height = 0;
-        List bitmaps = new ArrayList<>();
-        for(String input : inputs) {
-            Bitmap bitmap = getBitmapFromFile(input);
-
-            if( first ) {
-                bitmap_width = bitmap.getWidth();
-                bitmap_height = bitmap.getHeight();
-                Log.d(TAG, "bitmap_width: " + bitmap_width);
-                Log.d(TAG, "bitmap_height: " + bitmap_height);
-
-                final int max_height = 2080;
-                //final int max_height = 2079; // test non power of 2
-                if( bitmap_height > max_height ) {
-                    float scale = ((float)max_height) / ((float)bitmap_height);
-                    Log.d(TAG, "scale: " + scale);
-                    scale_matrix = new Matrix();
-                    scale_matrix.postScale(scale, scale);
-                }
-
-                first = false;
-            }
-
-            // downscale
-            if( scale_matrix != null ) {
-                Bitmap new_bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap_width, bitmap_height, scale_matrix, true);
-                bitmap.recycle();
-                bitmap = new_bitmap;
-            }
-
-            bitmaps.add(bitmap);
-        }
-
-        bitmap_width = bitmaps.get(0).getWidth();
-        bitmap_height = bitmaps.get(0).getHeight();
-        Log.d(TAG, "bitmap_width is now: " + bitmap_width);
-        Log.d(TAG, "bitmap_height is now: " + bitmap_height);
-
-
-        /*ImageSaver.GyroDebugInfo gyro_debug_info = null;
-        if( gyro_debug_info_filename != null ) {
-            InputStream inputStream;
-            try {
-                inputStream = new FileInputStream(gyro_debug_info_filename);
-            }
-            catch(FileNotFoundException e) {
-                Log.e(TAG, "failed to load gyro debug info file: " + gyro_debug_info_filename);
-                e.printStackTrace();
-                throw new RuntimeException();
-            }
-
-            gyro_debug_info = new ImageSaver.GyroDebugInfo();
-            if( !ImageSaver.readGyroDebugXml(inputStream, gyro_debug_info) ) {
-                Log.e(TAG, "failed to read gyro debug xml");
-                throw new RuntimeException();
-            }
-            else if( gyro_debug_info.image_info.size() != bitmaps.size() ) {
-                Log.e(TAG, "gyro debug xml has unexpected number of images: " + gyro_debug_info.image_info.size());
-                throw new RuntimeException();
-            }
-        }*/
-        //bitmaps.subList(2,bitmaps.size()).clear(); // test
-
-        Bitmap panorama = null;
-        try {
-            final boolean crop = true;
-            //final boolean crop = false; // test
-            panorama = mActivity.getApplicationInterface().getPanoramaProcessor().panorama(bitmaps, panorama_pics_per_screen, camera_angle_y, crop);
-        }
-        catch(PanoramaProcessorException e) {
-            e.printStackTrace();
-            fail();
-        }
-
-        saveBitmap(panorama, output_name);
-        Thread.sleep(500);
-
-        // check we've cropped correctly:
-        final float black_factor = 0.9f;
-        // top:
-        int n_black = 0;
-        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
-                n_black++;
-            }
-        }
-        if( n_black >= panorama.getWidth()*black_factor ) {
-            Log.e(TAG, "too many black pixels on top border: " + n_black);
-            fail();
-        }
-        // bottom:
-        n_black = 0;
-        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
-                n_black++;
-            }
-        }
-        if( n_black >= panorama.getWidth()*black_factor ) {
-            Log.e(TAG, "too many black pixels on bottom border: " + n_black);
-            fail();
-        }
-        // left:
-        n_black = 0;
-        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
-                n_black++;
-            }
-        }
-        if( n_black >= panorama.getHeight()*black_factor ) {
-            Log.e(TAG, "too many black pixels on left border: " + n_black);
-            fail();
-        }
-        // right:
-        n_black = 0;
-        for(int i=0;i> 16) & 0xff) == 0 && ((color >> 8) & 0xff) == 0 && ((color) & 0xff) == 0 ) {
-                n_black++;
-            }
-        }
-        if( n_black >= panorama.getHeight()*black_factor ) {
-            Log.e(TAG, "too many black pixels on right border: " + n_black);
-            fail();
-        }
+    private void subTestPanorama(List 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) {
+        TestUtils.subTestPanorama(mActivity, inputs, output_name, gyro_debug_info_filename, panorama_pics_per_screen, camera_angle_x, camera_angle_y, gyro_tol_degrees);
     }
 
     /** Tests panorama algorithm on test samples "testPanoramaWhite".
-- 
GitLab


From ab18117996a9eaca9ef15d5e7cfb31db159d91d6 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 9 Oct 2022 19:26:33 +0100
Subject: [PATCH 065/117] Add HDR, HDRN, Avg, Panorama tests to new JUnit4
 testing.

---
 .../sourceforge/opencamera/AvgTestSuite.java  |   10 +
 .../sourceforge/opencamera/HDRNTestSuite.java |   10 +
 .../sourceforge/opencamera/HDRTestSuite.java  |   10 +
 .../opencamera/InstrumentedTest.java          | 6045 +++++++++++++++++
 .../opencamera/PanoramaTestSuite.java         |   10 +
 .../opencamera/preview/Preview.java           |   10 +-
 6 files changed, 6094 insertions(+), 1 deletion(-)
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java
 create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java

diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java
new file mode 100644
index 000000000..26afc7802
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java
@@ -0,0 +1,10 @@
+package net.sourceforge.opencamera;
+
+import org.junit.experimental.categories.Categories;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Categories.class)
+@Categories.IncludeCategory(AvgTests.class)
+@Suite.SuiteClasses({InstrumentedTest.class})
+public class AvgTestSuite {}
diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java
new file mode 100644
index 000000000..4547725e4
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java
@@ -0,0 +1,10 @@
+package net.sourceforge.opencamera;
+
+import org.junit.experimental.categories.Categories;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Categories.class)
+@Categories.IncludeCategory(HDRNTests.class)
+@Suite.SuiteClasses({InstrumentedTest.class})
+public class HDRNTestSuite {}
diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java
new file mode 100644
index 000000000..e7f518050
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java
@@ -0,0 +1,10 @@
+package net.sourceforge.opencamera;
+
+import org.junit.experimental.categories.Categories;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Categories.class)
+@Categories.IncludeCategory(HDRTests.class)
+@Suite.SuiteClasses({InstrumentedTest.class})
+public class HDRTestSuite {}
diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java
new file mode 100644
index 000000000..85e7456ae
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java
@@ -0,0 +1,6045 @@
+package net.sourceforge.opencamera;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static org.junit.Assert.*;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import net.sourceforge.opencamera.ui.PopupView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+interface HDRTests {}
+
+interface HDRNTests {}
+
+interface AvgTests {}
+
+interface PanoramaTests {}
+
+@RunWith(AndroidJUnit4.class)
+public class InstrumentedTest {
+
+    private static final String TAG = "InstrumentedTest";
+    public static final boolean test_camera2 = false;
+    //public static final boolean test_camera2 = true;
+
+    static final Intent intent;
+    static {
+        // used for code to run before the activity is started
+        intent = new Intent(ApplicationProvider.getApplicationContext(), MainActivity.class);
+        TestUtils.setDefaultIntent(intent);
+        intent.putExtra("test_project_junit4", true);
+
+        // need to run this here, not in before(), so it's run before activity is created (otherwise Camera2 won't be enabled)
+        TestUtils.initTest(ApplicationProvider.getApplicationContext(), test_camera2);
+    }
+
+    @Rule
+    //public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class);
+    //public ActivityScenarioRule mActivityRule = new ActivityScenarioRule<>(MainActivity.class);
+    public ActivityScenarioRule mActivityRule = new ActivityScenarioRule<>(intent);
+
+    /** This is run before each test, but after the activity is started (unlike MainActivityTest.setUp() which
+     *  is run before the activity is started).
+     */
+    @Before
+    public void before() throws InterruptedException {
+        Log.d(TAG, "before");
+
+        // don't run TestUtils.initTest() here - instead we do it in the static code block, and then
+        // after each test
+
+        // the following was true for MainActivityTest (using ActivityInstrumentationTestCase2), unclear if it's true for
+        // InstrumentedTest:
+        // don't waitUntilCameraOpened() here, as if an assertion fails in setUp(), it can cause later tests to hang in the suite
+        // instead we now wait for camera to open in setToDefault()
+        //waitUntilCameraOpened();
+    }
+
+    @After
+    public void after() throws InterruptedException {
+        Log.d(TAG, "after");
+
+        // As noted above, we need to call TestUtils.initTest() before the activity starts (so in
+        // the static code block, and not before()). But we should still call initTest() before every
+        // subsequent test (so that settings are reset, and test static variables are reset), so
+        // easiest to do this after each test. This also means the application is left in a default
+        // state after running tests.
+        mActivityRule.getScenario().onActivity(activity -> {
+            Log.d(TAG, "after: init");
+            TestUtils.initTest(activity, test_camera2);
+        });
+
+        Log.d(TAG, "after done");
+    }
+
+    private interface GetActivityValueCallback {
+        T get(MainActivity activity);
+    }
+
+    /** This helper method simplifies getting data from the MainActivity.
+     *  We can't call MainActivity classes directly, but instead have to go via
+     *  mActivityRule.getScenario().onActivity().
+     */
+    private  T getActivityValue(GetActivityValueCallback cb) {
+        AtomicReference resultRef = new AtomicReference<>();
+        mActivityRule.getScenario().onActivity(activity -> resultRef.set( cb.get(activity) ));
+        return resultRef.get();
+    }
+
+    private void waitUntilCameraOpened() {
+        Log.d(TAG, "wait until camera opened");
+        long time_s = System.currentTimeMillis();
+
+        boolean done = false;
+        while( !done ) {
+            assertTrue( System.currentTimeMillis() - time_s < 20000 );
+            done = getActivityValue(activity -> activity.getPreview().openCameraAttempted());
+        }
+
+        Log.d(TAG, "camera is open!");
+
+        try {
+            Thread.sleep(100); // sleep a bit just to be safe
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /** Used to click when we have View instead of an Id. It should only be called from onActivity()
+     *  (so that we can be sure we're already on the UI thread).
+     */
+    private void clickView(final View view) {
+        Log.d(TAG, "clickView: "+ view);
+        assertEquals(Looper.getMainLooper().getThread(), Thread.currentThread()); // check on UI thread
+        assertEquals(view.getVisibility(), View.VISIBLE);
+        assertTrue(view.performClick());
+    }
+
+    private void openPopupMenu() {
+        Log.d(TAG, "openPopupMenu");
+        assertFalse( getActivityValue(MainActivity::popupIsOpen) );
+        onView(withId(R.id.popup)).check(matches(isDisplayed()));
+        onView(withId(R.id.popup)).perform(click());
+
+        Log.d(TAG, "wait for popup to open");
+
+        boolean done = false;
+        while( !done ) {
+            done = getActivityValue(MainActivity::popupIsOpen);
+        }
+        Log.d(TAG, "popup is now open");
+    }
+
+    private void switchToFlashValue(String required_flash_value) {
+        Log.d(TAG, "switchToFlashValue: "+ required_flash_value);
+        boolean supports_flash = getActivityValue(activity -> activity.getPreview().supportsFlash());
+        if( supports_flash ) {
+            String flash_value = getActivityValue(activity -> activity.getPreview().getCurrentFlashValue());
+            Log.d(TAG, "start flash_value: "+ flash_value);
+            if( !flash_value.equals(required_flash_value) ) {
+
+                openPopupMenu();
+
+                String flash_value_f = flash_value;
+                mActivityRule.getScenario().onActivity(activity -> {
+                    View currentFlashButton = activity.getUIButton("TEST_FLASH_" + flash_value_f);
+                    assertNotNull(currentFlashButton);
+                    assertEquals(currentFlashButton.getAlpha(), PopupView.ALPHA_BUTTON_SELECTED, 1.0e-5);
+                    View flashButton = activity.getUIButton("TEST_FLASH_" + required_flash_value);
+                    assertNotNull(flashButton);
+                    assertEquals(flashButton.getAlpha(), PopupView.ALPHA_BUTTON, 1.0e-5);
+                    clickView(flashButton);
+                });
+
+                flash_value = getActivityValue(activity -> activity.getPreview().getCurrentFlashValue());
+                Log.d(TAG, "changed flash_value to: "+ flash_value);
+            }
+            assertEquals(flash_value, required_flash_value);
+            String controller_flash_value = getActivityValue(activity -> activity.getPreview().getCameraController().getFlashValue());
+            Log.d(TAG, "controller_flash_value: "+ controller_flash_value);
+            if( flash_value.equals("flash_frontscreen_auto") || flash_value.equals("flash_frontscreen_on") ) {
+                // for frontscreen flash, the controller flash value will be "" (due to real flash not supported) - although on Galaxy Nexus this is "flash_off" due to parameters.getFlashMode() returning Camera.Parameters.FLASH_MODE_OFF
+                assertTrue(controller_flash_value.equals("") || controller_flash_value.equals("flash_off"));
+            }
+            else {
+                Log.d(TAG, "expected_flash_value: "+ flash_value);
+                assertEquals(flash_value, controller_flash_value);
+            }
+        }
+    }
+
+    private void switchToFocusValue(String required_focus_value) {
+        Log.d(TAG, "switchToFocusValue: "+ required_focus_value);
+        boolean supports_focus = getActivityValue(activity -> activity.getPreview().supportsFocus());
+        if( supports_focus ) {
+            String focus_value = getActivityValue(activity -> activity.getPreview().getCurrentFocusValue());
+            Log.d(TAG, "start focus_value: "+ focus_value);
+            if( !focus_value.equals(required_focus_value) ) {
+
+                openPopupMenu();
+
+                mActivityRule.getScenario().onActivity(activity -> {
+                    View focusButton = activity.getUIButton("TEST_FOCUS_" + required_focus_value);
+                    assertNotNull(focusButton);
+                    clickView(focusButton);
+                });
+
+                focus_value = getActivityValue(activity -> activity.getPreview().getCurrentFocusValue());
+                Log.d(TAG, "changed focus_value to: "+ focus_value);
+            }
+            assertEquals(focus_value, required_focus_value);
+            String controller_focus_value = getActivityValue(activity -> activity.getPreview().getCameraController().getFocusValue());
+            Log.d(TAG, "controller_focus_value: "+ controller_focus_value);
+            boolean using_camera2 = getActivityValue(activity -> activity.getPreview().usingCamera2API());
+            String compare_focus_value = focus_value;
+            if( compare_focus_value.equals("focus_mode_locked") )
+                compare_focus_value = "focus_mode_auto";
+            else if( compare_focus_value.equals("focus_mode_infinity") && using_camera2 )
+                compare_focus_value = "focus_mode_manual2";
+            assertEquals(compare_focus_value, controller_focus_value);
+        }
+    }
+
+    /* Sets the camera up to a predictable state:
+     * - Flash off (if flash supported)
+     * - Focus mode picture continuous (if focus modes supported)
+     * As a side-effect, the camera and/or camera parameters values may become invalid.
+     */
+    private void setToDefault() {
+        waitUntilCameraOpened();
+
+        assertFalse( getActivityValue(activity -> activity.getPreview().isVideo()) );
+
+        switchToFlashValue("flash_off");
+        switchToFocusValue("focus_mode_continuous_picture");
+
+        // pause for safety - needed for Nokia 8 at least otherwise some tests like testContinuousPictureFocusRepeat,
+        // testLocationOff result in hang whilst waiting for photo to be taken, and hit the timeout in waitForTakePhoto()
+        try {
+            Thread.sleep(200);
+        }
+        catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /*@Test
+    public void testDummy() {
+        Log.d(TAG, "testDummy");
+    }*/
+
+    private static void checkHistogramDetails(TestUtils.HistogramDetails hdrHistogramDetails, int exp_min_value, int exp_median_value, int exp_max_value) {
+        TestUtils.checkHistogramDetails(hdrHistogramDetails, exp_min_value, exp_median_value, exp_max_value);
+    }
+
+    /** Tests calling the DRO routine with 0.0 factor, and DROALGORITHM_NONE - and that the resultant image is identical.
+     */
+    @Category(HDRTests.class)
+    @Test
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public void testDROZero() throws IOException, InterruptedException {
+        Log.d(TAG, "testDROZero");
+
+        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
+            Log.d(TAG, "renderscript requires Android Lollipop or better");
+            return;
+        }
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            Bitmap bitmap = TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR3/input1.jpg");
+            Bitmap bitmap_saved = bitmap.copy(bitmap.getConfig(), false);
+
+            try {
+                Thread.sleep(1000); // wait for camera to open
+            }
+            catch(InterruptedException e) {
+                e.printStackTrace();
+            }
+
+            List inputs = new ArrayList<>();
+            inputs.add(bitmap);
+            try {
+                activity.getApplicationInterface().getHDRProcessor().processHDR(inputs, true, null, true, null, 0.0f, 4, true, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_NONE);
+            }
+            catch(HDRProcessorException e) {
+                e.printStackTrace();
+                throw new RuntimeException();
+            }
+
+            TestUtils.saveBitmap(activity, inputs.get(0), "droZerotestHDR3_output.jpg");
+            TestUtils.checkHistogram(activity, bitmap);
+
+            // check bitmaps are the same
+            Log.d(TAG, "compare bitmap " + bitmap);
+            Log.d(TAG, "with bitmap_saved " + bitmap_saved);
+            // sameAs doesn't seem to work
+            //assertTrue( bitmap.sameAs(bitmap_saved) );
+            assertEquals(bitmap.getWidth(), bitmap_saved.getWidth());
+            assertEquals(bitmap.getHeight(), bitmap_saved.getHeight());
+            int [] old_row = new int[bitmap.getWidth()];
+            int [] new_row = new int[bitmap.getWidth()];
+            for(int y=0;y { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.avg_images_path + "testAvg3/input0.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testDRODark0_output.jpg", true, -1, -1);
+        });
+    }
+
+    /** Tests DRO only on a dark image.
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Category(HDRTests.class)
+    @Test
+    public void testDRODark1() throws IOException, InterruptedException {
+        Log.d(TAG, "testDRODark1");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.avg_images_path + "testAvg8/input0.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testDRODark1_output.jpg", true, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "saintpaul".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR1() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR1");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input3.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input4.jpg") );
+
+            // actual ISO unknown, so guessing
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR1_output.jpg", false, 1600, 1000000000L);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 44, 253);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 42, 253);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 24, 254);
+            checkHistogramDetails(hdrHistogramDetails, 2, 30, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "saintpaul", but with 5 images.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR1_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR1_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input3.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input4.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "saintpaul/input5.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR1_exp5_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0, 0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 3, 43, 251);
+            checkHistogramDetails(hdrHistogramDetails, 6, 42, 251);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "stlouis".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR2() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "stlouis/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "stlouis/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "stlouis/input3.jpg") );
+
+            // actual ISO unknown, so guessing
+            TestUtils.subTestHDR(activity, inputs, "testHDR2_output.jpg", false, 1600, (long)(1000000000L*2.5));
+
+            int [] exp_offsets_x = {0, 0, 2};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR3".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR3() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR3");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR3/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR3/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR3/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR3_output.jpg", false, 40, 1000000000L/680);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {1, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //TestUtils.checkHistogramDetails(hdrHistogramDetails, 3, 104, 255);
+            //TestUtils.checkHistogramDetails(hdrHistogramDetails, 4, 113, 255);
+            TestUtils.checkHistogramDetails(hdrHistogramDetails, 8, 113, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR4".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR4() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR4");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR4/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR4/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR4/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR4_output.jpg", true, 102, 1000000000L/60);
+
+            int [] exp_offsets_x = {-2, 0, 2};
+            int [] exp_offsets_y = {-1, 0, 1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR5".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR5/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR5/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR5/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR5_output.jpg", false, 40, 1000000000L/398);
+
+            // Nexus 6:
+            //int [] exp_offsets_x = {0, 0, 0};
+            //int [] exp_offsets_y = {-1, 0, 0};
+            // OnePlus 3T:
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR6".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR6() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR6");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR6/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR6/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR6/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR6_output.jpg", false, 40, 1000000000L/2458);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {1, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR7".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR7() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR7");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR7/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR7/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR7/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR7_output.jpg", false, 40, 1000000000L/538);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR8".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR8() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR8");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR8/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR8/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR8/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR8_output.jpg", false, 40, 1000000000L/148);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR9".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR9() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR9");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR9/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR9/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR9/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR9_output.jpg", false, 40, 1000000000L/1313);
+
+            int [] exp_offsets_x = {-1, 0, 1};
+            int [] exp_offsets_y = {0, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR10".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR10() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR10");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR10/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR10/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR10/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR10_output.jpg", false, 107, 1000000000L/120);
+
+            int [] exp_offsets_x = {2, 0, 0};
+            int [] exp_offsets_y = {5, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR11".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR11() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR11");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR11/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR11/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR11/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR11_output.jpg", true, 40, 1000000000L/2662);
+
+            int [] exp_offsets_x = {-2, 0, 1};
+            int [] exp_offsets_y = {1, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 48, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 65, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 62, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR12".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR12() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR12");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR12/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR12/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR12/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR12_output.jpg", true, 1196, 1000000000L/12);
+
+            int [] exp_offsets_x = {0, 0, 7};
+            int [] exp_offsets_y = {0, 0, 8};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR13".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR13() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR13");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR13/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR13/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR13/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR13_output.jpg", false, 323, 1000000000L/24);
+
+            int [] exp_offsets_x = {0, 0, 2};
+            int [] exp_offsets_y = {0, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR14".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR14() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR14");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR14/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR14/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR14/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR14_output.jpg", false, 40, 1000000000L/1229);
+
+            int [] exp_offsets_x = {0, 0, 1};
+            int [] exp_offsets_y = {0, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR15".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR15() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR15");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR15/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR15/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR15/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR15_output.jpg", false, 40, 1000000000L/767);
+
+            int [] exp_offsets_x = {1, 0, -1};
+            int [] exp_offsets_y = {2, 0, -3};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR16".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR16() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR16");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR16/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR16/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR16/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR16_output.jpg", false, 52, 1000000000L/120);
+
+            int [] exp_offsets_x = {-1, 0, 2};
+            int [] exp_offsets_y = {1, 0, -6};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR17".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR17() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR17");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR17/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR17/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR17/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR17_output.jpg", true, 557, 1000000000L/12);
+
+            // Nexus 6:
+            //int [] exp_offsets_x = {0, 0, -3};
+            //int [] exp_offsets_y = {0, 0, -4};
+            // OnePlus 3T:
+            int [] exp_offsets_x = {0, 0, -2};
+            int [] exp_offsets_y = {0, 0, -3};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR18".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR18() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR18");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR18/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR18/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR18/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR18_output.jpg", true, 100, 1000000000L/800);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 113, 254);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 119, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 5, 120, 255);
+            checkHistogramDetails(hdrHistogramDetails, 2, 120, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR19".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR19() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR19");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR19/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR19/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR19/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR19_output.jpg", true, 100, 1000000000L/160);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR20".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR20() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR20");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR20/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR20/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR20/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR20_output.jpg", true, 100, 1000000000L*2);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {-1, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR21".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR21() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR21");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR21/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR21/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR21/input2.jpg") );
+
+            // ISO and exposure unknown, so guessing
+            TestUtils.subTestHDR(activity, inputs, "testHDR21_output.jpg", true, 800, 1000000000L/12);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR22".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR22() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR22");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR22/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR22/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR22/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR22_output.jpg", true, 391, 1000000000L/12);
+
+            // Nexus 6:
+            //int [] exp_offsets_x = {1, 0, -5};
+            //int [] exp_offsets_y = {1, 0, -6};
+            // OnePlus 3T:
+            int [] exp_offsets_x = {0, 0, -5};
+            int [] exp_offsets_y = {1, 0, -6};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23", but with 2 images.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR23_exp2() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23_exp2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_exp2_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0};
+            int [] exp_offsets_y = {0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 13, 72, 250);
+            checkHistogramDetails(hdrHistogramDetails, 24, 72, 250);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23", but with 2 images, and greater exposure gap.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR23_exp2b() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23_exp2b");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_exp2b_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0};
+            int [] exp_offsets_y = {0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR23() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0066.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+
+            // ISO unknown, so guessing
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_output.jpg", false, 1600, 1000000000L);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 17, 81, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 32, 74, 255);
+            checkHistogramDetails(hdrHistogramDetails, 29, 68, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23", but with 4 images.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR23_exp4() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23_exp4");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_exp4_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 15, 69, 254);
+            checkHistogramDetails(hdrHistogramDetails, 24, 70, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23", but with 5 images.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR23_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0066.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_exp5_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0, 0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 17, 81, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 28, 82, 255);
+            checkHistogramDetails(hdrHistogramDetails, 21, 74, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23", but with 6 images.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR23_exp6() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23_exp6");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0072.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0061.png") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_exp6_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0, 0, 0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0, 0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 15, 70, 254);
+            checkHistogramDetails(hdrHistogramDetails, 25, 71, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR23", but with 7 images.
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR23_exp7() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR23_exp7");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0072.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0070.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0068.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0066.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0064.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0062.png") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR23/memorial0061.png") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR23_exp7_output.jpg", false, -1, -1);
+
+            int [] exp_offsets_x = {0, 0, 0, 0, 0, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0, 0, 0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 17, 81, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 28, 82, 255);
+            checkHistogramDetails(hdrHistogramDetails, 20, 72, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR24".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR24() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR24");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR24/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR24/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR24/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR24_output.jpg", true, 40, 1000000000L/422);
+
+            int [] exp_offsets_x = {0, 0, 1};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR25".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR25() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR25");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR25/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR25/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR25/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR25_output.jpg", true, 40, 1000000000L/1917);
+
+            int [] exp_offsets_x = {0, 0, 0};
+            int [] exp_offsets_y = {1, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR26".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR26() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR26");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR26/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR26/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR26/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR26_output.jpg", true, 40, 1000000000L/5325);
+
+            int [] exp_offsets_x = {-1, 0, 1};
+            int [] exp_offsets_y = {1, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 104, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 119, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR27".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR27() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR27");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR27/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR27/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR27/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR27_output.jpg", true, 40, 1000000000L/949);
+
+            int [] exp_offsets_x = {0, 0, 2};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR28".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR28() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR28");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR28/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR28/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR28/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR28_output.jpg", true, 294, 1000000000L/20);
+
+            int [] exp_offsets_x = {0, 0, 2};
+            int [] exp_offsets_y = {0, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR29".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR29() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR29");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR29/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR29/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR29/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR29_output.jpg", false, 40, 1000000000L/978);
+
+            int [] exp_offsets_x = {-1, 0, 3};
+            int [] exp_offsets_y = {0, 0, -1};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR30".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR30() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR30");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR30/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR30/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR30/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR30_output.jpg", false, 40, 1000000000L/978);
+
+            // offsets for full image
+            //int [] exp_offsets_x = {-6, 0, -1};
+            //int [] exp_offsets_y = {23, 0, -13};
+            // offsets using centre quarter image
+            int [] exp_offsets_x = {-5, 0, 0};
+            int [] exp_offsets_y = {22, 0, -13};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR31".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR31() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR31");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR31/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR31/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR31/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR31_output.jpg", false, 40, 1000000000L/422);
+
+            // offsets for full image
+            //int [] exp_offsets_x = {0, 0, 4};
+            //int [] exp_offsets_y = {21, 0, -11};
+            // offsets using centre quarter image
+            int [] exp_offsets_x = {0, 0, 3};
+            int [] exp_offsets_y = {21, 0, -11};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR32".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR32() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR32");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR32/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR32/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR32/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR32_output.jpg", true, 40, 1000000000L/1331);
+
+            int [] exp_offsets_x = {1, 0, 0};
+            int [] exp_offsets_y = {13, 0, -10};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 3, 101, 251);
+            //checkHistogramDetails(hdrHistogramDetails, 3, 109, 251);
+            //checkHistogramDetails(hdrHistogramDetails, 6, 111, 252);
+            checkHistogramDetails(hdrHistogramDetails, 2, 111, 252);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR33".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR33() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR33");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR33/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR33/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR33/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR33_output.jpg", true, 40, 1000000000L/354);
+
+            int [] exp_offsets_x = {13, 0, -10};
+            int [] exp_offsets_y = {24, 0, -12};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR34".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR34() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR34");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR34/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR34/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR34/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR34_output.jpg", true, 40, 1000000000L/4792);
+
+            int [] exp_offsets_x = {5, 0, -8};
+            int [] exp_offsets_y = {0, 0, -2};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR35".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR35() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR35");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR35/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR35/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR35/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR35_output.jpg", true, 40, 1000000000L/792);
+
+            int [] exp_offsets_x = {-10, 0, 3};
+            int [] exp_offsets_y = {7, 0, -3};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR36".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR36() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR36");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR36/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR36/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR36/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR36_output.jpg", false, 100, 1000000000L/1148);
+
+            int [] exp_offsets_x = {2, 0, -2};
+            int [] exp_offsets_y = {-4, 0, 2};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR37".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR37() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR37");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR37/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR37/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR37/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR37_output.jpg", false, 46, 1000000000L/120);
+
+            int [] exp_offsets_x = {0, 0, 3};
+            int [] exp_offsets_y = {2, 0, -19};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR38".
+     *  Tests with Filmic tonemapping.
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR38Filmic() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR38Filmic");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR38/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR38/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR38/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR38_filmic_output.jpg", false, 125, 1000000000L/2965, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FILMIC);
+
+            int [] exp_offsets_x = {-1, 0, 0};
+            int [] exp_offsets_y = {0, 0, 0};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 92, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 93, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR39".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR39() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR39");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR39/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR39/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR39/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR39_output.jpg", false, 125, 1000000000L/2135);
+
+            int [] exp_offsets_x = {-6, 0, -2};
+            int [] exp_offsets_y = {6, 0, -8};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 128, 222);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR40".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR40() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR40");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR40_output.jpg", false, 50, 1000000000L/262);
+
+            int [] exp_offsets_x = {5, 0, -2};
+            int [] exp_offsets_y = {13, 0, 24};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            checkHistogramDetails(hdrHistogramDetails, 1, 138, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR40" with Exponential tonemapping.
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR40Exponential() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR40Exponential");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR40_exponential_output.jpg", false, 50, 1000000000L/262, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_EXPONENTIAL);
+
+            int [] exp_offsets_x = {5, 0, -2};
+            int [] exp_offsets_y = {13, 0, 24};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            checkHistogramDetails(hdrHistogramDetails, 1, 138, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR40" with Filmic tonemapping.
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR40Filmic() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR40Filmic");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR40_filmic_output.jpg", false, 50, 1000000000L/262, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FILMIC);
+
+            int [] exp_offsets_x = {5, 0, -2};
+            int [] exp_offsets_y = {13, 0, 24};
+            TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y);
+
+            checkHistogramDetails(hdrHistogramDetails, 1, 130, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR41".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR41() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR41");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR41/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR41/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR41/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR41_output.jpg", false, 925, 1000000000L/25);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR42".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR42() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR42");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR42/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR42/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR42/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR42_output.jpg", false, 112, 1000000000L/679);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR43".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR43() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR43");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR43/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR43/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR43/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR43_output.jpg", false, 1196, 1000000000L/12);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR44".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR44() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR44");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR44/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR44/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR44/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR44_output.jpg", false, 100, 1000000000L/1016);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR45".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR45() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR45");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6314.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6312.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6310.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6309.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6311.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6313.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6315.jpg") );
+
+            // ISO 100, exposure time 2s, but pass in -1 since these are HDRNTests
+            TestUtils.subTestHDR(activity, inputs, "testHDR45_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR45".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR45_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR45_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6314.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6312.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6310.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6309.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6311.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6313.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6315.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR45_exp5_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR45".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR45_exp7() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR45_exp7");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6314.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6312.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6310.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6309.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6311.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6313.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR45/IMG_6315.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR45_exp7_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR46".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR46() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR46");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 06.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 05.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 04.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 03.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 02.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 01.jpg") );
+
+            // ISO 100, exposure time 1/60s, but pass in -1 since these are HDRNTests
+            TestUtils.subTestHDR(activity, inputs, "testHDR46_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR46".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR46_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR46_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 06.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 05.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 04.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 03.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 02.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR46/Izmir Harbor - ppw - 01.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR46_exp5_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR47".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR47_exp2() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR47_exp2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDR47_exp2_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR47".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR47() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR47");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
+
+            // ISO 400, exposure time 1/60s, but pass in -1 since these are HDRNTests
+            TestUtils.subTestHDR(activity, inputs, "testHDR47_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR47".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR47_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR47_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR47_exp5_output.jpg", false, -1, -1);
+
+            checkHistogramDetails(hdrHistogramDetails, 1, 73, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR47".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR47_exp7() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR47_exp7");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 08.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 07.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 06.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 05.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 04.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 03.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 02.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR47/High Five - ppw - 01.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR47_exp7_output.jpg", false, -1, -1);
+
+            checkHistogramDetails(hdrHistogramDetails, 1, 73, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR48".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR48() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR48");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input3.jpg") );
+            //inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input4.jpg") );
+
+            // ISO 100, exposure time 1/716s, but pass in -1 since these are HDRNTests
+            TestUtils.subTestHDR(activity, inputs, "testHDR48_output.jpg", false, -1, -1);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR48".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR48_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR48_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input3.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR48/input4.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR48_exp5_output.jpg", false, -1, -1);
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 59, 241);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR49".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR49_exp2() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR49_exp2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR49_exp2_output.jpg", false, -1, -1);
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 92, 250);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR49".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR49() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR49");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
+
+            // ISO 100, exposure time 1/417s, but pass in -1 since these are HDRNTests
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR49_output.jpg", false, -1, -1);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 81, 254);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR49".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR49_exp4() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR49_exp4");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input4.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR49_exp4_output.jpg", false, -1, -1);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 100, 245);
+            checkHistogramDetails(hdrHistogramDetails, 0, 94, 244);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR49".
+     */
+    @Category(HDRNTests.class)
+    @Test
+    public void testHDR49_exp5() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR49_exp5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input2.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input3.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR49/input4.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR49_exp5_output.jpg", false, -1, -1);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 72, 244);
+            checkHistogramDetails(hdrHistogramDetails, 0, 78, 243);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR50".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR50() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR50");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR50/IMG_20180626_221357_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR50/IMG_20180626_221357_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR50/IMG_20180626_221357_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR50_output.jpg", false, 867, 1000000000L/14);
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 69, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR51".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR51() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR51");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR51/IMG_20180323_104702_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR51/IMG_20180323_104702_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR51/IMG_20180323_104702_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR51_output.jpg", true, 1600, 1000000000L/11);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR52".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR52() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR52");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR52/IMG_20181023_143633_EXP0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR52/IMG_20181023_143633_EXP1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR52/IMG_20181023_143633_EXP2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR52_output.jpg", false, 100, 1000000000L/2105);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR53".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR53() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR53");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR53/IMG_20181106_135411_EXP0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR53/IMG_20181106_135411_EXP1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR53/IMG_20181106_135411_EXP2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR53_output.jpg", false, 103, 1000000000L/5381);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 55, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 64, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR54".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR54() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR54");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR54/IMG_20181107_115508_EXP0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR54/IMG_20181107_115508_EXP1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR54/IMG_20181107_115508_EXP2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR54_output.jpg", false, 752, 1000000000L/14);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR55".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR55() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR55");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR55/IMG_20181107_115608_EXP0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR55/IMG_20181107_115608_EXP1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR55/IMG_20181107_115608_EXP2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR55_output.jpg", false, 1505, 1000000000L/10);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR56".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR56() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR56");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR56/180502_141722_OC_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR56/180502_141722_OC_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR56/180502_141722_OC_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR56_output.jpg", false, 50, 1000000000L/40);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR57".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR57() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR57");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR57/IMG_20181119_145313_EXP0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR57/IMG_20181119_145313_EXP1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR57/IMG_20181119_145313_EXP2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR57_output.jpg", true, 100, 1000000000L/204);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR58".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR58() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR58");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR58/IMG_20190911_210146_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR58/IMG_20190911_210146_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR58/IMG_20190911_210146_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR58_output.jpg", false, 1250, 1000000000L/10);
+            //HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR58_output.jpg", false, 1250, 1000000000L/10, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP);
+
+            checkHistogramDetails(hdrHistogramDetails, 11, 119, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR59".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR59() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR59");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR59/IMG_20190911_210154_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR59/IMG_20190911_210154_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR59/IMG_20190911_210154_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR59_output.jpg", false, 1250, 1000000000L/10);
+            //HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR59_output.jpg", false, 1250, 1000000000L/10, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR60".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR60() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR60");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR60/IMG_20200507_020319_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR60/IMG_20200507_020319_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR60/IMG_20200507_020319_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR60_output.jpg", false, 491, 1000000000L/10);
+            //HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR60_output.jpg", false, 491, 1000000000L/10, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP);
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests HDR algorithm on test samples "testHDR61".
+     */
+    @Category(HDRTests.class)
+    @Test
+    public void testHDR61() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDR61");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR61/IMG_20191111_145230_0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR61/IMG_20191111_145230_1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR61/IMG_20191111_145230_2.jpg") );
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, 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};
+            TestUtils.checkHDROffsets(activity, 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 algorithm.
+     *  The test images should be copied to the test device into DCIM/testOpenCamera/testdata/hdrsamples/testHDRtemp/ .
+     */
+    @Test
+    public void testHDRtemp() throws IOException, InterruptedException {
+        Log.d(TAG, "testHDRtemp");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDRtemp/input0.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDRtemp/input1.jpg") );
+            inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDRtemp/input2.jpg") );
+
+            TestUtils.subTestHDR(activity, inputs, "testHDRtemp_output.jpg", true, 100, 1000000000L/100);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg1".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg1() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg1");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg1/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg1/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg1/input2.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity, inputs, "testAvg1_output.jpg", 1600, 1000000000L/17, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //int [] exp_offsets_x = {0, 3, 0};
+                        //int [] exp_offsets_y = {0, 1, 0};
+                        //int [] exp_offsets_x = {0, 4, 0};
+                        //int [] exp_offsets_y = {0, 1, 0};
+                        //int [] exp_offsets_x = {0, 2, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        int [] exp_offsets_x = {0, 4, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 2 ) {
+                        //int [] exp_offsets_x = {0, 6, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        //int [] exp_offsets_x = {0, 8, 0};
+                        //int [] exp_offsets_y = {0, 1, 0};
+                        //int [] exp_offsets_x = {0, 7, 0};
+                        //int [] exp_offsets_y = {0, -1, 0};
+                        //int [] exp_offsets_x = {0, 8, 0};
+                        //int [] exp_offsets_y = {0, -4, 0};
+                        int [] exp_offsets_x = {0, 8, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg2".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg2() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg2/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg2/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg2/input2.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg2_output.jpg", 1600, 1000000000L/17, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //int [] exp_offsets_x = {0, -15, 0};
+                        //int [] exp_offsets_y = {0, -10, 0};
+                        //int [] exp_offsets_x = {0, -15, 0};
+                        //int [] exp_offsets_y = {0, -11, 0};
+                        //int [] exp_offsets_x = {0, -12, 0};
+                        //int [] exp_offsets_y = {0, -12, 0};
+                        int [] exp_offsets_x = {0, -16, 0};
+                        int [] exp_offsets_y = {0, -12, 0};
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 2 ) {
+                        //int [] exp_offsets_x = {0, -15, 0};
+                        //int [] exp_offsets_y = {0, -10, 0};
+                        //int [] exp_offsets_x = {0, -13, 0};
+                        //int [] exp_offsets_y = {0, -12, 0};
+                        //int [] exp_offsets_x = {0, -12, 0};
+                        //int [] exp_offsets_y = {0, -14, 0};
+                        int [] exp_offsets_x = {0, -12, 0};
+                        int [] exp_offsets_y = {0, -12, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg3".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg3() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg3");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg3/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg3/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg3/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg3/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg3/input4.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg3_output.jpg", 1600, 1000000000L/16, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                /*if( index == 1 ) {
+                    //int [] exp_offsets_x = {0, 2, 0};
+                    //int [] exp_offsets_y = {0, -18, 0};
+                    //int [] exp_offsets_x = {0, -1, 0};
+                    //int [] exp_offsets_y = {0, 0, 0};
+                    //int [] exp_offsets_x = {0, -9, 0};
+                    //int [] exp_offsets_y = {0, -11, 0};
+                    //int [] exp_offsets_x = {0, -8, 0};
+                    //int [] exp_offsets_y = {0, -10, 0};
+                    int [] exp_offsets_x = {0, -8, 0};
+                    int [] exp_offsets_y = {0, -8, 0};
+                    assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 0);
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else if( index == 2 ) {
+                    //int [] exp_offsets_x = {0, -18, 0};
+                    //int [] exp_offsets_y = {0, 17, 0};
+                    //int [] exp_offsets_x = {0, -2, 0};
+                    //int [] exp_offsets_y = {0, 0, 0};
+                    //int [] exp_offsets_x = {0, -7, 0};
+                    //int [] exp_offsets_y = {0, -2, 0};
+                    //int [] exp_offsets_x = {0, -8, 0};
+                    //int [] exp_offsets_y = {0, -8, 0};
+                    int [] exp_offsets_x = {0, -12, 0};
+                    int [] exp_offsets_y = {0, 8, 0};
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else if( index == 3 ) {
+                    //int [] exp_offsets_x = {0, -12, 0};
+                    //int [] exp_offsets_y = {0, -25, 0};
+                    //int [] exp_offsets_x = {0, -2, 0};
+                    //int [] exp_offsets_y = {0, 0, 0};
+                    //int [] exp_offsets_x = {0, -9, 0};
+                    //int [] exp_offsets_y = {0, 14, 0};
+                    //int [] exp_offsets_x = {0, -8, 0};
+                    //int [] exp_offsets_y = {0, 2, 0};
+                    //int [] exp_offsets_x = {0, -12, 0};
+                    //int [] exp_offsets_y = {0, 12, 0};
+                    int [] exp_offsets_x = {0, -12, 0};
+                    int [] exp_offsets_y = {0, 4, 0};
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else if( index == 4 ) {
+                    //int [] exp_offsets_x = {0, -29, 0};
+                    //int [] exp_offsets_y = {0, -22, 0};
+                    //int [] exp_offsets_x = {0, -2, 0};
+                    //int [] exp_offsets_y = {0, 0, 0};
+                    //int [] exp_offsets_x = {0, -7, 0};
+                    //int [] exp_offsets_y = {0, 11, 0};
+                    //int [] exp_offsets_x = {0, -6, 0};
+                    //int [] exp_offsets_y = {0, 14, 0};
+                    //int [] exp_offsets_x = {0, -8, 0};
+                    //int [] exp_offsets_y = {0, 2, 0};
+                    //int [] exp_offsets_x = {0, -8, 0};
+                    //int [] exp_offsets_y = {0, 12, 0};
+                    int [] exp_offsets_x = {0, -8, 0};
+                    int [] exp_offsets_y = {0, 4, 0};
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else {
+                    assertTrue(false);
+                }*/
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 21, 177);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 21, 152);
+            checkHistogramDetails(hdrHistogramDetails, 0, 21, 166);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg4".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg4() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg4");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg4/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg4/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg4/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg4/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg4/input4.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg4_output.jpg", 1600, 1000000000L/16, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //int [] exp_offsets_x = {0, 5, 0};
+                        //int [] exp_offsets_y = {0, 2, 0};
+                        int [] exp_offsets_x = {0, 5, 0};
+                        int [] exp_offsets_y = {0, 1, 0};
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 2 ) {
+                        //int [] exp_offsets_x = {0, 3, 0};
+                        //int [] exp_offsets_y = {0, 5, 0};
+                        int [] exp_offsets_x = {0, 4, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        //int [] exp_offsets_x = {0, 0, 0};
+                        //int [] exp_offsets_y = {0, 7, 0};
+                        //int [] exp_offsets_x = {0, 1, 0};
+                        //int [] exp_offsets_y = {0, 6, 0};
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 8, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 4 ) {
+                        //int [] exp_offsets_x = {0, 4, 0};
+                        //int [] exp_offsets_y = {0, 8, 0};
+                        //int [] exp_offsets_x = {0, 3, 0};
+                        //int [] exp_offsets_y = {0, 7, 0};
+                        //int [] exp_offsets_x = {0, 3, 0};
+                        //int [] exp_offsets_y = {0, 8, 0};
+                        int [] exp_offsets_x = {0, 3, 0};
+                        int [] exp_offsets_y = {0, 9, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg5".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg5() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg5/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg5/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg5/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg5/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg5/input4.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg5_output.jpg", 1600, 1000000000L/16, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                /*if( index == 1 ) {
+                    //int [] exp_offsets_x = {0, 4, 0};
+                    //int [] exp_offsets_y = {0, -1, 0};
+                    //int [] exp_offsets_x = {0, 5, 0};
+                    //int [] exp_offsets_y = {0, 0, 0};
+                    //int [] exp_offsets_x = {0, 6, 0};
+                    //int [] exp_offsets_y = {0, -2, 0};
+                    int [] exp_offsets_x = {0, 4, 0};
+                    int [] exp_offsets_y = {0, 0, 0};
+                    assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 0);
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else if( index == 2 ) {
+                    //int [] exp_offsets_x = {0, 7, 0};
+                    //int [] exp_offsets_y = {0, -2, 0};
+                    //int [] exp_offsets_x = {0, 8, 0};
+                    //int [] exp_offsets_y = {0, -1, 0};
+                    int [] exp_offsets_x = {0, 8, 0};
+                    int [] exp_offsets_y = {0, -4, 0};
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else if( index == 3 ) {
+                    //int [] exp_offsets_x = {0, 9, 0};
+                    //int [] exp_offsets_y = {0, -2, 0};
+                    //int [] exp_offsets_x = {0, 8, 0};
+                    //int [] exp_offsets_y = {0, -1, 0};
+                    int [] exp_offsets_x = {0, 8, 0};
+                    int [] exp_offsets_y = {0, 0, 0};
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else if( index == 4 ) {
+                    //int [] exp_offsets_x = {0, 10, 0};
+                    //int [] exp_offsets_y = {0, -4, 0};
+                    int [] exp_offsets_x = {0, 11, 0};
+                    int [] exp_offsets_y = {0, -3, 0};
+                    TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                }
+                else {
+                    assertTrue(false);
+                }*/
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg6".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg6() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg6");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg6/input7.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg6_output.jpg", 1600, 1000000000L/17, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                /*if( true )
+                    return;*/
+                    if( index == 1 ) {
+                        //int [] exp_offsets_x = {0, 0, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        //int [] exp_offsets_x = {0, -2, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                    else if( index == 2 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 4 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 5 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 6 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 7 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 18, 51, 201);
+            //checkHistogramDetails(hdrHistogramDetails, 14, 38, 200);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 9, 193);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 9, 199);
+            //checkHistogramDetails(hdrHistogramDetails, 12, 46, 202);
+            //checkHistogramDetails(hdrHistogramDetails, 12, 46, 205);
+            //checkHistogramDetails(hdrHistogramDetails, 12, 44, 209);
+            //checkHistogramDetails(hdrHistogramDetails, 12, 44, 202);
+            //checkHistogramDetails(hdrHistogramDetails, 5, 16, 190);
+            checkHistogramDetails(hdrHistogramDetails, 5, 19, 199);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg7".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg7() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg7");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg7/input7.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg7_output.jpg", 1600, 1000000000L/16, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //int [] exp_offsets_x = {0, 0, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        //int [] exp_offsets_x = {0, -10, 0};
+                        //int [] exp_offsets_y = {0, 6, 0};
+                        //int [] exp_offsets_x = {0, -6, 0};
+                        //int [] exp_offsets_y = {0, 2, 0};
+                        //int [] exp_offsets_x = {0, -4, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        //int [] exp_offsets_x = {0, 0, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        //int [] exp_offsets_x = {0, -4, 0};
+                        //int [] exp_offsets_y = {0, 0, 0};
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg8".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg8() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg8");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg8/input7.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg8_output.jpg", 1600, 1000000000L/16, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 4, 26, 92);
+            //checkHistogramDetails(hdrHistogramDetails, 3, 19, 68);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 10, 60);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 8, 72);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 6, 64);
+            //checkHistogramDetails(hdrHistogramDetails, 1, 15, 75);
+            checkHistogramDetails(hdrHistogramDetails, 1, 16, 78);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg9".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg9() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg9");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            final boolean use_auto_photos = true;
+
+            if( use_auto_photos ) {
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto0.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto1.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto2.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto3.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto4.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto5.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto6.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input_auto7.jpg");
+            }
+            else {
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input0.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input1.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input2.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input3.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input4.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input5.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input6.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg9/input7.jpg");
+            }
+
+            String out_filename = use_auto_photos ? "testAvg9_auto_output.jpg" : "testAvg9_output.jpg";
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, out_filename, 1600, use_auto_photos ? 1000000000L/16 : 1000000000L/11, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg10".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg10() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg10");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            final boolean use_auto_photos = false;
+
+            if( use_auto_photos ) {
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto0.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto1.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto2.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto3.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto4.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto5.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto6.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input_auto7.jpg");
+            }
+            else {
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input0.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input1.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input2.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input3.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input4.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input5.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input6.jpg");
+                inputs.add(TestUtils.avg_images_path + "testAvg10/input7.jpg");
+            }
+
+            String out_filename = use_auto_photos ? "testAvg10_auto_output.jpg" : "testAvg10_output.jpg";
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, out_filename, 1196, use_auto_photos ? 1000000000L/12 : 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg11".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg11() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg11");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // note, we don't actually use 8 images for a bright scene like this, but it serves as a good test for
+            // misalignment/ghosting anyway
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg11/input7.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg11_output.jpg", 100, 1000000000L/338, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //int [] exp_offsets_x = {0, 4, 0};
+                        //int [] exp_offsets_y = {0, -8, 0};
+                        //int [] exp_offsets_x = {0, 6, 0};
+                        //int [] exp_offsets_y = {0, -8, 0};
+                        //int [] exp_offsets_x = {0, -6, 0};
+                        //int [] exp_offsets_y = {0, 8, 0};
+                        int [] exp_offsets_x = {0, -4, 0};
+                        int [] exp_offsets_y = {0, 8, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                        //assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 1);
+                    }
+                    else if( index == 2 ) {
+                        //int [] exp_offsets_x = {0, -5, 0};
+                        //int [] exp_offsets_y = {0, -1, 0};
+                        //int [] exp_offsets_x = {0, -10, 0};
+                        //int [] exp_offsets_y = {0, 6, 0};
+                        int [] exp_offsets_x = {0, -8, 0};
+                        int [] exp_offsets_y = {0, 8, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        //int [] exp_offsets_x = {0, -1, 0};
+                        //int [] exp_offsets_y = {0, -18, 0};
+                        //int [] exp_offsets_x = {0, 0, 0};
+                        //int [] exp_offsets_y = {0, -16, 0};
+                        //int [] exp_offsets_x = {0, -4, 0};
+                        //int [] exp_offsets_y = {0, -10, 0};
+                        //int [] exp_offsets_x = {0, -4, 0};
+                        //int [] exp_offsets_y = {0, -8, 0};
+                        int [] exp_offsets_x = {0, -4, 0};
+                        int [] exp_offsets_y = {0, -12, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 4 ) {
+                        //int [] exp_offsets_x = {0, -3, 0};
+                        //int [] exp_offsets_y = {0, -20, 0};
+                        //int [] exp_offsets_x = {0, -2, 0};
+                        //int [] exp_offsets_y = {0, -18, 0};
+                        //int [] exp_offsets_x = {0, -6, 0};
+                        //int [] exp_offsets_y = {0, -12, 0};
+                        int [] exp_offsets_x = {0, -8, 0};
+                        int [] exp_offsets_y = {0, -12, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 5 ) {
+                        //int [] exp_offsets_x = {0, -8, 0};
+                        //int [] exp_offsets_y = {0, 2, 0};
+                        //int [] exp_offsets_x = {0, -10, 0};
+                        //int [] exp_offsets_y = {0, 4, 0};
+                        //int [] exp_offsets_x = {0, -12, 0};
+                        //int [] exp_offsets_y = {0, 10, 0};
+                        int [] exp_offsets_x = {0, -12, 0};
+                        int [] exp_offsets_y = {0, 8, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 6 ) {
+                        //int [] exp_offsets_x = {0, 0, 0};
+                        //int [] exp_offsets_y = {0, -6, 0};
+                        //int [] exp_offsets_x = {0, 2, 0};
+                        //int [] exp_offsets_y = {0, -6, 0};
+                        //int [] exp_offsets_x = {0, -4, 0};
+                        //int [] exp_offsets_y = {0, 2, 0};
+                        int [] exp_offsets_x = {0, -4, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 7 ) {
+                        //int [] exp_offsets_x = {0, 7, 0};
+                        //int [] exp_offsets_y = {0, -2, 0};
+                        //int [] exp_offsets_x = {0, 6, 0};
+                        //int [] exp_offsets_y = {0, 6, 0};
+                        //int [] exp_offsets_x = {0, 4, 0};
+                        //int [] exp_offsets_y = {0, 4, 0};
+                        //int [] exp_offsets_x = {0, 8, 0};
+                        //int [] exp_offsets_y = {0, 8, 0};
+                        int [] exp_offsets_x = {0, 4, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg12".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg12() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg12");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg12/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg12/input1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg12_output.jpg", 100, 1000000000L/1617, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 1);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 30, 254);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 27, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 20, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 17, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 31, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg13".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg13() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg13");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg13/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg13/input1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg13_output.jpg", 100, 1000000000L/2482, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 1);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg14".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg14() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg14");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg14/input7.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg14_output.jpg", 1600, 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, -8, 0};
+                        int [] exp_offsets_y = {0, -8, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                    else if( index == 7 ) {
+                        //int [] exp_offsets_x = {0, 4, 0};
+                        //int [] exp_offsets_y = {0, 28, 0};
+                        int [] exp_offsets_x = {0, 4, 0};
+                        int [] exp_offsets_y = {0, 40, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                }
+            });
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 25, 245);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg15".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg15() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg15");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg15/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg15/input1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg15_output.jpg", 100, 1000000000L/1525, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 38, 254);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg16".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg16() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg16");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg16/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg16/input1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg16_output.jpg", 100, 1000000000L/293, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 1);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg17".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg17() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg17");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg17/input7.jpg");
+
+            // the input images record ISO=800, but they were taken with OnePlus 3T which has bug where ISO is reported as max
+            // of 800; in reality for a scene this dark, it was probably more like ISO 1600
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg17_output.jpg", 1600, 1000000000L/17, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, -8, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                    else if( index == 7 ) {
+                        int [] exp_offsets_x = {0, 12, 0};
+                        int [] exp_offsets_y = {0, 28, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 100, 233);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 100, 236);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 92, 234);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 102, 241);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 102, 238);
+            checkHistogramDetails(hdrHistogramDetails, 0, 103, 244);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg18".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg18() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg18");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg18/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg18/input1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg18_output.jpg", 100, 1000000000L/591, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        //assertTrue(activity.getApplicationInterface().getHDRProcessor().sharp_index == 1);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg19".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg19() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg19");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // repeat same image twice
+            inputs.add(TestUtils.avg_images_path + "testAvg19/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg19/input0.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg19_output.jpg", 100, 1000000000L/2483, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 88, 252);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 77, 252);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 87, 252);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 74, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 58, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg20".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg20() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg20");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // repeat same image twice
+            inputs.add(TestUtils.avg_images_path + "testAvg20/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg20/input0.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg20_output.jpg", 100, 1000000000L/3124, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg21".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg21() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg21");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // repeat same image twice
+            inputs.add(TestUtils.avg_images_path + "testAvg21/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg21/input0.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg21_output.jpg", 102, 1000000000L/6918, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg22".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg22() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg22");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // repeat same image twice
+            inputs.add(TestUtils.avg_images_path + "testAvg22/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg22/input0.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg22_output.jpg", 100, 1000000000L/3459, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg23".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg23() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg23");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_3.jpg");
+            // only test 4 images, to reflect latest behaviour that we take 4 images for this ISO
+        /*inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg23/IMG_20180520_111250_7.jpg");*/
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg23_output.jpg", 1044, 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, -4, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 2 ) {
+                        int [] exp_offsets_x = {0, -4, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        int [] exp_offsets_x = {0, -8, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 4 ) {
+                        int [] exp_offsets_x = {0, -8, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 5 ) {
+                        int [] exp_offsets_x = {0, -12, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 6 ) {
+                        int [] exp_offsets_x = {0, -12, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 7 ) {
+                        int [] exp_offsets_x = {0, -12, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 81, 251);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 80, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 83, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg24".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg24() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg24");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg24/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg24/input1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg24_output.jpg", 100, 1000000000L/2421, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 77, 250);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 74, 250);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 86, 250);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 86, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 80, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 56, 254);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg25".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg25() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg25");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg25/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg25/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg25/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg25/input3.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg25_output.jpg", 512, 1000000000L/20, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg26".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg26() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg26");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // note we now take only 3 images for bright scenes, but still test with 4 images as this serves as a good test
+            // against ghosting
+            inputs.add(TestUtils.avg_images_path + "testAvg26/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg26/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg26/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg26/input3.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg26_output.jpg", 100, 1000000000L/365, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                /*if( true )
+                    return;*/
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 2 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, -4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg27".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg27() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg27");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg27/IMG_20180610_205929_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg27/IMG_20180610_205929_1.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg27_output.jpg", 100, 1000000000L/482, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg28".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg28() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg28");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg28/input008.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg28_output.jpg", 811, 1000000000L/21, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 21, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 18, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 8, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 13, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg29".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg29() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg29");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input008.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg29/input009.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg29_output.jpg", 40, 1000000000L/2660, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 88, 127, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 92, 134, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg30".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg30() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg30");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg30/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg30/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg30/input003.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg30_output.jpg", 60, 1000000000L/411, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 2 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, -4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        int [] exp_offsets_x = {0, 0, 0};
+                        int [] exp_offsets_y = {0, -4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else {
+                        fail();
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 134, 254);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 144, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 107, 254);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg31".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg31() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg31");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input008.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input009.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg31/input010.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg31_output.jpg", 609, 1000000000L/25, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 24, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 9, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 13, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg32".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg32() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg32");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg32/input007.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg32_output.jpg", 335, 1000000000L/120, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 34, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 13, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 36, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 61, 254);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg33".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg33() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg33");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input008.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input009.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg33/input010.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg33_output.jpg", 948, 1000000000L/18, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 81, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 63, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg34".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg34() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg34");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg34/IMG_20180627_121959_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg34/IMG_20180627_121959_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg34/IMG_20180627_121959_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg34_output.jpg", 100, 1000000000L/289, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 86, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 108, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 114, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 103, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg35".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg35() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg35");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg35/IMG_20180711_144453_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg35/IMG_20180711_144453_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg35/IMG_20180711_144453_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg35_output.jpg", 100, 1000000000L/2549, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 165, 247);
+            checkHistogramDetails(hdrHistogramDetails, 0, 169, 248);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg36".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg36() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg36");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_3.jpg");
+            // only test 4 images, to reflect latest behaviour that we take 4 images for this ISO/exposure time
+        /*inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_4.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_5.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_6.jpg");
+        inputs.add(TestUtils.avg_images_path + "testAvg36/IMG_20180709_114831_7.jpg");*/
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg36_output.jpg", 752, 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, -12, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                    else if( index == 3 ) {
+                        int [] exp_offsets_x = {0, -28, 0};
+                        int [] exp_offsets_y = {0, 0, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 86, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg37".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg37() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg37");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg37/IMG_20180715_173155_3.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg37_output.jpg", 131, 1000000000L/50, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 12, 109, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 3, 99, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 99, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 125, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 94, 255);
+            checkHistogramDetails(hdrHistogramDetails, 6, 94, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg38".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg38() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg38");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg38/IMG_20180716_232102_7.jpg");
+
+            // n.b., this was a zoomed in photo, but can't quite remember the exact zoom level!
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg38_output.jpg", 1505, 1000000000L/10, 3.95f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg39".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg39() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg39");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input008.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input009.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg39/input010.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg39_output.jpg", 521, 1000000000L/27, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 64, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 25, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg40".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg40() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg40");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input008.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg40/input009.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg40_output.jpg", 199, 1000000000L/120, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 50, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 19, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 50, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 67, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg41".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg41() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg41");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            // example from Google HDR+ dataset
+            // note, the number of input images doesn't necessarily match what we'd take for this scene, but we want to compare
+            // to the Google HDR+ result
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input001.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input002.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input003.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input004.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input005.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input006.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input007.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input008.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input009.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg41/input010.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg41_output.jpg", 100, 1000000000L/869, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 49, 255);
+            //checkHistogramDetails(hdrHistogramDetails, 0, 37, 255);
+            checkHistogramDetails(hdrHistogramDetails, 0, 59, 254);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg42".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg42() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg42");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg42/IMG_20180822_145152_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg42/IMG_20180822_145152_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg42/IMG_20180822_145152_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg42_output.jpg", 100, 1000000000L/2061, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 67, 254);
+            checkHistogramDetails(hdrHistogramDetails, 0, 61, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg43".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg43() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg43");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg43/IMG_20180831_143226_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg43/IMG_20180831_143226_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg43/IMG_20180831_143226_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg43_output.jpg", 100, 1000000000L/2152, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 69, 253);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg44".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg44() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg44");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg44/IMG_20180830_133917_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg44/IMG_20180830_133917_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg44/IMG_20180830_133917_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg44_output.jpg", 40, 1000000000L/2130, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg45".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg45() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg45");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg45/IMG_20180719_133947_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg45/IMG_20180719_133947_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg45/IMG_20180719_133947_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg45_output.jpg", 100, 1000000000L/865, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 75, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg46".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg46() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg46");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg46/IMG_20180903_203141_7.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg46_output.jpg", 1505, 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 30, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg47".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg47() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg47");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg47/IMG_20180911_114752_3.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg47_output.jpg", 749, 1000000000L/12, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 30, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg48".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg48() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg48");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg48/IMG_20180911_110520_7.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg48_output.jpg", 1196, 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 30, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg49".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg49() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg49");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg49/IMG_20180911_120200_7.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg49_output.jpg", 1505, 1000000000L/10, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 30, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg50".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg50() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg50");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg50/IMG_20181015_144335_3.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg50_output.jpg", 114, 1000000000L/33, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            checkHistogramDetails(hdrHistogramDetails, 0, 91, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg51".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg51() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg51");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_3.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg51/IMG_20181025_182917_7.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg51_output.jpg", 1600, 1000000000L/3, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                    if( index == 1 ) {
+                        int [] exp_offsets_x = {0, 8, 0};
+                        int [] exp_offsets_y = {0, 4, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                        assertEquals(0, activity.getApplicationInterface().getHDRProcessor().sharp_index);
+                    }
+                    else if( index == 7 ) {
+                        int [] exp_offsets_x = {0, 60, 0};
+                        int [] exp_offsets_y = {0, 28, 0};
+                        TestUtils.checkHDROffsets(activity, exp_offsets_x, exp_offsets_y, activity.getApplicationInterface().getHDRProcessor().getAvgSampleSize());
+                    }
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 91, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvg52".
+     */
+    @Category(AvgTests.class)
+    @Test
+    public void testAvg52() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvg52");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvg52/IMG_20181119_144836_0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg52/IMG_20181119_144836_1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvg52/IMG_20181119_144836_2.jpg");
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvg52_output.jpg", 100, 1000000000L/297, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 0, 91, 255);
+        });
+    }
+
+    /** Tests Avg algorithm on test samples "testAvgtemp".
+     *  Used for one-off testing, or to recreate NR 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/testAvgtemp/ .
+     */
+    @Test
+    public void testAvgtemp() throws IOException, InterruptedException {
+        Log.d(TAG, "testAvgtemp");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input0.png");
+            /*inputs.add(TestUtils.avg_images_path + "testAvgtemp/input0.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input1.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input2.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input3.jpg");*/
+            /*inputs.add(TestUtils.avg_images_path + "testAvgtemp/input4.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input5.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input6.jpg");
+            inputs.add(TestUtils.avg_images_path + "testAvgtemp/input7.jpg");*/
+
+            TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestAvg(activity,inputs, "testAvgtemp_output.jpg", 250, 1000000000L/33, 1.0f, new TestUtils.TestAvgCallback() {
+                @Override
+                public void doneProcessAvg(int index) {
+                    Log.d(TAG, "doneProcessAvg: " + index);
+                }
+            });
+
+            //checkHistogramDetails(hdrHistogramDetails, 1, 39, 253);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanoramaWhite".
+     *  This tests that auto-alignment fails gracefully if we can't find any matches.
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanoramaWhite() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanoramaWhite");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            inputs.add(TestUtils.panorama_images_path + "testPanoramaWhite/input0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanoramaWhite/input0.jpg");
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+            float panorama_pics_per_screen = 2.0f;
+            String output_name = "testPanoramaWhite_output.jpg";
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, null, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 2.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama1".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama1() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama1");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input3.jpg");
+            float camera_angle_x = 62.93796f;
+            float camera_angle_y = 47.44656f;
+            float panorama_pics_per_screen = 2.0f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (47.44656/49.56283);
+            String output_name = "testPanorama1_output.jpg";
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, null, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 2.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama2".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama2() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            /*final float panorama_pics_per_screen = 1.0f;
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama2xxx/input0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2xxx/input1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2xxx/input2.jpg");*/
+            /*final float panorama_pics_per_screen = 2.0f;
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama1/input0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama1/input3.jpg");
+            String output_name = "testPanorama1_output.jpg";*/
+            float panorama_pics_per_screen = 4.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2/input0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2/input1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2/input2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2/input3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2/input4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama2/input5.jpg");
+            String output_name = "testPanorama2_output.jpg";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, null, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 2.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama3".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama3() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama3");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 4.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131249.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131252.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131255.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131258.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131301.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131303.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131305.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131307.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131315.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131317.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131320.jpg");
+            String output_name = "testPanorama3_output.jpg";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, null, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama3", with panorama_pics_per_screen set
+     *  to 4.0.
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama3_picsperscreen2() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama3_picsperscreen2");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 2.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131249.jpg");
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131252.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131255.jpg");
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131258.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131301.jpg");
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131303.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131305.jpg");
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131307.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131315.jpg");
+            //inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131317.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama3/IMG_20190214_131320.jpg");
+            String output_name = "testPanorama3_picsperscreen2_output.jpg";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, null, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama4".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama4() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama4");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 4.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317_7.jpg");
+            String output_name = "testPanorama4_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama4/IMG_20190222_225317.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama5".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama5() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama5");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 4.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524_7.jpg");
+            String output_name = "testPanorama5_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama5/IMG_20190223_220524.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama6".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama6() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama6");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 4.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232_7.jpg");
+            String output_name = "testPanorama6_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama6/IMG_20190225_154232.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama7".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama7() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama7");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 4.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510_8.jpg");
+            String output_name = "testPanorama7_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama7/IMG_20190225_155510.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama8".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama8() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama8");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 2.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431_3.jpg");
+            String output_name = "testPanorama8_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama8/IMG_20190227_001431.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/52.26029);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama9".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama9() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama9");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213_6.jpg");
+            String output_name = "testPanorama9_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama9/IMG_20190301_145213.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/50.44399);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+
+            try {
+                Thread.sleep(1000); // need to wait for debug images to be saved/broadcast?
+            }
+            catch(InterruptedException e) {
+                e.printStackTrace();
+            }
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama10".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama10() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama10");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_9.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_10.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_11.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948_12.jpg");
+            String output_name = "testPanorama10_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama10/IMG_20190301_144948.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/50.44399);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama11".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama11() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama11");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652_6.jpg");
+            String output_name = "testPanorama11_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama11/IMG_20190306_143652.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/50.44399);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama12".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama12() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama12");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008_9.jpg");
+            String output_name = "testPanorama12_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama12/IMG_20190308_152008.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+            // these images were taken with incorrect camera view angles, so we compensate in the test:
+            panorama_pics_per_screen *= (50.282097/50.44399);
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama13".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama13() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama13");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152_9.jpg");
+            String output_name = "testPanorama13_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama13/IMG_20190512_014152.xml";
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama14".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama14() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama14");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249_9.jpg");
+            String output_name = "testPanorama14_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama14/IMG_20190513_151249.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama15".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama15() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama15");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624_9.jpg");
+            String output_name = "testPanorama15_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama15/IMG_20190513_151624.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama16".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama16() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama16");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731_9.jpg");
+            String output_name = "testPanorama16_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama16/IMG_20190624_151731.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama17".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama17() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama17");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423_9.jpg");
+            String output_name = "testPanorama17_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama17/IMG_20190625_135423.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama18".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama18() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama18");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559_9.jpg");
+            String output_name = "testPanorama18_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama18/IMG_20190626_152559.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama19".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama19() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama19");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059_9.jpg");
+            String output_name = "testPanorama19_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama19/IMG_20190627_134059.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama20".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama20() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama20");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027_9.jpg");
+            String output_name = "testPanorama20_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama20/IMG_20190628_145027.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama21".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama21() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama21");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552_9.jpg");
+            String output_name = "testPanorama21_output.jpg";
+            String gyro_name = TestUtils.panorama_images_path + "testPanorama21/IMG_20190628_145552.xml";
+            //gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 0.5f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama22".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama22() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama22");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama22/IMG_20190629_165627_7.jpg");
+            String output_name = "testPanorama22_output.jpg";
+            String gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama23".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama23() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama23");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama23/IMG_20190702_145916_4.jpg");
+            String output_name = "testPanorama23_output.jpg";
+            String gyro_name = null;
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama24".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama24() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama24");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama24/IMG_20190703_154333_9.jpg");
+            String output_name = "testPanorama24_output.jpg";
+            String gyro_name = null;
+            // taken with OnePlus 3T, Camera2 API:
+            float camera_angle_x = 62.93796f;
+            float camera_angle_y = 47.44656f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama25".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama25() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama25");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            //float panorama_pics_per_screen = 3.33333f / 2.0f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama25/IMG_20190706_215940_6.jpg");
+            String output_name = "testPanorama25_output.jpg";
+            String gyro_name = null;
+            // taken with Nokia 8, Camera2 API:
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama26".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama26() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama26");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama26/IMG_20190706_214842_6.jpg");
+            String output_name = "testPanorama26_output.jpg";
+            String gyro_name = null;
+            // taken with Nokia 8, Camera2 API:
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama27".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama27() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama27");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama27/IMG_20190706_192120_6.jpg");
+            String output_name = "testPanorama27_output.jpg";
+            String gyro_name = null;
+            // taken with Nokia 8, Camera2 API:
+            float camera_angle_x = 66.708595f;
+            float camera_angle_y = 50.282097f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama28".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama28() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama28");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            // right-to-left:
+        /*inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_0.jpg");*/
+            // converted from original JPEGs to PNG using Nokia 8:
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_0.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_1.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_2.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_3.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_4.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_5.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_6.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_7.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_8.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/input_bitmap_9.png");
+            String output_name = "testPanorama28_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, Camera2 API, standard rear camera:
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama28", but with a nbnq similar set of
+     *  input images. Instead of converting the original JPEGs to PNG on Nokia 8, this was done on
+     *  the Samsung Galaxy S10e, which gives small differences, but enough to show up potential
+     *  stability issues.
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama28_galaxys10e() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama28_galaxys10e");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            // right-to-left:
+        /*inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_9.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama28/IMG_20190725_134756_0.jpg");*/
+            // converted from original JPEGs to PNG using Samsung Galaxy S10e:
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_0.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_1.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_2.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_3.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_4.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_5.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_6.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_7.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_8.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama28/galaxys10e_input_bitmap_9.png");
+            String output_name = "testPanorama28_galaxys10e_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, Camera2 API, standard rear camera:
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama29".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama29() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama29");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            // right-to-left:
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_9.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama29/IMG_20190719_145852_0.jpg");
+            String output_name = "testPanorama29_output.jpg";
+            String gyro_name = null;
+            // taken with Nokia 8, old API:
+            float camera_angle_x = 66.1062f;
+            float camera_angle_y = 49.88347f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama30".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama30() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama30");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+        /*inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_9.jpg");*/
+            // converted from original JPEGs to PNG using Nokia 8:
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_0.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_1.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_2.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_3.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_4.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_5.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_6.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_7.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_8.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/nokia8_input_bitmap_9.png");
+            String output_name = "testPanorama30_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, old API, standard rear camera:
+            // n.b., camera angles are indeed the exact same as with Camera2
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama30", but with a nbnq similar set of
+     *  input images. Instead of converting the original JPEGs to PNG on Nokia 8, this was done on
+     *  the Samsung Galaxy S10e, which gives small differences, but enough to show up potential
+     *  stability issues.
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama30_galaxys10e() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama30_galaxys10e");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+        /*inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_0.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_1.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_2.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_3.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_4.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_5.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_6.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_7.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_8.jpg");
+        inputs.add(TestUtils.panorama_images_path + "testPanorama30/IMG_20190723_142934_9.jpg");*/
+            // converted from original JPEGs to PNG using Samsung Galaxy S10e:
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_0.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_1.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_2.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_3.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_4.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_5.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_6.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_7.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_8.png");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama30/galaxys10e_input_bitmap_9.png");
+            String output_name = "testPanorama30_galaxys10e_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, old API, standard rear camera:
+            // n.b., camera angles are indeed the exact same as with Camera2
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama31".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama31() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama31");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama31/IMG_20190704_135633_6.jpg");
+            String output_name = "testPanorama31_output.jpg";
+            String gyro_name = null;
+            // taken with OnePlus 3T, Camera2 API:
+            float camera_angle_x = 62.93796f;
+            float camera_angle_y = 47.44656f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama3".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama32() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama32");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama32/IMG_20190705_145938_8.jpg");
+            String output_name = "testPanorama32_output.jpg";
+            String gyro_name = null;
+            // taken with OnePlus 3T, old API:
+            float camera_angle_x = 60.0f;
+            float camera_angle_y = 45.0f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama33".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama33() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama33");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama33/IMG_20190713_013437_5.jpg");
+            String output_name = "testPanorama33_output.jpg";
+            String gyro_name = null;
+            // taken with Nokia 8, old API:
+            float camera_angle_x = 66.1062f;
+            float camera_angle_y = 49.88347f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama34".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama34() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama34");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            // right-to-left:
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_9.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama34/IMG_20190717_144042_0.jpg");
+            String output_name = "testPanorama34_output.jpg";
+            String gyro_name = null;
+            // taken with Nexus 6, old API:
+            float camera_angle_x = 62.7533f;
+            float camera_angle_y = 47.298824f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama35".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama35() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama35");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama35/IMG_20190717_145114_9.jpg");
+            String output_name = "testPanorama35_output.jpg";
+            String gyro_name = null;
+            // taken with Nexus 7, old API:
+            float camera_angle_x = 55.0f;
+            float camera_angle_y = 41.401073f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama36".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama36() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama36");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama36/IMG_20190722_201331_7.jpg");
+            String output_name = "testPanorama36_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, Camera2 API, ultra wide rear camera:
+            float camera_angle_x = 104.00253f;
+            float camera_angle_y = 81.008804f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama37".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama37() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama37");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama37/IMG_20190723_203441_8.jpg");
+            String output_name = "testPanorama37_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, old API, standard rear camera:
+            // n.b., camera angles are indeed the exact same as with Camera2
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+
+    /** Tests panorama algorithm on test samples "testPanorama38".
+     */
+    @Category(PanoramaTests.class)
+    @Test
+    public void testPanorama38() throws IOException, InterruptedException {
+        Log.d(TAG, "testPanorama38");
+
+        setToDefault();
+
+        mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread
+            // list assets
+            List inputs = new ArrayList<>();
+
+            float panorama_pics_per_screen = 3.33333f;
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_0.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_1.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_2.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_3.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_4.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_5.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_6.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_7.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_8.jpg");
+            inputs.add(TestUtils.panorama_images_path + "testPanorama38/IMG_20190722_141148_9.jpg");
+            String output_name = "testPanorama38_output.jpg";
+            String gyro_name = null;
+            // taken with Samsung Galaxy S10e, Camera2 API, standard rear camera:
+            float camera_angle_x = 66.3177f;
+            float camera_angle_y = 50.04736f;
+
+            TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f);
+        });
+    }
+}
diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java
new file mode 100644
index 000000000..abb27d562
--- /dev/null
+++ b/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java
@@ -0,0 +1,10 @@
+package net.sourceforge.opencamera;
+
+import org.junit.experimental.categories.Categories;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Categories.class)
+@Categories.IncludeCategory(PanoramaTests.class)
+@Suite.SuiteClasses({InstrumentedTest.class})
+public class PanoramaTestSuite {}
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 e9c669010..7cf88e411 100644
--- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java
@@ -413,6 +413,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
 
     // for testing; must be volatile for test project reading the state
     private boolean is_test; // whether called from OpenCamera.test testing
+    private boolean is_test_junit4;
     public volatile int count_cameraStartPreview;
     public volatile int count_cameraAutoFocus;
     public volatile int count_cameraTakePicture;
@@ -438,9 +439,11 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
         if( activity.getIntent() != null && activity.getIntent().getExtras() != null ) {
             // whether called from testing
             is_test = activity.getIntent().getExtras().getBoolean("test_project");
+            is_test_junit4 = activity.getIntent().getExtras().getBoolean("test_project_junit4");
         }
         if( MyDebug.LOG ) {
             Log.d(TAG, "is_test: " + is_test);
+            Log.d(TAG, "is_test_junit4: " + is_test_junit4);
         }
 
         this.using_android_l = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && applicationInterface.useCamera2();
@@ -8572,8 +8575,13 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu
     	   pause/unpause the preview instead of reopening the camera).
     	 */
         //
-        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N )
+        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
+            if( is_test_junit4 ) {
+                // see https://stackoverflow.com/questions/29550508/espresso-freezing-on-view-with-looping-animation
+                return 32;
+            }
             return 16;
+        }
         // old behaviour: avoid overloading ui thread when taking photo
         return this.isTakingPhoto() ? 500 : 100;
     }
-- 
GitLab


From 303f4ebafbdbe28abfe75239dbf717e81aa4d700 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 9 Oct 2022 20:16:28 +0100
Subject: [PATCH 066/117] Update comments.

---
 app/src/main/rs/process_hdr.rs | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/app/src/main/rs/process_hdr.rs b/app/src/main/rs/process_hdr.rs
index 3821de08d..e4622e104 100644
--- a/app/src/main/rs/process_hdr.rs
+++ b/app/src/main/rs/process_hdr.rs
@@ -49,14 +49,14 @@ const float exposure = 1.2f;
 // for Reinhard:
 float tonemap_scale = 1.0f;
 
-// for Filmic Uncharted 2:
+// for Filmic:
 const float filmic_exposure_bias = 2.0f / 255.0f;
 float W = 11.2f;
 
 // for various:
 float linear_scale = 1.0f;
 
-static float Uncharted2Tonemap(float x) {
+static float FU2Tonemap(float x) {
     const float A = 0.15f;
     const float B = 0.50f;
     const float C = 0.10f;
@@ -122,11 +122,11 @@ static uchar4 tonemap(float3 hdr) {
         }
         case tonemap_algorithm_filmic_c:
         {
-            // Filmic Uncharted 2
-            float white_scale = 255.0f / Uncharted2Tonemap(W);
-            float curr_r = Uncharted2Tonemap(filmic_exposure_bias * hdr.r);
-            float curr_g = Uncharted2Tonemap(filmic_exposure_bias * hdr.g);
-            float curr_b = Uncharted2Tonemap(filmic_exposure_bias * hdr.b);
+            // Filmic
+            float white_scale = 255.0f / FU2Tonemap(W);
+            float curr_r = FU2Tonemap(filmic_exposure_bias * hdr.r);
+            float curr_g = FU2Tonemap(filmic_exposure_bias * hdr.g);
+            float curr_b = FU2Tonemap(filmic_exposure_bias * hdr.b);
             curr_r *= white_scale;
             curr_g *= white_scale;
             curr_b *= white_scale;
-- 
GitLab


From cb6edaf2960c7f14464e68863ecfa5cfa2773deb Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 9 Oct 2022 21:07:58 +0100
Subject: [PATCH 067/117] Renaming for consistency.

---
 .../net/sourceforge/opencamera/HDRProcessor.java | 16 ++++++++--------
 app/src/main/rs/process_hdr.rs                   | 16 ++++++++--------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java
index ec81b79da..ac4f928fb 100644
--- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java
+++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java
@@ -63,7 +63,7 @@ public class HDRProcessor {
         TONEMAPALGORITHM_CLAMP,
         TONEMAPALGORITHM_EXPONENTIAL,
         TONEMAPALGORITHM_REINHARD,
-        TONEMAPALGORITHM_FILMIC,
+        TONEMAPALGORITHM_FU2,
         TONEMAPALGORITHM_ACES
     }
     public enum DROTonemappingAlgorithm {
@@ -738,10 +738,10 @@ public class HDRProcessor {
                     Log.d(TAG, "tonemapping algorithm: reinhard");
                 processHDRScript.set_tonemap_algorithm( processHDRScript.get_tonemap_algorithm_reinhard_c() );
                 break;
-            case TONEMAPALGORITHM_FILMIC:
+            case TONEMAPALGORITHM_FU2:
                 if( MyDebug.LOG )
-                    Log.d(TAG, "tonemapping algorithm: filmic");
-                processHDRScript.set_tonemap_algorithm( processHDRScript.get_tonemap_algorithm_filmic_c() );
+                    Log.d(TAG, "tonemapping algorithm: fu2");
+                processHDRScript.set_tonemap_algorithm( processHDRScript.get_tonemap_algorithm_fu2_c() );
                 break;
             case TONEMAPALGORITHM_ACES:
                 if( MyDebug.LOG )
@@ -863,14 +863,14 @@ public class HDRProcessor {
                 processHDRScript.set_linear_scale(linear_scale);
                 break;
             }
-            case TONEMAPALGORITHM_FILMIC:
+            case TONEMAPALGORITHM_FU2:
             {
-                // For filmic, we have f(V) = U(EV) / U(W), where V is the HDR value, U is a function.
+                // For FU2, we have f(V) = U(EV) / U(W), where V is the HDR value, U is a function.
                 // We want f(Vmax) = 1, so EVmax = W
-                float E = processHDRScript.get_filmic_exposure_bias();
+                float E = processHDRScript.get_fu2_exposure_bias();
                 float W = E * max_possible_value;
                 if( MyDebug.LOG )
-                    Log.d(TAG, "filmic W: " + W);
+                    Log.d(TAG, "fu2 W: " + W);
                 processHDRScript.set_W(W);
                 break;
             }
diff --git a/app/src/main/rs/process_hdr.rs b/app/src/main/rs/process_hdr.rs
index e4622e104..59b3dc2e6 100644
--- a/app/src/main/rs/process_hdr.rs
+++ b/app/src/main/rs/process_hdr.rs
@@ -38,7 +38,7 @@ const float weight_scale_c = (float)((1.0-1.0/127.5)/127.5);
 const int tonemap_algorithm_clamp_c = 0;
 const int tonemap_algorithm_exponential_c = 1;
 const int tonemap_algorithm_reinhard_c = 2;
-const int tonemap_algorithm_filmic_c = 3;
+const int tonemap_algorithm_fu2_c = 3;
 const int tonemap_algorithm_aces_c = 4;
 
 int tonemap_algorithm = tonemap_algorithm_reinhard_c;
@@ -49,8 +49,8 @@ const float exposure = 1.2f;
 // for Reinhard:
 float tonemap_scale = 1.0f;
 
-// for Filmic:
-const float filmic_exposure_bias = 2.0f / 255.0f;
+// for FU2:
+const float fu2_exposure_bias = 2.0f / 255.0f;
 float W = 11.2f;
 
 // for various:
@@ -120,13 +120,13 @@ static uchar4 tonemap(float3 hdr) {
             }*/
             break;
         }
-        case tonemap_algorithm_filmic_c:
+        case tonemap_algorithm_fu2_c:
         {
-            // Filmic
+            // FU2 (Filmic)
             float white_scale = 255.0f / FU2Tonemap(W);
-            float curr_r = FU2Tonemap(filmic_exposure_bias * hdr.r);
-            float curr_g = FU2Tonemap(filmic_exposure_bias * hdr.g);
-            float curr_b = FU2Tonemap(filmic_exposure_bias * hdr.b);
+            float curr_r = FU2Tonemap(fu2_exposure_bias * hdr.r);
+            float curr_g = FU2Tonemap(fu2_exposure_bias * hdr.g);
+            float curr_b = FU2Tonemap(fu2_exposure_bias * hdr.b);
             curr_r *= white_scale;
             curr_g *= white_scale;
             curr_b *= white_scale;
-- 
GitLab


From a91ad175476d6b9d39e818c04e40087bef90c4aa Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sun, 9 Oct 2022 21:08:15 +0100
Subject: [PATCH 068/117] Add source for aces.

---
 app/src/main/rs/process_hdr.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/src/main/rs/process_hdr.rs b/app/src/main/rs/process_hdr.rs
index 59b3dc2e6..e72011113 100644
--- a/app/src/main/rs/process_hdr.rs
+++ b/app/src/main/rs/process_hdr.rs
@@ -138,6 +138,7 @@ static uchar4 tonemap(float3 hdr) {
         }
         case tonemap_algorithm_aces_c:
         {
+            // https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ (released under public domain cc0)
             const float a = 2.51f;
             const float b = 0.03f;
             const float c = 2.43f;
-- 
GitLab


From 34e1fde3d57ce5cf2bacfc554d350b85b55d0f1a Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Fri, 14 Oct 2022 21:23:07 +0100
Subject: [PATCH 069/117] Improve angle_step for larger zoom ratios.

---
 _docs/history.html                            |  1 +
 .../opencamera/ui/DrawPreview.java            | 21 +++++++++++++------
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index d6eef9c12..e76e0a158 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -68,6 +68,7 @@ UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can
 UPDATED Make zoom seekbar snap to powers of two (for Camera2 API).
 UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API).
 UPDATED Improved look of on-screen level line.
+UPDATED On-screen pitch and compass lines now show smaller intervals as camera zooms in.
 UPDATED Camera2 extension night mode now adds "_Night" to filename.
 UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera.
 UPDATED Use system toasts without custom views when appropriate.
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 d808c9f1f..00c39629c 100644
--- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
+++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java
@@ -2232,6 +2232,19 @@ public class DrawPreview {
         canvas.restore();
     }
 
+    private int getAngleStep() {
+        Preview preview = main_activity.getPreview();
+        int angle_step = 10;
+        float zoom_ratio = preview.getZoomRatio();
+        if( zoom_ratio >= 10.0f )
+            angle_step = 1;
+        else if( zoom_ratio >= 5.0f )
+            angle_step = 2;
+        else if( zoom_ratio >= 2.0f )
+            angle_step = 5;
+        return angle_step;
+    }
+
     private void drawAngleLines(Canvas canvas, int device_ui_rotation, long time_ms) {
         Preview preview = main_activity.getPreview();
         CameraController camera_controller = preview.getCameraController();
@@ -2382,9 +2395,7 @@ public class DrawPreview {
                 // 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 )
-                    angle_step = 5;
+                int angle_step = getAngleStep();
                 for(int latitude_angle=-90;latitude_angle<=90;latitude_angle+=angle_step) {
                     double this_angle = pitch_angle - latitude_angle;
                     if( Math.abs(this_angle) < 90.0 ) {
@@ -2423,9 +2434,7 @@ public class DrawPreview {
                 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;
-                if( preview.getZoomRatio() >= 2.0f )
-                    angle_step = 5;
+                int angle_step = getAngleStep();
                 for(int longitude_angle=0;longitude_angle<360;longitude_angle+=angle_step) {
                     double this_angle = longitude_angle - geo_angle;
 					/*if( MyDebug.LOG ) {
-- 
GitLab


From 59e10bbf701c52a9e6cb81c15f3f854c93392bd3 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 15 Oct 2022 21:26:21 +0100
Subject: [PATCH 070/117] Fix warning for redundant initialisation.

---
 .../opencamera/cameracontroller/CameraController2.java          | 2 +-
 1 file changed, 1 insertion(+), 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 12a2b64d7..3f3d8a51d 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -2505,7 +2505,7 @@ public class CameraController2 extends CameraController {
      * @return         Index of ratios list that is for 1x zoom.
      */
     public static int computeZoomRatios(List ratios, float min_zoom, float max_zoom) {
-        int zoom_value_1x = 0;
+        int zoom_value_1x;
 
         // prepare zoom rations > 1x
         // set 20 steps per 2x factor
-- 
GitLab


From 370c68e9fd273a7a610df5b6e69ff27d47b12db8 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Sat, 15 Oct 2022 21:33:24 +0100
Subject: [PATCH 071/117] Better to not switch to manual mode in DRO and NR, as
 it meant losing benefit of manufacturer algorithms.

---
 _docs/help.html                               |   2 -
 _docs/history.html                            |   2 +
 .../opencamera/MyApplicationInterface.java    |   6 -
 .../cameracontroller/CameraController.java    |   4 -
 .../cameracontroller/CameraController1.java   |   5 -
 .../cameracontroller/CameraController2.java   | 157 ++----------------
 .../preview/ApplicationInterface.java         |   1 -
 .../preview/BasicApplicationInterface.java    |   5 -
 .../opencamera/preview/Preview.java           |   2 -
 .../sourceforge/opencamera/test/UnitTest.java |  15 --
 10 files changed, 15 insertions(+), 184 deletions(-)

diff --git a/_docs/help.html b/_docs/help.html
index 534d9d337..0f5ab0020 100644
--- a/_docs/help.html
+++ b/_docs/help.html
@@ -368,8 +368,6 @@ regions will have their brightness boosted to bring out the detail. This mode is
 of brightness (e.g., on a bright sunny day) as well as being useful to automatically optimise photos in low light scenes. Also
 see DRO vs HDR.

-

Note that if Camera2 API is used, DRO will give improved quality in bright scenes.

-

HDR

High Dynamic Range Imaging (HDR) is a technique where the camera takes multiple shots at different exposures, and combines them diff --git a/_docs/history.html b/_docs/history.html index e76e0a158..fd59d3b7c 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -66,6 +66,8 @@ UPDATED Applied a timeout of 1 second for focusing with Camera2 API. UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can zoom out to ultra-wide camera. UPDATED Make zoom seekbar snap to powers of two (for Camera2 API). +UPDATED No longer switch to manual mode in DRO and NR photo modes, as on some devices this meant + losing the benefit of manufacturer algorithms. UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API). UPDATED Improved look of on-screen level line. UPDATED On-screen pitch and compass lines now show smaller intervals as camera zooms in. diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 30c71d6d3..908897415 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -1709,12 +1709,6 @@ public class MyApplicationInterface extends BasicApplicationInterface { return PhotoMode.Standard; } - @Override - public boolean getOptimiseAEForDROPref() { - PhotoMode photo_mode = getPhotoMode(); - return( photo_mode == PhotoMode.DRO ); - } - private ImageSaver.Request.ImageFormat getImageFormatPref() { switch( sharedPreferences.getString(PreferenceKeys.ImageFormatPreferenceKey, "preference_image_format_jpeg") ) { case "preference_image_format_webp": 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 4b3f99357..c696df0c0 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java @@ -443,10 +443,6 @@ public abstract class CameraController { * burst if known. If not known (e.g., for continuous burst mode), returns 0. */ public abstract int getBurstTotal(); - /** If optimise_ae_for_dro is true, then this is a hint that if in auto-exposure mode and flash/torch - * is not on, the CameraController should try to optimise for a DRO (dynamic range optimisation) mode. - */ - public abstract void setOptimiseAEForDRO(boolean optimise_ae_for_dro); /** * @param want_raw Whether to enable taking photos in RAW (DNG) format. 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 379da2c2b..f2e3ba48f 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController1.java @@ -882,11 +882,6 @@ public class CameraController1 extends CameraController { return n_burst; } - @Override - public void setOptimiseAEForDRO(boolean optimise_ae_for_dro) { - // not supported for CameraController1 - } - @Override public void setRaw(boolean want_raw, int max_raw_images) { // 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 3f3d8a51d..c80dd3569 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -72,7 +72,6 @@ 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; @@ -213,7 +212,6 @@ public class CameraController2 extends CameraController { 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; @@ -1990,12 +1988,11 @@ 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_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); @@ -4416,29 +4413,6 @@ public class CameraController2 extends CameraController { return n_burst_total; } - @Override - public void setOptimiseAEForDRO(boolean optimise_ae_for_dro) { - if( MyDebug.LOG ) - Log.d(TAG, "setOptimiseAEForDRO: " + optimise_ae_for_dro); - 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. - this.optimise_ae_for_dro = false; - 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; - } - } - @Override public void setBurstNImages(int burst_requested_n_images) { if( MyDebug.LOG ) @@ -6435,62 +6409,6 @@ public class CameraController2 extends CameraController { this.continuous_focus_move_callback = cb; } - static public double getScaleForExposureTime(long exposure_time, long fixed_exposure_time, long scaled_exposure_time, double full_exposure_time_scale) { - if( MyDebug.LOG ) - Log.d(TAG, "getScaleForExposureTime"); - double alpha = (exposure_time - fixed_exposure_time) / (double) (scaled_exposure_time - fixed_exposure_time); - if( alpha < 0.0 ) - alpha = 0.0; - else if( alpha > 1.0 ) - alpha = 1.0; - if( MyDebug.LOG ) { - Log.d(TAG, "exposure_time: " + exposure_time); - Log.d(TAG, "alpha: " + alpha); - } - // alpha==0 means exposure_time_scale==1; alpha==1 means exposure_time_scale==full_exposure_time_scale - return (1.0 - alpha) + alpha * full_exposure_time_scale; - } - - /** 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, boolean set_iso, int new_iso) { - if( MyDebug.LOG ) - Log.d(TAG, "setManualExposureTime: " + exposure_time); - Range iso_range = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); // may be null on some devices - 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 ) - exposure_time = max_exposure_time; - if (MyDebug.LOG) { - Log.d(TAG, "exposure_time: " + exposure_time); - } - stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_OFF); - { - // set ISO - int iso = 800; - 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 ) - stillBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, capture_result_frame_duration); - else - stillBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, 1000000000L/30); - stillBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposure_time); - } - } - private void takePictureAfterPrecapture() { if( MyDebug.LOG ) Log.d(TAG, "takePictureAfterPrecapture"); @@ -6551,22 +6469,10 @@ public class CameraController2 extends CameraController { stillBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH); test_fake_flash_photo++; } - if( !camera_settings.has_iso && this.optimise_ae_for_dro && capture_result_has_exposure_time && (camera_settings.flash_value.equals("flash_off") || camera_settings.flash_value.equals("flash_auto") || camera_settings.flash_value.equals("flash_frontscreen_auto") ) ) { - final double full_exposure_time_scale = Math.pow(2.0, -0.5); - final long fixed_exposure_time = 1000000000L/60; // we only scale the exposure time at all if it's less than this value - final long scaled_exposure_time = 1000000000L/120; // we only scale the exposure time by the full_exposure_time_scale if the exposure time is less than this value - long exposure_time = capture_result_exposure_time; - if( exposure_time <= fixed_exposure_time ) { - 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 ) { - Log.d(TAG, "reduce exposure shutter speed further, was: " + exposure_time); - Log.d(TAG, "exposure_time_scale: " + exposure_time_scale); - } - modified_from_camera_settings = true; - setManualExposureTime(stillBuilder, exposure_time, false, 0); - } - } + // Versions previous to 1.51 would switch to manual mode and underexpose in bright scenes; however on more modern devices such as Samsung and + // Pixels, this means that we lose the benefit of manufacturer algorithms creating a worse result. So we're better off staying in auto mode. + // (Even on old versions, we didn't do this on OnePlus devices due to OnePlus 3T having preview corruption / camera freezing problems when + // using manual shutter speeds.) //stillBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); //stillBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && sessionType != SessionType.SESSIONTYPE_EXTENSION ) { @@ -7228,59 +7134,22 @@ 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; - // 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 ) { - long exposure_time = noise_reduction_low_light ? 1000000000L/3 : 1000000000L/10; - if( capture_result_exposure_time < exposure_time ) { - if( MyDebug.LOG ) - Log.d(TAG, "also set long exposure time"); - modified_from_camera_settings = true; - - boolean set_new_iso = false; - int new_iso = 0; - { - // 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 ) - Log.d(TAG, "no need to extend exposure time for dark scene, already long enough: " + exposure_time); - } - } + // Versions previous to 1.51 would switch to manual mode and overexpose in bright scenes; however on more modern devices such as Samsung and + // Pixels, this means that we lose the benefit of manufacturer algorithms creating a worse result. So we're better off staying in auto mode. + // (Even on old versions, we didn't do this for OnePlus devices, due to bug on OnePlus 3T where manual mode can't be set above 800, so this + // would cause images to come out too dark.) } else if( capture_result_has_exposure_time ) { - //final double full_exposure_time_scale = 0.5; - final double full_exposure_time_scale = Math.pow(2.0, -0.5); - final long fixed_exposure_time = 1000000000L/60; // we only scale the exposure time at all if it's less than this value - final long scaled_exposure_time = 1000000000L/120; // we only scale the exposure time by the full_exposure_time_scale if the exposure time is less than this value + final long fixed_exposure_time = 1000000000L/60; long exposure_time = capture_result_exposure_time; if( exposure_time <= fixed_exposure_time ) { if( MyDebug.LOG ) Log.d(TAG, "optimise for bright scene"); //n_burst = 2; n_burst = 3; - // 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 ) { - Log.d(TAG, "reduce exposure shutter speed further, was: " + exposure_time); - Log.d(TAG, "exposure_time_scale: " + exposure_time_scale); - } - modified_from_camera_settings = true; - setManualExposureTime(stillBuilder, exposure_time, false, 0); - } + // Versions previous to 1.51 would switch to manual mode and underexpose in bright scenes; however on more modern devices such as + // Samsung and Pixels, this means that we lose the benefit of manufacturer algorithms creating a worse result. So we're better off + // staying in auto mode. } } } 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 a367b441a..c8b8caf4d 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/ApplicationInterface.java @@ -165,7 +165,6 @@ public interface ApplicationInterface { @RequiresApi(api = Build.VERSION_CODES.S) int getCameraExtensionPref(); // if isCameraExtensionPref() returns true, the camera extension mode to use float getAperturePref(); // get desired aperture (called if Preview.getSupportedApertures() returns non-null); return -1.0f for no preference - boolean getOptimiseAEForDROPref(); // see CameraController doc for setOptimiseAEForDRO(). enum RawPref { RAWPREF_JPEG_ONLY, // JPEG only RAWPREF_JPEG_DNG // JPEG and RAW (DNG) 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 b12b7b144..c79e17bc5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/BasicApplicationInterface.java @@ -369,11 +369,6 @@ public abstract class BasicApplicationInterface implements ApplicationInterface return -1.0f; } - @Override - public boolean getOptimiseAEForDROPref() { - return false; - } - @Override public RawPref getRawPref() { return RawPref.RAWPREF_JPEG_ONLY; 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 7cf88e411..be2778d5a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -2134,8 +2134,6 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu } } - camera_controller.setOptimiseAEForDRO( applicationInterface.getOptimiseAEForDROPref() ); - // Must set preview size before starting camera preview // and must do it after setting photo vs video mode // and after setting what camera extension we're using (if any) 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 804a7af5c..b33b27af0 100644 --- a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java +++ b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java @@ -515,21 +515,6 @@ public class UnitTest { } } - @Test - public void testScaleForExposureTime() { - Log.d(TAG, "testScaleForExposureTime"); - final double delta = 1.0e-6; - final double full_exposure_time_scale = 0.5f; - final long fixed_exposure_time = 1000000000L/60; // we only scale the exposure time at all if it's less than this value - final long scaled_exposure_time = 1000000000L/120; // we only scale the exposure time by the full_exposure_time_scale if the exposure time is less than this value - assertEquals( 1.0, CameraController2.getScaleForExposureTime(1000000000L/12, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale), delta ); - assertEquals( 1.0, CameraController2.getScaleForExposureTime(1000000000L/60, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale), delta ); - assertEquals( 1.0, CameraController2.getScaleForExposureTime(1000000000L/60, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale), delta ); - assertEquals( 2.0/3.0, CameraController2.getScaleForExposureTime(1000000000L/90, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale), delta ); - assertEquals( 0.5, CameraController2.getScaleForExposureTime(1000000000L/120, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale), delta ); - assertEquals( 0.5, CameraController2.getScaleForExposureTime(1000000000L/240, fixed_exposure_time, scaled_exposure_time, full_exposure_time_scale), delta ); - } - /*@Test public void testExponentialScaling() { Log.d(TAG, "testExponentialScaling"); -- GitLab From 52350a878db67f1ec786f818943832a0e27a18fa Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 16 Oct 2022 14:34:13 +0100 Subject: [PATCH 072/117] takePictureBurst() should use TEMPLATE_MANUAL for manual exposure. --- _docs/history.html | 1 + .../opencamera/cameracontroller/CameraController2.java | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index fd59d3b7c..50f080709 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -52,6 +52,7 @@ Version 1.51 (Work in progress) FIXED Gallery thumbnail had incorrect orientation on some Android 10+ devices. FIXED Focus bracketing images came out underexposed on some devices since version 1.50 (e.g. Pixel 6 Pro). +FIXED Problems with NR, fast burst and long manual exposures on some devices (e.g., Pixel 6 Pro). FIXED Face detection on-screen icon shouldn't show in camera vendor extension modes (as not supported). FIXED For Camera2 API, red eye flash was incorrectly being shown even on devices that didn't 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 c80dd3569..ee4a7b445 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -7065,9 +7065,10 @@ public class CameraController2 extends CameraController { Log.d(TAG, "imageReader surface: " + imageReader.getSurface().toString()); } - // n.b., takePictureBurst() not currently called if previewIsVideoMode==true, but have put this code here for possible future use - CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(previewIsVideoMode ? CameraDevice.TEMPLATE_VIDEO_SNAPSHOT : CameraDevice.TEMPLATE_STILL_CAPTURE); - stillBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, previewIsVideoMode ? CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT : CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE); + CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(previewIsVideoMode ? CameraDevice.TEMPLATE_VIDEO_SNAPSHOT : camera_settings.has_iso ? CameraDevice.TEMPLATE_MANUAL : CameraDevice.TEMPLATE_STILL_CAPTURE); + // N.B., takePictureBurst() not currently called if previewIsVideoMode==true, but have put this code here for possible future use. + // Important to use TEMPLATE_MANUAL for manual exposure: this fixes bug on Pixel 6 Pro where manual exposure is ignored when longer than the + // preview exposure time (e.g. for fast burst). // n.b., don't set RequestTagType.CAPTURE here - we only do it for the last of the burst captures (see below) camera_settings.setupBuilder(stillBuilder, true); if( use_fake_precapture_mode && fake_precapture_torch_performed ) { -- GitLab From f0e89036277cd7bd3cdb17904788bc593b69757c Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 16 Oct 2022 14:37:40 +0100 Subject: [PATCH 073/117] Fix the fix for focus bracketing - we should still use TEMPLATE_MANUAL after all if in manual mode. --- .../opencamera/cameracontroller/CameraController2.java | 10 ++++++---- 1 file changed, 6 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 ee4a7b445..a4ebf2abe 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -6699,11 +6699,13 @@ public class CameraController2 extends CameraController { } int n_dummy_requests = 0; - CaptureRequest.Builder stillBuilder = camera.createCaptureRequest(burst_type == BurstType.BURSTTYPE_EXPO ? CameraDevice.TEMPLATE_MANUAL : CameraDevice.TEMPLATE_STILL_CAPTURE); + CaptureRequest.Builder stillBuilder = camera.createCaptureRequest((burst_type == BurstType.BURSTTYPE_EXPO || camera_settings.has_iso) ? CameraDevice.TEMPLATE_MANUAL : CameraDevice.TEMPLATE_STILL_CAPTURE); // Needs to be TEMPLATE_MANUAL! Otherwise first image in burst may come out incorrectly (on Pixel 6 Pro, - // the first image incorrectly had HDR+ applied, which we don't want here). - // update: but only when doing burst for expo bracketing, not focus bracketing! (Only manual exposure - // should use TEMPLATE_MANUAL, otherwise focus bracketing images come out underexposed on Pixel 6 Pro) + // the first image incorrectly had HDR+ applied, which we don't want here). Also problem on Pixel 6 Pro + // where manual exposure is ignored when longer than the preview exposure. + // Update: but only when doing burst for expo bracketing, not focus bracketing (unless actually doing that + // in manual mode)! (Only manual exposure should use TEMPLATE_MANUAL, otherwise focus bracketing images + // come out underexposed on Pixel 6 Pro). // n.b., don't set RequestTagType.CAPTURE here - we only do it for the last of the burst captures (see below) camera_settings.setupBuilder(stillBuilder, true); -- GitLab From c0a0927d1b565dcacbef75e943e51583c19351ba Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 19 Oct 2022 22:21:23 +0100 Subject: [PATCH 074/117] New option for HDR tonemapping. --- _docs/help.html | 4 +++ _docs/history.html | 2 ++ .../sourceforge/opencamera/HDRProcessor.java | 1 + .../sourceforge/opencamera/ImageSaver.java | 13 +++++++++- .../sourceforge/opencamera/MainActivity.java | 1 + .../opencamera/MyApplicationInterface.java | 26 ++++++++++++++++++- .../opencamera/MyPreferenceFragment.java | 4 +++ .../opencamera/PreferenceKeys.java | 2 ++ app/src/main/res/values/arrays.xml | 12 +++++++++ app/src/main/res/values/strings.xml | 7 +++++ app/src/main/res/xml/preferences.xml | 10 +++++++ 11 files changed, 80 insertions(+), 2 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index 0f5ab0020..524dba2aa 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -886,6 +886,10 @@ create the final HDR image (although if you don't want Open Camera's HDR mode at Exposure Bracketing Photo Mode). Note this will make saving slower, especially if options like "Stamp photos" or Auto-level are also used.

+

HDR tonemapping - When using HDR mode, the high dynamic range image needs to be converted +back to a regular image, using a process called tonemapping. This option allows you to choose some different +tonemapping algorithms.

+

HDR contrast enhancement - When using HDR mode, in some (bright) scenes a local contrast enhancement algorithm is applied to improve the look of the image. It also gives such images a look that is stereotypically associated with "HDR". If you prefer not to apply this at all, you can change this option from "Smart" to "Off". Or you can diff --git a/_docs/history.html b/_docs/history.html index 50f080709..0c99f9161 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -63,6 +63,8 @@ FIXED Doesn't display error message if using volume keys to turn auto-level on ADDED Shading for auto-level and crop guides, to darken the preview outside of the region of interest. ADDED Display message to hold device steady when using X-Night photo mode. +ADDED New option Settings/Photo settings/"HDR tonemapping" to choose tonemapping algorithm used + for HDR photo mode. UPDATED Applied a timeout of 1 second for focusing with Camera2 API. UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can zoom out to ultra-wide camera. diff --git a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java index ac4f928fb..f91131735 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -66,6 +66,7 @@ public class HDRProcessor { TONEMAPALGORITHM_FU2, TONEMAPALGORITHM_ACES } + public static TonemappingAlgorithm default_tonemapping_algorithm_c = TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD; public enum DROTonemappingAlgorithm { DROALGORITHM_NONE, DROALGORITHM_GAINGAMMA diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 85c80971a..69dd28622 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -152,6 +152,7 @@ public class ImageSaver extends Thread { final boolean is_front_facing; boolean mirror; final Date current_date; + final HDRProcessor.TonemappingAlgorithm preference_hdr_tonemapping_algorithm; // for HDR final String preference_hdr_contrast_enhancement; // for HDR final int iso; // not applicable for RAW image final long exposure_time; // not applicable for RAW image @@ -191,6 +192,7 @@ public class ImageSaver extends Thread { boolean is_front_facing, boolean mirror, Date current_date, + HDRProcessor.TonemappingAlgorithm preference_hdr_tonemapping_algorithm, String preference_hdr_contrast_enhancement, int iso, long exposure_time, @@ -223,6 +225,7 @@ public class ImageSaver extends Thread { this.is_front_facing = is_front_facing; this.mirror = mirror; this.current_date = current_date; + this.preference_hdr_tonemapping_algorithm = preference_hdr_tonemapping_algorithm; this.preference_hdr_contrast_enhancement = preference_hdr_contrast_enhancement; this.iso = iso; this.exposure_time = exposure_time; @@ -267,6 +270,7 @@ public class ImageSaver extends Thread { this.is_front_facing, this.mirror, this.current_date, + this.preference_hdr_tonemapping_algorithm, this.preference_hdr_contrast_enhancement, this.iso, this.exposure_time, @@ -578,6 +582,7 @@ public class ImageSaver extends Thread { boolean is_front_facing, boolean mirror, Date current_date, + HDRProcessor.TonemappingAlgorithm preference_hdr_tonemapping_algorithm, String preference_hdr_contrast_enhancement, int iso, long exposure_time, @@ -611,6 +616,7 @@ public class ImageSaver extends Thread { is_front_facing, mirror, current_date, + preference_hdr_tonemapping_algorithm, preference_hdr_contrast_enhancement, iso, exposure_time, @@ -655,6 +661,7 @@ public class ImageSaver extends Thread { false, false, current_date, + HDRProcessor.default_tonemapping_algorithm_c, null, 0, 0, @@ -713,6 +720,7 @@ public class ImageSaver extends Thread { is_front_facing, mirror, current_date, + HDRProcessor.default_tonemapping_algorithm_c, null, iso, exposure_time, @@ -794,6 +802,7 @@ public class ImageSaver extends Thread { boolean is_front_facing, boolean mirror, Date current_date, + HDRProcessor.TonemappingAlgorithm preference_hdr_tonemapping_algorithm, String preference_hdr_contrast_enhancement, int iso, long exposure_time, @@ -829,6 +838,7 @@ public class ImageSaver extends Thread { is_front_facing, mirror, current_date, + preference_hdr_tonemapping_algorithm, preference_hdr_contrast_enhancement, iso, exposure_time, @@ -942,6 +952,7 @@ public class ImageSaver extends Thread { false, false, null, + HDRProcessor.default_tonemapping_algorithm_c, null, 0, 0, @@ -1573,7 +1584,7 @@ public class ImageSaver extends Thread { Log.d(TAG, "before HDR first bitmap: " + bitmaps.get(0) + " is mutable? " + bitmaps.get(0).isMutable()); try { if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { - hdrProcessor.processHDR(bitmaps, true, null, true, null, hdr_alpha, 4, true, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_REINHARD, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_GAINGAMMA); // this will recycle all the bitmaps except bitmaps.get(0), which will contain the hdr image + hdrProcessor.processHDR(bitmaps, true, null, true, null, hdr_alpha, 4, true, request.preference_hdr_tonemapping_algorithm, HDRProcessor.DROTonemappingAlgorithm.DROALGORITHM_GAINGAMMA); // this will recycle all the bitmaps except bitmaps.get(0), which will contain the hdr image } else { Log.e(TAG, "shouldn't have offered HDR as an option if not on Android 5"); diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 0b8512c37..c50014250 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -2406,6 +2406,7 @@ public class MainActivity extends AppCompatActivity { //case "preference_raw_focus_bracketing": // as above //case "preference_nr_save": // we could probably whitelist this, but have not done so in case in future we allow RAW to be saved for the base image //case "preference_hdr_save_expo": // we need to update if this is changed, as it affects whether we request RAW or not in HDR mode when RAW is enabled + case "preference_hdr_tonemapping": case "preference_hdr_contrast_enhancement": //case "preference_expo_bracketing_n_images": // need to set up camera controller //case "preference_expo_bracketing_stops": // need to set up camera controller diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 908897415..44f3300a9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -3292,7 +3292,6 @@ public class MyApplicationInterface extends BasicApplicationInterface { double geo_direction = main_activity.getPreview().hasGeoDirection() ? main_activity.getPreview().getGeoDirection() : 0.0; String custom_tag_artist = sharedPreferences.getString(PreferenceKeys.ExifArtistPreferenceKey, ""); String custom_tag_copyright = sharedPreferences.getString(PreferenceKeys.ExifCopyrightPreferenceKey, ""); - String preference_hdr_contrast_enhancement = sharedPreferences.getString(PreferenceKeys.HDRContrastEnhancementPreferenceKey, "preference_hdr_contrast_enhancement_smart"); int iso = 800; // default value if we can't get ISO long exposure_time = 1000000000L/30; // default value if we can't get shutter speed @@ -3427,6 +3426,30 @@ public class MyApplicationInterface extends BasicApplicationInterface { else processType = ImageSaver.Request.ProcessType.NORMAL; boolean force_suffix = forceSuffix(photo_mode); + + HDRProcessor.TonemappingAlgorithm preference_hdr_tonemapping_algorithm = HDRProcessor.default_tonemapping_algorithm_c; + { + String tonemapping_algorithm_pref = sharedPreferences.getString(PreferenceKeys.HDRTonemappingPreferenceKey, "preference_hdr_tonemapping_default"); + switch( tonemapping_algorithm_pref ) { + case "preference_hdr_tonemapping_clamp": + preference_hdr_tonemapping_algorithm = HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_CLAMP; + break; + case "preference_hdr_tonemapping_exponential": + preference_hdr_tonemapping_algorithm = HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_EXPONENTIAL; + break; + case "preference_hdr_tonemapping_default": // reinhard + preference_hdr_tonemapping_algorithm = HDRProcessor.default_tonemapping_algorithm_c; + break; + case "preference_hdr_tonemapping_aces": + preference_hdr_tonemapping_algorithm = HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_ACES; + break; + default: + Log.e(TAG, "unhandled case for tonemapping: " + tonemapping_algorithm_pref); + break; + } + } + String preference_hdr_contrast_enhancement = sharedPreferences.getString(PreferenceKeys.HDRContrastEnhancementPreferenceKey, "preference_hdr_contrast_enhancement_smart"); + success = imageSaver.saveImageJpeg(do_in_background, processType, force_suffix, // N.B., n_capture_images will be 1 for first image, not 0, so subtract 1 so we start off from _0. @@ -3441,6 +3464,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { is_front_facing, mirror, current_date, + preference_hdr_tonemapping_algorithm, preference_hdr_contrast_enhancement, iso, exposure_time, diff --git a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java index f2073d12e..048442bb0 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java @@ -394,6 +394,10 @@ public class MyPreferenceFragment extends PreferenceFragment implements OnShared PreferenceGroup pg = (PreferenceGroup)this.findPreference("preference_screen_photo_settings"); pg.removePreference(pref); + pref = findPreference("preference_hdr_tonemapping"); + pg = (PreferenceGroup)this.findPreference("preference_screen_photo_settings"); + pg.removePreference(pref); + pref = findPreference("preference_hdr_contrast_enhancement"); pg = (PreferenceGroup)this.findPreference("preference_screen_photo_settings"); 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 6f9cf39ac..0362c7629 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java +++ b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -118,6 +118,8 @@ public class PreferenceKeys { public static final String HDRSaveExpoPreferenceKey = "preference_hdr_save_expo"; + public static final String HDRTonemappingPreferenceKey = "preference_hdr_tonemapping"; + public static final String HDRContrastEnhancementPreferenceKey = "preference_hdr_contrast_enhancement"; public static final String NRSaveExpoPreferenceKey = "preference_nr_save"; diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 754f9ec2d..5793aeb81 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -383,6 +383,18 @@ preference_nr_save_single preference_nr_save_all + + @string/preference_hdr_tonemapping_clamp + @string/preference_hdr_tonemapping_exponential + @string/preference_hdr_tonemapping_default + @string/preference_hdr_tonemapping_aces + + + preference_hdr_tonemapping_clamp + preference_hdr_tonemapping_exponential + preference_hdr_tonemapping_default + preference_hdr_tonemapping_aces + @string/preference_hdr_contrast_enhancement_off @string/preference_hdr_contrast_enhancement_smart diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32c41e225..82b59a6ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1005,6 +1005,13 @@ X-Bty Extension: Beauty / Face Retouch + HDR tonemapping + Algorithm to use for tonemapping in HDR photo mode\n%s + Simple clamp + Exponential + Default + ACES + Privacy policy diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d54004960..69005d82d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -802,6 +802,16 @@ android:defaultValue="false" /> + + + Date: Wed, 19 Oct 2022 23:35:16 +0100 Subject: [PATCH 075/117] Refactor to new transferDeviceExif(). --- .../sourceforge/opencamera/ImageSaver.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 69dd28622..fab590fde 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2897,17 +2897,15 @@ public class ImageSaver extends Thread { } } - /** Transfers exif tags from exif to exif_new, and then applies any extra Exif tags according to the preferences in the request. - * Note that we use several ExifInterface tags that are now deprecated in API level 23 and 24. These are replaced with new tags that have - * the same string value (e.g., TAG_APERTURE replaced with TAG_F_NUMBER, but both have value "FNumber"). We use the deprecated versions - * to avoid complicating the code (we'd still have to read the deprecated values for older devices). + /** Transfers device exif info. */ - private void setExif(final Request request, ExifInterface exif, ExifInterface exif_new) throws IOException { + private void transferDeviceExif(ExifInterface exif, ExifInterface exif_new) { if( MyDebug.LOG ) - Log.d(TAG, "setExif"); + Log.d(TAG, "transferDeviceExif"); if( MyDebug.LOG ) Log.d(TAG, "read back EXIF data"); + String exif_aperture = exif.getAttribute(ExifInterface.TAG_F_NUMBER); // previously TAG_APERTURE String exif_datetime = exif.getAttribute(ExifInterface.TAG_DATETIME); String exif_exposure_time = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME); @@ -3187,6 +3185,18 @@ public class ImageSaver extends Thread { if( exif_user_comment != null ) exif_new.setAttribute(ExifInterface.TAG_USER_COMMENT, exif_user_comment); } + } + + /** Transfers exif tags from exif to exif_new, and then applies any extra Exif tags according to the preferences in the request. + * Note that we use several ExifInterface tags that are now deprecated in API level 23 and 24. These are replaced with new tags that have + * the same string value (e.g., TAG_APERTURE replaced with TAG_F_NUMBER, but both have value "FNumber"). We use the deprecated versions + * to avoid complicating the code (we'd still have to read the deprecated values for older devices). + */ + private void setExif(final Request request, ExifInterface exif, ExifInterface exif_new) throws IOException { + if( MyDebug.LOG ) + Log.d(TAG, "setExif"); + + transferDeviceExif(exif, exif_new); modifyExif(exif_new, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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); setDateTimeExif(exif_new); -- GitLab From e977a94e33a85e852f8aa5bad547b96d56a14627 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 19 Oct 2022 23:49:09 +0100 Subject: [PATCH 076/117] Reorder code in setExif() based on categories. --- .../sourceforge/opencamera/ImageSaver.java | 111 +++++++++--------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index fab590fde..2cac91f52 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2907,19 +2907,9 @@ public class ImageSaver extends Thread { Log.d(TAG, "read back EXIF data"); String exif_aperture = exif.getAttribute(ExifInterface.TAG_F_NUMBER); // previously TAG_APERTURE - String exif_datetime = exif.getAttribute(ExifInterface.TAG_DATETIME); String exif_exposure_time = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME); String exif_flash = exif.getAttribute(ExifInterface.TAG_FLASH); String exif_focal_length = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH); - String exif_gps_altitude = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE); - String exif_gps_altitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF); - String exif_gps_datestamp = exif.getAttribute(ExifInterface.TAG_GPS_DATESTAMP); - String exif_gps_latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - String exif_gps_latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); - String exif_gps_longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); - String exif_gps_longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); - String exif_gps_processing_method = exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD); - String exif_gps_timestamp = exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP); // leave width/height, as this may have changed! similarly TAG_IMAGE_LENGTH? //noinspection deprecation String exif_iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS); // previously TAG_ISO @@ -2928,19 +2918,6 @@ public class ImageSaver extends Thread { // leave orientation - since we rotate bitmaps to account for orientation, we don't want to write it to the saved image! String exif_white_balance = exif.getAttribute(ExifInterface.TAG_WHITE_BALANCE); - String exif_datetime_digitized; - String exif_subsec_time; - String exif_subsec_time_dig; - String exif_subsec_time_orig; - { - // tags that are new in Android M - note we skip tags unlikely to be relevant for camera photos - // update, now available in all Android versions thanks to using AndroidX ExifInterface - exif_datetime_digitized = exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED); - exif_subsec_time = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME); - exif_subsec_time_dig = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED); // previously TAG_SUBSEC_TIME_DIG - exif_subsec_time_orig = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL); // previously TAG_SUBSEC_TIME_ORIG - } - String exif_aperture_value; String exif_brightness_value; String exif_cfa_pattern; @@ -2949,7 +2926,6 @@ public class ImageSaver extends Thread { String exif_compressed_bits_per_pixel; String exif_compression; String exif_contrast; - String exif_datetime_original; String exif_device_setting_description; String exif_digital_zoom_ratio; String exif_exposure_bias_value; @@ -2992,7 +2968,6 @@ public class ImageSaver extends Thread { exif_compressed_bits_per_pixel = exif.getAttribute(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL); exif_compression = exif.getAttribute(ExifInterface.TAG_COMPRESSION); exif_contrast = exif.getAttribute(ExifInterface.TAG_CONTRAST); - exif_datetime_original = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL); exif_device_setting_description = exif.getAttribute(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION); exif_digital_zoom_ratio = exif.getAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO); // unclear if we should transfer TAG_EXIF_VERSION - don't want to risk conficting with whatever ExifInterface writes itself @@ -3056,32 +3031,12 @@ public class ImageSaver extends Thread { Log.d(TAG, "now write new EXIF data"); if( exif_aperture != null ) exif_new.setAttribute(ExifInterface.TAG_F_NUMBER, exif_aperture); - if( exif_datetime != null ) - exif_new.setAttribute(ExifInterface.TAG_DATETIME, exif_datetime); if( exif_exposure_time != null ) exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, exif_exposure_time); if( exif_flash != null ) exif_new.setAttribute(ExifInterface.TAG_FLASH, exif_flash); if( exif_focal_length != null ) exif_new.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, exif_focal_length); - if( exif_gps_altitude != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, exif_gps_altitude); - if( exif_gps_altitude_ref != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, exif_gps_altitude_ref); - if( exif_gps_datestamp != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, exif_gps_datestamp); - if( exif_gps_latitude != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE, exif_gps_latitude); - if( exif_gps_latitude_ref != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, exif_gps_latitude_ref); - if( exif_gps_longitude != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, exif_gps_longitude); - if( exif_gps_longitude_ref != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, exif_gps_longitude_ref); - if( exif_gps_processing_method != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, exif_gps_processing_method); - if( exif_gps_timestamp != null ) - exif_new.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, exif_gps_timestamp); if( exif_iso != null ) //noinspection deprecation exif_new.setAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS, exif_iso); @@ -3092,17 +3047,6 @@ public class ImageSaver extends Thread { if( exif_white_balance != null ) exif_new.setAttribute(ExifInterface.TAG_WHITE_BALANCE, exif_white_balance); - { - if( exif_datetime_digitized != null ) - exif_new.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, exif_datetime_digitized); - if( exif_subsec_time != null ) - exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME, exif_subsec_time); - if( exif_subsec_time_dig != null ) - exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, exif_subsec_time_dig); - if( exif_subsec_time_orig != null ) - exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, exif_subsec_time_orig); - } - { if( exif_aperture_value != null ) exif_new.setAttribute(ExifInterface.TAG_APERTURE_VALUE, exif_aperture_value); @@ -3120,8 +3064,6 @@ public class ImageSaver extends Thread { exif_new.setAttribute(ExifInterface.TAG_COMPRESSION, exif_compression); if( exif_contrast != null ) exif_new.setAttribute(ExifInterface.TAG_CONTRAST, exif_contrast); - if( exif_datetime_original != null ) - exif_new.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, exif_datetime_original); if( exif_device_setting_description != null ) exif_new.setAttribute(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, exif_device_setting_description); if( exif_digital_zoom_ratio != null ) @@ -3185,6 +3127,59 @@ public class ImageSaver extends Thread { if( exif_user_comment != null ) exif_new.setAttribute(ExifInterface.TAG_USER_COMMENT, exif_user_comment); } + + // tags related to date and time + + String exif_datetime = exif.getAttribute(ExifInterface.TAG_DATETIME); + String exif_datetime_original = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL); + String exif_datetime_digitized = exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED); + String exif_subsec_time = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME); + String exif_subsec_time_orig = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL); // previously TAG_SUBSEC_TIME_ORIG + String exif_subsec_time_dig = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED); // previously TAG_SUBSEC_TIME_DIG + + if( exif_datetime != null ) + exif_new.setAttribute(ExifInterface.TAG_DATETIME, exif_datetime); + if( exif_datetime_original != null ) + exif_new.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, exif_datetime_original); + if( exif_datetime_digitized != null ) + exif_new.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, exif_datetime_digitized); + if( exif_subsec_time != null ) + exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME, exif_subsec_time); + if( exif_subsec_time_orig != null ) + exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, exif_subsec_time_orig); + if( exif_subsec_time_dig != null ) + exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, exif_subsec_time_dig); + + // tags for gps info + + String exif_gps_processing_method = exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD); + String exif_gps_latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); + String exif_gps_latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); + String exif_gps_longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); + String exif_gps_longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); + String exif_gps_altitude = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE); + String exif_gps_altitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF); + String exif_gps_datestamp = exif.getAttribute(ExifInterface.TAG_GPS_DATESTAMP); + String exif_gps_timestamp = exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP); + + if( exif_gps_processing_method != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, exif_gps_processing_method); + if( exif_gps_latitude != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE, exif_gps_latitude); + if( exif_gps_latitude_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, exif_gps_latitude_ref); + if( exif_gps_longitude != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, exif_gps_longitude); + if( exif_gps_longitude_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, exif_gps_longitude_ref); + if( exif_gps_altitude != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, exif_gps_altitude); + if( exif_gps_altitude_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, exif_gps_altitude_ref); + if( exif_gps_datestamp != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, exif_gps_datestamp); + if( exif_gps_timestamp != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, exif_gps_timestamp); } /** Transfers exif tags from exif to exif_new, and then applies any extra Exif tags according to the preferences in the request. -- GitLab From 09979a6e2c274e35e80a40b28c5fe88a2ee04393 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 19 Oct 2022 23:55:40 +0100 Subject: [PATCH 077/117] Refactor split out code to new transferDeviceExifDateTime() and transferDeviceExifGPS(). --- .../sourceforge/opencamera/ImageSaver.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 2cac91f52..531fd7c01 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -3127,6 +3127,13 @@ public class ImageSaver extends Thread { if( exif_user_comment != null ) exif_new.setAttribute(ExifInterface.TAG_USER_COMMENT, exif_user_comment); } + } + + /** Transfers device exif info related to date and time. + */ + private void transferDeviceExifDateTime(ExifInterface exif, ExifInterface exif_new) { + if( MyDebug.LOG ) + Log.d(TAG, "transferDeviceExifDateTime"); // tags related to date and time @@ -3150,6 +3157,14 @@ public class ImageSaver extends Thread { if( exif_subsec_time_dig != null ) exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, exif_subsec_time_dig); + } + + /** Transfers device exif info related to gps location. + */ + private void transferDeviceExifGPS(ExifInterface exif, ExifInterface exif_new) { + if( MyDebug.LOG ) + Log.d(TAG, "transferDeviceExifGPS"); + // tags for gps info String exif_gps_processing_method = exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD); @@ -3193,6 +3208,10 @@ public class ImageSaver extends Thread { transferDeviceExif(exif, exif_new); + transferDeviceExifDateTime(exif, exif_new); + + transferDeviceExifGPS(exif, exif_new); + modifyExif(exif_new, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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); setDateTimeExif(exif_new); exif_new.saveAttributes(); -- GitLab From 51baf0b504af47b0f401e8122c1ed0681f9c3ce4 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 23 Oct 2022 21:23:31 +0100 Subject: [PATCH 078/117] Support for pause/unpause video for volume key down. --- _docs/help.html | 2 +- _docs/history.html | 2 ++ .../java/net/sourceforge/opencamera/MainActivity.java | 6 ++++++ .../main/java/net/sourceforge/opencamera/ui/MainUI.java | 9 ++++++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index 524dba2aa..77c4a3fa7 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -513,7 +513,7 @@ recording.

delay (see below).

Volume keys - You can set what happens when your device's volume keys are pressed:

    -
  • Take photo or start/stop video (depending on photo/video mode).
  • +
  • Take photo or start/stop video (depending on photo/video mode). On Android 7+, volume down will instead pause/resume video when recording video.
  • Trigger an autofocus - or if in manual mode, change the focus distance in/out. In this mode, holding down both volume keys will take a photo (or start/stop video). This makes your volume keys behave more like a physical camera button - hold down one key to focus, then both to take a photo.
  • diff --git a/_docs/history.html b/_docs/history.html index 0c99f9161..91482c942 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -71,6 +71,8 @@ UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can UPDATED Make zoom seekbar snap to powers of two (for Camera2 API). UPDATED No longer switch to manual mode in DRO and NR photo modes, as on some devices this meant losing the benefit of manufacturer algorithms. +UPDATED Volume key down now supports pause/resume video on Android 7+ when recording video, if + option for volume keys is set to "Take photo (or start/stop video recording)". UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API). UPDATED Improved look of on-screen level line. UPDATED On-screen pitch and compass lines now show smaller intervals as camera zooms in. diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index c50014250..269f566ee 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -1794,6 +1794,12 @@ public class MainActivity extends AppCompatActivity { public void clickedPauseVideo(View view) { if( MyDebug.LOG ) Log.d(TAG, "clickedPauseVideo"); + pauseVideo(); + } + + public void pauseVideo() { + if( MyDebug.LOG ) + Log.d(TAG, "pauseVideo"); if( preview.isVideoRecording() ) { // just in case preview.pauseVideo(); mainUI.setPauseVideoContentDescription(); 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 1c2393b90..d2d76addf 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java @@ -2779,7 +2779,14 @@ public class MainUI { switch(volume_keys) { case "volume_take_photo": - main_activity.takePicture(false); + boolean done = false; + if( keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && main_activity.getPreview().isVideoRecording() ) { + done = true; + main_activity.pauseVideo(); + } + if( !done ) { + main_activity.takePicture(false); + } return true; case "volume_focus": if(keydown_volume_up && keydown_volume_down) { -- GitLab From 915e21d70ce8619dd414a8059fa59cf1c5f6aea2 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Fri, 28 Oct 2022 22:19:07 +0100 Subject: [PATCH 079/117] New option Settings/Photo settings/"Remove device EXIF data" for JPEG photos. --- _docs/help.html | 5 + _docs/history.html | 2 + _docs/index.html | 1 + .../sourceforge/opencamera/ImageSaver.java | 266 ++++++++++++++++-- .../opencamera/MyApplicationInterface.java | 14 + .../opencamera/PreferenceKeys.java | 2 + app/src/main/res/values/arrays.xml | 10 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/preferences.xml | 9 + 9 files changed, 299 insertions(+), 16 deletions(-) diff --git a/_docs/help.html b/_docs/help.html index 77c4a3fa7..8014ac063 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -917,6 +917,11 @@ a third party File Explorer application to see and delete XML files.

    photos will still be as the camera (or other people) view the scene. This option can be used to mirror the resultant photo, so the resultant photo matches the mirrored image you see on the screen.

    +

    Remove device EXIF data - Whether to remove device EXIF metadata from JPEG photos. Note that + this will not remove exif tags applied by other Open Camera settings that apply EXIF metadata (e.g. + location/geotagging, artist, copyright etc). Those other options are independent and will override this + setting. Also note that RAW/DNG and videos are not affected.

    +

    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).

    diff --git a/_docs/history.html b/_docs/history.html index 91482c942..ba1643a0e 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -60,6 +60,8 @@ FIXED For Camera2 API, red eye flash was incorrectly being shown even on devic FIXED Not saving location exif information for Camera2 API on some devices (e.g., Pixel 6 Pro). FIXED Doesn't display error message if using volume keys to turn auto-level on or off in RAW or Panorama mode (unless device doesn't support auto-level at all). +ADDED New option Settings/Photo settings/"Remove device EXIF data" to remove device metadata from + JPEG photos. ADDED Shading for auto-level and crop guides, to darken the preview outside of the region of interest. ADDED Display message to hold device steady when using X-Night photo mode. diff --git a/_docs/index.html b/_docs/index.html index d3bd47db0..41b1abcf9 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -118,6 +118,7 @@ browsers -->
  • Overlay a choice of grids and crop guides.
  • Optional GPS location tagging (geotagging) of photos and videos; for photos this includes compass direction (GPSImgDirection, GPSImgDirectionRef).
  • Apply date and timestamp, location coordinates, and custom text to photos; store date/time and location as video subtitles (.SRT).
  • +
  • Option to remove device exif metadata from photos.
  • 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; camera vendor extensions; slow motion video; log profile video.
  • diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 531fd7c01..01003c686 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -168,6 +168,12 @@ public class ImageSaver extends Thread { //final String preference_stamp_geo_address; final String preference_units_distance; final boolean panorama_crop; // used for panorama + enum RemoveDeviceExif { + OFF, // don't remove any device exif tags + ON, // remove all device exif tags + KEEP_DATETIME // remove all device exif tags except datetime tags + } + final RemoveDeviceExif remove_device_exif; final boolean store_location; final Location location; final boolean store_geo_direction; @@ -201,6 +207,7 @@ public class ImageSaver extends Thread { //String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, + RemoveDeviceExif remove_device_exif, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, double pitch_angle, boolean store_ypr, String custom_tag_artist, @@ -241,6 +248,7 @@ public class ImageSaver extends Thread { //this.preference_stamp_geo_address = preference_stamp_geo_address; this.preference_units_distance = preference_units_distance; this.panorama_crop = panorama_crop; + this.remove_device_exif = remove_device_exif; this.store_location = store_location; this.location = location; this.store_geo_direction = store_geo_direction; @@ -278,7 +286,7 @@ public class ImageSaver extends Thread { this.preference_stamp, this.preference_textstamp, this.font_size, this.color, this.pref_style, this.preference_stamp_dateformat, this.preference_stamp_timeformat, this.preference_stamp_gpsformat, //this.preference_stamp_geo_address, this.preference_units_distance, - this.panorama_crop, this.store_location, this.location, this.store_geo_direction, this.geo_direction, + this.panorama_crop, this.remove_device_exif, this.store_location, this.location, this.store_geo_direction, this.geo_direction, this.pitch_angle, this.store_ypr, this.custom_tag_artist, this.custom_tag_copyright, @@ -591,6 +599,7 @@ public class ImageSaver extends Thread { //String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, + Request.RemoveDeviceExif remove_device_exif, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, double pitch_angle, boolean store_ypr, String custom_tag_artist, @@ -624,7 +633,7 @@ public class ImageSaver extends Thread { preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, //preference_stamp_geo_address, preference_units_distance, - panorama_crop, store_location, location, store_geo_direction, geo_direction, + panorama_crop, remove_device_exif, store_location, location, store_geo_direction, geo_direction, pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, @@ -669,7 +678,7 @@ public class ImageSaver extends Thread { null, null, 0, 0, null, null, null, null, //null, null, - false, false, null, false, 0.0, + false, Request.RemoveDeviceExif.OFF, false, null, false, 0.0, 0.0, false, null, null, 1); @@ -697,6 +706,7 @@ public class ImageSaver extends Thread { //String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, + Request.RemoveDeviceExif remove_device_exif, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, double pitch_angle, boolean store_ypr, String custom_tag_artist, @@ -728,7 +738,7 @@ public class ImageSaver extends Thread { preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, //preference_stamp_geo_address, preference_units_distance, - panorama_crop, store_location, location, store_geo_direction, geo_direction, + panorama_crop, remove_device_exif, store_location, location, store_geo_direction, geo_direction, pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, @@ -811,6 +821,7 @@ public class ImageSaver extends Thread { //String preference_stamp_geo_address, String preference_units_distance, boolean panorama_crop, + Request.RemoveDeviceExif remove_device_exif, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, double pitch_angle, boolean store_ypr, String custom_tag_artist, @@ -846,7 +857,7 @@ public class ImageSaver extends Thread { preference_stamp, preference_textstamp, font_size, color, pref_style, preference_stamp_dateformat, preference_stamp_timeformat, preference_stamp_gpsformat, //preference_stamp_geo_address, preference_units_distance, - panorama_crop, store_location, location, store_geo_direction, geo_direction, + panorama_crop, remove_device_exif, store_location, location, store_geo_direction, geo_direction, pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, @@ -960,7 +971,7 @@ public class ImageSaver extends Thread { null, null, 0, 0, null, null, null, null, //null, null, - false, false, null, false, 0.0, + false, Request.RemoveDeviceExif.OFF, false, null, false, 0.0, 0.0, false, null, null, 1); @@ -2390,6 +2401,18 @@ public class ImageSaver extends Thread { throw new IOException(); } } + if( request.remove_device_exif != Request.RemoveDeviceExif.OFF && bitmap == null ) { + if( MyDebug.LOG ) + Log.d(TAG, "need to decode bitmap to strip exif tags"); + // if removing device exif data, it's easier to do this by going through the codepath that + // resaves the bitmap, and then we avoid transferring/adding exif tags that we don't want + bitmap = loadBitmapWithRotation(data, true); + if( bitmap == null ) { + // if we can't load bitmap for removing device tags, don't want to continue + System.gc(); + throw new IOException(); + } + } bitmap = stampImage(request, data, bitmap); if( MyDebug.LOG ) { Log.d(TAG, "Save single image performance: time after photostamp: " + (System.currentTimeMillis() - time_s)); @@ -2897,7 +2920,7 @@ public class ImageSaver extends Thread { } } - /** Transfers device exif info. + /** Transfers device exif info. Should only be called if request.remove_device_exif == Request.RemoveDeviceExif.OFF. */ private void transferDeviceExif(ExifInterface exif, ExifInterface exif_new) { if( MyDebug.LOG ) @@ -2910,7 +2933,7 @@ public class ImageSaver extends Thread { String exif_exposure_time = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME); String exif_flash = exif.getAttribute(ExifInterface.TAG_FLASH); String exif_focal_length = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH); - // leave width/height, as this may have changed! similarly TAG_IMAGE_LENGTH? + // leave TAG_IMAGE_WIDTH/TAG_IMAGE_LENGTH, as this may have changed! //noinspection deprecation String exif_iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS); // previously TAG_ISO String exif_make = exif.getAttribute(ExifInterface.TAG_MAKE); @@ -3197,6 +3220,194 @@ public class ImageSaver extends Thread { exif_new.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, exif_gps_timestamp); } + /** Explicitly removes tags based on the RemoveDeviceExif option. + * Note that in theory this method is unnecessary: we implement the RemoveDeviceExif options + * (if not OFF) by resaving the JPEG via a bitmap, and then limiting what Exif tags are + * transferred across. This method is for extra paranoia: first to reduce the risk of future + * bugs, secondly just in case saving via a bitmap does ever add exif tags. + */ + private void removeExifTags(ExifInterface exif_new, final Request request) { + if( MyDebug.LOG ) + Log.d(TAG, "removeExifTags"); + + if( request.remove_device_exif != Request.RemoveDeviceExif.OFF ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove exif tags"); + exif_new.setAttribute(ExifInterface.TAG_F_NUMBER, null); + exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, null); + exif_new.setAttribute(ExifInterface.TAG_FLASH, null); + exif_new.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, null); + exif_new.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, null); + exif_new.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, null); + //noinspection deprecation + exif_new.setAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS, null); + exif_new.setAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, null); + exif_new.setAttribute(ExifInterface.TAG_MAKE, null); + exif_new.setAttribute(ExifInterface.TAG_MODEL, null); + exif_new.setAttribute(ExifInterface.TAG_WHITE_BALANCE, null); + exif_new.setAttribute(ExifInterface.TAG_APERTURE_VALUE, null); + exif_new.setAttribute(ExifInterface.TAG_BRIGHTNESS_VALUE, null); + exif_new.setAttribute(ExifInterface.TAG_CFA_PATTERN, null); + exif_new.setAttribute(ExifInterface.TAG_COLOR_SPACE, null); + exif_new.setAttribute(ExifInterface.TAG_COMPONENTS_CONFIGURATION, null); + exif_new.setAttribute(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, null); + exif_new.setAttribute(ExifInterface.TAG_COMPRESSION, null); + exif_new.setAttribute(ExifInterface.TAG_CONTRAST, null); + exif_new.setAttribute(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, null); + exif_new.setAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO, null); + exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_BIAS_VALUE, null); + exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_INDEX, null); + exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_MODE, null); + exif_new.setAttribute(ExifInterface.TAG_EXPOSURE_PROGRAM, null); + exif_new.setAttribute(ExifInterface.TAG_FLASH_ENERGY, null); + exif_new.setAttribute(ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, null); + exif_new.setAttribute(ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, null); + exif_new.setAttribute(ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, null); + exif_new.setAttribute(ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, null); + exif_new.setAttribute(ExifInterface.TAG_GAIN_CONTROL, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_AREA_INFORMATION, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_BEARING, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_BEARING_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_DISTANCE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_DISTANCE_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_LATITUDE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_LATITUDE_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_LONGITUDE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DIFFERENTIAL, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DOP, null); + if( !request.store_geo_direction ) { + exif_new.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, null); + } + exif_new.setAttribute(ExifInterface.TAG_GPS_MAP_DATUM, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_MEASURE_MODE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_SATELLITES, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_STATUS, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_TRACK, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_TRACK_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_VERSION_ID, null); + exif_new.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, null); + exif_new.setAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID, null); + exif_new.setAttribute(ExifInterface.TAG_INTEROPERABILITY_INDEX, null); + exif_new.setAttribute(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, null); + exif_new.setAttribute(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, null); + exif_new.setAttribute(ExifInterface.TAG_LIGHT_SOURCE, null); + exif_new.setAttribute(ExifInterface.TAG_MAKER_NOTE, null); + exif_new.setAttribute(ExifInterface.TAG_MAX_APERTURE_VALUE, null); + exif_new.setAttribute(ExifInterface.TAG_METERING_MODE, null); + exif_new.setAttribute(ExifInterface.TAG_OECF, null); + exif_new.setAttribute(ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, null); + exif_new.setAttribute(ExifInterface.TAG_PIXEL_X_DIMENSION, null); + exif_new.setAttribute(ExifInterface.TAG_PIXEL_Y_DIMENSION, null); + exif_new.setAttribute(ExifInterface.TAG_PLANAR_CONFIGURATION, null); + exif_new.setAttribute(ExifInterface.TAG_PRIMARY_CHROMATICITIES, null); + exif_new.setAttribute(ExifInterface.TAG_REFERENCE_BLACK_WHITE, null); + exif_new.setAttribute(ExifInterface.TAG_RESOLUTION_UNIT, null); + exif_new.setAttribute(ExifInterface.TAG_ROWS_PER_STRIP, null); + exif_new.setAttribute(ExifInterface.TAG_SAMPLES_PER_PIXEL, null); + exif_new.setAttribute(ExifInterface.TAG_SATURATION, null); + exif_new.setAttribute(ExifInterface.TAG_SCENE_CAPTURE_TYPE, null); + exif_new.setAttribute(ExifInterface.TAG_SCENE_TYPE, null); + exif_new.setAttribute(ExifInterface.TAG_SENSING_METHOD, null); + exif_new.setAttribute(ExifInterface.TAG_SHARPNESS, null); + exif_new.setAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE, null); + exif_new.setAttribute(ExifInterface.TAG_SOFTWARE, null); + exif_new.setAttribute(ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, null); + exif_new.setAttribute(ExifInterface.TAG_SPECTRAL_SENSITIVITY, null); + exif_new.setAttribute(ExifInterface.TAG_STRIP_BYTE_COUNTS, null); + exif_new.setAttribute(ExifInterface.TAG_STRIP_OFFSETS, null); + exif_new.setAttribute(ExifInterface.TAG_SUBJECT_AREA, null); + exif_new.setAttribute(ExifInterface.TAG_SUBJECT_DISTANCE, null); + exif_new.setAttribute(ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, null); + exif_new.setAttribute(ExifInterface.TAG_SUBJECT_LOCATION, null); + exif_new.setAttribute(ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, null); + exif_new.setAttribute(ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, null); + exif_new.setAttribute(ExifInterface.TAG_TRANSFER_FUNCTION, null); + if( !request.store_ypr ) { + exif_new.setAttribute(ExifInterface.TAG_USER_COMMENT, null); + } + exif_new.setAttribute(ExifInterface.TAG_WHITE_POINT, null); + exif_new.setAttribute(ExifInterface.TAG_X_RESOLUTION, null); + exif_new.setAttribute(ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, null); + exif_new.setAttribute(ExifInterface.TAG_Y_CB_CR_POSITIONING, null); + exif_new.setAttribute(ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, null); + exif_new.setAttribute(ExifInterface.TAG_Y_RESOLUTION, null); + if( !(request.custom_tag_artist != null && request.custom_tag_artist.length() > 0) ) { + exif_new.setAttribute(ExifInterface.TAG_ARTIST, null); + } + if( !(request.custom_tag_copyright != null && request.custom_tag_copyright.length() > 0) ) { + exif_new.setAttribute(ExifInterface.TAG_COPYRIGHT, null); + } + + exif_new.setAttribute(ExifInterface.TAG_BITS_PER_SAMPLE, null); + exif_new.setAttribute(ExifInterface.TAG_EXIF_VERSION, null); + exif_new.setAttribute(ExifInterface.TAG_FLASHPIX_VERSION, null); + exif_new.setAttribute(ExifInterface.TAG_GAMMA, null); + exif_new.setAttribute(ExifInterface.TAG_RELATED_SOUND_FILE, null); + exif_new.setAttribute(ExifInterface.TAG_SENSITIVITY_TYPE, null); + exif_new.setAttribute(ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, null); + exif_new.setAttribute(ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, null); + exif_new.setAttribute(ExifInterface.TAG_ISO_SPEED, null); + exif_new.setAttribute(ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, null); + exif_new.setAttribute(ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, null); + exif_new.setAttribute(ExifInterface.TAG_FILE_SOURCE, null); + exif_new.setAttribute(ExifInterface.TAG_CUSTOM_RENDERED, null); + exif_new.setAttribute(ExifInterface.TAG_CAMERA_OWNER_NAME, null); + exif_new.setAttribute(ExifInterface.TAG_BODY_SERIAL_NUMBER, null); + exif_new.setAttribute(ExifInterface.TAG_LENS_SPECIFICATION, null); + exif_new.setAttribute(ExifInterface.TAG_LENS_MAKE, null); + exif_new.setAttribute(ExifInterface.TAG_LENS_MODEL, null); + exif_new.setAttribute(ExifInterface.TAG_LENS_SERIAL_NUMBER, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_H_POSITIONING_ERROR, null); + exif_new.setAttribute(ExifInterface.TAG_DNG_VERSION, null); + exif_new.setAttribute(ExifInterface.TAG_DEFAULT_CROP_SIZE, null); + exif_new.setAttribute(ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, null); + exif_new.setAttribute(ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, null); + exif_new.setAttribute(ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, null); + exif_new.setAttribute(ExifInterface.TAG_ORF_ASPECT_FRAME, null); + exif_new.setAttribute(ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, null); + exif_new.setAttribute(ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, null); + exif_new.setAttribute(ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, null); + exif_new.setAttribute(ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, null); + exif_new.setAttribute(ExifInterface.TAG_RW2_ISO, null); + exif_new.setAttribute(ExifInterface.TAG_RW2_JPG_FROM_RAW, null); + exif_new.setAttribute(ExifInterface.TAG_XMP, null); + exif_new.setAttribute(ExifInterface.TAG_NEW_SUBFILE_TYPE, null); + exif_new.setAttribute(ExifInterface.TAG_SUBFILE_TYPE, null); + + if( request.remove_device_exif != Request.RemoveDeviceExif.KEEP_DATETIME ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove datetime tags"); + exif_new.setAttribute(ExifInterface.TAG_DATETIME, null); + exif_new.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null); + exif_new.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null); + exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null); + exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null); + exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null); + exif_new.setAttribute(ExifInterface.TAG_OFFSET_TIME, null); + exif_new.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, null); + exif_new.setAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED, null); + } + + if( !request.store_location ) { + if( MyDebug.LOG ) + Log.d(TAG, "remove gps tags"); + exif_new.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_SPEED, null); + exif_new.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null); + } + } + } + /** Transfers exif tags from exif to exif_new, and then applies any extra Exif tags according to the preferences in the request. * Note that we use several ExifInterface tags that are now deprecated in API level 23 and 24. These are replaced with new tags that have * the same string value (e.g., TAG_APERTURE replaced with TAG_F_NUMBER, but both have value "FNumber"). We use the deprecated versions @@ -3206,14 +3417,28 @@ public class ImageSaver extends Thread { if( MyDebug.LOG ) Log.d(TAG, "setExif"); - transferDeviceExif(exif, exif_new); + if( request.remove_device_exif == Request.RemoveDeviceExif.OFF ) { + transferDeviceExif(exif, exif_new); + } + + if( request.remove_device_exif == Request.RemoveDeviceExif.OFF || request.remove_device_exif == Request.RemoveDeviceExif.KEEP_DATETIME ) { + transferDeviceExifDateTime(exif, exif_new); + } - transferDeviceExifDateTime(exif, exif_new); + if( request.remove_device_exif == Request.RemoveDeviceExif.OFF || request.store_location ) { + // If geotagging is enabled, we explicitly override the remove_device_exif setting. + // Arguably we don't need an if statement here at all - but if there was some device strangely + // setting GPS tags even when we haven't set them, it's better to remove them if the user has + // requested RemoveDeviceExif.OFF. + transferDeviceExifGPS(exif, exif_new); + } - transferDeviceExifGPS(exif, exif_new); + modifyExif(exif_new, request.remove_device_exif, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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); + if( request.remove_device_exif == Request.RemoveDeviceExif.OFF || request.remove_device_exif == Request.RemoveDeviceExif.KEEP_DATETIME ) { + setDateTimeExif(exif_new); + } - modifyExif(exif_new, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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); - setDateTimeExif(exif_new); + removeExifTags(exif_new, request); // must be last, before saving attributes exif_new.saveAttributes(); } @@ -3584,7 +3809,7 @@ public class ImageSaver extends Thread { try { ExifInterface exif = exif_holder.getExif(); if( exif != null ) { - modifyExif(exif, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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); + modifyExif(exif, request.remove_device_exif, request.type == Request.Type.JPEG, request.using_camera2, request.using_camera_extensions, request.current_date, request.store_location, request.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); if( MyDebug.LOG ) Log.d(TAG, "*** time after modifyExif: " + (System.currentTimeMillis() - time_s)); @@ -3613,8 +3838,9 @@ public class ImageSaver extends Thread { } /** Makes various modifications to the exif data, if necessary. + * Any fix-ups should respect the setting of RemoveDeviceExif! */ - private void modifyExif(ExifInterface exif, boolean is_jpeg, boolean using_camera2, boolean using_camera_extensions, Date current_date, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, String custom_tag_artist, String custom_tag_copyright, double level_angle, double pitch_angle, boolean store_ypr) { + private void modifyExif(ExifInterface exif, Request.RemoveDeviceExif remove_device_exif, boolean is_jpeg, boolean using_camera2, boolean using_camera_extensions, Date current_date, boolean store_location, Location location, boolean store_geo_direction, double geo_direction, String custom_tag_artist, String custom_tag_copyright, double level_angle, double pitch_angle, boolean store_ypr) { if( MyDebug.LOG ) Log.d(TAG, "modifyExif"); setGPSDirectionExif(exif, store_geo_direction, geo_direction); @@ -3624,6 +3850,7 @@ public class ImageSaver extends Thread { geo_angle += 360.0f; } String encoding = "ASCII\0\0\0"; + // fine to ignore request.remove_device_exif, as this is a separate user option //exif.setAttribute(ExifInterface.TAG_USER_COMMENT,"Yaw:" + geo_angle + ",Pitch:" + pitch_angle + ",Roll:" + level_angle); exif.setAttribute(ExifInterface.TAG_USER_COMMENT,encoding + "Yaw:" + geo_angle + ",Pitch:" + pitch_angle + ",Roll:" + level_angle); if( MyDebug.LOG ) @@ -3634,15 +3861,19 @@ public class ImageSaver extends Thread { if( store_location && ( !exif.hasAttribute(ExifInterface.TAG_GPS_LATITUDE) || !exif.hasAttribute(ExifInterface.TAG_GPS_LATITUDE) ) ) { // We need this when using camera extensions (since Camera API doesn't support location for camera extensions). // But some devices (e.g., Pixel 6 Pro with Camera2 API) seem to not store location data, so we always check if we need to add it. + // fine to ignore request.remove_device_exif, as this is a separate user option if( MyDebug.LOG ) Log.d(TAG, "store location"); // don't log location for privacy reasons! exif.setGpsInfo(location); } if( using_camera_extensions ) { - addDateTimeExif(exif, current_date); + if( remove_device_exif == Request.RemoveDeviceExif.OFF || remove_device_exif == Request.RemoveDeviceExif.KEEP_DATETIME ) { + addDateTimeExif(exif, current_date); + } } else if( needGPSExifFix(is_jpeg, using_camera2, store_location) ) { + // fine to ignore request.remove_device_exif, as this is a separate user option fixGPSTimestamp(exif, current_date); } } @@ -3661,6 +3892,7 @@ public class ImageSaver extends Thread { String GPSImgDirection_string = Math.round(geo_angle*100) + "/100"; if( MyDebug.LOG ) Log.d(TAG, "GPSImgDirection_string: " + GPSImgDirection_string); + // fine to ignore request.remove_device_exif, as this is a separate user option exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, GPSImgDirection_string); exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "M"); } @@ -3684,11 +3916,13 @@ public class ImageSaver extends Thread { if( custom_tag_artist != null && custom_tag_artist.length() > 0 ) { if( MyDebug.LOG ) Log.d(TAG, "apply TAG_ARTIST: " + custom_tag_artist); + // fine to ignore request.remove_device_exif, as this is a separate user option exif.setAttribute(ExifInterface.TAG_ARTIST, custom_tag_artist); } if( custom_tag_copyright != null && custom_tag_copyright.length() > 0 ) { if( MyDebug.LOG ) Log.d(TAG, "apply TAG_COPYRIGHT: " + custom_tag_copyright); + // fine to ignore request.remove_device_exif, as this is a separate user option exif.setAttribute(ExifInterface.TAG_COPYRIGHT, custom_tag_copyright); } } diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 44f3300a9..4d3c119ce 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -1207,6 +1207,17 @@ public class MyApplicationInterface extends BasicApplicationInterface { return timer_delay; } + private ImageSaver.Request.RemoveDeviceExif getRemoveDeviceExifPref() { + switch( sharedPreferences.getString(PreferenceKeys.RemoveDeviceExifPreferenceKey, "preference_remove_device_exif_off") ) { + case "preference_remove_device_exif_on": + return ImageSaver.Request.RemoveDeviceExif.ON; + case "preference_remove_device_exif_keep_datetime": + return ImageSaver.Request.RemoveDeviceExif.KEEP_DATETIME; + default: + return ImageSaver.Request.RemoveDeviceExif.OFF; + } + } + @Override public boolean getGeotaggingPref() { return sharedPreferences.getBoolean(PreferenceKeys.LocationPreferenceKey, false); @@ -3286,6 +3297,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { //String preference_stamp_geo_address = this.getStampGeoAddressPref(); String preference_units_distance = this.getUnitsDistancePref(); boolean panorama_crop = sharedPreferences.getString(PreferenceKeys.PanoramaCropPreferenceKey, "preference_panorama_crop_on").equals("preference_panorama_crop_on"); + ImageSaver.Request.RemoveDeviceExif remove_device_exif = getRemoveDeviceExifPref(); boolean store_location = getGeotaggingPref() && getLocation() != null; Location location = store_location ? getLocation() : null; boolean store_geo_direction = main_activity.getPreview().hasGeoDirection() && getGeodirectionPref(); @@ -3397,6 +3409,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { //preference_stamp_geo_address, preference_units_distance, panorama_crop, + remove_device_exif, store_location, location, store_geo_direction, geo_direction, pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, @@ -3473,6 +3486,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { //preference_stamp_geo_address, preference_units_distance, false, // panorama doesn't use this codepath + remove_device_exif, store_location, location, store_geo_direction, geo_direction, pitch_angle, store_ypr, custom_tag_artist, custom_tag_copyright, diff --git a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java index 0362c7629..b11d613c4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java +++ b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -128,6 +128,8 @@ public class PreferenceKeys { public static final String LocationPreferenceKey = "preference_location"; + public static final String RemoveDeviceExifPreferenceKey = "preference_remove_device_exif"; + public static final String GPSDirectionPreferenceKey = "preference_gps_direction"; public static final String RequireLocationPreferenceKey = "preference_require_location"; diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 5793aeb81..fec4db929 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1009,4 +1009,14 @@ none noise + + @string/preference_remove_device_exif_off + @string/preference_remove_device_exif_on + @string/preference_remove_device_exif_keep_datetime + + + preference_remove_device_exif_off + preference_remove_device_exif_on + preference_remove_device_exif_keep_datetime + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82b59a6ca..7d008f1b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1012,6 +1012,12 @@ Default ACES + Remove device EXIF data + Whether to remove device EXIF metadata from JPEG photos. This will not remove exif tags applied by other settings that apply EXIF metadata (e.g. location/geotagging). Those other options are independent and will override this setting. RAW/DNG and videos are not affected.\n%s + Don\'t remove device EXIF data + Remove device EXIF data + Remove except date/time + Privacy policy diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 69005d82d..8c2f3aa2f 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -872,6 +872,15 @@ android:defaultValue="preference_front_camera_mirror_no" /> + + Date: Fri, 28 Oct 2022 22:20:25 +0100 Subject: [PATCH 080/117] Do more to remember last thumbnail/photo when removing device exif tags. --- .../net/sourceforge/opencamera/TestUtils.java | 2 +- .../sourceforge/opencamera/ImageSaver.java | 25 ++++++---- .../sourceforge/opencamera/MainActivity.java | 50 ++++++++++++++++++- .../opencamera/MyApplicationInterface.java | 12 +++-- .../opencamera/PanoramaProcessor.java | 4 +- .../opencamera/SaveLocationHistory.java | 8 ++- .../opencamera/SettingsManager.java | 2 +- .../sourceforge/opencamera/StorageUtils.java | 39 ++++++++++++--- .../opencamera/ui/DrawPreview.java | 7 +++ 9 files changed, 120 insertions(+), 29 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java index 1c4f75e8b..160c4a8c7 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java @@ -326,7 +326,7 @@ public class TestUtils { } } else { - activity.getStorageUtils().broadcastFile(file, true, false, true); + activity.getStorageUtils().broadcastFile(file, true, false, true, false, null); } } diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 01003c686..f49233d69 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -1716,10 +1716,10 @@ public class ImageSaver extends Thread { } if( saveFile != null ) { - storageUtils.broadcastFile(saveFile, false, false, false); + storageUtils.broadcastFile(saveFile, false, false, false, false, null); } else { - broadcastSAFFile(saveUri, false, false); + broadcastSAFFile(saveUri, false, false, false); } } catch(IOException e) { @@ -2700,9 +2700,11 @@ public class ImageSaver extends Thread { storageUtils.clearLastMediaScanned(); } + boolean hasnoexifdatetime = request.remove_device_exif != Request.RemoveDeviceExif.OFF && request.remove_device_exif != Request.RemoveDeviceExif.KEEP_DATETIME; + if( picFile != null && saveUri == null ) { // broadcast for SAF is done later, when we've actually written out the file - storageUtils.broadcastFile(picFile, true, false, update_thumbnail); + storageUtils.broadcastFile(picFile, true, false, update_thumbnail, hasnoexifdatetime, null); main_activity.test_last_saved_image = picFile.getAbsolutePath(); } @@ -2733,12 +2735,12 @@ public class ImageSaver extends Thread { storageUtils.announceUri(saveUri, true, false); if( update_thumbnail ) { // we also want to save the uri - we can use the media uri directly, rather than having to scan it - storageUtils.setLastMediaScanned(saveUri, false); + storageUtils.setLastMediaScanned(saveUri, false, hasnoexifdatetime, saveUri); } } } else { - broadcastSAFFile(saveUri, update_thumbnail, request.image_capture_intent); + broadcastSAFFile(saveUri, update_thumbnail, hasnoexifdatetime, request.image_capture_intent); } main_activity.test_last_saved_imageuri = saveUri; @@ -2892,11 +2894,11 @@ public class ImageSaver extends Thread { } } - private void broadcastSAFFile(Uri saveUri, boolean set_last_scanned, boolean image_capture_intent) { + private void broadcastSAFFile(Uri saveUri, boolean set_last_scanned, boolean hasnoexifdatetime, boolean image_capture_intent) { if( MyDebug.LOG ) Log.d(TAG, "broadcastSAFFile"); StorageUtils storageUtils = main_activity.getStorageUtils(); - storageUtils.broadcastUri(saveUri, true, false, set_last_scanned, image_capture_intent); + storageUtils.broadcastUri(saveUri, true, false, set_last_scanned, hasnoexifdatetime, image_capture_intent); } /** As setExifFromFile, but can read the Exif tags directly from the jpeg data, and to a file descriptor, rather than a file. @@ -3557,8 +3559,11 @@ public class ImageSaver extends Thread { storageUtils.clearLastMediaScanned(); } + // n.b., at time of writing, remove_device_exif will always be OFF for RAW, but have added the code for future proofing + boolean hasnoexifdatetime = request.remove_device_exif != Request.RemoveDeviceExif.OFF && request.remove_device_exif != Request.RemoveDeviceExif.KEEP_DATETIME; + if( saveUri == null ) { - storageUtils.broadcastFile(picFile, true, false, raw_only); + storageUtils.broadcastFile(picFile, true, false, raw_only, hasnoexifdatetime, null); } else if( use_media_store ) { if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) { @@ -3576,11 +3581,11 @@ public class ImageSaver extends Thread { if( raw_only ) { // we also want to save the uri - we can use the media uri directly, rather than having to scan it - storageUtils.setLastMediaScanned(saveUri, true); + storageUtils.setLastMediaScanned(saveUri, true, hasnoexifdatetime, saveUri); } } else { - storageUtils.broadcastUri(saveUri, true, false, raw_only, false); + storageUtils.broadcastUri(saveUri, true, false, raw_only, hasnoexifdatetime, false); } } catch(FileNotFoundException e) { diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 269f566ee..1f904269b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -1399,7 +1399,53 @@ public class MainActivity extends AppCompatActivity { resetCachedSystemOrientation(); // just in case? mainUI.layoutUI(); - updateGalleryIcon(); // update in case images deleted whilst idle + // If the cached last media has exif datetime info, it's fine to just call updateGalleryIcon(), + // which will find the most recent media (and takes care of if the cached last image may have + // been deleted). + // If it doesn't have exif datetime tags, updateGalleryIcon() may not be able to find the most + // recent media, so we stick with the cached uri if we can test that it's still accessible. + if( !getStorageUtils().getLastMediaScannedHasNoExifDateTime() ) { + updateGalleryIcon(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, "last media has no exif datetime, so check it still exists"); + boolean uri_exists = false; + InputStream inputStream = null; + Uri check_uri = getStorageUtils().getLastMediaScannedCheckUri(); + if( MyDebug.LOG ) + Log.d(TAG, "check_uri: " + check_uri); + try { + inputStream = this.getContentResolver().openInputStream(check_uri); + if( inputStream != null ) + uri_exists = true; + } + catch(Exception ignored) { + } + finally { + if( inputStream != null ) { + try { + inputStream.close(); + } + catch(IOException e) { + e.printStackTrace(); + } + } + } + + if( uri_exists ) { + if( MyDebug.LOG ) + Log.d(TAG, " most recent uri exists"); + // also re-allow ghost image again in case that option is set (since we won't be + // doing this via updateGalleryIcon()) + applicationInterface.getDrawPreview().allowGhostImage(); + } + else { + if( MyDebug.LOG ) + Log.d(TAG, " most recent uri no longer valid"); + updateGalleryIcon(); + } + } applicationInterface.reset(false); // should be called before opening the camera in preview.onResume() @@ -3920,7 +3966,7 @@ public class MainActivity extends AppCompatActivity { Log.d(TAG, "found media uri: " + uri); Log.d(TAG, " is_raw?: " + is_raw); } - applicationInterface.getStorageUtils().setLastMediaScanned(uri, is_raw); + applicationInterface.getStorageUtils().setLastMediaScanned(uri, is_raw, false, null); } if( thumbnail != null ) { if( MyDebug.LOG ) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 4d3c119ce..10c3fcbf8 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -2535,7 +2535,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { 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, false); + storageUtils.setLastMediaScanned(uri, false, false, null); done = true; } @@ -2543,14 +2543,14 @@ public class MyApplicationInterface extends BasicApplicationInterface { else if( video_method == VideoMethod.FILE ) { if( filename != null ) { File file = new File(filename); - storageUtils.broadcastFile(file, false, true, true); + storageUtils.broadcastFile(file, false, true, true, false, null); done = true; } } else { if( uri != null ) { // see note in onPictureTaken() for where we call broadcastFile for SAF photos - storageUtils.broadcastUri(uri, false, true, true, false); + storageUtils.broadcastUri(uri, false, true, true, false, false); done = true; } } @@ -3700,7 +3700,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { preview.showToast(null, R.string.photo_deleted, true); if( file != null ) { // SAF doesn't broadcast when deleting them - storageUtils.broadcastFile(file, false, false, true); + storageUtils.broadcastFile(file, false, false, true, false, null); } } } @@ -3733,7 +3733,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "successfully deleted " + image_name); if( from_user ) preview.showToast(photo_delete_toast, R.string.photo_deleted, true); - storageUtils.broadcastFile(file, false, false, true); + storageUtils.broadcastFile(file, false, false, true, false, null); } } } @@ -3753,6 +3753,8 @@ public class MyApplicationInterface extends BasicApplicationInterface { } // Calling updateGalleryIcon() immediately has problem that it still returns the latest image that we've just deleted! // But works okay if we call after a delay. 100ms works fine on Nexus 7 and Galaxy Nexus, but set to 500 just to be safe. + // Also note that if using option to strip all exif tags, we won't be able to find the previous most recent image - but not + // much we can do here when the user is using that option. final Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override diff --git a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java index ed2f74045..16f15b9dd 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/PanoramaProcessor.java @@ -489,7 +489,7 @@ public class PanoramaProcessor { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream); outputStream.close(); MainActivity mActivity = (MainActivity) context; - mActivity.getStorageUtils().broadcastFile(file, true, false, true); + mActivity.getStorageUtils().broadcastFile(file, true, false, true, false, null); } catch(IOException e) { e.printStackTrace(); @@ -1957,7 +1957,7 @@ public class PanoramaProcessor { bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); outputStream.close(); MainActivity mActivity = (MainActivity) context; - mActivity.getStorageUtils().broadcastFile(file, true, false, true); + mActivity.getStorageUtils().broadcastFile(file, true, false, true, false, null); } catch(IOException e) { e.printStackTrace(); diff --git a/app/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java b/app/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java index 0ce4172cf..1e634b36c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java +++ b/app/src/main/java/net/sourceforge/opencamera/SaveLocationHistory.java @@ -52,7 +52,13 @@ public class SaveLocationHistory { void updateFolderHistory(String folder_name, boolean update_icon) { updateFolderHistory(folder_name); if( update_icon ) { - main_activity.updateGalleryIcon(); // if the folder has changed, need to update the gallery icon + // If the folder has changed, need to update the gallery icon. + // Note that if using option to strip all exif tags, we won't be able to find the most recent image - so seems + // better to stick with the current gallery thumbnail. (Also beware that we call this method when changing + // non-trivial settings, even if the save folder wasn't actually changed.) + if( !main_activity.getStorageUtils().getLastMediaScannedHasNoExifDateTime() ) { + main_activity.updateGalleryIcon(); + } } } diff --git a/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java b/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java index 9a5c0b7f0..0bb8f9655 100644 --- a/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java +++ b/app/src/main/java/net/sourceforge/opencamera/SettingsManager.java @@ -261,7 +261,7 @@ public class SettingsManager { storageUtils.broadcastUri(uri, false, false, false); } else*/ { - storageUtils.broadcastFile(file, false, false, false); + storageUtils.broadcastFile(file, false, false, false, false, null); } } catch(IOException e) { diff --git a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java index 6f292e573..7855e1ea1 100644 --- a/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java +++ b/app/src/main/java/net/sourceforge/opencamera/StorageUtils.java @@ -54,8 +54,16 @@ public class StorageUtils { private final Context context; private final MyApplicationInterface applicationInterface; - private Uri last_media_scanned; + private Uri last_media_scanned; // mediastore uri private boolean last_media_scanned_is_raw; + private boolean last_media_scanned_hasnoexifdatetime; + private Uri last_media_scanned_check_uri; + // If last_media_scanned_hasnoexifdatetime==true, it means that the last media saved had the + // option to strip exif tags. Therefore we should do more to remember the last media scanned, + // as we otherwise won't be able to find it again. + // last_media_scanned_check_uri is only non-null if last_media_scanned_hasnoexifdatetime==true. + // It stores a uri that can be used to test if the media still exists. In practice this will be + // the last_media_scanned uri, except for SAF images, when it'll be a SAF uri. private final static String RELATIVE_FOLDER_BASE = Environment.DIRECTORY_DCIM; @@ -75,19 +83,36 @@ public class StorageUtils { return last_media_scanned_is_raw; } + boolean getLastMediaScannedHasNoExifDateTime() { + return last_media_scanned_hasnoexifdatetime; + } + + Uri getLastMediaScannedCheckUri() { + return last_media_scanned_check_uri; + } + void clearLastMediaScanned() { if( MyDebug.LOG ) Log.d(TAG, "clearLastMediaScanned"); last_media_scanned = null; last_media_scanned_is_raw = false; + last_media_scanned_hasnoexifdatetime = false; + last_media_scanned_check_uri = null; } - void setLastMediaScanned(Uri uri, boolean is_raw) { + void setLastMediaScanned(Uri uri, boolean is_raw, boolean hasnoexifdatetime, Uri check_uri) { last_media_scanned = uri; last_media_scanned_is_raw = is_raw; + last_media_scanned_hasnoexifdatetime = hasnoexifdatetime; + if( hasnoexifdatetime ) + last_media_scanned_check_uri = check_uri; + else + last_media_scanned_check_uri = null; if( MyDebug.LOG ) { Log.d(TAG, "set last_media_scanned to " + last_media_scanned); Log.d(TAG, " last_media_scanned_is_raw: " + last_media_scanned_is_raw); + Log.d(TAG, " last_media_scanned_hasnoexifdatetime: " + last_media_scanned_hasnoexifdatetime); + Log.d(TAG, " last_media_scanned_check_uri: " + check_uri); } } @@ -234,7 +259,7 @@ public class StorageUtils { * This may well be intentional, since most gallery applications won't read DNG files anyway. But it's still important to * call this function for DNGs, so that they show up on MTP. */ - public void broadcastFile(final File file, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned) { + public void broadcastFile(final File file, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned, final boolean hasnoexifdatetime, final Uri saf_uri) { if( MyDebug.LOG ) Log.d(TAG, "broadcastFile: " + file.getAbsolutePath()); // note that the new method means that the new folder shows up as a file when connected to a PC via MTP (at least tested on Windows 8) @@ -260,7 +285,7 @@ public class StorageUtils { } if( set_last_scanned ) { boolean is_raw = filenameIsRaw(file.getName()); - setLastMediaScanned(uri, is_raw); + setLastMediaScanned(uri, is_raw, hasnoexifdatetime, saf_uri != null ? saf_uri : uri); } announceUri(uri, is_new_picture, is_new_video); applicationInterface.scannedFile(file, uri); @@ -283,7 +308,7 @@ public class StorageUtils { /** Wrapper for broadcastFile, when we only have a Uri (e.g., for SAF) */ - public void 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 void broadcastUri(final Uri uri, final boolean is_new_picture, final boolean is_new_video, final boolean set_last_scanned, final boolean hasnoexifdatetime, final boolean image_capture_intent) { if( MyDebug.LOG ) Log.d(TAG, "broadcastUri: " + uri); /* We still need to broadcastFile for SAF for various reasons: @@ -307,7 +332,7 @@ public class StorageUtils { Log.d(TAG, "broadcast file"); //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); + broadcastFile(real_file, is_new_picture, is_new_video, set_last_scanned, hasnoexifdatetime, uri); } else if( !image_capture_intent ) { if( MyDebug.LOG ) @@ -703,7 +728,7 @@ public class StorageUtils { Log.e(TAG, "failed to create directory"); throw new IOException(); } - broadcastFile(folder, false, false, false); + broadcastFile(folder, false, false, false, false, null); } } 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 00c39629c..c8c7273dc 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/DrawPreview.java @@ -496,6 +496,13 @@ public class DrawPreview { this.show_last_image = false; } + public void allowGhostImage() { + if( MyDebug.LOG ) + Log.d(TAG, "allowGhostImage"); + if( last_thumbnail != null ) + this.allow_ghost_last_image = true; + } + public void clearGhostImage() { if( MyDebug.LOG ) Log.d(TAG, "clearGhostImage"); -- GitLab From 65a59401f8d004ea9be140b8dbd90be2ece8f0ba Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 30 Oct 2022 20:54:52 +0000 Subject: [PATCH 081/117] Refactor - simplify code. --- .../net/sourceforge/opencamera/ImageSaver.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index f49233d69..69cf96a77 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -2369,19 +2369,15 @@ public class ImageSaver extends Thread { Log.d(TAG, "postProcessBitmap"); long time_s = System.currentTimeMillis(); - boolean dategeo_stamp = request.preference_stamp.equals("preference_stamp_yes"); - boolean text_stamp = request.preference_textstamp.length() > 0; - if( bitmap != null || request.image_format != Request.ImageFormat.STD || request.do_auto_stabilise || request.mirror || dategeo_stamp || text_stamp ) { - // either we have a bitmap, or will need to decode the bitmap to do post-processing - if( !ignore_exif_orientation ) { - if( bitmap != null ) { - // rotate the bitmap if necessary for exif tags - if( MyDebug.LOG ) - Log.d(TAG, "rotate pre-existing bitmap for exif tags?"); - bitmap = rotateForExif(bitmap, data); - } + if( !ignore_exif_orientation ) { + if( bitmap != null ) { + // rotate the bitmap if necessary for exif tags + if( MyDebug.LOG ) + Log.d(TAG, "rotate pre-existing bitmap for exif tags?"); + bitmap = rotateForExif(bitmap, data); } } + if( request.do_auto_stabilise ) { bitmap = autoStabilise(data, bitmap, request.level_angle, request.is_front_facing); } -- GitLab From e043e2c48f70ce44d3302da8db46e6f15c5799c2 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 30 Oct 2022 21:17:33 +0000 Subject: [PATCH 082/117] Transfer more exif tags when saving as bitmap. --- .../sourceforge/opencamera/ImageSaver.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java index 69cf96a77..9e100effc 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java +++ b/app/src/main/java/net/sourceforge/opencamera/ImageSaver.java @@ -3011,7 +3011,7 @@ public class ImageSaver extends Thread { // don't care about TAG_GPS_MAP_DATUM? exif_gps_measure_mode = exif.getAttribute(ExifInterface.TAG_GPS_MEASURE_MODE); // don't care about TAG_GPS_SATELLITES? - // don't care about TAG_GPS_SPEED, TAG_GPS_SPEED_REF, TAG_GPS_STATUS, TAG_GPS_TRACK, TAG_GPS_TRACK_REF, TAG_GPS_VERSION_ID + // don't care about TAG_GPS_STATUS, TAG_GPS_TRACK, TAG_GPS_TRACK_REF, TAG_GPS_VERSION_ID exif_image_description = exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION); // unclear what TAG_IMAGE_UNIQUE_ID, TAG_INTEROPERABILITY_INDEX are // TAG_ISO_SPEED_RATINGS same as TAG_ISO @@ -3048,6 +3048,16 @@ public class ImageSaver extends Thread { // don't care about TAG_Y_*? } + String exif_photographic_sensitivity = exif.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY); + String exif_sensitivity_type = exif.getAttribute(ExifInterface.TAG_SENSITIVITY_TYPE); + String exif_standard_output_sensitivity = exif.getAttribute(ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY); + String exif_recommended_exposure_index = exif.getAttribute(ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX); + String exif_iso_speed = exif.getAttribute(ExifInterface.TAG_ISO_SPEED); + String exif_custom_rendered = exif.getAttribute(ExifInterface.TAG_CUSTOM_RENDERED); + String exif_lens_specification = exif.getAttribute(ExifInterface.TAG_LENS_SPECIFICATION); + String exif_lens_name = exif.getAttribute(ExifInterface.TAG_LENS_MAKE); + String exif_lens_model = exif.getAttribute(ExifInterface.TAG_LENS_MODEL); + if( MyDebug.LOG ) Log.d(TAG, "now write new EXIF data"); if( exif_aperture != null ) @@ -3148,6 +3158,26 @@ public class ImageSaver extends Thread { if( exif_user_comment != null ) exif_new.setAttribute(ExifInterface.TAG_USER_COMMENT, exif_user_comment); } + + if( exif_photographic_sensitivity != null ) + exif_new.setAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, exif_photographic_sensitivity); + if( exif_sensitivity_type != null ) + exif_new.setAttribute(ExifInterface.TAG_SENSITIVITY_TYPE, exif_sensitivity_type); + if( exif_standard_output_sensitivity != null ) + exif_new.setAttribute(ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, exif_standard_output_sensitivity); + if( exif_recommended_exposure_index != null ) + exif_new.setAttribute(ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, exif_recommended_exposure_index); + if( exif_iso_speed != null ) + exif_new.setAttribute(ExifInterface.TAG_ISO_SPEED, exif_iso_speed); + if( exif_custom_rendered != null ) + exif_new.setAttribute(ExifInterface.TAG_CUSTOM_RENDERED, exif_custom_rendered); + if( exif_lens_specification != null ) + exif_new.setAttribute(ExifInterface.TAG_LENS_SPECIFICATION, exif_lens_specification); + if( exif_lens_name != null ) + exif_new.setAttribute(ExifInterface.TAG_LENS_MAKE, exif_lens_name); + if( exif_lens_model != null ) + exif_new.setAttribute(ExifInterface.TAG_LENS_MODEL, exif_lens_model); + } /** Transfers device exif info related to date and time. @@ -3164,6 +3194,9 @@ public class ImageSaver extends Thread { String exif_subsec_time = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME); String exif_subsec_time_orig = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL); // previously TAG_SUBSEC_TIME_ORIG String exif_subsec_time_dig = exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED); // previously TAG_SUBSEC_TIME_DIG + String exif_offset_time = exif.getAttribute(ExifInterface.TAG_OFFSET_TIME); + String exif_offset_time_orig = exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL); + String exif_offset_time_dig = exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED); if( exif_datetime != null ) exif_new.setAttribute(ExifInterface.TAG_DATETIME, exif_datetime); @@ -3177,6 +3210,12 @@ public class ImageSaver extends Thread { exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, exif_subsec_time_orig); if( exif_subsec_time_dig != null ) exif_new.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, exif_subsec_time_dig); + if( exif_offset_time != null ) + exif_new.setAttribute(ExifInterface.TAG_OFFSET_TIME, exif_offset_time); + if( exif_offset_time_orig != null ) + exif_new.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, exif_offset_time_orig); + if( exif_offset_time_dig != null ) + exif_new.setAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED, exif_offset_time_dig); } @@ -3197,6 +3236,8 @@ public class ImageSaver extends Thread { String exif_gps_altitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF); String exif_gps_datestamp = exif.getAttribute(ExifInterface.TAG_GPS_DATESTAMP); String exif_gps_timestamp = exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP); + String exif_gps_speed = exif.getAttribute(ExifInterface.TAG_GPS_SPEED); + String exif_gps_speed_ref = exif.getAttribute(ExifInterface.TAG_GPS_SPEED_REF); if( exif_gps_processing_method != null ) exif_new.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, exif_gps_processing_method); @@ -3216,6 +3257,10 @@ public class ImageSaver extends Thread { exif_new.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, exif_gps_datestamp); if( exif_gps_timestamp != null ) exif_new.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, exif_gps_timestamp); + if( exif_gps_speed != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_SPEED, exif_gps_speed); + if( exif_gps_speed_ref != null ) + exif_new.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, exif_gps_speed_ref); } /** Explicitly removes tags based on the RemoveDeviceExif option. -- GitLab From d5f287358a96a3d03c6e169bc263162a01f4cca5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 6 Nov 2022 13:36:52 +0000 Subject: [PATCH 083/117] Add comments. --- .../java/net/sourceforge/opencamera/AvgTestSuite.java | 7 +++++++ .../java/net/sourceforge/opencamera/HDRNTestSuite.java | 7 +++++++ .../java/net/sourceforge/opencamera/HDRTestSuite.java | 7 +++++++ .../java/net/sourceforge/opencamera/PanoramaTestSuite.java | 7 +++++++ .../java/net/sourceforge/opencamera/test/AvgTests.java | 1 + .../java/net/sourceforge/opencamera/test/HDRNTests.java | 1 + .../java/net/sourceforge/opencamera/test/HDRTests.java | 1 + .../net/sourceforge/opencamera/test/PanoramaTests.java | 1 + 8 files changed, 32 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java index 26afc7802..82f3d6670 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/AvgTestSuite.java @@ -4,6 +4,13 @@ import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; +/** Tests for Avg algorithm - only need to run on a single device + * Should manually look over the images dumped onto DCIM/ + * To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/ + * folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes + * time to transfer to the device every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. + */ @RunWith(Categories.class) @Categories.IncludeCategory(AvgTests.class) @Suite.SuiteClasses({InstrumentedTest.class}) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java index 4547725e4..94314c209 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/HDRNTestSuite.java @@ -4,6 +4,13 @@ import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; +/** Tests for HDR algorithm with more than 3 images - only need to run on a single device + * Should manually look over the images dumped onto DCIM/ + * To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/ + * folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes + * time to transfer to the device every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. + */ @RunWith(Categories.class) @Categories.IncludeCategory(HDRNTests.class) @Suite.SuiteClasses({InstrumentedTest.class}) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java index e7f518050..7ecd61239 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/HDRTestSuite.java @@ -4,6 +4,13 @@ import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; +/** Tests for HDR algorithm - only need to run on a single device + * Should manually look over the images dumped onto DCIM/ + * To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/ + * folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes + * time to transfer to the device every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. + */ @RunWith(Categories.class) @Categories.IncludeCategory(HDRTests.class) @Suite.SuiteClasses({InstrumentedTest.class}) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java index abb27d562..84186f783 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/PanoramaTestSuite.java @@ -4,6 +4,13 @@ import org.junit.experimental.categories.Categories; import org.junit.runner.RunWith; import org.junit.runners.Suite; +/** Tests for Panorama algorithm - only need to run on a single device + * Should manually look over the images dumped onto DCIM/ + * To use these tests, the testdata/ subfolder should be manually copied to the test device in the DCIM/testOpenCamera/ + * folder (so you have DCIM/testOpenCamera/testdata/). We don't use assets/ as we'd end up with huge APK sizes which takes + * time to transfer to the device every time we run the tests. + * On Android 10+, scoped storage permission needs to be given to Open Camera for the DCIM/testOpenCamera/ folder. + */ @RunWith(Categories.class) @Categories.IncludeCategory(PanoramaTests.class) @Suite.SuiteClasses({InstrumentedTest.class}) 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 6e5334ce5..fbc1119b6 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/AvgTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/AvgTests.java @@ -10,6 +10,7 @@ public class AvgTests { * 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. + * UPDATE: now deprecated, replaced with AvgTestSuite. */ 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 4c82cfd8a..fe14fab5c 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRNTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRNTests.java @@ -10,6 +10,7 @@ public class HDRNTests { * 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. + * UPDATE: now deprecated, replaced with HDRNTestSuite. */ 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 5c6b56224..029065386 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/HDRTests.java @@ -10,6 +10,7 @@ public class HDRTests { * 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. + * UPDATE: now deprecated, replaced with HDRTestSuite. */ 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 554ac0b79..d97cff41a 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/PanoramaTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/PanoramaTests.java @@ -10,6 +10,7 @@ public class PanoramaTests { * 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. + * UPDATE: now deprecated, replaced with PanoramaTestSuite. */ public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); -- GitLab From 4120f4bdde8718bd546938e1124539b005799dc0 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 6 Nov 2022 13:37:15 +0000 Subject: [PATCH 084/117] Fix enum name. --- .../java/net/sourceforge/opencamera/InstrumentedTest.java | 4 ++-- .../net/sourceforge/opencamera/test/MainActivityTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java index 85e7456ae..65f9c4a51 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java @@ -1526,7 +1526,7 @@ public class InstrumentedTest { inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR38/input1.jpg") ); inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR38/input2.jpg") ); - TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR38_filmic_output.jpg", false, 125, 1000000000L/2965, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FILMIC); + TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR38_filmic_output.jpg", false, 125, 1000000000L/2965, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FU2); int [] exp_offsets_x = {-1, 0, 0}; int [] exp_offsets_y = {0, 0, 0}; @@ -1631,7 +1631,7 @@ public class InstrumentedTest { inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input1.jpg") ); inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR40/input2.jpg") ); - TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR40_filmic_output.jpg", false, 50, 1000000000L/262, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FILMIC); + TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR40_filmic_output.jpg", false, 50, 1000000000L/262, HDRProcessor.TonemappingAlgorithm.TONEMAPALGORITHM_FU2); int [] exp_offsets_x = {5, 0, -2}; int [] exp_offsets_y = {13, 0, 24}; 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 681d3de72..e51d096b2 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -13648,7 +13648,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Sun, 6 Nov 2022 13:37:59 +0000 Subject: [PATCH 085/117] Add icon to fastlane metadata. --- fastlane/metadata/android/en-US/images/icon.png | Bin 0 -> 19126 bytes makesrcarchive.bat | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/en-US/images/icon.png diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..77816ce80de495972866e29c5eda6fbed6148bf3 GIT binary patch literal 19126 zcmZr%XCPe9*Pbm{eI-O^5jD|!U(uttkfJ3z2?>H=trikJO=op}2uX;}DiMO{iI!-| z>Rr^eUjOg!hkNJCy)$QK&Y9<&=bU+AYOG63#YP1Hpw-vYGQVi?|9cSR7jN?!FA4x| zhU#l+SO!jQWK-B!T2Fn||G=!pkW4qq(kR)I;q_5p&iPaCiL-p~BSUF_c`GBcNT%!i zVric&Fwrwt{94->l8g zw>EfhW}XuMzecW<9WV!8#ZtRgQZVxj7y{b*6u42w!CN_iR@r&@cp4~C~i|5{BWwx;!TueHy^gs(>Z7b%j! z+qPKVQyaRLKev?_F_>pR9U%Se`@~^JH*2u%k*u8}wnuaMUVwwGS=?FFkx+#IUfIMy`8 zn82SUkcVB4oS=<7brBh1XMtl94VkQm4o$u0O(s7?w>06}mD7fqWWfA#j8a_fXWjhL zTGcbd6ahwU%q@|lQ{JBkHx>`6_$Y|?-qXq4M@xLt~-b#7gEaT11 zZa?8`#9=6Z|L4Dl5oWDSR1z;-wp7_IhJwj|=`!!5M-ORGUl{lC2yZ(xIVz|WdYt?1 zY^a=6+Y${F{`(qanC-*ofXidNoW#tbSpjZV6-FJU!wn8`m&ZzYrkC|LwugIGkE2@* z@_G+%oTbXEj^9o@$l_thVX@Uulxc&|9RF%2-#`9!mJ;;q5^6u6MC}>VvW;=qvKzE| zwQy%XY@s!~+?_5!GunyR*vgFiES%P={!Z%{}#Y+H3l7GN!~DhUwoL{BipUXi5rqFkI8|V7yu;@ zTf4*CwwS@&tcaNTe;?_m zMd6UW?Mez;e@}l;V%0?a!cs7{vjL_kI~me#{zZB+Aoh;L0z(5xk&Y=$i6LrBCaC3S zlP-+8UO&EVQ}F!AU)@=A^gK!WJiWfd&Y}OPQ}*mJFR)Rn$dTrNpO60i=dw0;es874 zXEuxD-y&7hIh5A^OFXyt)p6F<kuBz~}5$d(*2WL-3{L5d5O@p<}QyimC<3jHap zpY{BD{L$IY6_zlee(sI?!?Wa|x+dt?*_|vl24qc83-{V53}uR6+>HF!VwDfkzrrG( zd(RbMR15>I%bxorkThSDST3{DqJSQ)kId0)W&S6o-#U5EZ36?C0FhRy-V_6E?Zfuc2+-H~Foygt~zJfZ9!y!o7Zahq01N zJXocQo8GNc*_S3I|Gp4k&c58XSz;%-dZ?dIGKJ2042QXaW%7)BHtZ6}C&ABTSu0H% zE%Mtl)FJm2LT&9omX&U;4xfiW@D4IP8<5rY14e~-`#;a|~#2bs+kOo^{8oTN2b$3(g%N z4#Ft{RfpFj(64_zmhf_J_)Z&6_WEurqu&75-K3}?WY(-K>6%U$^E$}}3PwQG91SA} zrP6;Lw{F>O=Ez776SC|k{;|7}u6U5_jfzx$q|IiZDvO_`K+xlR*CNo_lRxr}|7#ro zCGfX@@JE`7{j{&VOjvFaAwV~*J`A~4!??kxFQrq0Cj+`cr3$=bpT6%KwK%3;v z7SaFCzT>+uuPtQd2s%`LPs{SCT8UOY<#fgb>~lBviW>}wL-88b1izG7PkR$pdFG)P zU)}FrSmLR;ZjtKJ%j3a+%i?4}r0OtUq?|UYQsr22zeihYFbmd#@gN;VDo zoDVTEP+%l)qfPL?eYNGD*z}p?eM~+Cq0uH%z#1j+u4wU~bWC{({j~(Q#PG=R?zy5X zMmY^LM6kxT|1D5|D^l0QU95>xsf0vbSKRMcG-H(B`|4U}OKvpF|9+K&i%^FE<^8{$ zQ+W53L|^@eB6y#a-Qxf>8g_%X@6lhi4xniJur%dJ3%T}eoHAITyK+s{&HeY5+uk() ziKg`4$M4I3PE)#_ejHst31CZ!IQC75#QE|7H?t6(x07@omy) z_rRIuFU(@Gcc)2V?+9mN4>4qmaX_e0LMo*yAj8VZE=GHVT1pO@oW%DeJw9^oB`AI3 zpqe}i>-b?AiHmM>>_Pca)no*5eyZ@I*%D&CR zZ}}2h4j?T)64$vZCH#<1&=rC?LH$&1VZ@f_-HBF-)k#Ne*o@_>bRi)% z{ddiA6Ah!Y*wi(h!1Q_FyV`$iYYUO>t-66Eg(E`lB-adAIUNmc1*nO%R(3?~f-P?vwXh>i9Xy8cyTE_wuj&ZhYufRQ%S*qAnoh3f;N=Zqru7TH_wnnLc7Wz z$){s4sl^^YYT~t<`7xnxvpDUAp*G_F^qIQPrf`XgmX`bB$k9E&L3bwFTv6F(-1YpP z3W2!7uO5U=MM~2e(^_*@3XPa?ck?L@I~O1U>%f7Yl}&5sfQTU`!7!>o_r-+zYcflC zgtbUWidek(Iw{K}bRp{@=9*!Z7wJ*@iuR&zrUdEXxfkrNiC@Ij4=z*JB9Dir{s!`T zlfTKu22wka?N=drM$kyQ8(4nykYo7qTa6)>*PH``B{v-19--#v0){-9srO)FuTJe* zGN&=Qfsd*BzAx3ejwRK0>}&m^GW$KWKRmQOEWk_R+^P!~H&D{>45o1N&Hpb`x&6P& zN(UV~KAA8+E>>Qtl#CZwC1Uw5)Q*?NR3}0FEQ7qfuV$SbeF!)x(aYanqvrl5&@WoL z^|;3Nz8a^7+TT+r*CE<2yuF>p{_lZk!oiarom5sXYBtp=y+_<+7TN5x?qr!s0SUnH$94(h-Lulf9yj!;r& zI+(t_@1j7($hqAb{2OkLq@Vxjo)Kfqk~&Q)4b)JYE@(UJ7f;x4Ug`a5n3i*4%Z1Ux zgd?H4w#8i038Lsj%?Fs`lXu3+A1lf3W!$CrThZ!94{0F%SR$^_K^19`z)r_uh~+I5 z5Kz@UL>nK*us&l99R>LCOM}XnTDFBG7;Sm3oE7-qC1*O@aK-F(C-us^5arVE zJbrmuyP#!az@1#aWt8S~_OcJ_p0sScpW8oz05aE#8dA!Hj>VytQ3$Ok$in}PBc_Rz z(SyP{rAFhBrGJ5t#svTu6@Usd!oz2fprK<<9=0=0SAqEF&u2JYoj1K-uT;vk%^XQy z8m-fh=jN9VsnIxnm3IFAFth!zXhSpSfW<9T^wAOg4g+CP;!*txBfvVa_AZah}3976QFSi%t33LIdl7F4g%>fnk3}vj@3L1 z+AIc461ZOrCLEY$trV2a7v!}Ue7aj*cy=OCkvZ!QWxqKUQ!!U-DT|D_s&J{R&^j`9 z%!4L^jxqulSrCDU;CL>88*sbww)X|>M&!7_Hw2<<{R-NF8r1MHe$3~9H}4O4%`iNf zTCbisY*TpjaEaVA{~Xs}HL)t=N_yH(lpNY0-MicFOjbEJQY&hc`d6NPd8Aq3KOG4^ z<}wKJAX}$;&9bDgNL6S2H{}m6cc?YNGIAw-9K|JEPTuAE(|lZGTvHZ%#8Iq+jCu15 z!7E9_i|pu0o`f3B-rz}YM?u25A@OL3rsHTc)b&~hxY$H3<+I0yqXaUWP42gMLTP14 z=SXl7^qkR2w+v6(mK{G}><>Ge-k7U8e?&3aG}ZpI$kySmw*1kKQ>y-k=}_LPqZ8&G zU9DfluM#DEB9&dX&2-uJl3>x{^4f)oMf{7IYGqFq`Ci*l7>wH5s8SHcf&nL zmJZyIPyih}>pskg+n9Ch+$FWv@gPo~SDvvOjafFv2yBo+{FQA3D+_v00`*~gT zJsIIol8+2v{_;m=(D946m*XI|g%4BF%iljjDW?A?#(i+cd%At+#F*IKK9?}6Qc|p<6>Es=sg3)^S$+p9G${PY1eQYER@8s(7-KTDBeIcqv z*iv3JUCewz+RZvI4wy#Y&YmVF8kRl5M9FHnWSb$zULt#R!oBv8Hc)h++<#=x2BV7c z_m66S-Nng&O-@XPz0<8;=KfIZrDN#mEe}N%XW;`gQe|!$F$liFg zr)(p*I^7=0okc!1#)i``k3q#EOL^cPG#Z&gMP~RM;B>ejaqIpbubIKGzFq-OuuzeZ6?$NrCSoUvlb zIeq@@Tf;xnk0hxSbZU z$z}kFWv!qw_L3k6w%BGf3yn);*KA#-Sd`t{I3xd&)IE@W8DlAD)KR~U8`yUvAdu3$V;J8NI;`4nl-9(C*IfFYU>-&2}J zxcXpYkcFaW;&Id~(fGh}-rJq(_duz)2jr{B8ou?TNA;c?(}dYQ%BQJWd2$5&})pcWoL-a)W3-BUoRQ$QTwmfjO%QdD*G%L0}E!R7Syh@3-SoPdr z%#P<}vvMg@zTZ6i*m7F)K|;u>bhlZzwMyD82yh$}yFI$~63A#;jmw6&d^L{zH=Pvz zbM~BpDC^5t@A+4Lj^Jz`$(|ZQ_6XbP-urW|?piD4kUxxq@W-3nQ*D;M^;ZCtF}DKd zpCJ048vn8riM@Y^0>=O#K3R-kJpV(%jSBfQ+&vhvggOdv+g5>=O8+4gviEs2B8M}n z#Q=JQ+dYr7s=}1(9EZ`|409lDnJ%(J@Rp<`xlhfzRs{y>aMtrt&Z4=7h=IB8C~@@I z%pev+`iVY2IYhTPNDql?O+k4}{kAjEiJZB-lh*fA}d2VDAk5HWYZ`;X zo=9D-t8Zth24<5$tk^QE&xNgPhbaO@CyJkaVo-cZ46`v_U+A1v|#a3%uYq`T)%b z$Q@)2qb1;ejLCixJt5d{7Y9!OebnFm2wc(ElNRpaQw%P^=I$6!-Z9 zt5cXi@?H~??R?zZv1?W*q0LrCO6@%|D(7LQ+e8QV8K;nxeU_HlB(^D$NDyakyUYCc@G zl05gdQCk^5ZXh2~C(j*;R0z1L&;e|2U4`KoOCK}z1FS4Q<0b?44I9q5{OjchA`t9% z4H&Xp_zhEs_xi&$pXoiNdlda?(j7mbfrTQDKgT7>T3Twr<^^OK`C)mPfWueDeBajYvoseSZ<4Pn z`3bYaC86rhU9WTu>UsX5>Uk;=_j2#R-`@)bTH>#@&x*8^U>kz7ntbFTiRmvg+nIx} zWl^Iu<`FpK>wKU>MdJr~r6QS3uhj?xDAF+fZ(j$QGcM@{I%u?&H2Q9a4`zj_yinm# zxaJ&R8!Z8Is8K6g)y^Q%D0Hn;nKvH1zXsVJ{&C62sH3j1?^CQna8&7eK^L*7)U59y z;;vL2o;^1gaBozSWg2ASf4>%;!#YzmtBC1^ddvf9`qF>_~()&yu|%pSn>%e%=mu z$f?lLVYt%J&=fz%aKypS{EbY*b;Rq$?~hDxP1C(5I;prW$myJOfTPQsT}6sXqI{tIH?3w zT>8VaXbh0g#V2+O_8+MT`dWV4)9CsMqE4RqlizCcZzYljeJ|Kwr zG$&>WdaPia{fPG;cv4K*4nuQ@)f`>?TyTcIGz3^yyRS$LWMyTjF|`)l1hebv(Wwp! z82B7SsQsa-tXM=}xXW7_EaRm!D;mIZMg}C{qF0&Ny!M$P3hb{&0MVZG6t*kz?;{iF z&+un>pQEbMtxKpm!-ClUt(A*~v*o~Y>Xzea`@)j80J;7 z{~KO3mi5C%+h;)^>uY!I+faN-;(<>&Yn8_B&orHC_-lUZgw&H4qdgGVXWEjMl;I!& zTmm9Ipdd<69x6@LIA|TwRQYPV^X9O(%0#dHP^JsdCZ11fl$qgind!pw2XuF@ z3qpNtsPH!xfz>52ChR{!BnC{;P$;V|wY}xE1^x=_Aw25R6$Bru0{Rhy}6*~aI%9bR6Aru5(b1V#4A70VK z1B9VMNcZXQLgnPtv#pyfp2C1x3Pjiu*BeVCexEm}fq}Wdw22TV@3iqBVvfmN!bvtNG`$>(Beu#0}uG;oL7da5xNAhi2w z*?Wp;_)Y^A58~+0S3|d+aDli3vU_?KxX0~-Rc%)?&Rwm^QI|5*rKll8bJcVgT49be z!JP(P?s=!J0v+NXaEo4I&wU%IBv{!e9$xn2--@mlc*jhM#FfT_`s>FC4&hbKdd3bo zD8JBL7|MzjSP)V{a^mLnzzuu?XbU_e4b1Y0k23a1fo1w6>Y#(javaa;JqBzK$4Aaf z>9Z*S*HTyBDlAw=&k3<1aeqjIR5SOVrS9oq7ryEE`0w(-KUURfP%WE)QAGBfeSs-{vZLJ8<22gt$|}b9J#O*C=Mu62O?n@)}KBbk%Bdj zBD+2@y(+tx?z!`F=pwBDzy`LOn3pfoEalJHuzF5J6}Gb&fK{zdi`Sbl{=UWlCio^l zv$T21PXtx`m?!AU_P!dti3)@RFA`G>91Mlm>e07Y(}}Fz&4%HrKGQ5j%_HzIEOWao z6_}f}3mVL&Knw5oy&ZQ%Ky0ZPwx_-;4PN6)bCd;KtJA|;6ZQJm>PRSUfS1q({H^7$ zay$XX@--0#@3v&%4w7}=%$O{q)r-y~5#dS?5Y{u9B;W031h!X~O3)8{Ghq-R9xxZ} zQ?#IyX7xt}<^PceYw3n5^?66C{!2%!xX6#`@&PU~CnWBRPqocKZe7v+Lc%lE3s#cg z)^}l}j<{kSm)cE#0g!TOuZbKORUIy-_p{QA5iQ|a_>-=PWtZvu;=!E0q8sbHXb8eqIAyT&S1fT?ahwSi%z@FomHJ} z2a+8wdhpB1`Au-|ojH`wN>162Yhg;pEkqj__W;xyw!?x27zseCMRpBzuyk}0IT$~^ z%^K#m8|?Rs?~7kHv3Hw%s^^zBG$!QN_&z}E&cLaepKh9_(bfX+5_=E(S;GzI=^wwp@S-a6^P%gG?!4)$0(!)OsGhx_5_4!^#;Dn(AUeCBd<_K5%@h}1nK^QBMg)9*lMgKU`FF2QjdbeFB z`uvtRudNK|nj=80$TFu->B>Vm+5{5l9=Ui3KM%I2HTIoI&+c%4xbH{)#IG09?6lAj za4=ECg}y5(zg`a7Za1*HF~>Ei8;5PL^v5*pm$1y7{&;M9YjpK0aHD$D1vBO0cwTfL z6#meMAPom!&b`ynCHZ8LBtRp@Py^b+b*TXML@;>L9sGL0%bn{Iiq-EZ9C88hFzdd0;o1<+5eIMbH&Q;c zek{%-BR((?mN~yN6M92XJ+|SThZTH5vFV%>H);bH<41ws8wgsyvm6EHd&gx?GNHKW zzSD<>!D+`Y$hTe6zQj-z&$OK={&DpF_3Ls4IbQytu9R9vcsnJF9VgtR&sKPkf@W&_ z78LiD5TFKxs?HC5RAcSwsLi+lhSBDxi6>e&6`wM9FrKN=-hIi`Y}C!FwXLLvt@)tc z&Zjq%JzUN)Rc4o(AP7)rHP zQFT_rk-J}P+Z@AAcHkRxYlGS35f;~VeFna1JV`UsrUsScF6)rMQQP7)AF?~VjC`6k z-r*MZ$4T`l#DpyEoW$kxYlIu!6(PEdTxmDN)`6 zN9yYl8d{j2w`1655#X7ICOwctTTutjZ4^*}^-c<_-odTmjxjxI3l=}zkMTaah|`hJ z*pk^PO1o1Zahv@e%=S0+1FY`uQ|$DuN6^%c*ec!b7+G@TAVVF@$U%H`+2DWN9%u{C zQzQg^rK1cE{&NCjqY2^(fsm}L|0Kz=MuaVpH)(rhdcAE)ZJNG?JzkZA&12{4MeL%= zjQD-(VQtNLAN+W#Obc32KVg8=;$vk6Yxv6B+8yx{#?kLK1T%l6W~jABRV*&`w>9etl}}YZpd-B zxX}Yk4lu6*dZmCl5-bo>CDh0kQWd7%FnK_y4>vn;xK^la7tCT3QC5}aHo$>p1m>zH z2aTIM6R}wb7H*8#;`M;ol^7mym2(v+C&`E3j@H!}dty$J{PZtmrP#k{l)c`9qcFv8 zYT=JyqiMTx=s&aSqqWha8B^EH3oNaTfMUhtx&Vr~o9|d1j$(x-kD&VpVaYr&hh)On zv#=V380Qsyl~+YJ^8B^7a;BGKu$1?+&c;JA1GyXaJ zr+PWCt`OtBCY#D7K?kV|OWl}fN_bRXoxp0JYf1B{?;31t^FNAzqS>1FG35C13s$+y z9GP>K0(%AD%ZRlkM;AZ=)$r_{z1N%cjY>4F=$tL-+A=UGLv{j8=^x$~!9x7&8D>6xhI`vEC@WAD zxgH6z*>kNF5!#%LCvImf3KYH56zF z2s(KXcRc)MXB%!ah#uONnv-tw5#7E1Z&S$gF7B0avg>xOU1fG>0(r2_qS_wzT)z2`KG=Wno~m)U51Y)K9^ieF?gUV@mjR6q+2$RGhc>nmLqCr^ zV;Xj6d7(ZuI_FYZq&Y@6Cin3FZds4=rWr<10-wxTTKT$Zw&SL#YntR@V&oR2H!n}l zNsDL2CSR;y*5zFUFCBcUHx|tVd+4)k%KIL9Y80U1hLIeOq}b+1--!g^_7}K4aUabx zxC9)I!!F63f4hhigrz8|GyKC>wSd#t+$iVI!ocD6aWwUP`1jc#lJh6JTRL|C5Q#er zyJ49CxrQyX_&QqYoVRLK(y9xVcbmRX{W=PI1fJ6ZR%bX?iwc`eiv5I#f_m`i7C?K2 zKA-j?iG}zdC6CMEx6=PL;dHGg%DX1wIw0fFwdO9v8B@Qw9ZN9EXNO@f zH0&GOAG6hQ$;jUGzy2VTe*3U><869lMX`(L28pL;3eMq+YS zIN1YHdteS)-;TugU*xdzTp$u2n0dq-Y(wUh;Jtry**yhZ5C#jk^Yi?6DC#{;wn`AM z-Mv5a48{5)#}SO>#Z4Nw*k`Z9IX@a-pd1Ax&@B}9TWwjc^m3xhb#J)YwIs?FdUA)0 zt$bb;4tl8PWG3gW(-*p!rP|zqVR~*BU}*k$=}7gs{YnPuC@?T6h_3kD`#LR~@^`^0 z=E@Irn9bL1JbVjgsJ5msQ=K|zMl8VJ=b(ZNML_ERWh;faRZQa&f2u#Vgwh&^34wAw z2#2wJM->0-Wp5g+1QojUHZV-%&W2?CGP=?xIzulo+vLZyOQXcQ=FRN<|nSbox!MUJH_hb_ZF@||rVANNO`Hf?p?4^1hkH|_?8YHGJO&twTfSWa_1kV~0qL z1?mIm{Fx4Hg|66(s+MoCWc|V258Xr>Y}RiQj$b$Y(B796uA<+f*q*bJq`~%QzWjzi zdg#yWK#S4q;UO{zXjGvn)R;Ij%r@5tZv05RjF3p}CHldCMvo92dEb<5JejV{@1d<^ zPGUYS(~ojn64?sXiR1QCcZ-H|)9s;HGJ@+@?)-&--|x5v*SL=PHc3ZO$5YB@lI!F~ z88W>O)@`&hlvL*NVcSB<)1l6WvsU$jBALNcWtTM_bHwL@m!=e?fB0I;MqG4-v1PDPC?~P}t7i8{|%A{venl zzcF!UmczKW&9Wsa%gDr*H_D}V7AH`!v+ke+$rip%gPouqDH_Lmn#XAn=)bnV$Pftm zFJl`2nL7K}qwLd!e#`BGEch+iD z53xvV$0cUd(Fgi%SWNBfICwA)O^R6fV=LGT2RtBm5Lh`_8H3yCYWP!U)>ymd%lGO+ zq>tMAH|9P>aJO?9n=r29Gd?|44vY}a4Q)0eW@ssEF-bh06`yM(6+}G!Rzs~~R*HjK zOc_}Uoi^k57r9d1c->V*xs*ao5H-we(`(N&c69#NiA-5$>`QI4~v}H)QneUr; z=lA6dXoUrBdLKxYUOk~`{XiY~tHD8RcP_1W6@7bW4G9XrY)4W4uHVMOpNlHi7^MCZ zi-#%&>wlnImICO^=rNIKaHGt~8%(|Nz!_Z^D5=Yfd^pT{uD8tcDtbEn>AYONw&n67 z+1;|wtjxh2Bu=V`HhWkug(t&^F|V7DfUJ4Pqf%QNwu)cd!57~}sag`hT6%9n?uaC4 z5f^(Gd-fzY+Ra-Um<_V4lRVT-c|_F^mIm~r|43e+Q`6K9FCEiba7&)-392QREARf| z&g>t|KmT@f?)1;s7~CQ62a2yrxvQlMKLaJ#=jRn&K2kw&yP4T%@k+H%(J=(F-9`#O zx=6GVm7CdWG~NP=D$tJt#adHV&PU5WoWCDNKegwCHvZK{e|_pMs)~>?>|^W-wSVSf z!Vr+6WIUr!$99xu(-+sP&A*-Zaer0hNJ`UCcZJD%Syd_U7VhV^aY?tJbjxmy?jzDOv6E2?ZmB{+H9e4Arz+Y+MFb7| z!$~l^fMzxBAn$nVKnb<8f$%R?AMOWo@YA<|(B=G=%oIvlA4a^3KN4J&MRH0Zrf7J2 z9FSwsU#pAg7#JAn(DBL1dPQ+%182r2DSwB~;YWS&5hV%|`{T97YR_#MUdg zy+ZQlR3GLYf4MwcNZ6|WO}u{6`E&WSOa1BSGPj|s!uOOfRXgd9HEWnB;9J|6TG&03 z^}md7=RBhxj-n1GphmG-so~U^4BRo&tfsn=YzeK=U*65J?2A+Ubmf}Y&D2MxsR(G% zd^AShW4D*zr}l`TdaBgjDUY#j+&X;Wmn@WBRnZm9U(;<|D>Op~eUF`6ch- zpWfP+_jgk z%?-nEo?px+e*DNvF~*Sfmxrk6pReAm>^;v61qJI7Va@U1+(n`k?8U5-fMg=j#3ljZ;?d z@x6MoS7}lL!M}9GFq#_W98Wx)fAKZr1CMXh3B|(;%Bs=H9Vt&Gln57v!V$)_QRvsV z=y^)$?fJ!8xD6D(aJI!LsEMg+^iyfTZ?GJyh`1${jlYy%(^cBlV{WAvZO1Mn_Ctg>9Q%YDCF^4HTV*Gd@FqqBDfjQ!H(&mHdMcy$^KqVXM}_|x-S z1$!930+mS1qY2H0mXz;_q?@(alCNpPkhK2CuNNFN?nT9uTjWXaTK0&HX2S6@a5gVY z;jRC!P{Al}C~?r@%&Hx&d*kEP#ZyB#b^neF(nMK^{O&35jpl_{Dqg_iKYh(gMgGf@ zpPtDwHEzpPIq_$ATe~>&DxSLY1PGq`mG^$Nk=-ZB1Y}5CNqL_ zkNEMUi@PShS(EAT2Nu44Z#&f9W!gQfe#`PNGusgp^1}pwjXf+ouy1zQ(OPNsgYd`i zgUP@_LL3{gn6#_a`C4sm>pX~p_ESdIdyLnu7=T6%v6O1Qfgq>~pf_J2;bT2_uC~tz zmNxv&cd;L!WV=(q_=Q$Yuwzho*xhX6Xq-yw0jB*=mU&?0&xJiDvZAq+(dp7{E<3Nl zD;o`#F>UqHT}Myl)D1#W|6}g4W%SNfx;eWnvStaYgg>oqMVxsbj%0pI+PT@Gr8`E? zUnP?@-5lHIF8#iywuV5+L?|W1Mxvn+9X(icUN@A{uJCpLU47#cr~hQw8mx{^>i2nv zKkj$E5)xx;>~1#q+_swNBpZM1Zot_h!;r8rF-N=bWi{$ro%~Q}lT@eguv%7X*M9%e z#w)9wNV&G^-%D4OmRGxCUtm)COoHNb7$a15nh7-*auwk98o4@M$GXs39$jNiSH9EP zK3!)vx~0rYxel=8`u8#Xh{pIN@?r<WPF8ti)K_(hY$)3Yy93p zB^+MQzb!|rXqHKM=Gkvr zqgp8zyW$gV7w9!KlQ3@EOV^G!O+qX1uaOiQy9gRP0 z97rM28%X>xI@#fbO`~h3HY+~emhH=q6Nj|F2$O7`S&{txvXO+eYyd0Tr~?bh;J(+;ctgJK3l%++rNFK%tei|-_h{~HssHVAz~bzhBXms_^o zKIXY(?&&)frpCf$R~2EhQJw(@LvLAx?aN~3ac%xnB{tfTHGPa2%UzGaSFeBeKG|2& z7+E%Y`x_Oj{RCFjHy*`g%>S8}$^4yP!;$pyTP1~icH&u+laHz+Mw@pY%Bz1r<*6RE zJ2^=0t!q7!+NJrI*mnJ2!%%0m_wzF3?@DR1jj0z0VHG~k%O5YX*yeqB(zHPz&VU@d z;Jpb$e$1^DKy|IFnR}(py@BpknYRE%N6ki(dxDJ{nvqVs8*p zmI~F$T-bG1+l>*<`7`9+vJWynpc`Dg<^-F$VS z?(>zT-}IUW0lrEUI7bQ&DCEr){^;F*7C~3h!icz&n*W{5PP|Q7TGv}yUzB@7rTIKX&wGH0Ra#Wxh{-cQgOfJ2@;Qe0P;IZtat?To#qjzbu z%X$vs$D^VqL0+9)Az_Psj&X+H^}AwJl8G7D+UH&9kqbVL3l=NsU|+ zVq;Qy?VYAdePhUh2AV3-d~pR~ZKl5QIGgkM!|`Pk(B`V{zT3D!_!kr}JnjRKdsH(85 zzNf#}RnC1x?MgCzzx?;~j5#oiPrg;i<+al?JsrE^@6>qs&+~sIoONqCt@hI3A3t6M z`MoKksYv0v819=F>!rOI+T0#>tS`yA2+v%sdgG7aRw&CqXy!WG7RHGoeLCG#g_^O+BNQ3KR$umeh83;Nb zJ6Z|I&r-kAy7Xa;IQl(0ad2*`fQxH>pZk)saoENHDeQExJ=s||@52;@Wy7yCG0%_( z$U?&(q_mszZslT58USxO;9pbI?x!vUZr{NK%=wMl)u{|6u z@trvde&Gh(`+L5BGSE&*@%R}-&d<=NLJ8hKh_C!g$DR4Y>Msu@nREFeAI+^PhY#BC z1;p>{wvp?S9;BSI`Q>ghr|BdaPb*UIsddshl`FRF4g3; zxzme29hMY;_{v9?g&ChRwxlX72|~mS+lkECCIoph$a_ka?qEs8LMWHvZ6@OCkKE27 z9-~6d3mUL<+UXfPs(Os**!hL{M-sNayz4#~_Al;G)xdl>IQHjZ^5L7m#HE(w1PwgZ zqG?jfBA=>GVi(fz_}7dp%>Lv*C8rfLOAh+dPmZ>8FT0l*>1Y`q!J!8I+;Z#vCVOXJ zbg{D*+&_&QS0TAi%h<2Jtu2-sv9K4F3uPO$L$KNgWyzbE_(0ha4Bl=#c>3cVoo8fhv--=}uqv@h3bOt28923kXqtMZd zD7PFjX3XV2f60C~Xzr$;Lsyic+5myj?+nvhYGegy3wDFBH8rRMBcw$xVSFPHgqbyen`@aLraJhP=Hz%(n=r+HN5bzzMNi>%vDbdLB(ZXmjV#KAs_pi$EfBws+;DjQwPd2ybtO||@ zE@6C5*i4yi!^8Y+zb-0r;cG-YxBQ;_chi)6mSn^K`Q=>HkLFSyU;1A0ogTOO2iYcN zb-_d~B|<%A9X}dOh`r@*pNI4Q9r(%*eZfmhNb!jwupI<@89!WZECzwAr9Hy;ajMfF z3LhJ^>=^KW@tZ!M2QxmOp#RN1V$}CROB!_ay6C7#zm{+Qq6?`#R0zvb7E<)x(1v|>cCr|H8@84YbNca?4rXM7y-k?~=k zA24_0uRaeKZ1cSK;W_Aki~q;)e~td*!kQmd374OnfUf{{s&YlCqZ1T-qlKp;$e{GIIq zbAR|HIN=aJ5LT7jGr-a#p+I$(@f9I(qWNWM4lHm7a7%M>G4Ox+`(Jz>UAKc8e+El; zNUlFpkN?MFuFpq(cZ+ifM+201PNCh05SkW6qRs@X4{&O9-D8jYKAiVOzzbit1ufhQ zLRMAbG$cG5L4ZLR7z_x5+A7GWLyj=N2*H40eE7T}t7|~u!u?>{^Ix`4{Y%!Dp4ZKO zf<}Cj=aY0NlBPQ$^nW~s{?FY}e+VIvw@X>4Xeu3=L-9-`st;r!Oo+bi4?g#=%AfZ1 z$K?U9Ztq70b7)E<0s(E{I4gm{0wBWdqAo+@57zvd^jsfmdX(b8Xb(`dN4v8E=5Igi z_vDAiz5lqfU_$gwe~=iTEMthGv$>)4e>#=x@W|VnZu1ZEXdq9Ee7O1mgMd?`v6H4V z5H6nJb&c08FU<3qo&U|Wzkl8k^u{g_4D(;yMujkh=k)d)j1C+SVfGd)jBPD=@nbOn zco!BH>fa;Nr?&vK89m^Q-2nZqQyH-W1U0)-I)ojis?@Vhb>@{H38>LS zvpNRgbN54Y_Y=c+okjNqnobZ34Ii9XfhVj2UfuSS-?N`opub_#9j`8#pu6t(w5TP< zM}ITU@5jZ`UOvn511vDU+x){AgsHTUL;dS;+|olu-iUE=9w6^d-=PbfCYPS*CcplC4{08pzVJ+@**&DdIxCg(JvO9R4g?yCdtoYb2AMH zJFd{-wW*EpxlaLfv|zx3y}h zYKG}tWW98>*R<^c-qT9H2J^$U7i068>;=D`%eXY%u)1MpGVgH z;PYU{C*Jq1$eD7-tIH-{`|z_Uof$o8+Vz~-FX3KSJbtZWdZU+_K1derSv!*zw{GK~ z2OwNzWK@_;^WSOQ$e^NyVFZkz(b!buCgD_qMAxy?6`)|6K}~x3#S^@)8#OKB*0Eui z-8DYqy8FgQ-ZX1`-0k;cp^xF$(D%^Kpr56S%`WBg_Dc-Rj-DI71`pD`iN~)Ey+-s} z$!k_|k$xKe+a?={_7}O0fBtoi_)%e$0t{1WiV#5aWF81QLco*>?R&+`1SY}gJMGf3 z-a(fVI6N zQ9&3Lco@df6k!t0lNk{HG?fUUy>b;zF|^c-5!9HGWYoZb#9^U!BNB?*yx7;-&k=)1 zD{-%!k`2M z8zIKhv|<7^;3QgDPNBw}N=@oZ4a-1b@~7gz#(vJ1K6WboT~p}qo Date: Sun, 6 Nov 2022 13:53:28 +0000 Subject: [PATCH 086/117] No point setting set_last_scanned==true when deleting image. --- .../net/sourceforge/opencamera/MyApplicationInterface.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java index 10c3fcbf8..6a66afe25 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyApplicationInterface.java @@ -3700,7 +3700,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { preview.showToast(null, R.string.photo_deleted, true); if( file != null ) { // SAF doesn't broadcast when deleting them - storageUtils.broadcastFile(file, false, false, true, false, null); + storageUtils.broadcastFile(file, false, false, false, false, null); } } } @@ -3733,7 +3733,7 @@ public class MyApplicationInterface extends BasicApplicationInterface { Log.d(TAG, "successfully deleted " + image_name); if( from_user ) preview.showToast(photo_delete_toast, R.string.photo_deleted, true); - storageUtils.broadcastFile(file, false, false, true, false, null); + storageUtils.broadcastFile(file, false, false, false, false, null); } } } -- GitLab From 78814ec2c51c13fce770eb20ff944facf14dbd23 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Wed, 9 Nov 2022 22:19:57 +0000 Subject: [PATCH 087/117] Default to flash off instead of flash auto. --- _docs/history.html | 1 + .../java/net/sourceforge/opencamera/preview/Preview.java | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index ba1643a0e..baa817dad 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -79,6 +79,7 @@ UPDATED Apply a dimmed effect when reopening camera or switching modes (for Came UPDATED Improved look of on-screen level line. UPDATED On-screen pitch and compass lines now show smaller intervals as camera zooms in. UPDATED Camera2 extension night mode now adds "_Night" to filename. +UPDATED Default to flash off instead of flash auto. UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera. UPDATED Use system toasts without custom views when appropriate. UPDATED Display current value for photo stamp font size and colour in preference summary. 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 be2778d5a..596cda4cc 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -3106,10 +3106,13 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // whilst devices with flash should support flash_auto, we'll also be in this codepath for front cameras with // no flash, as instead the available options will be flash_off, flash_frontscreen_auto, flash_frontscreen_on // see testTakePhotoFrontCameraScreenFlash - if( supported_flash_values.contains("flash_auto") ) + /*if( supported_flash_values.contains("flash_auto") ) updateFlash("flash_auto", true); else - updateFlash("flash_off", true); + updateFlash("flash_off", true);*/ + // update, we now default to flash off - flash is increasingly less useful on modern cameras, + // plus reduces problems from risk of buggy flash on Camera2 API... + updateFlash("flash_off", true); } } else { -- GitLab From 7b6c13d6185761b23258a5f041c0eba6eff3c474 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 12 Nov 2022 15:19:20 +0000 Subject: [PATCH 088/117] More logging. --- .../java/net/sourceforge/opencamera/preview/VideoProfile.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java b/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java index 8de628050..e8ff19615 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java @@ -91,10 +91,12 @@ public class VideoProfile { // also match order of MediaRecorder.setProfile() just to be safe, see https://stackoverflow.com/questions/5524672/is-it-possible-to-use-camcorderprofile-without-audio-source media_recorder.setOutputFormat(this.fileFormat); media_recorder.setVideoFrameRate(this.videoFrameRate); + if( MyDebug.LOG ) + Log.d(TAG, "set frame rate: " + this.videoFrameRate); // it's probably safe to always call setCaptureRate, but to be safe (and keep compatibility with old Open Camera versions), we only do so when needed if( this.videoCaptureRate != (double)this.videoFrameRate ) { if( MyDebug.LOG ) - Log.d(TAG, "set capture rate"); + Log.d(TAG, "set capture rate: " + this.videoCaptureRate); media_recorder.setCaptureRate(this.videoCaptureRate); } media_recorder.setVideoSize(this.videoFrameWidth, this.videoFrameHeight); -- GitLab From 273fc0f9216ee6db4cb2db4f3ddd443174a614ec Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 12 Nov 2022 15:43:37 +0000 Subject: [PATCH 089/117] Add comment. --- .../opencamera/cameracontroller/CameraController.java | 4 ++++ 1 file changed, 4 insertions(+) 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 c696df0c0..f7ba7889e 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java @@ -119,6 +119,10 @@ public abstract class CameraController { return false; } + /** + * @param return_closest If true, return a match for the width/height, even if the fps doesn't + * match. + */ public static Size findSize(List sizes, Size size, double fps, boolean return_closest) { Size last_s = null; for(Size s : sizes) { -- GitLab From 723c0863aec5255013c3e65ed9e51ff3655fbd88 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 12 Nov 2022 15:59:15 +0000 Subject: [PATCH 090/117] Fix problems when video resolution doesn't support frame rate set by the CamcorderProfile. --- _docs/history.html | 2 ++ .../cameracontroller/CameraController.java | 22 ++++++++++++- .../opencamera/preview/Preview.java | 31 ++++++++++++++++--- .../preview/VideoQualityHandler.java | 6 ++-- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index baa817dad..192dfad45 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -58,6 +58,8 @@ FIXED Face detection on-screen icon shouldn't show in camera vendor extension FIXED For Camera2 API, red eye flash was incorrectly being shown even on devices that didn't support it. FIXED Not saving location exif information for Camera2 API on some devices (e.g., Pixel 6 Pro). +FIXED Crashed recording video on some devices and resolutions (e.g. Pixel 6 Pro at 1920x1440) if + those resolutions didn't support the same frame rate as other resolutions. FIXED Doesn't display error message if using volume keys to turn auto-level on or off in RAW or Panorama mode (unless device doesn't support auto-level at all). ADDED New option Settings/Photo settings/"Remove device EXIF data" to remove device metadata from 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 f7ba7889e..01d0eaa93 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController.java @@ -194,7 +194,7 @@ public abstract class CameraController { return supported_extensions != null && supported_extensions.contains(extension); } - boolean supportsFrameRate(double fps) { + public boolean supportsFrameRate(double fps) { for (int[] f : this.fps_ranges) { if (f[0] <= fps && fps <= f[1]) return true; @@ -202,6 +202,26 @@ public abstract class CameraController { return false; } + public int closestFrameRate(double fps) { + int closest_fps = -1; + int closest_dist = -1; + for (int[] f : this.fps_ranges) { + if (f[0] <= fps && fps <= f[1]) + return (int)fps; + int this_fps; + if( fps < f[0] ) + this_fps = f[0]; + else + this_fps = f[1]; + int dist = Math.abs(this_fps - (int)fps); + if( closest_dist == -1 || dist < closest_dist ) { + closest_fps = this_fps; + closest_dist = dist; + } + } + return closest_fps; + } + @Override public boolean equals(Object o) { if( !(o instanceof Size) ) 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 596cda4cc..0d2f41dd4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -3020,11 +3020,14 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu VideoProfile profile = getVideoProfile(); if( MyDebug.LOG ) Log.d(TAG, "check if we need high speed video for " + profile.videoFrameWidth + " x " + profile.videoFrameHeight + " at fps " + profile.videoCaptureRate); - CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(profile.videoFrameWidth, profile.videoFrameHeight, profile.videoCaptureRate); + CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(profile.videoFrameWidth, profile.videoFrameHeight, profile.videoFrameRate, false); - if( best_video_size == null && video_quality_handler.getSupportedVideoSizesHighSpeed() != null ) { + if( best_video_size == null && fpsIsHighSpeed("" + profile.videoFrameRate) && video_quality_handler.getSupportedVideoSizesHighSpeed() != null ) { Log.e(TAG, "can't find match for capture rate: " + profile.videoCaptureRate + " and video size: " + profile.videoFrameWidth + " x " + profile.videoFrameHeight + " at fps " + profile.videoCaptureRate); - // try falling back to one of the supported high speed resolutions + // If fpsIsHighSpeed() returns true for profile.videoFrameRate, then it means an fps is one that isn't + // supported by any standard video sizes, but it is supported by a high speed video size. If + // best_video_size==null, then we must have an incompatible size for this fps. + // So try falling back to one of the supported high speed resolutions. CameraController.Size requested_size = video_quality_handler.getMaxSupportedVideoSizeHighSpeed(); profile.videoFrameWidth = requested_size.width; profile.videoFrameHeight = requested_size.height; @@ -3442,7 +3445,25 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu //video_profile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; //video_profile.videoCodec = MediaRecorder.VideoEncoder.H264; - if( !fps_value.equals("default") ) { + if( fps_value.equals("default") ) { + if( video_profile.videoFrameWidth != 0 && video_profile.videoFrameHeight != 0 ) { + // check videoFrameRate is actually supported by requested video resolution + // we need this as sometimes the CamcorderProfile we use may store a frame rate not actually + // supported for the resolution (e.g., on Pixel 6 Pro, 1920x1080 and 3840x2160 support 60fps, + // and the CamcorderProfiles set 60fps, but the intermediate resolutions such as 1920x1440 only + // support 30fps) + CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(video_profile.videoFrameWidth, video_profile.videoFrameHeight, video_profile.videoFrameRate, true); + if( best_video_size != null && !best_video_size.supportsFrameRate(video_profile.videoFrameRate) ) { + if( MyDebug.LOG ) + Log.d(TAG, "video resolution " + video_profile.videoFrameWidth + " x " + video_profile.videoFrameHeight + " doesn't support requested fps " + video_profile.videoFrameWidth); + int closest_fps = best_video_size.closestFrameRate(video_profile.videoFrameRate); + if( MyDebug.LOG ) + Log.d(TAG, " instead choose valid fps: " + closest_fps); + video_profile.videoFrameRate = closest_fps; + } + } + } + else { try { int fps = Integer.parseInt(fps_value); if( MyDebug.LOG ) @@ -7409,7 +7430,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, " width: " + profile.videoFrameWidth); Log.d(TAG, " height: " + profile.videoFrameHeight); } - CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(profile.videoFrameWidth, profile.videoFrameHeight, fps); + CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(profile.videoFrameWidth, profile.videoFrameHeight, fps, false); if( best_video_size != null ) { if( MyDebug.LOG ) Log.d(TAG, " requested frame rate is supported"); diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/VideoQualityHandler.java b/app/src/main/java/net/sourceforge/opencamera/preview/VideoQualityHandler.java index e89f6b9cd..cf8681e18 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/VideoQualityHandler.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/VideoQualityHandler.java @@ -171,7 +171,7 @@ public class VideoQualityHandler { return CameraController.CameraFeatures.supportsFrameRate(this.video_sizes_high_speed, fps); } - CameraController.Size findVideoSizeForFrameRate(int width, int height, double fps) { + CameraController.Size findVideoSizeForFrameRate(int width, int height, double fps, boolean return_closest) { if( MyDebug.LOG ) { Log.d(TAG, "findVideoSizeForFrameRate"); Log.d(TAG, "width: " + width); @@ -179,12 +179,12 @@ public class VideoQualityHandler { Log.d(TAG, "fps: " + fps); } CameraController.Size requested_size = new CameraController.Size(width, height); - CameraController.Size best_video_size = CameraController.CameraFeatures.findSize(this.getSupportedVideoSizes(), requested_size, fps, false); + CameraController.Size best_video_size = CameraController.CameraFeatures.findSize(this.getSupportedVideoSizes(), requested_size, fps, return_closest); if( best_video_size == null && this.getSupportedVideoSizesHighSpeed() != null ) { if( MyDebug.LOG ) Log.d(TAG, "need to check high speed sizes"); // check high speed - best_video_size = CameraController.CameraFeatures.findSize(this.getSupportedVideoSizesHighSpeed(), requested_size, fps, false); + best_video_size = CameraController.CameraFeatures.findSize(this.getSupportedVideoSizesHighSpeed(), requested_size, fps, return_closest); } return best_video_size; } -- GitLab From ac729ef1a0bd1bae50c3a6a94344b79ccc3bf4c0 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Nov 2022 20:11:42 +0000 Subject: [PATCH 091/117] New testHDR62. --- .../opencamera/InstrumentedTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java index 65f9c4a51..3b3ee4508 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java @@ -2357,6 +2357,32 @@ public class InstrumentedTest { }); } + /** Tests HDR algorithm on test samples "testHDR62". + */ + @Category(HDRTests.class) + @Test + public void testHDR62() throws IOException, InterruptedException { + Log.d(TAG, "testHDR62"); + + setToDefault(); + + mActivityRule.getScenario().onActivity(activity -> { // for simplicity, run the entire test on the UI thread + // list assets + List inputs = new ArrayList<>(); + inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR62/input0.jpg") ); + inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR62/input1.jpg") ); + inputs.add( TestUtils.getBitmapFromFile(activity, TestUtils.hdr_images_path + "testHDR62/input2.jpg") ); + + TestUtils.HistogramDetails hdrHistogramDetails = TestUtils.subTestHDR(activity, inputs, "testHDR62_output.jpg", false, 100, 1000000000L/485); + + checkHistogramDetails(hdrHistogramDetails, 0, 113, 247); + + int [] exp_offsets_x = {0, 0, -3}; + int [] exp_offsets_y = {3, 0, -6}; + TestUtils.checkHDROffsets(activity, 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 algorithm. * The test images should be copied to the test device into DCIM/testOpenCamera/testdata/hdrsamples/testHDRtemp/ . -- GitLab From f0139c9e8141c0fb4b50a7087d7a930df64c7bad Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Nov 2022 20:23:45 +0000 Subject: [PATCH 092/117] Add comments. --- _docs/help.html | 2 ++ _docs/index.html | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/_docs/help.html b/_docs/help.html index 8014ac063..6677eb0df 100644 --- a/_docs/help.html +++ b/_docs/help.html @@ -80,6 +80,8 @@
  • Contacting me (bugs etc)
+ diff --git a/_docs/index.html b/_docs/index.html index 41b1abcf9..4eadf0785 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -80,6 +80,8 @@ responsive ads won't show (at least on my Nexus 6)! If any code is changed here, make sure layout is still good and ads show on laptop, Nexus 6 and Galaxy Nexus. --> + @@ -152,6 +154,8 @@ browsers -->
+ -- GitLab From 6b281df6a53ce8265bf754b636a686603f567bfc Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Nov 2022 23:16:51 +0000 Subject: [PATCH 093/117] Fix typo. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 192dfad45..bc961ce5d 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -60,7 +60,7 @@ FIXED For Camera2 API, red eye flash was incorrectly being shown even on devic FIXED Not saving location exif information for Camera2 API on some devices (e.g., Pixel 6 Pro). FIXED Crashed recording video on some devices and resolutions (e.g. Pixel 6 Pro at 1920x1440) if those resolutions didn't support the same frame rate as other resolutions. -FIXED Doesn't display error message if using volume keys to turn auto-level on or off in RAW or +FIXED Don't display error message if using volume keys to turn auto-level on or off in RAW or Panorama mode (unless device doesn't support auto-level at all). ADDED New option Settings/Photo settings/"Remove device EXIF data" to remove device metadata from JPEG photos. -- GitLab From 945103a7253cf3372718935caffbe016425a9d75 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 19 Nov 2022 23:17:57 +0000 Subject: [PATCH 094/117] Fix newline. --- _docs/history.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index bc961ce5d..bf00cec99 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -70,8 +70,8 @@ ADDED Display message to hold device steady when using X-Night photo mode. ADDED New option Settings/Photo settings/"HDR tonemapping" to choose tonemapping algorithm used for HDR photo mode. UPDATED Applied a timeout of 1 second for focusing with Camera2 API. -UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can - zoom out to ultra-wide camera. +UPDATED Made it easier to zoom to 1x zoom using seekbar on devices that can zoom out to ultra-wide + camera. UPDATED Make zoom seekbar snap to powers of two (for Camera2 API). UPDATED No longer switch to manual mode in DRO and NR photo modes, as on some devices this meant losing the benefit of manufacturer algorithms. -- GitLab From ec74bb696d95eb5a9ebb5758fc107440c5531d52 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Tue, 22 Nov 2022 18:59:26 +0000 Subject: [PATCH 095/117] Refactor code related to subTestTakePhoto() to TestUtils. --- .../net/sourceforge/opencamera/TestUtils.java | 614 ++++++++++++++++++ .../opencamera/test/MainActivityTest.java | 593 +---------------- 2 files changed, 636 insertions(+), 571 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java index 160c4a8c7..b88c8c985 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java @@ -21,10 +21,13 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.renderscript.Allocation; import android.util.Log; +import android.view.View; import androidx.annotation.RequiresApi; import androidx.exifinterface.media.ExifInterface; +import net.sourceforge.opencamera.preview.Preview; + import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -32,8 +35,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Locale; /** Helper class for testing. This method should not include any code specific to any test framework * (e.g., shouldn't be specific to ActivityInstrumentationTestCase2). @@ -765,4 +771,612 @@ public class TestUtils { fail(); } } + + public static void waitForTakePhotoChecks(MainActivity activity, long time_s) { + Preview preview = activity.getPreview(); + View switchCameraButton = activity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + View switchMultiCameraButton = activity.findViewById(net.sourceforge.opencamera.R.id.switch_multi_camera); + View switchVideoButton = activity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + //View flashButton = activity.findViewById(net.sourceforge.opencamera.R.id.flash); + //View focusButton = activity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + View exposureButton = activity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureLockButton = activity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + View audioControlButton = activity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + View popupButton = activity.findViewById(net.sourceforge.opencamera.R.id.popup); + View trashButton = activity.findViewById(net.sourceforge.opencamera.R.id.trash); + View shareButton = activity.findViewById(net.sourceforge.opencamera.R.id.share); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean is_focus_bracketing = activity.supportsFocusBracketing() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_focus_bracketing"); + boolean is_panorama = activity.supportsPanorama() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_panorama"); + + // make sure the test fails rather than hanging, if for some reason we get stuck (note that testTakePhotoManualISOExposure takes over 10s on Nexus 6) + // also see note at end of setToDefault for Nokia 8, need to sleep briefly to avoid hanging here + if( !is_focus_bracketing ) { + assertTrue(System.currentTimeMillis() - time_s < (is_panorama ? 50000 : 20000)); // need longer for panorama on Nexus 7 for testTakePhotoPanoramaMax + } + assertTrue(!preview.isTakingPhoto() || switchCameraButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || switchMultiCameraButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || switchVideoButton.getVisibility() == View.GONE); + //assertTrue(!preview.isTakingPhoto() || flashButton.getVisibility() == View.GONE); + //assertTrue(!preview.isTakingPhoto() || focusButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || exposureButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || exposureLockButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || audioControlButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || popupButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || trashButton.getVisibility() == View.GONE); + assertTrue(!preview.isTakingPhoto() || shareButton.getVisibility() == View.GONE); + } + + private static void checkFocusInitial(MainActivity activity, final String focus_value, final String focus_value_ui) { + String new_focus_value_ui = activity.getPreview().getCurrentFocusValue(); + //noinspection StringEquality + assertTrue(new_focus_value_ui == focus_value_ui || new_focus_value_ui.equals(focus_value_ui)); // also need to do == check, as strings may be null if focus not supported + assertEquals(activity.getPreview().getCameraController().getFocusValue(), focus_value); + } + + public static void checkFocusAfterTakePhoto(MainActivity activity, final String focus_value, final String focus_value_ui) { + // focus should be back to normal now: + String new_focus_value_ui = activity.getPreview().getCurrentFocusValue(); + Log.d(TAG, "focus_value_ui: " + focus_value_ui); + Log.d(TAG, "new new_focus_value_ui: " + new_focus_value_ui); + //noinspection StringEquality + assertTrue(new_focus_value_ui == focus_value_ui || new_focus_value_ui.equals(focus_value_ui)); // also need to do == check, as strings may be null if focus not supported + String new_focus_value = activity.getPreview().getCameraController().getFocusValue(); + Log.d(TAG, "focus_value: " + focus_value); + Log.d(TAG, "new focus_value: " + new_focus_value); + if( new_focus_value_ui != null && new_focus_value_ui.equals("focus_mode_continuous_picture") && focus_value.equals("focus_mode_auto") && new_focus_value.equals("focus_mode_continuous_picture") ) { + // this is fine, it just means we were temporarily in touch-to-focus mode + } + else { + assertEquals(new_focus_value, focus_value); + } + } + + public static void checkFocusAfterTakePhoto2(MainActivity activity, final boolean touch_to_focus, final boolean single_tap_photo, final boolean double_tap_photo, final boolean test_wait_capture_result, final boolean locked_focus, final boolean can_auto_focus, final boolean can_focus_area, final int saved_count) { + Preview preview = activity.getPreview(); + // in locked focus mode, taking photo should never redo an auto-focus + // if photo mode, we may do a refocus if the previous auto-focus failed, but not if it succeeded + Log.d(TAG, "2 count_cameraAutoFocus: " + preview.count_cameraAutoFocus); + if( locked_focus ) { + assertEquals(preview.count_cameraAutoFocus, (can_auto_focus ? saved_count + 1 : saved_count)); + } + if( test_wait_capture_result ) { + // if test_wait_capture_result, then we'll have waited too long, so focus settings may have changed + } + else if( touch_to_focus ) { + Log.d(TAG, "can_focus_area?: " + can_focus_area); + Log.d(TAG, "hasFocusArea?: " + preview.hasFocusArea()); + if( single_tap_photo || double_tap_photo ) { + assertFalse(preview.hasFocusArea()); + assertNull(preview.getCameraController().getFocusAreas()); + assertNull(preview.getCameraController().getMeteringAreas()); + } + else if( can_focus_area ) { + assertTrue(preview.hasFocusArea()); + assertNotNull(preview.getCameraController().getFocusAreas()); + assertEquals(1, preview.getCameraController().getFocusAreas().size()); + assertNotNull(preview.getCameraController().getMeteringAreas()); + assertEquals(1, preview.getCameraController().getMeteringAreas().size()); + } + else { + assertFalse(preview.hasFocusArea()); + assertNull(preview.getCameraController().getFocusAreas()); + + if( preview.getCameraController().supportsMetering() ) { + // we still set metering areas + assertNotNull(preview.getCameraController().getMeteringAreas()); + assertEquals(1, preview.getCameraController().getMeteringAreas().size()); + } + else { + assertNull(preview.getCameraController().getMeteringAreas()); + } + } + } + else { + assertFalse(preview.hasFocusArea()); + assertNull(preview.getCameraController().getFocusAreas()); + assertNull(preview.getCameraController().getMeteringAreas()); + } + } + + private static int getExpNNewFiles(MainActivity activity, final boolean is_raw) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean hdr_save_expo = sharedPreferences.getBoolean(PreferenceKeys.HDRSaveExpoPreferenceKey, false); + boolean is_hdr = activity.supportsHDR() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_hdr"); + boolean is_expo = activity.supportsExpoBracketing() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_expo_bracketing"); + boolean is_focus_bracketing = activity.supportsFocusBracketing() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_focus_bracketing"); + boolean is_fast_burst = activity.supportsFastBurst() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_fast_burst"); + String n_expo_images_s = sharedPreferences.getString(PreferenceKeys.ExpoBracketingNImagesPreferenceKey, "3"); + int n_expo_images = Integer.parseInt(n_expo_images_s); + String n_focus_bracketing_images_s = sharedPreferences.getString(PreferenceKeys.FocusBracketingNImagesPreferenceKey, "3"); + int n_focus_bracketing_images = Integer.parseInt(n_focus_bracketing_images_s); + String n_fast_burst_images_s = sharedPreferences.getString(PreferenceKeys.FastBurstNImagesPreferenceKey, "5"); + int n_fast_burst_images = Integer.parseInt(n_fast_burst_images_s); + + int exp_n_new_files; + if( is_hdr && hdr_save_expo ) { + exp_n_new_files = 4; + if( is_raw && !activity.getApplicationInterface().isRawOnly() ) { + exp_n_new_files += 3; + } + } + else if( is_expo ) { + exp_n_new_files = n_expo_images; + if( is_raw && !activity.getApplicationInterface().isRawOnly() ) { + exp_n_new_files *= 2; + } + } + else if( is_focus_bracketing ) { + exp_n_new_files = n_focus_bracketing_images; + if( is_raw && !activity.getApplicationInterface().isRawOnly() ) { + exp_n_new_files *= 2; + } + } + else if( is_fast_burst ) + exp_n_new_files = n_fast_burst_images; + else { + exp_n_new_files = 1; + if( is_raw && !activity.getApplicationInterface().isRawOnly() ) { + exp_n_new_files *= 2; + } + } + Log.d(TAG, "exp_n_new_files: " + exp_n_new_files); + return exp_n_new_files; + } + + private static void checkFilenames(MainActivity activity, final boolean is_raw, final String [] files, final String [] files2) { + Log.d(TAG, "checkFilenames"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean hdr_save_expo = sharedPreferences.getBoolean(PreferenceKeys.HDRSaveExpoPreferenceKey, false); + boolean is_hdr = activity.supportsHDR() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_hdr"); + boolean is_fast_burst = activity.supportsFastBurst() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_fast_burst"); + boolean is_expo = activity.supportsExpoBracketing() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_expo_bracketing"); + boolean is_focus_bracketing = activity.supportsFocusBracketing() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_focus_bracketing"); + + // check files have names as expected + String filename_jpeg = null; + String filename_dng = null; + int n_files = files == null ? 0 : files.length; + for(String file : files2) { + Log.d(TAG, "check file: " + file); + boolean is_new = true; + for(int j=0;j mediaFilesinSaveFolder(MainActivity activity, 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 = activity.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. + */ + public static String [] filesInSaveFolder(MainActivity activity) { + Log.d(TAG, "filesInSaveFolder"); + if( MainActivity.useScopedStorage() ) { + List files = new ArrayList<>(); + if( activity.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 = activity.getStorageUtils().getTreeUriSAF(); + Uri baseUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); + files.addAll( mediaFilesinSaveFolder(activity, baseUri, null, UriType.STORAGE_ACCESS_FRAMEWORK) ); + } + else { + String save_folder = activity.getStorageUtils().getImageFolderPath(); + String bucket_id = String.valueOf(save_folder.toLowerCase().hashCode()); + files.addAll( mediaFilesinSaveFolder(activity, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, bucket_id, UriType.MEDIASTORE_IMAGES) ); + files.addAll( mediaFilesinSaveFolder(activity, 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 = activity.getImageFolder(); + File [] files = folder.listFiles(); + if( files == null ) + return null; + String [] filenames = new String[files.length]; + for(int i=0;i 1 ? View.VISIBLE : View.GONE)); + assertEquals(switchMultiCameraButton.getVisibility(), (activity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); + assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); + if( !immersive_mode ) { + assertEquals(exposureButton.getVisibility(), exposureVisibility); + assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); + } + assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), View.VISIBLE); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + } + } + + public static class SubTestTakePhotoInfo { + public boolean has_thumbnail_anim; + public boolean is_hdr; + public boolean is_nr; + public boolean is_expo; + public int exposureVisibility; + public int exposureLockVisibility; + public String focus_value; + public String focus_value_ui; + public boolean can_auto_focus; + public boolean manual_can_auto_focus; + public boolean can_focus_area; + } + + public static SubTestTakePhotoInfo getSubTestTakePhotoInfo(MainActivity activity, boolean immersive_mode, boolean single_tap_photo, boolean double_tap_photo) { + assertTrue(activity.getPreview().isPreviewStarted()); + assertFalse(activity.getApplicationInterface().getImageSaver().test_queue_blocked); + + SubTestTakePhotoInfo info = new SubTestTakePhotoInfo(); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); + + info.has_thumbnail_anim = sharedPreferences.getBoolean(PreferenceKeys.ThumbnailAnimationPreferenceKey, true); + info.is_hdr = activity.supportsHDR() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_hdr"); + info.is_nr = activity.supportsNoiseReduction() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_noise_reduction"); + info.is_expo = activity.supportsExpoBracketing() && sharedPreferences.getString(PreferenceKeys.PhotoModePreferenceKey, "preference_photo_mode_std").equals("preference_photo_mode_expo_bracketing"); + + boolean has_audio_control_button = !sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none").equals("none"); + + View switchCameraButton = activity.findViewById(net.sourceforge.opencamera.R.id.switch_camera); + View switchMultiCameraButton = activity.findViewById(net.sourceforge.opencamera.R.id.switch_multi_camera); + View switchVideoButton = activity.findViewById(net.sourceforge.opencamera.R.id.switch_video); + //View flashButton = activity.findViewById(net.sourceforge.opencamera.R.id.flash); + //View focusButton = activity.findViewById(net.sourceforge.opencamera.R.id.focus_mode); + View exposureButton = activity.findViewById(net.sourceforge.opencamera.R.id.exposure); + View exposureLockButton = activity.findViewById(net.sourceforge.opencamera.R.id.exposure_lock); + View audioControlButton = activity.findViewById(net.sourceforge.opencamera.R.id.audio_control); + View popupButton = activity.findViewById(net.sourceforge.opencamera.R.id.popup); + View trashButton = activity.findViewById(net.sourceforge.opencamera.R.id.trash); + View shareButton = activity.findViewById(net.sourceforge.opencamera.R.id.share); + assertEquals(switchCameraButton.getVisibility(), (immersive_mode ? View.GONE : (activity.getPreview().getCameraControllerManager().getNumberOfCameras() > 1 ? View.VISIBLE : View.GONE))); + assertEquals(switchMultiCameraButton.getVisibility(), (immersive_mode ? View.GONE : (activity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE))); + assertEquals(switchVideoButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + info.exposureVisibility = exposureButton.getVisibility(); + info.exposureLockVisibility = exposureLockButton.getVisibility(); + assertEquals(audioControlButton.getVisibility(), ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); + assertEquals(popupButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); + assertEquals(trashButton.getVisibility(), View.GONE); + assertEquals(shareButton.getVisibility(), View.GONE); + + info.focus_value = activity.getPreview().getCameraController().getFocusValue(); + info.focus_value_ui = activity.getPreview().getCurrentFocusValue(); + info.can_auto_focus = false; + info.manual_can_auto_focus = false; + info.can_focus_area = false; + if( info.focus_value.equals("focus_mode_auto") || info.focus_value.equals("focus_mode_macro") ) { + info.can_auto_focus = true; + } + + if( info.focus_value.equals("focus_mode_auto") || info.focus_value.equals("focus_mode_macro") ) { + info.manual_can_auto_focus = true; + } + else if( info.focus_value.equals("focus_mode_continuous_picture") && !single_tap_photo && !double_tap_photo ) { + // if single_tap_photo or double_tap_photo, and continuous mode, we go straight to taking a photo rather than doing a touch to focus + info.manual_can_auto_focus = true; + } + + if( activity.getPreview().getMaxNumFocusAreas() != 0 && ( info.focus_value.equals("focus_mode_auto") || info.focus_value.equals("focus_mode_macro") || info.focus_value.equals("focus_mode_continuous_picture") || info.focus_value.equals("focus_mode_continuous_video") || info.focus_value.equals("focus_mode_manual2") ) ) { + info.can_focus_area = true; + } + Log.d(TAG, "focus_value? " + info.focus_value); + Log.d(TAG, "can_auto_focus? " + info.can_auto_focus); + Log.d(TAG, "manual_can_auto_focus? " + info.manual_can_auto_focus); + Log.d(TAG, "can_focus_area? " + info.can_focus_area); + + checkFocusInitial(activity, info.focus_value, info.focus_value_ui); + + return info; + } + + public static void touchToFocusChecks(MainActivity activity, final boolean single_tap_photo, final boolean double_tap_photo, final boolean manual_can_auto_focus, final boolean can_focus_area, final String focus_value, final String focus_value_ui, int saved_count) { + Preview preview = activity.getPreview(); + Log.d(TAG, "1 count_cameraAutoFocus: " + preview.count_cameraAutoFocus); + assertEquals((manual_can_auto_focus ? saved_count + 1 : saved_count), preview.count_cameraAutoFocus); + Log.d(TAG, "has focus area?: " + preview.hasFocusArea()); + if( single_tap_photo || double_tap_photo ) { + assertFalse(preview.hasFocusArea()); + assertNull(preview.getCameraController().getFocusAreas()); + assertNull(preview.getCameraController().getMeteringAreas()); + } + else if( can_focus_area ) { + assertTrue(preview.hasFocusArea()); + assertNotNull(preview.getCameraController().getFocusAreas()); + assertEquals(1, preview.getCameraController().getFocusAreas().size()); + assertNotNull(preview.getCameraController().getMeteringAreas()); + assertEquals(1, preview.getCameraController().getMeteringAreas().size()); + } + else { + assertFalse(preview.hasFocusArea()); + assertNull(preview.getCameraController().getFocusAreas()); + if( preview.getCameraController().supportsMetering() ) { + // we still set metering areas + assertNotNull(preview.getCameraController().getMeteringAreas()); + assertEquals(1, preview.getCameraController().getMeteringAreas().size()); + } + else { + assertNull(preview.getCameraController().getMeteringAreas()); + } + } + String new_focus_value_ui = preview.getCurrentFocusValue(); + //noinspection StringEquality + assertTrue(new_focus_value_ui == focus_value_ui || new_focus_value_ui.equals(focus_value_ui)); // also need to do == check, as strings may be null if focus not supported + if( focus_value.equals("focus_mode_continuous_picture") && !single_tap_photo && !double_tap_photo && preview.supportsFocus() && preview.getSupportedFocusValues().contains("focus_mode_auto") ) + assertEquals("focus_mode_auto", preview.getCameraController().getFocusValue()); // continuous focus mode switches to auto focus on touch (unless single_tap_photo, or auto focus not supported) + else + assertEquals(preview.getCameraController().getFocusValue(), focus_value); + } + } 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 e51d096b2..6eff7c589 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -5,10 +5,8 @@ import java.io.File; //import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -16,7 +14,6 @@ import java.util.Set; import net.sourceforge.opencamera.LocationSupplier; import net.sourceforge.opencamera.MyPreferenceFragment; -import net.sourceforge.opencamera.PanoramaProcessorException; import net.sourceforge.opencamera.TestUtils; import net.sourceforge.opencamera.cameracontroller.CameraController2; import net.sourceforge.opencamera.HDRProcessor; @@ -40,10 +37,8 @@ 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.Color; -import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.hardware.camera2.CameraMetadata; @@ -56,7 +51,6 @@ import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; -import android.provider.DocumentsContract; import android.provider.MediaStore; import android.test.ActivityInstrumentationTestCase2; import android.test.TouchUtils; @@ -3268,40 +3262,10 @@ 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 ? View.VISIBLE : View.GONE)); - assertEquals(switchMultiCameraButton.getVisibility(), (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE)); - assertEquals(switchVideoButton.getVisibility(), View.VISIBLE); - if( !immersive_mode ) { - assertEquals(exposureButton.getVisibility(), exposureVisibility); - assertEquals(exposureLockButton.getVisibility(), exposureLockVisibility); - } - assertEquals(audioControlButton.getVisibility(), (has_audio_control_button ? View.VISIBLE : View.GONE)); - assertEquals(popupButton.getVisibility(), View.VISIBLE); - assertEquals(trashButton.getVisibility(), View.GONE); - assertEquals(shareButton.getVisibility(), View.GONE); - } + return TestUtils.filesInSaveFolder(mActivity); } /* @@ -3778,24 +3316,19 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ? View.VISIBLE : View.GONE))); - assertEquals(switchMultiCameraButton.getVisibility(), (immersive_mode ? View.GONE : (mActivity.showSwitchMultiCamIcon() ? View.VISIBLE : View.GONE))); - assertEquals(switchVideoButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); - int exposureVisibility = exposureButton.getVisibility(); - int exposureLockVisibility = exposureLockButton.getVisibility(); - assertEquals(audioControlButton.getVisibility(), ((has_audio_control_button && !immersive_mode) ? View.VISIBLE : View.GONE)); - assertEquals(popupButton.getVisibility(), (immersive_mode ? View.GONE : View.VISIBLE)); - assertEquals(trashButton.getVisibility(), View.GONE); - assertEquals(shareButton.getVisibility(), View.GONE); - - String focus_value = mPreview.getCameraController().getFocusValue(); - String focus_value_ui = mPreview.getCurrentFocusValue(); - boolean can_auto_focus = false; - boolean manual_can_auto_focus = false; - boolean can_focus_area = false; - if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") ) { - can_auto_focus = true; - } - - if( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") ) { - manual_can_auto_focus = true; - } - else if( focus_value.equals("focus_mode_continuous_picture") && !single_tap_photo && !double_tap_photo ) { - // if single_tap_photo or double_tap_photo, and continuous mode, we go straight to taking a photo rather than doing a touch to focus - manual_can_auto_focus = true; - } - - if( mPreview.getMaxNumFocusAreas() != 0 && ( focus_value.equals("focus_mode_auto") || focus_value.equals("focus_mode_macro") || focus_value.equals("focus_mode_continuous_picture") || focus_value.equals("focus_mode_continuous_video") || focus_value.equals("focus_mode_manual2") ) ) { - can_focus_area = true; - } - Log.d(TAG, "focus_value? " + focus_value); - Log.d(TAG, "can_auto_focus? " + can_auto_focus); - Log.d(TAG, "manual_can_auto_focus? " + manual_can_auto_focus); - Log.d(TAG, "can_focus_area? " + can_focus_area); int saved_count = mPreview.count_cameraAutoFocus; - checkFocusInitial(focus_value, focus_value_ui); + //checkFocusInitial(focus_value, focus_value_ui); int saved_thumbnail_count = mActivity.getApplicationInterface().getDrawPreview().test_thumbnail_anim_count; Log.d(TAG, "saved_thumbnail_count: " + saved_thumbnail_count); @@ -3871,43 +3359,6 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Tue, 22 Nov 2022 19:44:06 +0000 Subject: [PATCH 096/117] Refactor code to TestUtils. --- .../net/sourceforge/opencamera/TestUtils.java | 91 +++++++++++++++++++ .../opencamera/test/MainActivityTest.java | 78 +++------------- .../opencamera/test/Nexus7Tests.java | 4 +- 3 files changed, 108 insertions(+), 65 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java index b88c8c985..d9d657f94 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java @@ -79,6 +79,10 @@ public class TestUtils { Log.d(TAG, "initTest: done"); } + public static boolean isEmulator() { + return Build.MODEL.contains("Android SDK built for x86"); + } + /** Converts a path to a Uri for com.android.providers.media.documents. */ private static Uri getDocumentUri(String filename) throws FileNotFoundException { @@ -1379,4 +1383,91 @@ public class TestUtils { assertEquals(preview.getCameraController().getFocusValue(), focus_value); } + /** Tests the Exif tags in the resultant file. If the file is null, the uri will be + * used instead to read the Exif tags. + */ + public static void testExif(MainActivity activity, String file, Uri uri, boolean expect_device_tags, boolean expect_datetime, boolean expect_gps) throws IOException { + //final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; + //final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; + InputStream inputStream = null; + ExifInterface exif; + if( file != null ) { + assertNull(uri); // should only supply one of file or uri + exif = new ExifInterface(file); + } + else { + assertNotNull(uri); + inputStream = activity.getContentResolver().openInputStream(uri); + exif = new ExifInterface(inputStream); + } + + assertNotNull(exif.getAttribute(ExifInterface.TAG_ORIENTATION)); + if( !( isEmulator() && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1 ) ) { + // older Android emulator versions don't store exif info in photos + if( expect_device_tags ) { + assertNotNull(exif.getAttribute(ExifInterface.TAG_MAKE)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_MODEL)); + } + else { + assertNull(exif.getAttribute(ExifInterface.TAG_MAKE)); + assertNull(exif.getAttribute(ExifInterface.TAG_MODEL)); + + assertNull(exif.getAttribute(ExifInterface.TAG_F_NUMBER)); + assertNull(exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)); + assertNull(exif.getAttribute(ExifInterface.TAG_FLASH)); + assertNull(exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)); + assertNull(exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION)); + assertNull(exif.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)); + assertNull(exif.getAttribute(ExifInterface.TAG_USER_COMMENT)); + assertNull(exif.getAttribute(ExifInterface.TAG_ARTIST)); + assertNull(exif.getAttribute(ExifInterface.TAG_COPYRIGHT)); + } + + if( expect_datetime ) { + assertNotNull(exif.getAttribute(ExifInterface.TAG_DATETIME)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED)); + } + else { + assertNull(exif.getAttribute(ExifInterface.TAG_DATETIME)); + assertNull(exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); + assertNull(exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED)); + assertNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME)); + assertNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL)); + assertNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED)); + assertNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME)); + assertNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); + assertNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED)); + } + + if( expect_gps ) { + assertNotNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)); + // can't read custom tags, even though we can write them?! + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION) != null); + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION_REF) != null); + } + else { + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)); + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)); + // can't read custom tags, even though we can write them?! + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION) == null); + //assertTrue(exif.getAttribute(TAG_GPS_IMG_DIRECTION_REF) == null); + } + } + + if( inputStream != null ) { + inputStream.close(); + } + } } 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 6eff7c589..39947c38d 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -3,7 +3,7 @@ package net.sourceforge.opencamera.test; import java.io.File; //import java.io.FileInputStream; //import java.io.FileNotFoundException; -import java.io.InputStream; +//import java.io.InputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -46,7 +46,6 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.TonemapCurve; 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; @@ -77,10 +76,6 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 1 ) { @@ -9070,7 +9020,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Tue, 22 Nov 2022 19:45:22 +0000 Subject: [PATCH 097/117] New tests for testing remove exif options, in new PhotoTestSuite. --- .../opencamera/InstrumentedTest.java | 262 ++++++++++++++++++ .../opencamera/PhotoTestSuite.java | 13 + 2 files changed, 275 insertions(+) create mode 100644 app/src/androidTest/java/net/sourceforge/opencamera/PhotoTestSuite.java diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java index 3b3ee4508..a7fd9bdd8 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/InstrumentedTest.java @@ -5,20 +5,26 @@ import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.endsWith; import static org.junit.Assert.*; import android.annotation.TargetApi; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.os.Build; import android.os.Looper; +import android.preference.PreferenceManager; import android.util.Log; import android.view.View; import androidx.test.core.app.ApplicationProvider; +import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; +import net.sourceforge.opencamera.ui.DrawPreview; import net.sourceforge.opencamera.ui.PopupView; import org.junit.After; @@ -33,6 +39,8 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +interface PhotoTests {} + interface HDRTests {} interface HDRNTests {} @@ -131,6 +139,24 @@ public class InstrumentedTest { } } + private void updateForSettings(MainActivity activity) { + Log.d(TAG, "updateForSettings"); + assertEquals(Looper.getMainLooper().getThread(), Thread.currentThread()); // check on UI thread + // updateForSettings has code that must run on UI thread + activity.initLocation(); // initLocation now called via MainActivity.setWindowFlagsForCamera() rather than updateForSettings() + activity.getApplicationInterface().getDrawPreview().updateSettings(); + activity.updateForSettings(true); + + waitUntilCameraOpened(); // may need to wait if camera is reopened, e.g., when changing scene mode - see testSceneMode() + // but we also need to wait for the delay if instead we've stopped and restarted the preview, the latter now only happens after dim_effect_time_c + try { + Thread.sleep(DrawPreview.dim_effect_time_c+50); // wait for updateForSettings + } + catch(InterruptedException e) { + e.printStackTrace(); + } + } + /** Used to click when we have View instead of an Id. It should only be called from onActivity() * (so that we can be sure we're already on the UI thread). */ @@ -6068,4 +6094,240 @@ public class InstrumentedTest { TestUtils.subTestPanorama(activity, inputs, output_name, gyro_name, panorama_pics_per_screen, camera_angle_x, camera_angle_y, 1.0f); }); } + + private void waitForTakePhoto() { + Log.d(TAG, "wait until finished taking photo"); + long time_s = System.currentTimeMillis(); + while(true) { + boolean waiting = getActivityValue(activity -> (activity.getPreview().isTakingPhoto() || !activity.getApplicationInterface().canTakeNewPhoto())); + if( !waiting ) { + break; + } + mActivityRule.getScenario().onActivity(activity -> { + TestUtils.waitForTakePhotoChecks(activity, time_s); + }); + } + + Log.d(TAG, "done taking photo"); + } + + private void subTestTouchToFocus(final boolean wait_after_focus, final boolean single_tap_photo, final boolean double_tap_photo, final boolean manual_can_auto_focus, final boolean can_focus_area, final String focus_value, final String focus_value_ui) throws InterruptedException { + // touch to auto-focus with focus area (will also exit immersive mode) + // autofocus shouldn't be immediately, but after a delay + // and Galaxy S10e needs a longer delay for some reason, for the subsequent touch of the preview view to register + Thread.sleep(2000); + int saved_count = getActivityValue(activity -> activity.getPreview().count_cameraAutoFocus); + Log.d(TAG, "saved count_cameraAutoFocus: " + saved_count); + Log.d(TAG, "### about to click preview for autofocus"); + + onView(anyOf(ViewMatchers.withClassName(endsWith("MySurfaceView")), ViewMatchers.withClassName(endsWith("MyTextureView")))).perform(click()); + + Log.d(TAG, "### done click preview for autofocus"); + + mActivityRule.getScenario().onActivity(activity -> { + TestUtils.touchToFocusChecks(activity, single_tap_photo, double_tap_photo, manual_can_auto_focus, can_focus_area, focus_value, focus_value_ui, saved_count); + }); + + if( double_tap_photo ) { + Thread.sleep(100); + Log.d(TAG, "about to click preview again for double tap"); + //onView(withId(preview_view_id)).perform(ViewActions.doubleClick()); + mActivityRule.getScenario().onActivity(activity -> { + //onView(anyOf(ViewMatchers.withClassName(endsWith("MySurfaceView")), ViewMatchers.withClassName(endsWith("MyTextureView")))).perform(click()); + activity.getPreview().onDoubleTap(); // calling tapView twice doesn't seem to work consistently, so we call this directly! + }); + } + if( wait_after_focus && !single_tap_photo && !double_tap_photo) { + // don't wait after single or double tap photo taking, as the photo taking operation is already started + Log.d(TAG, "wait after focus..."); + Thread.sleep(3000); + } + } + + private void subTestTakePhoto(boolean locked_focus, boolean immersive_mode, boolean touch_to_focus, boolean wait_after_focus, boolean single_tap_photo, boolean double_tap_photo, boolean is_raw, boolean test_wait_capture_result) throws InterruptedException { + Thread.sleep(500); + + TestUtils.SubTestTakePhotoInfo info = getActivityValue(activity -> TestUtils.getSubTestTakePhotoInfo(activity, immersive_mode, single_tap_photo, double_tap_photo)); + + int saved_count_cameraTakePicture = getActivityValue(activity -> activity.getPreview().count_cameraTakePicture); + + // count initial files in folder + String [] files = getActivityValue(activity -> TestUtils.filesInSaveFolder(activity)); + int n_files = files == null ? 0 : files.length; + Log.d(TAG, "n_files at start: " + n_files); + + int saved_count = getActivityValue(activity -> activity.getPreview().count_cameraAutoFocus); + + int saved_thumbnail_count = getActivityValue(activity -> activity.getApplicationInterface().getDrawPreview().test_thumbnail_anim_count); + Log.d(TAG, "saved_thumbnail_count: " + saved_thumbnail_count); + + if( touch_to_focus ) { + subTestTouchToFocus(wait_after_focus, single_tap_photo, double_tap_photo, info.manual_can_auto_focus, info.can_focus_area, info.focus_value, info.focus_value_ui); + } + Log.d(TAG, "saved count_cameraAutoFocus: " + saved_count); + + if( !single_tap_photo && !double_tap_photo ) { + mActivityRule.getScenario().onActivity(activity -> { + View takePhotoButton = activity.findViewById(net.sourceforge.opencamera.R.id.take_photo); + assertFalse( activity.hasThumbnailAnimation() ); + Log.d(TAG, "about to click take photo"); + clickView(takePhotoButton); + Log.d(TAG, "done clicking take photo"); + }); + } + + waitForTakePhoto(); + + int new_count_cameraTakePicture = getActivityValue(activity -> activity.getPreview().count_cameraTakePicture); + Log.d(TAG, "take picture count: " + new_count_cameraTakePicture); + assertEquals(new_count_cameraTakePicture, saved_count_cameraTakePicture + 1); + + /*if( test_wait_capture_result ) { + // if test_wait_capture_result, then we'll have waited too long for thumbnail animation + } + else if( info.is_focus_bracketing ) { + // thumbnail animation may have already occurred (e.g., see testTakePhotoFocusBracketingHeavy() + } + else*/ if( info.has_thumbnail_anim ) { + long time_s = System.currentTimeMillis(); + for(;;) { + //boolean waiting = getActivityValue(activity -> !activity.hasThumbnailAnimation()); + boolean waiting = getActivityValue(activity -> (activity.getApplicationInterface().getDrawPreview().test_thumbnail_anim_count <= saved_thumbnail_count)); + if( !waiting ) { + break; + } + Log.d(TAG, "waiting for thumbnail animation"); + Thread.sleep(10); + int allowed_time_ms = 10000; + if( info.is_hdr || info.is_nr || info.is_expo ) { + // some devices need longer time (especially Nexus 6) + allowed_time_ms = 16000; + } + assertTrue( System.currentTimeMillis() - time_s < allowed_time_ms ); + } + } + else { + boolean has_thumbnail_animation = getActivityValue(activity -> activity.hasThumbnailAnimation()); + assertFalse( has_thumbnail_animation ); + int new_thumbnail_count = getActivityValue(activity -> activity.getApplicationInterface().getDrawPreview().test_thumbnail_anim_count); + assertEquals(saved_thumbnail_count, new_thumbnail_count); + } + + mActivityRule.getScenario().onActivity(activity -> { + activity.waitUntilImageQueueEmpty(); + + TestUtils.checkFocusAfterTakePhoto(activity, info.focus_value, info.focus_value_ui); + + try { + TestUtils.checkFilesAfterTakePhoto(activity, is_raw, test_wait_capture_result, files); + } + catch(InterruptedException e) { + e.printStackTrace(); + } + + TestUtils.checkFocusAfterTakePhoto2(activity, touch_to_focus, single_tap_photo, double_tap_photo, test_wait_capture_result, locked_focus, info.can_auto_focus, info.can_focus_area, saved_count); + + TestUtils.postTakePhotoChecks(activity, immersive_mode, info.exposureVisibility, info.exposureLockVisibility); + + assertFalse(activity.getApplicationInterface().getImageSaver().test_queue_blocked); + assertTrue( activity.getPreview().getCameraController() == null || activity.getPreview().getCameraController().count_camera_parameters_exception == 0 ); + }); + + } + + /*@Category(PhotoTests.class) + @Test + public void testTakePhoto() throws InterruptedException { + Log.d(TAG, "testTakePhoto"); + setToDefault(); + subTestTakePhoto(false, false, true, true, false, false, false, false); + }*/ + + /** Tests option to remove device exif info. + */ + @Category(PhotoTests.class) + @Test + public void testTakePhotoRemoveExifOn() throws InterruptedException { + Log.d(TAG, "testTakePhotoRemoveExifOn"); + setToDefault(); + + mActivityRule.getScenario().onActivity(activity -> { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(activity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.RemoveDeviceExifPreferenceKey, "preference_remove_device_exif_on"); + editor.apply(); + updateForSettings(activity); + }); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + mActivityRule.getScenario().onActivity(activity -> { + try { + TestUtils.testExif(activity, activity.test_last_saved_image, activity.test_last_saved_imageuri, false, false, false); + } + catch(IOException e) { + e.printStackTrace(); + fail(); + } + }); + } + + /** Tests option to remove device exif info, but with auto-level to test codepath where we + * resave the bitmap. + */ + @Category(PhotoTests.class) + @Test + public void testTakePhotoRemoveExifOn2() throws InterruptedException { + Log.d(TAG, "testTakePhotoRemoveExifOn2"); + setToDefault(); + + mActivityRule.getScenario().onActivity(activity -> { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(activity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.RemoveDeviceExifPreferenceKey, "preference_remove_device_exif_on"); + editor.putBoolean(PreferenceKeys.AutoStabilisePreferenceKey, true); + editor.apply(); + updateForSettings(activity); + }); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + mActivityRule.getScenario().onActivity(activity -> { + try { + TestUtils.testExif(activity, activity.test_last_saved_image, activity.test_last_saved_imageuri, false, false, false); + } + catch(IOException e) { + e.printStackTrace(); + fail(); + } + }); + } + /** Tests option to remove device exif info, but keeping datetime tags. + */ + @Category(PhotoTests.class) + @Test + public void testTakePhotoRemoveExifKeepDatetime() throws InterruptedException { + Log.d(TAG, "testTakePhotoRemoveExifKeepDatetime"); + setToDefault(); + + mActivityRule.getScenario().onActivity(activity -> { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(activity); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PreferenceKeys.RemoveDeviceExifPreferenceKey, "preference_remove_device_exif_keep_datetime"); + editor.apply(); + updateForSettings(activity); + }); + + subTestTakePhoto(false, false, true, true, false, false, false, false); + + mActivityRule.getScenario().onActivity(activity -> { + try { + TestUtils.testExif(activity, activity.test_last_saved_image, activity.test_last_saved_imageuri, false, true, false); + } + catch(IOException e) { + e.printStackTrace(); + fail(); + } + }); + } } diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/PhotoTestSuite.java b/app/src/androidTest/java/net/sourceforge/opencamera/PhotoTestSuite.java new file mode 100644 index 000000000..05a311294 --- /dev/null +++ b/app/src/androidTest/java/net/sourceforge/opencamera/PhotoTestSuite.java @@ -0,0 +1,13 @@ +package net.sourceforge.opencamera; + +import org.junit.experimental.categories.Categories; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** Tests related to taking photos; note that tests to do with photo mode that don't take photos are still part of MainTests. + */ + +@RunWith(Categories.class) +@Categories.IncludeCategory(PhotoTests.class) +@Suite.SuiteClasses({InstrumentedTest.class}) +public class PhotoTestSuite {} -- GitLab From a997990d90d933e494e9da372ab27262bf9327a7 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 26 Nov 2022 22:48:39 +0000 Subject: [PATCH 098/117] Default to Camera2 API for some devices. --- _docs/history.html | 2 + .../sourceforge/opencamera/MainActivity.java | 58 ++++++++++++++++--- .../CameraControllerManager2.java | 4 +- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/_docs/history.html b/_docs/history.html index bf00cec99..8ba676d08 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -81,6 +81,8 @@ UPDATED Apply a dimmed effect when reopening camera or switching modes (for Came UPDATED Improved look of on-screen level line. UPDATED On-screen pitch and compass lines now show smaller intervals as camera zooms in. UPDATED Camera2 extension night mode now adds "_Night" to filename. +UPDATED Default to Camera2 API for some devices (will only take affect for new installs or if + resetting settings). UPDATED Default to flash off instead of flash auto. UPDATED DRO, HDR, NR modes no longer activate showing full on-screen info toast when opening camera. UPDATED Use system toasts without custom views when appropriate. diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 1f904269b..41cf053d2 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -19,7 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.List; -//import java.util.Locale; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -308,8 +308,19 @@ public class MainActivity extends AppCompatActivity { //speechControl = new SpeechControl(this); // determine whether we support Camera2 API + // must be done before setDeviceDefaults() initCamera2Support(); + // set some per-device defaults + // must be done before creating the Preview (as setDeviceDefaults() may set Camera2 API) + boolean has_done_first_time = sharedPreferences.contains(PreferenceKeys.FirstTimePreferenceKey); + if( MyDebug.LOG ) + Log.d(TAG, "has_done_first_time: " + has_done_first_time); + if( !has_done_first_time ) { + // must be done after initCamera2Support() + setDeviceDefaults(); + } + // set up window flags for normal operation setWindowFlagsForCamera(); if( MyDebug.LOG ) @@ -523,13 +534,7 @@ public class MainActivity extends AppCompatActivity { if( MyDebug.LOG ) Log.d(TAG, "onCreate: time after setting system ui visibility listener: " + (System.currentTimeMillis() - debug_time)); - // show "about" dialog for first time use; also set some per-device defaults - boolean has_done_first_time = sharedPreferences.contains(PreferenceKeys.FirstTimePreferenceKey); - if( MyDebug.LOG ) - Log.d(TAG, "has_done_first_time: " + has_done_first_time); - if( !has_done_first_time ) { - setDeviceDefaults(); - } + // show "about" dialog for first time use if( !has_done_first_time ) { if( !is_test ) { AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); @@ -776,6 +781,41 @@ public class MainActivity extends AppCompatActivity { editor.putBoolean(PreferenceKeys.getCamera2FastBurstPreferenceKey(), false); editor.apply(); }*/ + if( supports_camera2 && !is_test ) { + // n.b., when testing, we explicitly decide whether to run with Camera2 API or not + CameraControllerManager2 manager2 = new CameraControllerManager2(this); + int n_cameras = manager2.getNumberOfCameras(); + boolean supports_camera2_full = false; // whether at least one camera has FULL support for Camera2 + for(int i=0;i= Build.VERSION_CODES.S ) + default_to_camera2 = true; + else if( is_nokia && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ) + default_to_camera2 = true; + else if( is_samsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ) + default_to_camera2 = true; + + if( default_to_camera2 ) { + if( MyDebug.LOG ) + Log.d(TAG, "default to camera2 API"); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PreferenceKeys.CameraAPIPreferenceKey, "preference_camera_api_camera2"); + editor.apply(); + } + } + } } /** Switches modes if required, if called from a relevant intent/tile. @@ -898,7 +938,7 @@ public class MainActivity extends AppCompatActivity { supports_camera2 = false; } for(int i=0;i Date: Sun, 27 Nov 2022 13:51:29 +0000 Subject: [PATCH 099/117] Update translation. --- app/src/main/res/values-pl/arrays.xml | 6 +- app/src/main/res/values-pl/strings.xml | 152 ++++++++++++++----------- 2 files changed, 87 insertions(+), 71 deletions(-) diff --git a/app/src/main/res/values-pl/arrays.xml b/app/src/main/res/values-pl/arrays.xml index c41c7de95..c79cc139a 100644 --- a/app/src/main/res/values-pl/arrays.xml +++ b/app/src/main/res/values-pl/arrays.xml @@ -69,7 +69,7 @@ preference_preview_size_wysiwyg - Wyłączone + Wyłączono Pojedyncze dotknięcie Podwójne dotknięcie @@ -169,7 +169,7 @@ preference_stamp_timeformat_none - Domyślny + Domyślnie Stopnie/minuty/sekundy Brak @@ -261,7 +261,7 @@ -0.5 do +0.5 -1 do +1 - -2 do +2 + -2 do +2 -3 do +3 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c432d78df..f25256b6a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -4,24 +4,22 @@ Ustawienia Ustawienia wyskakujących okienek OK (Ta wiadomość nie pokaże się ponownie) - Włączono - - Wyłączono - + + Włączono + Wyłączono + Wybierz miejsce zapisywania: Wyczyść historię folderów Wyczyścić historię folderów? Wybierz inny folder Zmieniono miejsce zapisywania na: Zatrzymano nagrywanie filmu - pozostało powtórzeń - + pozostało powtórzeń Nie udało się połączyć z aparatem Błąd, nagranie może być uszkodzone - Niewspierane na Twoim urządzeniu - - Nieznany błąd, wideo wstrzymane - Błąd serwera, wideo wstrzymane + Niewspierane na twoim urządzeniu + Nieznany błąd, wstrzymano wideo + Błąd serwera, wstrzymano wideo Nagranie osiągnęło maksymalną długość Nagranie osiągnęło maksymalny rozmiar pliku Korekta ekspozycji @@ -32,8 +30,8 @@ Tylny aparat Zdjęcie Film - Ekspozycja zablokowana - Ekspozycja odblokowana + Zablokowano ekspozycję + Odblokowano ekspozycję Anulowano samowyzwalacz Anulowano serię Włączono timer @@ -42,7 +40,7 @@ Rozpoczęto nagrywanie wideo Niestety Nie udało się zapisać zdjęcia - Nie udało się zapisać zdjęcia w RAW + Nie udało się zapisać zdjęcia jako RAW Nie udało się wypoziomować Nie udało się uruchomić podglądu aparatu Robienie zdjęcia @@ -53,7 +51,7 @@ Ekran zablokowany\nprzesuń palcem, aby odblokować Odblokowano Autopoziomowanie niedostępne\nna tym urządzeniu - Dźwięk wyłączony + Wyłączono dźwięk Maksymalny czas trwania Efekt koloru Tryb scenerii @@ -67,26 +65,26 @@ ISO Przybliżenie Wolne - [ZABLOKOWANE: + [ZABLOKOWANO: Przesuń palcem, aby odblokować] Nie udało się dodać stempla m Efekty aparatu Autopoziomowanie - Zdjęcia będą obracane i wyrównywane do poziomu (wolniejszy zapis zdjęcia, może się nie udać na urządzeniach z małą ilością pamięci) + Zdjęcia będą obracane i wyrównywane do poziomu (wolniejszy zapis zdjęcia, może nie działać na urządzeniach z małą ilością pamięci) Nałóż efekt kolorystyczny Nakłada efekt kolorystyczny na zdjęcie Zastosuj tryb scenerii - Optymalizuj zdjęcie dla innych scenerii + Optymalizuje zdjęcie dla innych scenerii Ustaw balans bieli - Wybierz to ustawienie, aby dopasować balans bieli + Dopasowuje balans bieli Ustaw czułość ISO Wyższa wartość oznacza większą czułość na światło (może nie działać na niektórych urządzeniach) Ustaw korektę ekspozycji Ustaw 0 dla domyślnej korekty ekspozycji Zablokuj orientację zdjęcia/filmu - Jeśli ustawione, orientacja urządzenia będzie ignorowana, priorytet będzie miała wybrana opcja\n%s + Jeśli włączono, orientacja urządzenia będzie ignorowana, priorytet będzie miała wybrana opcja\n%s Wykrywanie twarzy Używa wykrywania twarzy zamiast obszarów ostrości @@ -100,15 +98,15 @@ Interwał Serii Więcej ustawień aparatu… Dotknij, aby zrobić zdjęcie - Zrób zdjęcie poprzez pojedyncze lub podwójne dotknięcie + Robi zdjęcie poprzez pojedyncze lub podwójne dotknięcie Pauza po zrobieniu zdjęcia - Wstrzymaj podgląd po zrobieniu zdjęcia, z możliwością udostępnienia lub usunięcia + Wstrzymuje podgląd po zrobieniu zdjęcia, z możliwością udostępnienia lub usunięcia Dźwięk migawki Odtwarza dźwięk podczas robienia zdjęcia Klawisze głośności Aktywacja za pomocą dźwięków - Wrażliwość sterowania dźwiękiem - Poziom wrażliwości na głośne dźwięki aktywujące aparat + Czułość sterowania dźwiękiem + Poziom czułości na głośne dźwięki aktywujące aparat Lokalizacja zapisu Folder, do którego będą zapisywane pliki Używa Storage Access Framework do zapisu plików zdjęć i filmów @@ -117,8 +115,8 @@ Przedrostek nazw filmów Przedrostek używany w nazwach plików filmów Format czasu w nazwie pliku - Pokaż aparat gdy zablokowany - Jeśli włączone, aplikacja Open Camera będzie widoczna ponad ekranem blokady (dostęp do opcji, galerii itp. wymaga odblokowania) + Pokaż aparat, gdy zablokowany + Jeśli włączono, aplikacja Open Camera będzie widoczna ponad ekranem blokady (dostęp do opcji, galerii itp. wymaga odblokowania) Automatyczna ostrość podczas uruchamiania Autofokus będzie uruchamiany podczas włączania aplikacji. Jeśli występuje problem z włączaniem lampy błyskowej podczas uruchamiania, wyłącz tę opcję Blokuj ekranu podczas nagrywania filmu @@ -163,7 +161,7 @@ Ramka Wyświetla ramkę wokół ekranu po wykonaniu zdjęcia Utrzymuj ekran włączony - Ekran nie wyłączy się jeśli główny ekran aplikacji Open Camera będzie aktywny + Ekran nie wyłączy się jeśli aplikacja Open Camera będzie aktywna Wymuś maksymalną jasność Wymusza maksymalną jasność ekranu pomijając ustawienie systemu operacyjnego urządzenia Ustawienia zdjęć i filmów @@ -196,11 +194,11 @@ Określa styl tekstu stempla\n%s Użyj wątku w tle Używa wątku aplikacji działającego w tle do zapisu zdjęć (dla szybszego działania) - Rozdzielczość Wideo + Rozdzielczość wideo Wymuś nagrywanie filmów 4K UHD (działa tylko na niektórych urządzeniach) Włącza rozdzielczość 3840x2160 przy nagrywaniu filmów z tylnego aparatu. Ta opcja to hack, który pozwala urządzeniom z obsługą 4K na nagrywanie filmów w 4K, mimo zablokowania takiej możliwości dla niefirmowych aplikacji nagrywających. Nie ma gwarancji, że to zadziała, proszę przetestować. Cyfrowa stabilizacja obrazu - Stabilizacja redukuje wstrząsy wywołane ruchem urządzenia. Działa podczas podglądu oraz nagrywania filmów. Ta funkcja może być zbędna, jeżeli urządzenie zostało wyposażone w optyczną stabilizację obrazu (OIS) + Redukuje wstrząsy wywołane ruchem urządzenia. Działa podczas podglądu oraz nagrywania filmów. Ta funkcja może być zbędna, jeżeli urządzenie zostało wyposażone w optyczną stabilizację obrazu (OIS) Bitrate filmów (przybliżony) Ustawia przybliżony bitrate filmów (wyższa wartość oznacza lepszą jakość, ale zabiera więcej pamięci; ustawienie nieobsługiwanego bitrate może spowodować błąd zapisu)\n%s Częstotliwość klatek (przybliżona) @@ -214,18 +212,18 @@ Restart nagrywania według rozmiaru Nagrywanie wznowi się automatycznie, gdy rozmiar pliku osiągnie ustawioną maksymalną wartość Nagrywanie z dźwiękiem - Nagrywanie filmu wraz z dźwiękiem + Nagrywa film wraz z dźwiękiem Źródło dźwięku Wybór mikrofonu, z którego będzie nagrywany dźwięk\n%s Źródło dźwięku Wybór między dźwiękiem mono a stereo (dostępne tylko na niektórych urządzeniach) Błysk podczas nagrywania - Jeśli włączone, lampa błyskowa będzie się włączać i wyłączać (przydatne w przypadku gdy chcemy z większej odległości stwierdzić czy kamera wciąż nagrywa) + Jeśli włączono, lampa błyskowa będzie się włączać i wyłączać (przydatne w przypadku gdy chcemy z większej odległości stwierdzić czy kamera wciąż nagrywa) Różne Pomoc on-line Otwiera stronę aplikacji Open Camera w przeglądarce internetowej Przekaż darowiznę - Jeśli spodobała Ci się ta aplikacja rozważ przekazanie darowizny, która wesprze rozwój aplikacji. Możesz to zrobić poprzez zakup specjalnej aplikacji - dotknij tutaj aby otworzyć stronę. Dzięki! + Jeśli spodobała ci się ta aplikacja rozważ przekazanie darowizny, która wesprze rozwój aplikacji. Możesz to zrobić poprzez zakup specjalnej aplikacji - dotknij tutaj, aby otworzyć stronę. Dzięki! Camera2 API Włącza interfejs Camera2 API, który oferuje wiele dodatkowych funkcji, ale może działać niepoprawnie na niektórych urządzeniach (powoduje restart aplikacji) O aplikacji @@ -285,17 +283,17 @@ Stempel zdjęcia Zapisz wszystkie zdjęcia dla HDR W trybie HDR zapisuje wszystkie zdjęcia, które posłużyły do powstania obrazu HDR. Może to spowodować wolniejszy zapis, zwłaszcza przy włączonej opcji stemplowania lub autopoziomowania. - POZWOLENIA NIE SĄ DOSTĘPNE + POZWOLENIA NIEDOSTĘPNE Wymagane pozwolenie Wymagane pozwolenie na dostęp do aparatu Opcje debugowania Opcje debugowania Użyj alternatywnej lampy błyskowej - Włącz jeśli w Twoim urządzeniu lampa błyskowa przejawia dziwne zachowanie z Camera2 API + Włącz jeśli w twoim urządzeniu lampa błyskowa przejawia dziwne zachowanie z Camera2 API Bracketing ekspozycji Ilość obrazów w trybie bracketingu\n%s Stopniowanie Bracketingu - Jaki ustawić zakres wartości dla jaśniejszej/ciemniejszej ekspozycji\n%s + Ustawia zakres wartości dla jaśniejszej/ciemniejszej ekspozycji\n%s Brak wystarczającej ilości pamięci, aby nagrać film Filmowanie zatrzymane\nKrytyczny poziom baterii Sprawdzanie niskiego poziomu baterii @@ -305,7 +303,7 @@ Czas migawki Poważny błąd aparatu Szybka seria (HDR/bracketing) - Pozwala na szybsze wykonywanie zdjęć HDR lub bracketing. Wyłącz, jeśli na Twoim urządzeniu powoduje błędy. + Pozwala na szybsze wykonywanie zdjęć HDR lub bracketing. Wyłącz, jeśli na twoim urządzeniu powoduje błędy. Aparat Nagraj film Autoportret @@ -320,11 +318,11 @@ Reset kalibracji poziomu DRO Linie pochylenia - Wyświetla poziome linie przechylenia + Wyświetla poziome linie podziałki Linie kompasu Wyświetla linie kierunkowe kompasu Napisy - Tworzy napisy (w formacie SRT), w których zostaną zapisane: data, czas i dane GPS (jeśli włączone)\n%s + Tworzy napisy (w formacie SRT), w których zostaną zapisane: data, czas i dane GPS (jeśli włączono)\n%s Bez limitu 3 sekundy 5 sekund @@ -351,8 +349,8 @@ 1 godzina Zrób zdjęcie Pokazuje przycisk do robienia zdjęć i nagrywania filmów. Można go wyłączyć na urządzeniach, które posiadają fizyczny przycisk aparatu. - Film wstrzymany - Film wznowiony + Wstrzymano film + Wznowiono film Wstrzymaj filmowanie Wznów filmowanie Przechwytywanie… @@ -377,11 +375,11 @@ Brak Lampa wyłączona - Lampa auto + Lampa automatyczna Lampa włączona Latarka - Czerwone oczy - Błysk ekranowy auto + Redukcja czerwonych oczu + Błysk ekranowy automatyczny Błysk ekranowy włączony Domyślnie @@ -444,7 +442,7 @@ 1h 2h - Domyślny urządzenia + Domyślnie dla urządzenia 100MB 200MB 300MB @@ -473,7 +471,7 @@ 0 (domyślnie) -1 -2 - -3 (nika czułość) + -3 (niska czułość) Interfejs dla leworęcznych Interfejs dla praworęcznych @@ -524,8 +522,8 @@ Szybkość Normalnie - Zwolnione tempo włączone - Zwolnione tempo wyłączone + Włączono zwolnione tempo + Wyłączono zwolnione tempo Ekranowa latarka @@ -537,7 +535,7 @@ Wyłącz Efekt "ducha" - Nakłada obraz, aby pomóc w wyrównaiu.\n%s + Nakłada obraz, aby pomóc w wyrównaniu.\n%s Wyłączony Ostatnie zrobione zdjęcie Wybrane zdjęcie @@ -546,9 +544,9 @@ Nie można otworzyć tego obrazu Zezwalaj na zdjęcia podczas nagrywania filmu - Umożliwia robienie zdjęć podczas nagrywania filmów. Wyłącz tę opcję, jeśli występują problemy z nagrywaniem wideo przy włączonym interfejsie Camera2 API na Twoim urządzeniu. + Umożliwia robienie zdjęć podczas nagrywania filmów. Wyłącz tę opcję, jeśli występują problemy z nagrywaniem wideo przy włączonym interfejsie Camera2 API na twoim urządzeniu. - Profile Wideo + Profile wideo Ustaw standardowy lub płaski profil obrazu dla trybu wideo\n%s Domyślny Log (Drobny) @@ -579,7 +577,7 @@ Dodaj nieskończoną odległość Dokładność kompasu - Kompas Twojego urządzenia wymaga kalibracji, aby poprawić jego dokładność. Można to zrobić, poruszając urządzenie ruchem ósemkowym.\n\nAktualna dokładność: + Kompas twojego urządzenia wymaga kalibracji, aby poprawić jego dokładność. Można to zrobić, poruszając urządzenie ruchem ósemkowym.\n\nAktualna dokładność: Niedokładny Niski @@ -628,10 +626,10 @@ Sprytna Zawsze - Format Wideo + Format wideo Format plików wideo, audio oraz kodeki\n%s - Domyślny + Domyślnie MPEG4 H264 MPEG4 HEVC 3GPP @@ -640,7 +638,7 @@ Menedżer ustawień Zapisz ustawienia - Zapisuje wszystkie ustawienia aplikacji Open Camera w pliku + Zapisuje wszystkie ustawienia aplikacji Open Camera do pliku Nazwa zapisanych ustawień Przywróć ustawienia @@ -666,8 +664,8 @@ Wyświetla ikonę na ekranie do włączania lub wyłączania wykrywania twarzy Włącz wykrywanie twarzy Wyłącz wykrywanie twarzy - Wykrywanie twarzy włączone - Wykrywanie twarzy wyłączone + Włączono wykrywanie twarzy + Wyłączono wykrywanie twarzy Automatyczne poziomowanie Wyświetla ikonę na ekranie do włączania lub wyłączania automatycznego poziomowania. Gdy automatyczne poziomowanie jest włączone, zdjęcia będą obracane, tak aby były automatycznie wyrównane z horyzontem. @@ -678,8 +676,8 @@ Wyświetla ikonę na ekranie do włączania lub wyłączania stempla fotograficznego Włącz stemplowanie zdjęć Wyłącz stemplowanie zdjęć - Stemplowanie zdjęć włączone - Stemplowanie zdjęć wyłączone + Włączono stemplowanie zdjęć + Wyłączono stemplowanie zdjęć Własny stempel tekstowy Pokazuje ikonę na ekranie do określenia własnego tekstu, który ma zostać umieszczony na zdjęciu @@ -688,8 +686,8 @@ Wyświetla na ekranie ikonę do blokowania lub odblokowywania automatycznego balansu bieli Zablokuj balans bieli Odblokuj balans bieli - Balans bieli zablokowany - Balans bieli odblokowany + Zablokowano balans bieli + Odblokowano balans bieli Blokada automatycznej ekspozycji Wyświetla ikonę na ekranie do blokowania lub odblokowywania ekspozycji @@ -791,7 +789,7 @@ Automatyczne kadrowanie panoramy W trybie panoramy: Usuwa faliste granice\n%s - Aby zrobić zdjęcie panoramiczne, trzymając urządzenie pionowo dotknij przycisk "Zrób zdjęcie". Następnie skieruj urządzenie w lewo lub w prawo, tak aby białe kółko (na środku ekranu) pokryło się z niebieską kropką. Po przechwyceniu każdego nowego zdjęcia kieruj urządzenie, aby zakryć po kolei każdą nową niebieską kropkę.\n\nDotknij ikonkę "fajki", aby zapisać panoramę, lub ikonę "krzyżyka", aby anulować.\n\nPamiętaj, że przetwarzanie i zapisywanie zdjęć panoramicznych może zająć trochę czasu. + Aby zrobić zdjęcie panoramiczne, trzymając urządzenie pionowo dotknij przycisku "Zrób zdjęcie". Następnie skieruj urządzenie w lewo lub w prawo tak, aby białe kółko (na środku ekranu) pokryło się z niebieską kropką. Po przechwyceniu każdego nowego zdjęcia kieruj urządzenie, aby zakryć po kolei każdą nową niebieską kropkę.\n\nDotknij ikonkę "fajki", aby zapisać panoramę, lub ikonę "krzyżyka", aby anulować.\n\nPamiętaj, że przetwarzanie i zapisywanie zdjęć panoramicznych może zająć trochę czasu. Oryginalne zdjęcia w trybie panoramy Zapisuje oryginalne obrazy w trybie panoramy. Pamiętaj, że spowolni to zapisywanie zdjęć panoramicznych. Daje także możliwość zapisania pliku XML, co może być przydatne przy zgłaszaniu problemów z panoramą.\n%s @@ -817,7 +815,7 @@ Jeśli to możliwe, uzyskaj adres z lokalizacji GPS (do stempla, lub napisów do filmów). - Jeśli ta opcja jest włączona, Twoje urządzenie będzie przesyłało dane o lokalizacji przez Internet do strony trzeciej + Jeśli ta opcja jest włączona, twoje urządzenie będzie przesyłało dane o lokalizacji przez Internet do strony trzeciej w celu konwersji współrzędnych GPS na adres. Zobacz stronę https://developer.android.com/reference/android/location/Geocoder . Ta funkcja nie gwarantuje dostępności ani dokładności. Nie ma gwarancji, że wyniki będą znaczące lub poprawne. \n%s @@ -866,13 +864,13 @@ Biały Aparat zewnętrzny - Bardzo szeroki + Ultra szeroki Przełącz na aparat zewnętrzny Przełącz aparat Przełączaj się między wieloma aparatami Ikona wielu aparatów - Jeśli ta opcja jest włączona, użyj oddzielnych przycisków do przełączania między przednimi/tylnymi aparatami oraz do przełączania między wieloma przednimi/tylnymi kamerami. Jeśli jest wyłączona, ikona Przełącz aparat będzie przełączać się między wszystkimi aparatami. + Jeśli ta opcja jest włączona, użyj oddzielnych przycisków do przełączania między przednimi/tylnymi aparatami oraz do przełączania między wieloma przednimi/tylnymi kamerami. Jeśli jest wyłączona, ikona "Przełącz aparat" będzie przełączać między wszystkimi aparatami. Pokaż ID aparatu Wyświetla aktualny numer ID aparatu na ekranie @@ -888,11 +886,11 @@ Zwolnione tempo Przechowuj odchylenie, nachylenie i przechylenie - Zapisz odchylenie, nachylenie i przechylenie urządzenia w komentarzu użytkownika Exif do zdjęcia (tylko format JPEG) + Zapisuje odchylenie, nachylenie i przechylenie urządzenia w komentarzu użytkownika Exif do zdjęcia (tylko format JPEG) Pasek do zmiany bieżącej wartości preferencji - Automatyczny + Automatycznie Pochmurno Światło dzienne Świetlówka @@ -913,8 +911,8 @@ Impreza Portret Śnieg - Sporty - Stateczne zdjęcie + Sport + Statyczne zdjęcie Zachód słońca Teatr @@ -942,7 +940,25 @@ Duża prędkość - Wyłączone + Wyłączono Głośny dźwięk Komenda głosowa: \"cheese\" + + Włącz pozorną korektę HDR/ekspozycji + Spróbuj włączyć tę opcję, jeśli w twoim urządzeniu występują problemy z wykonywaniem zdjęć w trybach HDR lub Ekspo (jeśli zdjęcia ekspo mają taką samą wartość ekspozycji). Ma to znaczenie tylko wtedy, gdy włączona jest także poniższa opcja "Szybka seria HDR/bracketing. + + Uprawnienia do skanowania/połączenia Bluetooth są wymagane do wykrywania i łączenia się z urządzeniami zdalnego sterowania Bluetooth LE. + Nieznane urządzenie (niedostępne zezwolenie na połączenie Bluetooth) + + X-Auto + Rozszerzenie: Auto + X-HDR + Rozszerzenie: HDR + X-Noc + Rozszerzenie: Night + X-Bokeh + Rozszerzenie: Bokeh + X-Bty + Rozszerzenie: Piękno / Retusz twarzy + -- GitLab From e7b7fdba46baac724ab0819b4b8fd3fcff3c9ea5 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 3 Dec 2022 20:27:44 +0000 Subject: [PATCH 100/117] Comment out excessive logging. --- .../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 f91131735..7496d45d7 100644 --- a/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java +++ b/app/src/main/java/net/sourceforge/opencamera/HDRProcessor.java @@ -2606,11 +2606,11 @@ public class HDRProcessor { // * Lastly, we only modify a given histogram value if pixels of that brightness // would be made darker by the CLAHE algorithm. We can do this by looking at // the cumulative histogram (as computed before modifying any values). - if( MyDebug.LOG ) { + /*if( MyDebug.LOG ) { for(int x=0;x<256;x++) { Log.d(TAG, "pre-brighten histogram[" + x + "] = " + histogram[x]); } - } + }*/ temp_c_histogram[0] = histogram[0]; for(int x=1;x<256;x++) { -- GitLab From 055049af0b9be97782fdaa04396e0e2a5b8a05ad Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 10 Dec 2022 14:41:11 +0000 Subject: [PATCH 101/117] Update. --- opencamera_source.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opencamera_source.txt b/opencamera_source.txt index a3a7cc236..65e866c97 100644 --- a/opencamera_source.txt +++ b/opencamera_source.txt @@ -21,8 +21,8 @@ Application: You might prefer to use Open Camera in its entirety, as an Activity If you are using the Open Camera source code, there are some obvious things to change: * Package name -* Homepage (see MainActivity.getOnlineHelpUrl()) -* Email contact (see "preference_privacy_policy_text" in strings.xml) +* Online help / homepage (see MainActivity.getOnlineHelpUrl()) +* Privacy policy information, including name and email contact (see "preference_privacy_policy_text" in strings.xml) Android inspection warnings/errors ================================== @@ -57,4 +57,4 @@ Homepage: http://opencamera.org.uk/ Google Play download: https://play.google.com/store/apps/details?id=net.sourceforge.opencamera -Mark Harman 16 August 2021 +Mark Harman 6 December 2022 -- GitLab From c522f90fee01025fdd3aefdb2e25fb10a012c973 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 10 Dec 2022 15:17:08 +0000 Subject: [PATCH 102/117] Fix warnings. --- app/src/main/AndroidManifest.xml | 2 +- .../sourceforge/opencamera/HDRProcessor.java | 24 +++++------- .../opencamera/PanoramaProcessor.java | 38 +++++++++---------- .../opencamera/preview/Preview.java | 9 +++-- .../net/sourceforge/opencamera/ui/MainUI.java | 2 +- app/src/main/res/xml/widget_info.xml | 3 +- .../main/res/xml/widget_info_take_photo.xml | 3 +- 7 files changed, 38 insertions(+), 43 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 179d65b75..0e418663b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ /> + tools:targetApi="s" /> bitmaps, long time_s) { List histogramInfos = new ArrayList<>(); @@ -2514,23 +2514,21 @@ public class PanoramaProcessor { if( MyDebug.LOG ) Log.d(TAG, " adjust exposure for image: " + i); - /* // use local average - float local_mean_brightness = median_brightnesses.get(i); - int count = 1; - if( i > 0 ) { - local_mean_brightness += median_brightnesses.get(i-1); - count++; - } - if( i < bitmaps.size()-1 ) { - local_mean_brightness += median_brightnesses.get(i+1); - count++; - } - local_mean_brightness /= count; - if( MyDebug.LOG ) - Log.d(TAG, " local_mean_brightness: " + local_mean_brightness); - final int brightness_target = (int)(local_mean_brightness + 0.1f); - */ + //float local_mean_brightness = median_brightnesses.get(i); + //int count = 1; + //if( i > 0 ) { + // local_mean_brightness += median_brightnesses.get(i-1); + // count++; + //} + //if( i < bitmaps.size()-1 ) { + // local_mean_brightness += median_brightnesses.get(i+1); + // count++; + //} + //local_mean_brightness /= count; + //if( MyDebug.LOG ) + // Log.d(TAG, " local_mean_brightness: " + local_mean_brightness); + //final int brightness_target = (int)(local_mean_brightness + 0.1f); min_preferred_scale = Math.min(min_preferred_scale, brightness_target/(float)histogramInfo.median_brightness); max_preferred_scale = Math.max(max_preferred_scale, brightness_target/(float)histogramInfo.median_brightness); @@ -2554,7 +2552,7 @@ public class PanoramaProcessor { Log.d(TAG, "max_preferred_scale: " + max_preferred_scale); Log.d(TAG, "### time after adjusting brightnesses: " + (System.currentTimeMillis() - time_s)); } - } + }*/ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void computePanoramaTransforms(List cumulative_transforms, List align_x_values, List dst_offset_x_values, @@ -2564,7 +2562,7 @@ public class PanoramaProcessor { Matrix cumulative_transform = new Matrix(); int align_x = 0, align_y = 0; int dst_offset_x = 0; - List align_y_values = new ArrayList<>(); + //List align_y_values = new ArrayList<>(); final boolean use_auto_align = true; //final boolean use_auto_align = false; @@ -2712,7 +2710,7 @@ public class PanoramaProcessor { } align_x_values.add(align_x); - align_y_values.add(align_y); + //align_y_values.add(align_y); dst_offset_x_values.add(dst_offset_x); cumulative_transforms.add(new Matrix(cumulative_transform)); 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 0d2f41dd4..1a4675e05 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -81,7 +81,7 @@ import android.util.Log; import android.util.Pair; import android.view.Display; import android.view.GestureDetector; -import android.view.Gravity; +//import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.OrientationEventListener; @@ -5379,8 +5379,9 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu private void onVideoInfo(int what, int extra) { if( MyDebug.LOG ) Log.d(TAG, "onVideoInfo: " + what + " extra: " + extra); - boolean allow_seamless_restart = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - if( allow_seamless_restart && what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING && video_restart_on_max_filesize ) { + // n.b., we shouldn't refactor "Build.VERSION.SDK_INT >= Build.VERSION_CODES.O" to a single variable, as it means we'll then get the Android + // warnings of "Call requires API level 26" + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING && video_restart_on_max_filesize ) { if( MyDebug.LOG ) Log.d(TAG, "seamless restart due to max filesize approaching - try setNextOutputFile"); if( video_recorder == null ) { @@ -5452,7 +5453,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // no need to explicitly stop if createVideoFile() or setNextOutputFile() fails - just let video reach max filesize // normally } - else if( allow_seamless_restart && what == MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED && video_restart_on_max_filesize ) { + else if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && what == MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED && video_restart_on_max_filesize ) { if( MyDebug.LOG ) Log.d(TAG, "seamless restart with setNextOutputFile has now occurred"); if( nextVideoFileInfo == null ) { 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 d2d76addf..50a98fbcd 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java +++ b/app/src/main/java/net/sourceforge/opencamera/ui/MainUI.java @@ -966,7 +966,7 @@ public class MainUI { 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); + setMarginsForSystemUI(layoutParams, diff+left, -diff+top, diff+right, -diff+bottom); } else { setMarginsForSystemUI(layoutParams, left, top, right, bottom); diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml index a5f10adf3..84ada1f5e 100644 --- a/app/src/main/res/xml/widget_info.xml +++ b/app/src/main/res/xml/widget_info.xml @@ -1,6 +1,7 @@ + tools:targetApi="jelly_bean_mr1"> diff --git a/app/src/main/res/xml/widget_info_take_photo.xml b/app/src/main/res/xml/widget_info_take_photo.xml index 0f2494336..6a14ec4a4 100644 --- a/app/src/main/res/xml/widget_info_take_photo.xml +++ b/app/src/main/res/xml/widget_info_take_photo.xml @@ -1,8 +1,9 @@ + tools:targetApi="jelly_bean_mr1"> -- GitLab From dd7228de0bca4c599d69047ff804d79bf443343d Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 11 Dec 2022 18:03:41 +0000 Subject: [PATCH 103/117] Improvement to HDR algorithm for dark scenes. --- _docs/history.html | 1 + app/src/main/rs/process_hdr.rs | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 8ba676d08..664d89c7f 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -80,6 +80,7 @@ UPDATED Volume key down now supports pause/resume video on Android 7+ when recor UPDATED Apply a dimmed effect when reopening camera or switching modes (for Camera2 API). UPDATED Improved look of on-screen level line. UPDATED On-screen pitch and compass lines now show smaller intervals as camera zooms in. +UPDATED Improvement to HDR algorithm for dark scenes. UPDATED Camera2 extension night mode now adds "_Night" to filename. UPDATED Default to Camera2 API for some devices (will only take affect for new installs or if resetting settings). diff --git a/app/src/main/rs/process_hdr.rs b/app/src/main/rs/process_hdr.rs index e72011113..ae3696c43 100644 --- a/app/src/main/rs/process_hdr.rs +++ b/app/src/main/rs/process_hdr.rs @@ -267,7 +267,25 @@ uchar4 __attribute__((kernel)) hdr(uchar4 in, uint32_t x, uint32_t y) { float avg = (rgb.r+rgb.g+rgb.b) / 3.0f; float diff = fabs( avg - 127.5f ); float weight = 1.0f; - if( diff > safe_range_c ) { + if( avg <= 127.5f ) { + // We now intentionally have the weights be non-symmetric, and have the weight fall to 0 + // faster for dark pixels than bright pixels. This fixes ghosting problems of testHDR62, + // where we have very dark regions where we get ghosting between the middle and bright + // images, and the image is too dark for the deghosting algorithm below to resolve this. + // We're better off using smaller weight, so that more of the pixel comes from the + // bright image. + // This also gives improved lighting/colour in: testHDR1, testHDR2, testHDR11, + // testHDR12, testHDR21, testHDR52. + const float range_low_c = 32.0f; + const float range_high_c = 48.0f; + if( avg <= range_low_c ) { + weight = 0.0f; + } + else if( avg <= range_high_c ) { + weight = (avg - range_low_c) / (range_high_c - range_low_c); + } + } + else if( diff > safe_range_c ) { // scaling chosen so that 0 and 255 map to a non-zero weight of 0.01 weight = 1.0f - 0.99f * (diff - safe_range_c) / (127.5f - safe_range_c); } -- GitLab From 20b1ba8daad126de81159889f010f492b69d5852 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 11 Dec 2022 18:10:52 +0000 Subject: [PATCH 104/117] Bump to v1.51. --- app/src/main/AndroidManifest.xml | 4 ++-- .../sourceforge/opencamera/MainActivity.java | 2 +- app/src/main/res/values/strings.xml | 17 ++++++++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e418663b..c3d6b0dab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 41cf053d2..3b30f22c5 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -581,7 +581,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 = 84; // 1.50 + int whats_new_version = 86; // 1.51 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 7d008f1b2..932d1e097 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1069,14 +1069,17 @@ [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.50:\n + \n\nv1.51:\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
  • +
  • New option Settings/Photo settings/\"Remove device EXIF data\" to remove device metadata from JPEG photos.\n
  • +
  • New option Settings/Photo settings/\"HDR tonemapping\" to choose tonemapping algorithm used for HDR photo mode.\n
  • +
  • Fixed slow focusing for Camera2 API on some devices.\n
  • +
  • Fixed incorrect thumbnail orientation on some devices.\n
  • +
  • Fixed focus bracketing on some devices.\n
  • +
  • Fixed geotagging not working on some devices.\n
  • +
  • Improvement to HDR algorithm for dark scenes.\n
  • +
  • Default to Camera2 API for some devices (will only take affect for new installs or if resetting settings).\n
  • +
  • Various other UI improvements and bug fixes.\n
-- GitLab From df3e0f36e695ec3790e1b97ee4b15be52dc97820 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 11 Dec 2022 20:46:46 +0000 Subject: [PATCH 105/117] Fix logging order. --- .../java/net/sourceforge/opencamera/preview/VideoProfile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java b/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java index e8ff19615..6fae2d60a 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/VideoProfile.java @@ -90,9 +90,9 @@ public class VideoProfile { // n.b., order may be important - output format should be first, at least // also match order of MediaRecorder.setProfile() just to be safe, see https://stackoverflow.com/questions/5524672/is-it-possible-to-use-camcorderprofile-without-audio-source media_recorder.setOutputFormat(this.fileFormat); - media_recorder.setVideoFrameRate(this.videoFrameRate); if( MyDebug.LOG ) Log.d(TAG, "set frame rate: " + this.videoFrameRate); + media_recorder.setVideoFrameRate(this.videoFrameRate); // it's probably safe to always call setCaptureRate, but to be safe (and keep compatibility with old Open Camera versions), we only do so when needed if( this.videoCaptureRate != (double)this.videoFrameRate ) { if( MyDebug.LOG ) -- GitLab From 41ae7188b8be03ee129d9043c31fc64208577ef3 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 11 Dec 2022 21:07:29 +0000 Subject: [PATCH 106/117] Fix video recording on old API. --- .../sourceforge/opencamera/preview/Preview.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 1a4675e05..ead7077d9 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -3446,20 +3446,24 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu //video_profile.videoCodec = MediaRecorder.VideoEncoder.H264; if( fps_value.equals("default") ) { - if( video_profile.videoFrameWidth != 0 && video_profile.videoFrameHeight != 0 ) { - // check videoFrameRate is actually supported by requested video resolution - // we need this as sometimes the CamcorderProfile we use may store a frame rate not actually + if( supports_video_high_speed && video_profile.videoFrameWidth != 0 && video_profile.videoFrameHeight != 0 ) { + // Check videoFrameRate is actually supported by requested video resolution. + // We need this as sometimes the CamcorderProfile we use may store a frame rate not actually // supported for the resolution (e.g., on Pixel 6 Pro, 1920x1080 and 3840x2160 support 60fps, // and the CamcorderProfiles set 60fps, but the intermediate resolutions such as 1920x1440 only - // support 30fps) + // support 30fps). + // Limited to supports_video_high_speed - at the least, we don't want this code for old camera API where + // supported frame rates aren't available. CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(video_profile.videoFrameWidth, video_profile.videoFrameHeight, video_profile.videoFrameRate, true); if( best_video_size != null && !best_video_size.supportsFrameRate(video_profile.videoFrameRate) ) { if( MyDebug.LOG ) - Log.d(TAG, "video resolution " + video_profile.videoFrameWidth + " x " + video_profile.videoFrameHeight + " doesn't support requested fps " + video_profile.videoFrameWidth); + Log.d(TAG, "video resolution " + video_profile.videoFrameWidth + " x " + video_profile.videoFrameHeight + " doesn't support requested fps " + video_profile.videoFrameRate); int closest_fps = best_video_size.closestFrameRate(video_profile.videoFrameRate); if( MyDebug.LOG ) Log.d(TAG, " instead choose valid fps: " + closest_fps); - video_profile.videoFrameRate = closest_fps; + if( closest_fps != -1 ) { // just in case? + video_profile.videoFrameRate = closest_fps; + } } } } -- GitLab From fd6f485d0895bbe353d4b5f156d043f70bb430cb Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sun, 11 Dec 2022 21:21:07 +0000 Subject: [PATCH 107/117] Relax test. --- .../androidTest/java/net/sourceforge/opencamera/TestUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java index d9d657f94..099a38ddf 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java @@ -1138,7 +1138,7 @@ public class TestUtils { Date date = new Date(); String suffix = ""; - int max_time_s = 2; + int max_time_s = 3; if( is_dro ) { suffix = "_DRO"; } -- GitLab From fa7404e3bb7946aedd4598bf62a064163da2b59e Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Thu, 15 Dec 2022 21:38:24 +0000 Subject: [PATCH 108/117] Fix for Galaxy Nexus. --- .../net/sourceforge/opencamera/TestUtils.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java index 099a38ddf..eb7a39340 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java @@ -1427,12 +1427,15 @@ public class TestUtils { assertNotNull(exif.getAttribute(ExifInterface.TAG_DATETIME)); assertNotNull(exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); assertNotNull(exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED)); - assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME)); - assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL)); - assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED)); - assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME)); - assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); - assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED)); + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) { + // not available on Galaxy Nexus Android 4.3 at least + assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); + assertNotNull(exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED)); + } } else { assertNull(exif.getAttribute(ExifInterface.TAG_DATETIME)); -- GitLab From c53f2e01c40c2b90ac689550beda1ab050fc45d4 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Thu, 15 Dec 2022 22:00:56 +0000 Subject: [PATCH 109/117] Fix for when min and max zoom are both 1.0; add unit test for this. --- .../opencamera/cameracontroller/CameraController2.java | 2 +- app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java | 2 ++ 2 files changed, 3 insertions(+), 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 a4ebf2abe..8492bbe76 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -2515,7 +2515,7 @@ public class CameraController2 extends CameraController { zoom *= scale_factor_c; } int max_zoom_ratio = (int)(max_zoom*100); - if( zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) != max_zoom_ratio ) { + if( zoom_ratios_above_one.size() == 0 || zoom_ratios_above_one.get(zoom_ratios_above_one.size()-1) != max_zoom_ratio ) { zoom_ratios_above_one.add(max_zoom_ratio); } int n_steps_above_one = zoom_ratios_above_one.size(); 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 b33b27af0..a38d9a17a 100644 --- a/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java +++ b/app/src/test/java/net/sourceforge/opencamera/test/UnitTest.java @@ -1049,6 +1049,8 @@ public class UnitTest { public void testCameraController2ZoomRatios() { Log.d(TAG, "testCameraController2ZoomRatios"); + checkCameraController2ZoomRatios(1.0f, 1.0f); + checkCameraController2ZoomRatios(1.0f, 2.0f); checkCameraController2ZoomRatios(1.0f, 4.0f); checkCameraController2ZoomRatios(1.0f, 8.0f); -- GitLab From d360da9029c458026a698f540f55a3d33e93bf75 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Thu, 15 Dec 2022 23:50:22 +0000 Subject: [PATCH 110/117] Checks should be on videoCaptureRate not videoFrameRate. --- .../net/sourceforge/opencamera/preview/Preview.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 ead7077d9..0c664059c 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -3020,7 +3020,8 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu VideoProfile profile = getVideoProfile(); if( MyDebug.LOG ) Log.d(TAG, "check if we need high speed video for " + profile.videoFrameWidth + " x " + profile.videoFrameHeight + " at fps " + profile.videoCaptureRate); - CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(profile.videoFrameWidth, profile.videoFrameHeight, profile.videoFrameRate, false); + CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(profile.videoFrameWidth, profile.videoFrameHeight, profile.videoCaptureRate, false); + // n.b., we should pass videoCaptureRate and not videoFrameRate (as for slow motion, it's videoCaptureRate that will be high, not videoFrameRate) if( best_video_size == null && fpsIsHighSpeed("" + profile.videoFrameRate) && video_quality_handler.getSupportedVideoSizesHighSpeed() != null ) { Log.e(TAG, "can't find match for capture rate: " + profile.videoCaptureRate + " and video size: " + profile.videoFrameWidth + " x " + profile.videoFrameHeight + " at fps " + profile.videoCaptureRate); @@ -3454,8 +3455,10 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu // support 30fps). // Limited to supports_video_high_speed - at the least, we don't want this code for old camera API where // supported frame rates aren't available. - CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(video_profile.videoFrameWidth, video_profile.videoFrameHeight, video_profile.videoFrameRate, true); - if( best_video_size != null && !best_video_size.supportsFrameRate(video_profile.videoFrameRate) ) { + // N.B., we should pass videoCaptureRate and not videoFrameRate (as for slow motion, it's videoCaptureRate + // that will be high, not videoFrameRate). + CameraController.Size best_video_size = video_quality_handler.findVideoSizeForFrameRate(video_profile.videoFrameWidth, video_profile.videoFrameHeight, video_profile.videoCaptureRate, true); + if( best_video_size != null && !best_video_size.supportsFrameRate(video_profile.videoCaptureRate) ) { if( MyDebug.LOG ) Log.d(TAG, "video resolution " + video_profile.videoFrameWidth + " x " + video_profile.videoFrameHeight + " doesn't support requested fps " + video_profile.videoFrameRate); int closest_fps = best_video_size.closestFrameRate(video_profile.videoFrameRate); @@ -3463,6 +3466,7 @@ public class Preview implements SurfaceHolder.Callback, TextureView.SurfaceTextu Log.d(TAG, " instead choose valid fps: " + closest_fps); if( closest_fps != -1 ) { // just in case? video_profile.videoFrameRate = closest_fps; + video_profile.videoCaptureRate = closest_fps; } } } -- GitLab From f747db00d6aee39f5e03cfac0f64fc3682ebc589 Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 17 Dec 2022 12:50:40 +0000 Subject: [PATCH 111/117] Defaulting to camera2 should instead require all devices have at least LIMITED support. --- .../net/sourceforge/opencamera/MainActivity.java | 14 +++++++------- .../cameracontroller/CameraControllerManager2.java | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 3b30f22c5..73e20b7df 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -785,16 +785,16 @@ public class MainActivity extends AppCompatActivity { // n.b., when testing, we explicitly decide whether to run with Camera2 API or not CameraControllerManager2 manager2 = new CameraControllerManager2(this); int n_cameras = manager2.getNumberOfCameras(); - boolean supports_camera2_full = false; // whether at least one camera has FULL support for Camera2 - for(int i=0;i Date: Sat, 17 Dec 2022 19:02:56 +0000 Subject: [PATCH 112/117] Make test more tolerant. --- .../androidTest/java/net/sourceforge/opencamera/TestUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java index eb7a39340..92c980f52 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/TestUtils.java @@ -1149,6 +1149,8 @@ public class TestUtils { suffix = "_NR"; if( activity.getApplicationInterface().getNRModePref() == MyApplicationInterface.NRModePref.NRMODE_LOW_LIGHT ) max_time_s += 6; // takes longer to save low light photo + else + max_time_s += 5; } else if( is_expo ) { suffix = "_" + (n_expo_images-1); -- GitLab From bc2f201b8cf6ed00b4f708f6d7a2968c9e7d841b Mon Sep 17 00:00:00 2001 From: Mark Harman Date: Sat, 17 Dec 2022 19:03:25 +0000 Subject: [PATCH 113/117] Avoid crash if camera controller is null. --- .../java/net/sourceforge/opencamera/test/MainActivityTest.java | 3 ++- 1 file changed, 2 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 39947c38d..e78bc0753 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -5840,6 +5840,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2 Date: Tue, 20 Dec 2022 22:21:01 +0000 Subject: [PATCH 114/117] Update for release. --- _docs/history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/history.html b/_docs/history.html index 664d89c7f..d51f72b8c 100644 --- a/_docs/history.html +++ b/_docs/history.html @@ -47,7 +47,7 @@

< Main Page.

-Version 1.51 (Work in progress)
+Version 1.51 (2022/12/21)
 
 FIXED   Gallery thumbnail had incorrect orientation on some Android 10+ devices.
 FIXED   Focus bracketing images came out underexposed on some devices since
-- 
GitLab


From 0b552d1c029b6812f126153c01b226b7ed8c6e0c Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Mon, 2 Jan 2023 17:37:32 +0000
Subject: [PATCH 115/117] Quick exit from setCameraExtension() if no change.

---
 .../opencamera/cameracontroller/CameraController2.java      | 6 ++++++
 1 file changed, 6 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 8492bbe76..1c65ba29b 100644
--- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
+++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java
@@ -4272,6 +4272,12 @@ public class CameraController2 extends CameraController {
                 Log.e(TAG, "no camera");
             return;
         }
+        if( sessionType == (enabled ? SessionType.SESSIONTYPE_EXTENSION : SessionType.SESSIONTYPE_NORMAL) && this.camera_extension == (enabled ? extension : 0) ) {
+            // quick exit
+            if( MyDebug.LOG )
+                Log.d(TAG, "    no change");
+            return;
+        }
         if( hasCaptureSession() ) {
             // can only call this when captureSession not created - as it affects how we create the imageReader
             if( MyDebug.LOG )
-- 
GitLab


From 394a576a7a414d9bf7c09163ee1f0d4f2d983507 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Mon, 2 Jan 2023 17:37:47 +0000
Subject: [PATCH 116/117] Quick exit if null camera controller.

---
 .../main/java/net/sourceforge/opencamera/MainActivity.java   | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
index 73e20b7df..2942b034f 100644
--- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
+++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
@@ -4965,6 +4965,11 @@ public class MainActivity extends AppCompatActivity {
             Log.d(TAG, "cameraSetup");
             debug_time = System.currentTimeMillis();
         }
+        if( preview.getCameraController() == null ) {
+            if( MyDebug.LOG )
+                Log.d(TAG, "camera controller is null");
+            return;
+        }
 
         boolean old_want_no_limits = want_no_limits;
         this.want_no_limits = false;
-- 
GitLab


From 31a38fc8062f46c69e6599f12ee65517e8ee6520 Mon Sep 17 00:00:00 2001
From: Mark Harman 
Date: Mon, 2 Jan 2023 17:38:24 +0000
Subject: [PATCH 117/117] Bump to version 1.51.1.

---
 _docs/history.html               | 4 ++++
 app/src/main/AndroidManifest.xml | 4 ++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/_docs/history.html b/_docs/history.html
index d51f72b8c..0a9bde2b3 100644
--- a/_docs/history.html
+++ b/_docs/history.html
@@ -47,6 +47,10 @@
 

< Main Page.

+Version 1.51.1 (2023/01/02)
+
+FIXED   Fix crashes for Camera2 API.
+
 Version 1.51 (2022/12/21)
 
 FIXED   Gallery thumbnail had incorrect orientation on some Android 10+ devices.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3d6b0dab..a9c8e3dd2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,8 +2,8 @@
 
     
-- 
GitLab