package net.sourceforge.opencamera;

import net.sourceforge.opencamera.cameracontroller.CameraController;
import net.sourceforge.opencamera.cameracontroller.CameraControllerManager;
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;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import android.Manifest;
import android.app.Fragment;
import android.content.pm.PackageInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.speech.tts.TextToSpeech;

import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.exifinterface.media.ExifInterface;

import android.text.Html;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spanned;
import android.util.Log;
import android.util.Size;
import android.util.SizeF;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

/** The main Activity for Open Camera.
 */
public class MainActivity extends AppCompatActivity implements PreferenceFragment.OnPreferenceStartFragmentCallback {
    private static final String TAG = "MainActivity";

    private static int activity_count = 0;

    private boolean app_is_paused = true;

    private SensorManager mSensorManager;
    private Sensor mSensorAccelerometer;

    // components: always non-null (after onCreate())
    private BluetoothRemoteControl bluetoothRemoteControl;
    private PermissionHandler permissionHandler;
    private SettingsManager settingsManager;
    private MainUI mainUI;
    private ManualSeekbars manualSeekbars;
    private MyApplicationInterface applicationInterface;
    private TextFormatter textFormatter;
    private SoundPoolManager soundPoolManager;
    private MagneticSensor magneticSensor;
    //private SpeechControl speechControl;

    private Preview preview;
    private OrientationEventListener orientationEventListener;
    private View.OnLayoutChangeListener layoutChangeListener;
    private int large_heap_memory;
    private boolean supports_auto_stabilise;
    private boolean supports_force_video_4k;
    private boolean supports_camera2;
    private SaveLocationHistory save_location_history; // save location for non-SAF
    private SaveLocationHistory save_location_history_saf; // save location for SAF (only initialised when SAF is used)
    private boolean saf_dialog_from_preferences; // if a SAF dialog is opened, this records whether we opened it from the Preferences
    private boolean camera_in_background; // whether the camera is covered by a fragment/dialog (such as settings or folder picker)
    private GestureDetector gestureDetector;
    private boolean screen_is_locked; // whether screen is "locked" - this is Open Camera's own lock to guard against accidental presses, not the standard Android lock
    private final Map<Integer, Bitmap> preloaded_bitmap_resources = new Hashtable<>();
    private ValueAnimator gallery_save_anim;
    private boolean last_continuous_fast_burst; // whether the last photo operation was a continuous_fast_burst
    private Future<?> update_gallery_future;

    private TextToSpeech textToSpeech;
    private boolean textToSpeechSuccess;

    private AudioListener audio_listener; // may be null - created when needed

    //private boolean ui_placement_right = true;

    //private final boolean edge_to_edge_mode = false; // whether running always in edge-to-edge mode
    //private final boolean edge_to_edge_mode = true; // whether running always in edge-to-edge mode
    private final boolean edge_to_edge_mode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM; // whether running always in edge-to-edge mode
    private boolean want_no_limits; // whether we want to run with FLAG_LAYOUT_NO_LIMITS
    private boolean set_window_insets_listener; // whether we've enabled a setOnApplyWindowInsetsListener()
    private int navigation_gap; // gap for navigation bar along bottom (portrait) or right (landscape)
    private int navigation_gap_landscape; // gap for navigation bar along left (portrait) or bottom (landscape); only set for edge_to_edge_mode==true
    private int navigation_gap_reverse_landscape; // gap for navigation bar along right (portrait) or top (landscape); only set for edge_to_edge_mode==true
    public static volatile boolean test_preview_want_no_limits; // test flag, if set to true then instead use test_preview_want_no_limits_value; needs to be static, as it needs to be set before activity is created to take effect
    public static volatile boolean test_preview_want_no_limits_value;
    public volatile boolean test_set_show_under_navigation; // test flag, the value of enable for the last call of showUnderNavigation() (or false if not yet called)
    public static volatile boolean test_force_system_orientation; // test flag, if set to true, that getSystemOrientation() returns test_system_orientation
    public static volatile SystemOrientation test_system_orientation = SystemOrientation.PORTRAIT;
    public static volatile boolean test_force_window_insets; // test flag, if set to true, then the OnApplyWindowInsetsListener will read from the following flags
    public static volatile Insets test_insets; // test insets for WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()
    public static volatile Insets test_cutout_insets; // test insets for WindowInsets.Type.displayCutout()

    // whether this is a multi-camera device (note, this isn't simply having more than 1 camera, but also having more than one with the same facing)
    // note that in most cases, code should check the MultiCamButtonPreferenceKey preference as well as the is_multi_cam flag,
    // this can be done via isMultiCamEnabled().
    private boolean is_multi_cam;
    // These lists are lists of camera IDs with the same "facing" (front, back or external).
    // Only initialised if is_multi_cam==true.
    private List<Integer> back_camera_ids;
    private List<Integer> front_camera_ids;
    private List<Integer> other_camera_ids;

    private final ToastBoxer switch_video_toast = new ToastBoxer();
    private final ToastBoxer screen_locked_toast = new ToastBoxer();
    private final ToastBoxer stamp_toast = new ToastBoxer();
    private final ToastBoxer changed_auto_stabilise_toast = new ToastBoxer();
    private final ToastBoxer white_balance_lock_toast = new ToastBoxer();
    private final ToastBoxer exposure_lock_toast = new ToastBoxer();
    private final ToastBoxer audio_control_toast = new ToastBoxer();
    private final ToastBoxer store_location_toast = new ToastBoxer();
    private boolean block_startup_toast = false; // used when returning from Settings/Popup - if we're displaying a toast anyway, don't want to display the info toast too
    private String push_info_toast_text; // can be used to "push" extra text to the info text for showPhotoVideoToast()
    private boolean push_switched_camera = false; // whether to display animation for switching front/back cameras

    // application shortcuts:
    static private final String ACTION_SHORTCUT_CAMERA = "net.sourceforge.opencamera.SHORTCUT_CAMERA";
    static private final String ACTION_SHORTCUT_SELFIE = "net.sourceforge.opencamera.SHORTCUT_SELFIE";
    static private final String ACTION_SHORTCUT_VIDEO = "net.sourceforge.opencamera.SHORTCUT_VIDEO";
    static private final String ACTION_SHORTCUT_GALLERY = "net.sourceforge.opencamera.SHORTCUT_GALLERY";
    static private final String ACTION_SHORTCUT_SETTINGS = "net.sourceforge.opencamera.SHORTCUT_SETTINGS";

    private static final int CHOOSE_SAVE_FOLDER_SAF_CODE = 42;
    private static final int CHOOSE_GHOST_IMAGE_SAF_CODE = 43;
    private static final int CHOOSE_LOAD_SETTINGS_SAF_CODE = 44;

    // for testing; must be volatile for test project reading the state
    // n.b., avoid using static, as static variables are shared between different instances of an application,
    // and won't be reset in subsequent tests in a suite!
    public boolean is_test; // whether called from OpenCamera.test testing
    public volatile Bitmap gallery_bitmap;
    public volatile boolean test_low_memory;
    public volatile boolean test_have_angle;
    public volatile float test_angle;
    public volatile Uri test_last_saved_imageuri; // uri of last image; set if using scoped storage OR using SAF
    public volatile String test_last_saved_image; // filename (including full path) of last image; set if not using scoped storage nor using SAF (i.e., writing using File API)
    public static boolean test_force_supports_camera2; // okay to be static, as this is set for an entire test suite
    public volatile String test_save_settings_file;

    // update: notifications now removed due to needing permissions on Android 13+
    //private boolean has_notification;
    //private final String CHANNEL_ID = "open_camera_channel";
    //private final int image_saving_notification_id = 1;

    private static final float WATER_DENSITY_FRESHWATER = 1.0f;
    private static final float WATER_DENSITY_SALTWATER = 1.03f;
    private float mWaterDensity = 1.0f;

    // whether to lock to landscape orientation, or allow switching between portrait and landscape orientations
    //public static final boolean lock_to_landscape = true;
    public static final boolean lock_to_landscape = false;

    // handling for lock_to_landscape==false:

    public enum SystemOrientation {
        LANDSCAPE,
        PORTRAIT,
        REVERSE_LANDSCAPE
    }

    private MyDisplayListener displayListener;

    private boolean has_cached_system_orientation;
    private SystemOrientation cached_system_orientation;

    private boolean hasOldSystemOrientation;
    private SystemOrientation oldSystemOrientation;

    private boolean has_cached_display_rotation;
    private long cached_display_rotation_time_ms;
    private int cached_display_rotation;

    List<Integer> exposure_seekbar_values; // mapping from exposure_seekbar progress value to preview exposure compensation
    private int exposure_seekbar_values_zero; // index in exposure_seekbar_values that maps to zero preview exposure compensation

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        long debug_time = 0;
        if( MyDebug.LOG ) {
            Log.d(TAG, "onCreate: " + this);
            debug_time = System.currentTimeMillis();
        }
        activity_count++;
        if( MyDebug.LOG )
            Log.d(TAG, "activity_count: " + activity_count);
        //EdgeToEdge.enable(this, SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), SystemBarStyle.dark(Color.TRANSPARENT)); // test edge-to-edge on pre-Android 15
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // initialise any unset preferences to their default values
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after setting default preference values: " + (System.currentTimeMillis() - debug_time));

        if( getIntent() != null && getIntent().getExtras() != null ) {
            // whether called from testing
            is_test = getIntent().getExtras().getBoolean("test_project");
            if( MyDebug.LOG )
                Log.d(TAG, "is_test: " + is_test);
        }
        /*if( getIntent() != null && getIntent().getExtras() != null ) {
            // whether called from Take Photo widget
            if( MyDebug.LOG )
                Log.d(TAG, "take_photo?: " + getIntent().getExtras().getBoolean(TakePhoto.TAKE_PHOTO));
        }*/
        if( MyDebug.LOG ) {
            // whether called from Take Photo widget
            Log.d(TAG, "take_photo?: " + TakePhoto.TAKE_PHOTO);
        }
        if( getIntent() != null && getIntent().getAction() != null ) {
            // invoked via the manifest shortcut?
            if( MyDebug.LOG )
                Log.d(TAG, "shortcut: " + getIntent().getAction());
        }
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        // determine whether we should support "auto stabilise" feature
        // risk of running out of memory on lower end devices, due to manipulation of large bitmaps
        ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        if( MyDebug.LOG ) {
            Log.d(TAG, "large max memory = " + activityManager.getLargeMemoryClass() + "MB");
        }
        large_heap_memory = activityManager.getLargeMemoryClass();
        if( large_heap_memory >= 128 ) {
            supports_auto_stabilise = true;
        }
        if( MyDebug.LOG )
            Log.d(TAG, "supports_auto_stabilise? " + supports_auto_stabilise);

        // hack to rule out phones unlikely to have 4K video, so no point even offering the option!
        // both S5 and Note 3 have 128MB standard and 512MB large heap (tested via Samsung RTL), as does Galaxy K Zoom
        if( activityManager.getLargeMemoryClass() >= 512 ) {
            supports_force_video_4k = true;
        }
        if( MyDebug.LOG )
            Log.d(TAG, "supports_force_video_4k? " + supports_force_video_4k);

        // set up components
        bluetoothRemoteControl = new BluetoothRemoteControl(this);
        permissionHandler = new PermissionHandler(this);
        settingsManager = new SettingsManager(this);
        mainUI = new MainUI(this);
        manualSeekbars = new ManualSeekbars();
        applicationInterface = new MyApplicationInterface(this, savedInstanceState);
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after creating application interface: " + (System.currentTimeMillis() - debug_time));
        textFormatter = new TextFormatter(this);
        soundPoolManager = new SoundPoolManager(this);
        magneticSensor = new MagneticSensor(this);
        //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();
        }

        boolean settings_is_open = settingsIsOpen();
        if( MyDebug.LOG )
            Log.d(TAG, "settings_is_open?: " + settings_is_open);
        // settings_is_open==true can happen if application is recreated when settings is open
        // to reproduce: go to settings, then turn screen off and on (and unlock)
        if( !settings_is_open ) {
            // set up window flags for normal operation
            setWindowFlagsForCamera();
        }
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after setting window flags: " + (System.currentTimeMillis() - debug_time));

        save_location_history = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistoryBasePreferenceKey, getStorageUtils().getSaveLocation());
        checkSaveLocations();
        if( applicationInterface.getStorageUtils().isUsingSAF() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "create new SaveLocationHistory for SAF");
            save_location_history_saf = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistorySAFBasePreferenceKey, getStorageUtils().getSaveLocationSAF());
        }
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after updating folder history: " + (System.currentTimeMillis() - debug_time));

        // set up sensors
        mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);

        // accelerometer sensor (for device orientation)
        if( mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ) {
            if( MyDebug.LOG )
                Log.d(TAG, "found accelerometer");
            mSensorAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "no support for accelerometer");
        }
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after creating accelerometer sensor: " + (System.currentTimeMillis() - debug_time));

        // magnetic sensor (for compass direction)
        magneticSensor.initSensor(mSensorManager);
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after creating magnetic sensor: " + (System.currentTimeMillis() - debug_time));

        // clear any seek bars (just in case??)
        mainUI.closeExposureUI();

        // set up the camera and its preview
        preview = new Preview(applicationInterface, (this.findViewById(R.id.preview)));
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after creating preview: " + (System.currentTimeMillis() - debug_time));

        if( settings_is_open ) {
            // must be done after creating preview
            setWindowFlagsForSettings();
        }

        {
            // don't show orientation animations
            // must be done after creating Preview (so we know if Camera2 API or not)
            WindowManager.LayoutParams layout = getWindow().getAttributes();
            // If locked to landscape, ROTATION_ANIMATION_SEAMLESS/JUMPCUT has the problem that when going to
            // Settings in portrait, we briefly see the UI change - this is because we set the flag
            // to no longer lock to landscape, and that change happens too quickly.
            // This isn't a problem when lock_to_landscape==false, and we want
            // ROTATION_ANIMATION_SEAMLESS so that there is no/minimal pause from the preview when
            // rotating the device. However if using old camera API, we get an ugly transition with
            // ROTATION_ANIMATION_SEAMLESS (probably related to not using TextureView?)
            if( lock_to_landscape || !preview.usingCamera2API() )
                layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
            else if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O )
                layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
            else
                layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
            getWindow().setAttributes(layout);
        }

        // Setup multi-camera buttons (must be done after creating preview so we know which Camera API is being used,
        // and before initialising on-screen visibility).
        // We only allow the separate icon for switching cameras if:
        // - there are at least 2 types of "facing" camera, and
        // - there are at least 2 cameras with the same "facing".
        // If there are multiple cameras but all with different "facing", then the switch camera
        // icon is used to iterate over all cameras.
        // If there are more than two cameras, but all cameras have the same "facing, we still stick
        // with using the switch camera icon to iterate over all cameras.
        int n_cameras = preview.getCameraControllerManager().getNumberOfCameras();
        if( n_cameras > 2 ) {
            this.back_camera_ids = new ArrayList<>();
            this.front_camera_ids = new ArrayList<>();
            this.other_camera_ids = new ArrayList<>();
            for(int i=0;i<n_cameras;i++) {
                switch( preview.getCameraControllerManager().getFacing(i) ) {
                    case FACING_BACK:
                        back_camera_ids.add(i);
                        break;
                    case FACING_FRONT:
                        front_camera_ids.add(i);
                        break;
                    default:
                        // we assume any unknown cameras are also external
                        other_camera_ids.add(i);
                        break;
                }
            }
            boolean multi_same_facing = back_camera_ids.size() >= 2 || front_camera_ids.size() >= 2 || other_camera_ids.size() >= 2;
            int n_facing = 0;
            if( !back_camera_ids.isEmpty() )
                n_facing++;
            if( !front_camera_ids.isEmpty() )
                n_facing++;
            if( !other_camera_ids.isEmpty() )
                n_facing++;
            this.is_multi_cam = multi_same_facing && n_facing >= 2;
            //this.is_multi_cam = false; // test
            if( MyDebug.LOG ) {
                Log.d(TAG, "multi_same_facing: " + multi_same_facing);
                Log.d(TAG, "n_facing: " + n_facing);
                Log.d(TAG, "is_multi_cam: " + is_multi_cam);
            }

            if( !is_multi_cam ) {
                this.back_camera_ids = null;
                this.front_camera_ids = null;
                this.other_camera_ids = null;
            }
        }

        // initialise on-screen button visibility
        View switchCameraButton = findViewById(R.id.switch_camera);
        switchCameraButton.setVisibility(n_cameras > 1 ? View.VISIBLE : View.GONE);
        // switchMultiCameraButton visibility updated below in mainUI.updateOnScreenIcons(), as it also depends on user preference
        View speechRecognizerButton = findViewById(R.id.audio_control);
        speechRecognizerButton.setVisibility(View.GONE); // disabled by default, until the speech recognizer is created
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after setting button visibility: " + (System.currentTimeMillis() - debug_time));
        View pauseVideoButton = findViewById(R.id.pause_video);
        pauseVideoButton.setVisibility(View.GONE);
        View takePhotoVideoButton = findViewById(R.id.take_photo_when_video_recording);
        takePhotoVideoButton.setVisibility(View.GONE);
        View cancelPanoramaButton = findViewById(R.id.cancel_panorama);
        cancelPanoramaButton.setVisibility(View.GONE);

        // We initialise optional controls to invisible/gone, so they don't show while the camera is opening - the actual visibility is
        // set in cameraSetup().
        // Note that ideally we'd set this in the xml, but doing so for R.id.zoom causes a crash on Galaxy Nexus startup beneath
        // setContentView()!
        // To be safe, we also do so for take_photo and zoom_seekbar (we already know we've had no reported crashes for focus_seekbar,
        // however).
        View takePhotoButton = findViewById(R.id.take_photo);
        takePhotoButton.setVisibility(View.INVISIBLE);
        View zoomSeekbar = findViewById(R.id.zoom_seekbar);
        zoomSeekbar.setVisibility(View.INVISIBLE);

        // initialise state of on-screen icons
        mainUI.updateOnScreenIcons();

        if( MainActivity.lock_to_landscape ) {
            // listen for orientation event change (only required if lock_to_landscape==true
            // (MainUI.onOrientationChanged() does nothing if lock_to_landscape==false)
            orientationEventListener = new OrientationEventListener(this) {
                @Override
                public void onOrientationChanged(int orientation) {
                    MainActivity.this.mainUI.onOrientationChanged(orientation);
                }
            };
            if( MyDebug.LOG )
                Log.d(TAG, "onCreate: time after setting orientation event listener: " + (System.currentTimeMillis() - debug_time));
        }

        layoutChangeListener = new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if( MyDebug.LOG )
                    Log.d(TAG, "onLayoutChange");

                if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode() ) {
                    Point display_size = new Point();
                    applicationInterface.getDisplaySize(display_size, true);
                    if( MyDebug.LOG ) {
                        Log.d(TAG, "    display width: " + display_size.x);
                        Log.d(TAG, "    display height: " + display_size.y);
                        Log.d(TAG, "    layoutUI display width: " + mainUI.layoutUI_display_w);
                        Log.d(TAG, "    layoutUI display height: " + mainUI.layoutUI_display_h);
                    }
                    // We need to call layoutUI when the window is resized without an orientation change -
                    // this can happen in split-screen or multi-window mode, where onConfigurationChanged
                    // is not guaranteed to be called.
                    // We check against the size of when layoutUI was last called, to avoid repeated calls
                    // when the resize is due to the device rotating and onConfigurationChanged is called -
                    // in fact we'd have a problem of repeatedly calling layoutUI, since doing layoutUI
                    // causes onLayoutChange() to be called again.
                    if( display_size.x != mainUI.layoutUI_display_w || display_size.y != mainUI.layoutUI_display_h ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "call layoutUI due to resize");
                        mainUI.layoutUI();
                    }
                }
            }
        };

        // set up take photo long click
        takePhotoButton.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if( !allowLongPress() ) {
                    // return false, so a regular click will still be triggered when the user releases the touch
                    return false;
                }
                return longClickedTakePhoto();
            }
        });
        // set up on touch listener so we can detect if we've released from a long click
        takePhotoButton.setOnTouchListener(new View.OnTouchListener() {
            // the suppressed warning ClickableViewAccessibility suggests calling view.performClick for ACTION_UP, but this
            // results in an additional call to clickedTakePhoto() - that is, if there is no long press, we get two calls to
            // clickedTakePhoto instead one one; and if there is a long press, we get one call to clickedTakePhoto where
            // there should be none.
            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if( motionEvent.getAction() == MotionEvent.ACTION_UP ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "takePhotoButton ACTION_UP");
                    takePhotoButtonLongClickCancelled();
                    if( MyDebug.LOG )
                        Log.d(TAG, "takePhotoButton ACTION_UP done");
                }
                return false;
            }
        });

        // set up gallery button long click
        View galleryButton = findViewById(R.id.gallery);
        galleryButton.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if( !allowLongPress() ) {
                    // return false, so a regular click will still be triggered when the user releases the touch
                    return false;
                }
                //preview.showToast(null, "Long click");
                longClickedGallery();
                return true;
            }
        });

        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after setting long click listeners: " + (System.currentTimeMillis() - debug_time));

        // listen for gestures
        gestureDetector = new GestureDetector(this, new MyGestureDetector());
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after creating gesture detector: " + (System.currentTimeMillis() - debug_time));

        setupSystemUiVisibilityListener();
        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
        if( !has_done_first_time ) {
            if( !is_test ) {
                AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
                alertDialog.setTitle(R.string.app_name);
                alertDialog.setMessage(R.string.intro_text);
                alertDialog.setPositiveButton(android.R.string.ok, null);
                alertDialog.setNegativeButton(R.string.preference_online_help, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "online help");
                        launchOnlineHelp();
                    }
                });
                alertDialog.show();
            }

            setFirstTimeFlag();
        }

        {
            // handle What's New dialog
            int version_code = -1;
            try {
                PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
                version_code = pInfo.versionCode;
            }
            catch(PackageManager.NameNotFoundException e) {
                MyDebug.logStackTrace(TAG, "NameNotFoundException exception trying to get version number", e);
            }
            if( version_code != -1 ) {
                int latest_version = sharedPreferences.getInt(PreferenceKeys.LatestVersionPreferenceKey, 0);
                if( MyDebug.LOG ) {
                    Log.d(TAG, "version_code: " + version_code);
                    Log.d(TAG, "latest_version: " + latest_version);
                }
                //final boolean whats_new_enabled = false;
                final boolean whats_new_enabled = true;
                if( whats_new_enabled ) {
                    // whats_new_version is the version code that the What's New text is written for. Normally it will equal the
                    // current release (version_code), but it some cases we may want to leave it unchanged.
                    // 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 = 93; // 1.55
                    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);
                    }
                    final boolean force_whats_new = false;
                    //final boolean force_whats_new = true; // for testing
                    boolean allow_show_whats_new = sharedPreferences.getBoolean(PreferenceKeys.ShowWhatsNewPreferenceKey, true);
                    if( MyDebug.LOG )
                        Log.d(TAG, "allow_show_whats_new: " + allow_show_whats_new);
                    // don't show What's New if this is the first time the user has run
                    if( has_done_first_time && allow_show_whats_new && ( force_whats_new || whats_new_version > latest_version ) ) {
                        AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
                        alertDialog.setTitle(R.string.whats_new);
                        alertDialog.setMessage(R.string.whats_new_text);
                        alertDialog.setPositiveButton(android.R.string.ok, null);
                        alertDialog.show();
                    }
                }
                // We set the latest_version whether or not the dialog is shown - if we showed the first time dialog, we don't
                // want to then show the What's New dialog next time we run! Similarly if the user had disabled showing the dialog,
                // but then enables it, we still shouldn't show the dialog until the new time Open Camera upgrades.
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putInt(PreferenceKeys.LatestVersionPreferenceKey, version_code);
                editor.apply();
            }
        }

        setModeFromIntents(savedInstanceState);

        // load icons
        preloadIcons(R.array.flash_icons);
        preloadIcons(R.array.focus_mode_icons);
        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: time after preloading icons: " + (System.currentTimeMillis() - debug_time));

        // initialise text to speech engine
        textToSpeechSuccess = false;
        // run in separate thread so as to not delay startup time
        new Thread(new Runnable() {
            public void run() {
                textToSpeech = new TextToSpeech(MainActivity.this, new TextToSpeech.OnInitListener() {
                    @Override
                    public void onInit(int status) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "TextToSpeech initialised");
                        if( status == TextToSpeech.SUCCESS ) {
                            textToSpeechSuccess = true;
                            if( MyDebug.LOG )
                                Log.d(TAG, "TextToSpeech succeeded");
                        }
                        else {
                            if( MyDebug.LOG )
                                Log.d(TAG, "TextToSpeech failed");
                        }
                    }
                });
            }
        }).start();

        // handle on back behaviour
        popupOnBackPressedCallback = new PopupOnBackPressedCallback(false);
        this.getOnBackPressedDispatcher().addCallback(this, popupOnBackPressedCallback);
        pausePreviewOnBackPressedCallback = new PausePreviewOnBackPressedCallback(false);
        this.getOnBackPressedDispatcher().addCallback(this, pausePreviewOnBackPressedCallback);
        screenLockOnBackPressedCallback = new ScreenLockOnBackPressedCallback(false);
        this.getOnBackPressedDispatcher().addCallback(this, screenLockOnBackPressedCallback);

        // create notification channel - only needed on Android 8+
        // update: notifications now removed due to needing permissions on Android 13+
        /*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
            CharSequence name = "Open Camera Image Saving";
            String description = "Notification channel for processing and saving images in the background";
            int importance = NotificationManager.IMPORTANCE_LOW;
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
            channel.setDescription(description);
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }*/

        // so we get the icons rotation even when rotating for the first time - see onSystemOrientationChanged
        this.hasOldSystemOrientation = true;
        this.oldSystemOrientation = getSystemOrientation();

        if( MyDebug.LOG )
            Log.d(TAG, "onCreate: total time for Activity startup: " + (System.currentTimeMillis() - debug_time));
    }

    /** Whether to use codepaths that are compatible with scoped storage.
     */
    public static boolean useScopedStorage() {
        //return false;
        //return true;
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
    }

    /** Whether this is a multi camera device, and the user preference is set to enable the multi-camera button.
     */
    public boolean isMultiCamEnabled() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        return is_multi_cam && sharedPreferences.getBoolean(PreferenceKeys.MultiCamButtonPreferenceKey, true);
    }

    /** Whether this is a multi camera device, whether or not the user preference is set to enable
     *  the multi-camera button.
     */
    public boolean isMultiCam() {
        return is_multi_cam;
    }

    /* Returns the camera Id in use by the preview - or the one we requested, if the camera failed
     * to open.
     * Needed as Preview.getCameraId() returns 0 if camera_controller==null, but if the camera
     * fails to open, we want the switch camera icons to still work as expected!
     */
    private int getActualCameraId() {
        if( preview.getCameraController() == null )
            return applicationInterface.getCameraIdPref();
        else
            return preview.getCameraId();
    }

    /** Whether the icon switch_multi_camera should be displayed. This is if the following are all
     *  true:
     *  - The device is a multi camera device (MainActivity.is_multi_cam==true).
     *  - The user preference for using the separate icons is enabled
     *    (PreferenceKeys.MultiCamButtonPreferenceKey).
     *  - For the current camera ID, there are at least two cameras with the same front/back/external
     *    "facing" (e.g., imagine a device with two back cameras, but only one front camera - no point
     *    showing the multi-cam icon for just a single logical front camera).
     *  OR there are physical cameras for the current camera, and again the user preference
     *  PreferenceKeys.MultiCamButtonPreferenceKey is enabled.
     */
    public boolean showSwitchMultiCamIcon() {
        if( preview.hasPhysicalCameras() ) {
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            if( sharedPreferences.getBoolean(PreferenceKeys.MultiCamButtonPreferenceKey, true) )
                return true;
        }
        if( isMultiCamEnabled() ) {
            int cameraId = getActualCameraId();
            switch( preview.getCameraControllerManager().getFacing(cameraId) ) {
                case FACING_BACK:
                    if( back_camera_ids.size() > 1 )
                        return true;
                    break;
                case FACING_FRONT:
                    if( front_camera_ids.size() > 1 )
                        return true;
                    break;
                default:
                    if( other_camera_ids.size() > 1 )
                        return true;
                    break;
            }
        }
        return false;
    }

    /** Whether user preference is set to allow long press actions.
     */
    private boolean allowLongPress() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        return sharedPreferences.getBoolean(PreferenceKeys.AllowLongPressPreferenceKey, true);
    }

    /* This method sets the preference defaults which are set specific for a particular device.
     * This method should be called when Open Camera is run for the very first time after installation,
     * or when the user has requested to "Reset settings".
     */
    void setDeviceDefaults() {
        if( MyDebug.LOG )
            Log.d(TAG, "setDeviceDefaults");
        boolean is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung");
        //SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        //boolean is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung");
        //boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus");
        //boolean is_nexus = Build.MODEL.toLowerCase(Locale.US).contains("nexus");
        //boolean is_nexus6 = Build.MODEL.toLowerCase(Locale.US).contains("nexus 6");
        //boolean is_pixel_phone = Build.DEVICE != null && Build.DEVICE.equals("sailfish");
        //boolean is_pixel_xl_phone = Build.DEVICE != null && Build.DEVICE.equals("marlin");
        /*if( MyDebug.LOG ) {
            //Log.d(TAG, "is_samsung? " + is_samsung);
            //Log.d(TAG, "is_oneplus? " + is_oneplus);
            //Log.d(TAG, "is_nexus? " + is_nexus);
            //Log.d(TAG, "is_nexus6? " + is_nexus6);
            //Log.d(TAG, "is_pixel_phone? " + is_pixel_phone);
            //Log.d(TAG, "is_pixel_xl_phone? " + is_pixel_xl_phone);
        }*/
        /*if( is_samsung || is_oneplus ) {
            // The problems we used to have on Samsung Galaxy devices are now fixed, by setting
            // TEMPLATE_PREVIEW for the precaptureBuilder in CameraController2. This also fixes the
            // problems with OnePlus 3T having blue tinge if flash is on, and the scene is bright
            // enough not to need it
            if( MyDebug.LOG )
                Log.d(TAG, "set fake flash for camera2");
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putBoolean(PreferenceKeys.Camera2FakeFlashPreferenceKey, true);
            editor.apply();
        }*/
		/*if( is_nexus6 ) {
			// Nexus 6 captureBurst() started having problems with Android 7 upgrade - images appeared in wrong order (and with wrong order of shutter speeds in exif info), as well as problems with the camera failing with serious errors
			// we set this even for Nexus 6 devices not on Android 7, as at some point they'll likely be upgraded to Android 7
			// Update: now fixed in v1.37, this was due to bug where we set RequestTag.CAPTURE for all captures in takePictureBurstExpoBracketing(), rather than just the last!
			if( MyDebug.LOG )
				Log.d(TAG, "disable fast burst for camera2");
			SharedPreferences.Editor editor = sharedPreferences.edit();
			editor.putBoolean(PreferenceKeys.Camera2FastBurstPreferenceKey, false);
			editor.apply();
		}*/
        if( is_samsung && !is_test ) {
            // Samsung Galaxy devices (including S10e, S24) have problems with HDR/expo - base images come out with wrong exposures.
            // This can be fixed by not using fast bast, allowing us to adjust the preview exposure to match.
            if( MyDebug.LOG )
                Log.d(TAG, "disable fast burst for camera2");
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putBoolean(PreferenceKeys.Camera2FastBurstPreferenceKey, 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 all_supports_camera2 = true; // whether all cameras have at least LIMITED support for Camera2 (risky to default to Camera2 if any cameras are LEGACY, as not easy to test such devices)
            for(int i=0;i<n_cameras && all_supports_camera2;i++) {
                if( !manager2.allowCamera2Support(i) ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "camera " + i + " doesn't have at least LIMITED support for Camera2 API");
                    all_supports_camera2 = false;
                }
            }

            if( all_supports_camera2 ) {
                boolean default_to_camera2 = false;
                boolean is_google = Build.MANUFACTURER.toLowerCase(Locale.US).contains("google");
                boolean is_nokia = Build.MANUFACTURER.toLowerCase(Locale.US).contains("hmd global");
                boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus");
                if( is_google && Build.VERSION.SDK_INT >= 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;
                else if( is_oneplus && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE )
                    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.
     */
    private void setModeFromIntents(Bundle savedInstanceState) {
        if( MyDebug.LOG )
            Log.d(TAG, "setModeFromIntents");
        if( savedInstanceState != null ) {
            // If we're restoring from a saved state, we shouldn't be resetting any modes
            if( MyDebug.LOG )
                Log.d(TAG, "restoring from saved state");
            return;
        }
        boolean done_facing = false;
        String action = this.getIntent().getAction();
        if( MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(action) || MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from video intent");
            applicationInterface.setVideoPref(true);
        }
        else if( MediaStore.ACTION_IMAGE_CAPTURE.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(action) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from photo intent");
            applicationInterface.setVideoPref(false);
        }
        else if( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && MyTileService.TILE_ID.equals(action)) || ACTION_SHORTCUT_CAMERA.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from quick settings tile or application shortcut for Open Camera: photo mode");
            applicationInterface.setVideoPref(false);
        }
        else if( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && MyTileServiceVideo.TILE_ID.equals(action)) || ACTION_SHORTCUT_VIDEO.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from quick settings tile or application shortcut for Open Camera: video mode");
            applicationInterface.setVideoPref(true);
        }
        else if( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && MyTileServiceFrontCamera.TILE_ID.equals(action)) || ACTION_SHORTCUT_SELFIE.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from quick settings tile or application shortcut for Open Camera: selfie mode");
            done_facing = true;
            applicationInterface.switchToCamera(true);
        }
        else if( ACTION_SHORTCUT_GALLERY.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from application shortcut for Open Camera: gallery");
            openGallery();
        }
        else if( ACTION_SHORTCUT_SETTINGS.equals(action) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "launching from application shortcut for Open Camera: settings");
            openSettings();
        }

        Bundle extras = this.getIntent().getExtras();
        if( extras != null ) {
            if( MyDebug.LOG )
                Log.d(TAG, "handle intent extra information");
            if( !done_facing ) {
                int camera_facing = extras.getInt("android.intent.extras.CAMERA_FACING", -1);
                if( camera_facing == 0 || camera_facing == 1 ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "found android.intent.extras.CAMERA_FACING: " + camera_facing);
                    applicationInterface.switchToCamera(camera_facing==1);
                    done_facing = true;
                }
            }
            if( !done_facing ) {
                if( extras.getInt("android.intent.extras.LENS_FACING_FRONT", -1) == 1 ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "found android.intent.extras.LENS_FACING_FRONT");
                    applicationInterface.switchToCamera(true);
                    done_facing = true;
                }
            }
            if( !done_facing ) {
                if( extras.getInt("android.intent.extras.LENS_FACING_BACK", -1) == 1 ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "found android.intent.extras.LENS_FACING_BACK");
                    applicationInterface.switchToCamera(false);
                    done_facing = true;
                }
            }
            if( !done_facing ) {
                if( extras.getBoolean("android.intent.extra.USE_FRONT_CAMERA", false) ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "found android.intent.extra.USE_FRONT_CAMERA");
                    applicationInterface.switchToCamera(true);
                    done_facing = true;
                }
            }
        }

        // N.B., in practice the hasSetCameraId() check is pointless as we don't save the camera ID in shared preferences, so it will always
        // be false when application is started from onCreate(), unless resuming from saved instance (in which case we shouldn't be here anyway)
        if( !done_facing && !applicationInterface.hasSetCameraId() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "initialise to back camera");
            // most devices have first camera as back camera anyway so this wouldn't be needed, but some (e.g., LG G6) have first camera
            // as front camera, so we should explicitly switch to back camera
            applicationInterface.switchToCamera(false);
        }
    }

    /** Determine whether we support Camera2 API.
     */
    private void initCamera2Support() {
        if( MyDebug.LOG )
            Log.d(TAG, "initCamera2Support");
        supports_camera2 = false;
        {
            // originally we allowed Camera2 if all cameras support at least LIMITED
            // as of 1.45, we allow Camera2 if at least one camera supports at least LIMITED - this
            // is to support devices that might have a camera with LIMITED or better support, but
            // also a LEGACY camera
            CameraControllerManager2 manager2 = new CameraControllerManager2(this);
            supports_camera2 = false;
            int n_cameras = manager2.getNumberOfCameras();
            if( n_cameras == 0 ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "Camera2 reports 0 cameras");
                supports_camera2 = false;
            }
            for(int i=0;i<n_cameras && !supports_camera2;i++) {
                if( manager2.allowCamera2Support(i) ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "camera " + i + " has at least limited support for Camera2 API");
                    supports_camera2 = true;
                }
            }
        }

        //test_force_supports_camera2 = true; // test
        if( test_force_supports_camera2 ) {
            if( MyDebug.LOG )
                Log.d(TAG, "forcing supports_camera2");
            supports_camera2 = true;
        }

        if( MyDebug.LOG )
            Log.d(TAG, "supports_camera2? " + supports_camera2);

        // handle the switch from a boolean preference_use_camera2 to String preference_camera_api
        // that occurred in v1.48
        if( supports_camera2 ) {
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            if( !sharedPreferences.contains(PreferenceKeys.CameraAPIPreferenceKey) // doesn't have the new key set yet
                    && sharedPreferences.contains("preference_use_camera2") // has the old key set
                    && sharedPreferences.getBoolean("preference_use_camera2", false) // and camera2 was enabled
            ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "transfer legacy camera2 boolean preference to new api option");
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString(PreferenceKeys.CameraAPIPreferenceKey, "preference_camera_api_camera2");
                editor.remove("preference_use_camera2"); // remove the old key, just in case
                editor.apply();
            }
        }
    }

    /** Handles users updating to a version with scoped storage (this could be Android 10 users upgrading
     *  to the version of Open Camera with scoped storage; or users who later upgrade to Android 10).
     *  With scoped storage, we no longer support saving outside of DCIM/ when not using SAF.
     *  This updates if necessary both the current save location, and the save folder history.
     */
    private void checkSaveLocations() {
        if( MyDebug.LOG )
            Log.d(TAG, "checkSaveLocations");
        if( useScopedStorage() ) {
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            boolean any_changes = false;
            String save_location = getStorageUtils().getSaveLocation();
            CheckSaveLocationResult res = checkSaveLocation(save_location);
            if( !res.res ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "save_location not valid with scoped storage: " + save_location);
                String new_folder;
                if( res.alt == null ) {
                    // no alternative, fall back to default
                    new_folder = "OpenCamera";
                }
                else {
                    // replace with the alternative
                    if( MyDebug.LOG )
                        Log.d(TAG, "alternative: " + res.alt);
                    new_folder = res.alt;
                }
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString(PreferenceKeys.SaveLocationPreferenceKey, new_folder);
                editor.apply();
                any_changes = true;
            }

            // now check history
            // go backwards so we can remove easily
            for(int i=save_location_history.size()-1;i>=0;i--) {
                String this_location = save_location_history.get(i);
                res = checkSaveLocation(this_location);
                if( !res.res ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "save_location in history " + i + " not valid with scoped storage: " + this_location);
                    if( res.alt == null ) {
                        // no alternative, remove
                        save_location_history.remove(i);
                    }
                    else {
                        // replace with the alternative
                        if( MyDebug.LOG )
                            Log.d(TAG, "alternative: " + res.alt);
                        save_location_history.set(i, res.alt);
                    }
                    any_changes = true;
                }
            }

            if( any_changes ) {
                this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), false);
            }
        }
    }

    /** Result from checkSaveLocation. Ideally we'd just use android.util.Pair, but that's not mocked
     *  for use in unit tests.
     *  See checkSaveLocation() for documentation.
     */
    public static class CheckSaveLocationResult {
        final boolean res;
        final String alt;

        public CheckSaveLocationResult(boolean res, String alt) {
            this.res = res;
            this.alt = alt;
        }

        @Override
        public boolean equals(Object o) {
            if( !(o instanceof CheckSaveLocationResult) ) {
                return false;
            }
            CheckSaveLocationResult that = (CheckSaveLocationResult)o;
            // stop dumb inspection that suggests replacing warning with an error(!) (Objects class is not available on all API versions)
            // and the other inspection suggests replacing with code that would cause a nullpointerexception
            //noinspection EqualsReplaceableByObjectsCall,StringEquality
            return that.res == this.res && ( (that.alt == this.alt) || (that.alt != null && that.alt.equals(this.alt) ) );
            //return that.res == this.res && ( (that.alt == this.alt) || (that.alt != null && that.alt.equals(this.alt) ) );
        }

        @Override
        public int hashCode() {
            return (res ? 1249 : 1259) ^ (alt == null ? 0 : alt.hashCode());
        }

        @NonNull
        @Override
        public String toString() {
            return "CheckSaveLocationResult{" + res + " , " + alt + "}";
        }
    }

    public static CheckSaveLocationResult checkSaveLocation(final String folder) {
        return checkSaveLocation(folder, null);
    }

    /** Checks to see if the supplied folder (in the format as used by our preferences) is supported
     *  with scoped storage.
     * @return The Boolean is always non-null, and returns whether the save location is valid.
     *         If the return is false, then if the String is non-null, this stores an alternative
     *         form that is valid. If null, there is no valid alternative.
     * @param base_folder This should normally be null, but can be used to specify manually the
     *                    folder instead of using StorageUtils.getBaseFolder() - needed for unit
     *                    tests as Environment class (for Environment.getExternalStoragePublicDirectory())
     *                    is not mocked.
     */
    public static CheckSaveLocationResult checkSaveLocation(final String folder, String base_folder) {
        /*if( MyDebug.LOG )
            Log.d(TAG, "DCIM path: " + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());*/
        if( StorageUtils.saveFolderIsFull(folder) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "checkSaveLocation for full path: " + folder);
            // But still check to see if the full path is part of DCIM. Since when using the
            // file dialog method with non-scoped storage, if the user specifies multiple subfolders
            // e.g. DCIM/blah_a/blah_b, we don't spot that in FolderChooserDialog.useFolder(), and
            // instead still store that as the full path.

            if( base_folder == null )
                base_folder = StorageUtils.getBaseFolder().getAbsolutePath();
            // strip '/' as last character - makes it easier to also spot cases where the folder is the
            // DCIM folder, but doesn't have a '/' last character
            if( !base_folder.isEmpty() && base_folder.charAt(base_folder.length()-1) == '/' )
                base_folder = base_folder.substring(0, base_folder.length()-1);
            if( MyDebug.LOG )
                Log.d(TAG, "    compare to base_folder: " + base_folder);
            String alt_folder = null;
            if( folder.startsWith(base_folder) ) {
                alt_folder = folder.substring(base_folder.length());
                // also need to strip the first '/' if it exists
                if( !alt_folder.isEmpty() && alt_folder.charAt(0) == '/' )
                    alt_folder = alt_folder.substring(1);
            }

            return new CheckSaveLocationResult(false, alt_folder);
        }
        else {
            // already in expected format (indicates a sub-folder of DCIM)
            return new CheckSaveLocationResult(true, null);
        }
    }

    private void preloadIcons(int icons_id) {
        long debug_time = 0;
        if( MyDebug.LOG ) {
            Log.d(TAG, "preloadIcons: " + icons_id);
            debug_time = System.currentTimeMillis();
        }
        String [] icons = getResources().getStringArray(icons_id);
        for(String icon : icons) {
            int resource = getResources().getIdentifier(icon, null, this.getApplicationContext().getPackageName());
            if( MyDebug.LOG )
                Log.d(TAG, "load resource: " + resource);
            Bitmap bm = BitmapFactory.decodeResource(getResources(), resource);
            this.preloaded_bitmap_resources.put(resource, bm);
        }
        if( MyDebug.LOG ) {
            Log.d(TAG, "preloadIcons: total time for preloadIcons: " + (System.currentTimeMillis() - debug_time));
            Log.d(TAG, "size of preloaded_bitmap_resources: " + preloaded_bitmap_resources.size());
        }
    }

    @Override
    protected void onStop() {
        if( MyDebug.LOG )
            Log.d(TAG, "onStop");
        super.onStop();

        // we stop location listening in onPause, but done here again just to be certain!
        applicationInterface.getLocationSupplier().freeLocationListeners();
    }

    @Override
    protected void onDestroy() {
        if( MyDebug.LOG ) {
            Log.d(TAG, "onDestroy");
            Log.d(TAG, "size of preloaded_bitmap_resources: " + preloaded_bitmap_resources.size());
        }
        activity_count--;
        if( MyDebug.LOG )
            Log.d(TAG, "activity_count: " + activity_count);

        // should do asap before waiting for images to be saved - as risk the application will be killed whilst waiting for that to happen,
        // and we want to avoid notifications hanging around
        cancelImageSavingNotification();

        if( want_no_limits && navigation_gap != 0 ) {
            if( MyDebug.LOG )
                Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
            // it's unclear why this matters - but there is a bug when exiting split-screen mode, if the split-screen mode had set want_no_limits:
            // even though the application is created when leaving split-screen mode, we still end up with the window flags for showing
            // under the navigation bar!
            // update: this issue is also fixed by not allowing want_no_limits mode in multi-window mode, but still good to reset things here
            // just in case
            showUnderNavigation(false);
        }

        // reduce risk of losing any images
        // we don't do this in onPause or onStop, due to risk of ANRs
        // note that even if we did call this earlier in onPause or onStop, we'd still want to wait again here: as it can happen
        // that a new image appears after onPause/onStop is called, in which case we want to wait until images are saved,
        waitUntilImageQueueEmpty();

        preview.onDestroy();
        if( applicationInterface != null ) {
            applicationInterface.onDestroy();
        }
        // Need to recycle to avoid out of memory when running tests - probably good practice to do anyway
        for(Map.Entry<Integer, Bitmap> entry : preloaded_bitmap_resources.entrySet()) {
            if( MyDebug.LOG )
                Log.d(TAG, "recycle: " + entry.getKey());
            entry.getValue().recycle();
        }
        preloaded_bitmap_resources.clear();
        if( textToSpeech != null ) {
            // http://stackoverflow.com/questions/4242401/tts-error-leaked-serviceconnection-android-speech-tts-texttospeech-solved
            if( MyDebug.LOG )
                Log.d(TAG, "free textToSpeech");
            textToSpeech.stop();
            textToSpeech.shutdown();
            textToSpeech = null;
        }

        // we stop location listening in onPause, but done here again just to be certain!
        applicationInterface.getLocationSupplier().freeLocationListeners();

        super.onDestroy();
        if( MyDebug.LOG )
            Log.d(TAG, "onDestroy done");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    private void setFirstTimeFlag() {
        if( MyDebug.LOG )
            Log.d(TAG, "setFirstTimeFlag");
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(PreferenceKeys.FirstTimePreferenceKey, true);
        editor.apply();
    }

    private static String getOnlineHelpUrl(String append) {
        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;
    }

    void launchOnlineHelp() {
        if( MyDebug.LOG )
            Log.d(TAG, "launchOnlineHelp");
        // if we change this, remember that any page linked to must abide by Google Play developer policies!
        Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("")));
        startActivity(browserIntent);
    }

    void launchOnlinePrivacyPolicy() {
        if( MyDebug.LOG )
            Log.d(TAG, "launchOnlinePrivacyPolicy");
        // if we change this, remember that any page linked to must abide by Google Play developer policies!
        //Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("index.html#privacy")));
        Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("privacy_oc.html")));
        startActivity(browserIntent);
    }

    void launchOnlineLicences() {
        if( MyDebug.LOG )
            Log.d(TAG, "launchOnlineLicences");
        // if we change this, remember that any page linked to must abide by Google Play developer policies!
        Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("#licence")));
        startActivity(browserIntent);
    }

    /* Audio trigger - either loud sound, or speech recognition.
     * This performs some additional checks before taking a photo.
     */
    void audioTrigger() {
        if( MyDebug.LOG )
            Log.d(TAG, "ignore audio trigger due to popup open");
        if( popupIsOpen() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "ignore audio trigger due to popup open");
        }
        else if( camera_in_background ) {
            if( MyDebug.LOG )
                Log.d(TAG, "ignore audio trigger due to camera in background");
        }
        else if( preview.isTakingPhotoOrOnTimer() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "ignore audio trigger due to already taking photo or on timer");
        }
        else if( preview.isVideoRecording() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "ignore audio trigger due to already recording video");
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "schedule take picture due to loud noise");
            //takePicture();
            this.runOnUiThread(new Runnable() {
                public void run() {
                    if( MyDebug.LOG )
                        Log.d(TAG, "taking picture due to audio trigger");
                    takePicture(false);
                }
            });
        }
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if( MyDebug.LOG )
            Log.d(TAG, "onKeyDown: " + keyCode);
        if( camera_in_background ) {
            // don't allow keys such as volume keys for taking photo when camera in background!
            if( MyDebug.LOG )
                Log.d(TAG, "camera is in background");
        }
        else {
            boolean handled = mainUI.onKeyDown(keyCode, event);
            if( handled )
                return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if( MyDebug.LOG )
            Log.d(TAG, "onKeyUp: " + keyCode);
        if( camera_in_background ) {
            // don't allow keys such as volume keys for taking photo when camera in background!
            if( MyDebug.LOG )
                Log.d(TAG, "camera is in background");
        }
        else {
            mainUI.onKeyUp(keyCode, event);
        }
        return super.onKeyUp(keyCode, event);
    }

    private void zoomByStep(int change) {
        if( MyDebug.LOG )
            Log.d(TAG, "zoomByStep: " + change);
        if( preview.supportsZoom() && change != 0 ) {
            if( preview.getCameraController() != null ) {
                // If the minimum zoom is < 1.0, the seekbar will have repeated entries for 1x zoom
                // (so it's easier for the user to zoom to exactly 1.0x). But if using the -/+ buttons,
                // volume keys etc to zoom, we want to skip over these repeated values.
                int zoom_factor = preview.getCameraController().getZoom();
                int new_zoom_factor = zoom_factor + change;
                if( MyDebug.LOG )
                    Log.d(TAG, "new_zoom_factor: " + new_zoom_factor);
                while( new_zoom_factor > 0 && new_zoom_factor < preview.getMaxZoom() && preview.getZoomRatio(new_zoom_factor) == preview.getZoomRatio() ) {
                    if( change > 0 )
                        change++;
                    else
                        change--;
                    new_zoom_factor = zoom_factor + change;
                    if( MyDebug.LOG )
                        Log.d(TAG, "skip over constant region: " + new_zoom_factor);
                }
            }

            mainUI.changeSeekbar(R.id.zoom_seekbar, -change); // seekbar is opposite direction to zoom array
        }
    }

    public void zoomIn() {
        zoomByStep(1);
    }

    public void zoomOut() {
        zoomByStep(-1);
    }

    public void changeExposure(int change) {
        if( preview.supportsExposures() ) {
            if( exposure_seekbar_values != null ) {
                SeekBar seekBar = this.findViewById(R.id.exposure_seekbar);
                int progress = seekBar.getProgress();
                int new_progress = progress + change;
                int current_exposure = getExposureSeekbarValue(progress);
                if( new_progress < 0 || new_progress > exposure_seekbar_values.size()-1 ) {
                    // skip
                }
                else if( getExposureSeekbarValue(new_progress) == 0 && current_exposure != 0 ) {
                    // snap to the central repeated zero
                    new_progress = exposure_seekbar_values_zero;
                    change = new_progress - progress;
                }
                else {
                    // skip over the repeated zeroes
                    while( new_progress > 0 && new_progress < exposure_seekbar_values.size()-1 && getExposureSeekbarValue(new_progress) == current_exposure ) {
                        if( change > 0 )
                            change++;
                        else
                            change--;
                        new_progress = progress + change;
                        if( MyDebug.LOG )
                            Log.d(TAG, "skip over constant region: " + new_progress);
                    }
                }
            }
            mainUI.changeSeekbar(R.id.exposure_seekbar, change);
        }
    }

    public int getExposureSeekbarProgressZero() {
        return exposure_seekbar_values_zero;
    }

    /** Returns the exposure compensation corresponding to a progress on the seekbar.
     *  Caller is responsible for checking that progress is within valid range.
     */
    public int getExposureSeekbarValue(int progress) {
        return exposure_seekbar_values.get(progress);
    }

    public void changeISO(int change) {
        if( preview.supportsISORange() ) {
            mainUI.changeSeekbar(R.id.iso_seekbar, change);
        }
    }

    public void changeFocusDistance(int change, boolean is_target_distance) {
        mainUI.changeSeekbar(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar, change);
    }

    private final SensorEventListener accelerometerListener = new SensorEventListener() {
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        }

        @Override
        public void onSensorChanged(SensorEvent event) {
            preview.onAccelerometerSensorChanged(event);
        }
    };

    public float getWaterDensity() {
        return this.mWaterDensity;
    }

    @Override
    protected void onResume() {
        long debug_time = 0;
        if( MyDebug.LOG ) {
            Log.d(TAG, "onResume");
            debug_time = System.currentTimeMillis();
        }
        super.onResume();
        this.app_is_paused = false; // must be set before initLocation() at least

        // this is intentionally true, not false, as the uncovering happens in DrawPreview when we receive frames from the camera after it's opened
        // (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
        // Note that we do it here rather than customising the theme's android:windowBackground, so this doesn't affect other views - in particular, the MyPreferenceFragment settings
        getWindow().getDecorView().getRootView().setBackgroundColor(Color.BLACK);

        if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM ) {
            // needed on Android 15, otherwise the navigation bar is not transparent
            getWindow().setNavigationBarContrastEnforced(false);
        }

        registerDisplayListener();

        mSensorManager.registerListener(accelerometerListener, mSensorAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
        magneticSensor.registerMagneticListener(mSensorManager);
        if( orientationEventListener != null ) {
            orientationEventListener.enable();
        }
        getWindow().getDecorView().addOnLayoutChangeListener(layoutChangeListener);

        // if BLE remote control is enabled, then start the background BLE service
        bluetoothRemoteControl.startRemoteControl();

        //speechControl.initSpeechRecognizer();
        initLocation();
        initGyroSensors();
        applicationInterface.getImageSaver().onResume();
        soundPoolManager.initSound();
        soundPoolManager.loadSound(R.raw.mybeep);
        soundPoolManager.loadSound(R.raw.mybeep_hi);

        resetCachedSystemOrientation(); // just in case?
        mainUI.layoutUI();

        // 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) {
                        MyDebug.logStackTrace(TAG, "failed to close inputStream", e);
                    }
                }
            }

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

        if( !camera_in_background ) {
            // don't restart camera if we're showing a dialog or settings
            preview.onResume();
        }

        {
            // show a toast for the camera if it's not the first for front of back facing (otherwise on multi-front/back camera
            // devices, it's easy to forget if set to a different camera)
            // but we only show this when resuming, not every time the camera opens
            // OR show the toast for the camera if it's a physical camera
            int cameraId = applicationInterface.getCameraIdPref();
            String cameraIdSPhysical = applicationInterface.getCameraIdSPhysicalPref();
            if( cameraId > 0 || cameraIdSPhysical != null ) {
                CameraControllerManager camera_controller_manager = preview.getCameraControllerManager();
                CameraController.Facing front_facing = camera_controller_manager.getFacing(cameraId);
                if( MyDebug.LOG )
                    Log.d(TAG, "front_facing: " + front_facing);
                if( camera_controller_manager.getNumberOfCameras() > 2 || cameraIdSPhysical != null ) {
                    boolean camera_is_default = true;
                    if( cameraIdSPhysical != null )
                        camera_is_default = false;
                    for(int i=0;i<cameraId && camera_is_default;i++) {
                        CameraController.Facing that_front_facing = camera_controller_manager.getFacing(i);
                        if( MyDebug.LOG )
                            Log.d(TAG, "camera " + i + " that_front_facing: " + that_front_facing);
                        if( that_front_facing == front_facing ) {
                            // found an earlier camera with same front/back facing
                            camera_is_default = false;
                        }
                    }
                    if( MyDebug.LOG )
                        Log.d(TAG, "camera_is_default: " + camera_is_default);
                    if( !camera_is_default ) {
                        this.pushCameraIdToast(cameraId, cameraIdSPhysical);
                    }
                }
            }

            this.announceCameraForAccessibility(cameraId, cameraIdSPhysical);
        }

        push_switched_camera = false; // just in case

        if( MyDebug.LOG ) {
            Log.d(TAG, "onResume: total time to resume: " + (System.currentTimeMillis() - debug_time));
        }
    }

    /** Give details on the camera for talkback. Should be called when resuming (but not every time the
     *  camera is reopened), or if switching camera.
     */
    private void announceCameraForAccessibility(int cameraId, String cameraIdSPhysical) {
        String description = cameraIdSPhysical != null ?
                preview.getCameraControllerManager().getDescription(null, this, cameraIdSPhysical, true, false) :
                preview.getCameraControllerManager().getDescription(this, cameraId);
        if( description != null ) {
            String talkback_string = description;
            if( cameraIdSPhysical == null )
                talkback_string += " " + getResources().getString(R.string.camera_id) + " " + cameraId;
            else
                talkback_string += " " + getResources().getString(R.string.lens) + " " + cameraIdSPhysical;
            if( MyDebug.LOG )
                Log.d(TAG, "talkback_string: " + talkback_string);
            this.preview.getView().announceForAccessibility(talkback_string);
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        if( MyDebug.LOG )
            Log.d(TAG, "onWindowFocusChanged: " + hasFocus);
        super.onWindowFocusChanged(hasFocus);
        if( !this.camera_in_background && hasFocus ) {
            // low profile mode is cleared when app goes into background
            // and for Kit Kat immersive mode, we want to set up the timer
            // we do in onWindowFocusChanged rather than onResume(), to also catch when window lost focus due to notification bar being dragged down (which prevents resetting of immersive mode)
            initImmersiveMode();
        }
    }

    @Override
    protected void onPause() {
        long debug_time = 0;
        if( MyDebug.LOG ) {
            Log.d(TAG, "onPause");
            debug_time = System.currentTimeMillis();
        }
        super.onPause(); // docs say to call this before freeing other things
        this.app_is_paused = true;

        mainUI.destroyPopup(); // important as user could change/reset settings from Android settings when pausing
        if( this.switch_multi_camera_dialog != null ) {
            // to be safe - again, camera ID could reset
            if( MyDebug.LOG )
                Log.d(TAG, "clear switch_multi_camera_dialog");
            this.switch_multi_camera_dialog = null;
        }
        unregisterDisplayListener();
        mSensorManager.unregisterListener(accelerometerListener);
        magneticSensor.unregisterMagneticListener(mSensorManager);
        if( orientationEventListener != null ) {
            orientationEventListener.disable();
        }
        getWindow().getDecorView().removeOnLayoutChangeListener(layoutChangeListener);
        bluetoothRemoteControl.stopRemoteControl();
        freeAudioListener(false);
        //speechControl.stopSpeechRecognizer();
        applicationInterface.getLocationSupplier().freeLocationListeners();
        applicationInterface.stopPanorama(true); // in practice not needed as we should stop panorama when camera is closed, but good to do it explicitly here, before disabling the gyro sensors
        applicationInterface.getGyroSensor().disableSensors();
        applicationInterface.getImageSaver().onPause();
        soundPoolManager.releaseSound();
        applicationInterface.clearLastImages(); // this should happen when pausing the preview, but call explicitly just to be safe
        applicationInterface.getDrawPreview().clearGhostImage();
        preview.onPause();
        applicationInterface.getDrawPreview().setCoverPreview(true); // must be after we've closed the preview (otherwise risk that further frames from preview will unset the cover_preview flag in DrawPreview)

        if( applicationInterface.getImageSaver().getNImagesToSave() > 0) {
            createImageSavingNotification();
        }

        if( update_gallery_future != null ) {
            update_gallery_future.cancel(true);
        }

        // intentionally do this again, just in case something turned location on since - keep this right at the end:
        applicationInterface.getLocationSupplier().freeLocationListeners();

        // don't want to enter immersive mode when in background
        // needs to be last in case anything above indirectly called initImmersiveMode()
        cancelImmersiveTimer();

        if( MyDebug.LOG ) {
            Log.d(TAG, "onPause: total time to pause: " + (System.currentTimeMillis() - debug_time));
        }
    }

    private class MyDisplayListener implements DisplayManager.DisplayListener {
        private int old_rotation;

        private MyDisplayListener() {
            int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation();
            if( MyDebug.LOG ) {
                Log.d(TAG, "MyDisplayListener");
                Log.d(TAG, "rotation: " + rotation);
            }
            old_rotation = rotation;
        }

        @Override
        public void onDisplayAdded(int displayId) {
        }

        @Override
        public void onDisplayRemoved(int displayId) {
        }

        @Override
        public void onDisplayChanged(int displayId) {
            int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation();
            if( MyDebug.LOG ) {
                Log.d(TAG, "onDisplayChanged: " + displayId);
                Log.d(TAG, "rotation: " + rotation);
                Log.d(TAG, "old_rotation: " + old_rotation);
            }
            if( ( rotation == Surface.ROTATION_0 && old_rotation == Surface.ROTATION_180 ) ||
                    ( rotation == Surface.ROTATION_180 && old_rotation == Surface.ROTATION_0 ) ||
                    ( rotation == Surface.ROTATION_90 && old_rotation == Surface.ROTATION_270 ) ||
                    ( rotation == Surface.ROTATION_270 && old_rotation == Surface.ROTATION_90 )
            ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "onDisplayChanged: switched between landscape and reverse orientation");
                onSystemOrientationChanged();
            }

            old_rotation = rotation;
        }
    }

    /** Creates and registers a display listener, needed to handle switches between landscape and
     *  reverse landscape (without going via portrait) when lock_to_landscape==false.
     */
    private void registerDisplayListener() {
        if( MyDebug.LOG )
            Log.d(TAG, "registerDisplayListener");
        if( !lock_to_landscape ) {
            displayListener = new MyDisplayListener();
            DisplayManager displayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE);
            displayManager.registerDisplayListener(displayListener, null);
        }
    }

    private void unregisterDisplayListener() {
        if( MyDebug.LOG )
            Log.d(TAG, "unregisterDisplayListener");
        if( displayListener != null ) {
            DisplayManager displayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE);
            displayManager.unregisterDisplayListener(displayListener);
            displayListener = null;
        }
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        if( MyDebug.LOG )
            Log.d(TAG, "onConfigurationChanged(): " + newConfig.orientation);
        // configuration change can include screen orientation (landscape/portrait) when not locked (when settings is open)
        // needed if app is paused/resumed when settings is open and device is in portrait mode
        // update: need this all the time when lock_to_landscape==false
        onSystemOrientationChanged();
        super.onConfigurationChanged(newConfig);
    }

    private void onSystemOrientationChanged() {
        if( MyDebug.LOG )
            Log.d(TAG, "onSystemOrientationChanged");

        // n.b., need to call this first, before preview.setCameraDisplayOrientation(), since
        // preview.setCameraDisplayOrientation() will call getDisplayRotation() and we don't want
        // to be using the outdated cached value now that the rotation has changed!
        // update: no longer relevant, as preview.setCameraDisplayOrientation() now sets
        // prefer_later to true to avoid using cached value. But might as well call it first anyway.
        resetCachedSystemOrientation();

        preview.setCameraDisplayOrientation();
        if( !lock_to_landscape ) {
            SystemOrientation newSystemOrientation = getSystemOrientation();
            if( hasOldSystemOrientation && oldSystemOrientation == newSystemOrientation ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "onSystemOrientationChanged: orientation hasn't changed");
            }
            else {
                if( hasOldSystemOrientation ) {
                    // handle rotation animation
                    int start_rotation = getRotationFromSystemOrientation(oldSystemOrientation) - getRotationFromSystemOrientation(newSystemOrientation);
                    if( MyDebug.LOG )
                        Log.d(TAG, "start_rotation: " + start_rotation);
                    if( start_rotation < -180 )
                        start_rotation += 360;
                    else if( start_rotation > 180 )
                        start_rotation -= 360;
                    mainUI.layoutUIWithRotation(start_rotation);
                }
                else {
                    mainUI.layoutUI();
                }
                applicationInterface.getDrawPreview().updateSettings();

                hasOldSystemOrientation = true;
                oldSystemOrientation = newSystemOrientation;
            }
        }
    }

    /** Returns the current system orientation.
     *  Note if lock_to_landscape is true, this always returns LANDSCAPE even if called when we're
     *  allowing configuration changes (e.g., in Settings or a dialog is showing). (This method,
     *  and hence calls to it, were added to support lock_to_landscape==false behaviour, and we
     *  want to avoid changing behaviour for lock_to_landscape==true behaviour.)
     *  Note that this also caches the orientation: firstly for performance (as this is called from
     *  DrawPreview), secondly to support REVERSE_LANDSCAPE, we don't want a sudden change if
     *  getDefaultDisplay().getRotation() changes after the configuration changes.
     */
    public SystemOrientation getSystemOrientation() {
        if( test_force_system_orientation ) {
            return test_system_orientation;
        }
        if( lock_to_landscape ) {
            return SystemOrientation.LANDSCAPE;
        }
        if( has_cached_system_orientation ) {
            return cached_system_orientation;
        }
        SystemOrientation result;
        int system_orientation = getResources().getConfiguration().orientation;
        if( MyDebug.LOG )
            Log.d(TAG, "system orientation: " + system_orientation);
        switch( system_orientation ) {
            case Configuration.ORIENTATION_LANDSCAPE:
                result = SystemOrientation.LANDSCAPE;
                // now try to distinguish between landscape and reverse landscape
                {
                    int rotation = getWindowManager().getDefaultDisplay().getRotation();
                    if( MyDebug.LOG )
                        Log.d(TAG, "rotation: " + rotation);
                    switch( rotation ) {
                        case Surface.ROTATION_0:
                        case Surface.ROTATION_90:
                            // landscape
                            if( MyDebug.LOG )
                                Log.d(TAG, "landscape");
                            break;
                        case Surface.ROTATION_180:
                        case Surface.ROTATION_270:
                            // reverse landscape
                            if( MyDebug.LOG )
                                Log.d(TAG, "reverse landscape");
                            result = SystemOrientation.REVERSE_LANDSCAPE;
                            break;
                        default:
                            if( MyDebug.LOG )
                                Log.e(TAG, "unknown rotation: " + rotation);
                            break;
                    }
                }
                break;
            case Configuration.ORIENTATION_PORTRAIT:
                result = SystemOrientation.PORTRAIT;
                break;
            case Configuration.ORIENTATION_UNDEFINED:
            default:
                if( MyDebug.LOG )
                    Log.e(TAG, "unknown system orientation: " + system_orientation);
                result = SystemOrientation.LANDSCAPE;
                break;
        }
        if( MyDebug.LOG )
            Log.d(TAG, "system orientation is now: " + result);
        this.has_cached_system_orientation = true;
        this.cached_system_orientation = result;
        return result;
    }

    /** Returns rotation in degrees (as a multiple of 90 degrees) corresponding to the supplied
     *  system orientation.
     */
    public static int getRotationFromSystemOrientation(SystemOrientation system_orientation) {
        int rotation;
        if( system_orientation == MainActivity.SystemOrientation.PORTRAIT )
            rotation = 270;
        else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE )
            rotation = 180;
        else
            rotation = 0;
        return rotation;
    }

    private void resetCachedSystemOrientation() {
        this.has_cached_system_orientation = false;
        this.has_cached_display_rotation = false;
    }

    /** A wrapper for getWindowManager().getDefaultDisplay().getRotation(), except if
     *  lock_to_landscape==false && prefer_later==false, this uses a cached value.
     */
    public int getDisplayRotation(boolean prefer_later) {
        /*if( MyDebug.LOG ) {
            Log.d(TAG, "getDisplayRotationDegrees");
            Log.d(TAG, "prefer_later: " + prefer_later);
        }*/
        if( lock_to_landscape || prefer_later ) {
            return getWindowManager().getDefaultDisplay().getRotation();
        }
        // we cache to reduce effect of annoying problem where rotation changes shortly before the
        // configuration actually changes (several frames), so on-screen elements would briefly show
        // in wrong location when device rotates from/to portrait and landscape; also not a bad idea
        // to cache for performance anyway, to avoid calling
        // getWindowManager().getDefaultDisplay().getRotation() every frame
        long time_ms = System.currentTimeMillis();
        if( has_cached_display_rotation && time_ms < cached_display_rotation_time_ms + 1000 ) {
            return cached_display_rotation;
        }
        has_cached_display_rotation = true;
        int rotation = getWindowManager().getDefaultDisplay().getRotation();
        cached_display_rotation = rotation;
        cached_display_rotation_time_ms = time_ms;
        return rotation;
    }

    public void waitUntilImageQueueEmpty() {
        if( MyDebug.LOG )
            Log.d(TAG, "waitUntilImageQueueEmpty");
        applicationInterface.getImageSaver().waitUntilDone();
    }

    /**
     * @return True if the long-click is handled, otherwise return false to indicate that regular
     * click should still be triggered when the user releases the touch.
     */
    private boolean longClickedTakePhoto() {
        if( MyDebug.LOG )
            Log.d(TAG, "longClickedTakePhoto");
        if( preview.isVideo() ) {
            // no long-click action for video mode
        }
        else if( supportsFastBurst() ) {
            // need to check whether fast burst is supported (including for the current resolution),
            // in case we're in Standard photo mode
            CameraController.Size current_size = preview.getCurrentPictureSize();
            if( current_size != null && current_size.supports_burst ) {
                MyApplicationInterface.PhotoMode photo_mode = applicationInterface.getPhotoMode();
                if( photo_mode == MyApplicationInterface.PhotoMode.Standard &&
                        applicationInterface.isRawOnly(photo_mode) ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "fast burst not supported in RAW-only mode");
                    // in JPEG+RAW mode, a continuous fast burst will only produce JPEGs which is fine; but in RAW only mode,
                    // no images at all would be saved! (Or we could switch to produce JPEGs anyway, but this seems misleading
                    // in RAW only mode.)
                }
                else if( photo_mode == MyApplicationInterface.PhotoMode.Standard ||
                        photo_mode == MyApplicationInterface.PhotoMode.FastBurst ) {
                    this.takePicturePressed(false, true);
                    return true;
                }
            }
            else {
                if( MyDebug.LOG )
                    Log.d(TAG, "fast burst not supported for this resolution");
            }
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "fast burst not supported");
        }
        // return false, so a regular click will still be triggered when the user releases the touch
        return false;
    }

    public void clickedTakePhoto(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedTakePhoto");
        this.takePicture(false);
    }

    /** User has clicked button to take a photo snapshot whilst video recording.
     */
    public void clickedTakePhotoVideoSnapshot(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedTakePhotoVideoSnapshot");
        this.takePicture(true);
    }

    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();
        }
    }

    public void clickedCancelPanorama(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedCancelPanorama");
        applicationInterface.stopPanorama(true);
    }

    public void clickedCycleRaw(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedCycleRaw");

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String new_value = null;
        switch( sharedPreferences.getString(PreferenceKeys.RawPreferenceKey, "preference_raw_no") ) {
            case "preference_raw_no":
                new_value = "preference_raw_yes";
                break;
            case "preference_raw_yes":
                new_value = "preference_raw_only";
                break;
            case "preference_raw_only":
                new_value = "preference_raw_no";
                break;
            default:
                Log.e(TAG, "unrecognised raw preference");
                break;
        }
        if( new_value != null ) {
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putString(PreferenceKeys.RawPreferenceKey, new_value);
            editor.apply();

            mainUI.updateCycleRawIcon();
            applicationInterface.getDrawPreview().updateSettings();
            preview.reopenCamera(); // needed for RAW options to take effect
        }
    }

    public void clickedStoreLocation(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedStoreLocation");
        boolean value = applicationInterface.getGeotaggingPref();
        value = !value;

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(PreferenceKeys.LocationPreferenceKey, value);
        editor.apply();

        mainUI.updateStoreLocationIcon();
        applicationInterface.getDrawPreview().updateSettings(); // because we cache the geotagging setting
        initLocation(); // required to enable or disable GPS, also requests permission if necessary
        this.closePopup();

        String message = getResources().getString(R.string.preference_location) + ": " + getResources().getString(value ? R.string.on : R.string.off);
        preview.showToast(store_location_toast, message, true);
    }

    public void clickedTextStamp(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedTextStamp");
        this.closePopup();

        AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
        alertDialog.setTitle(R.string.preference_textstamp);

        final View dialog_view = LayoutInflater.from(this).inflate(R.layout.alertdialog_edittext, null);
        final EditText editText = dialog_view.findViewById(R.id.edit_text);
        // set hint instead of content description for EditText, see https://support.google.com/accessibility/android/answer/6378120
        editText.setHint(getResources().getString(R.string.preference_textstamp));
        editText.setText(applicationInterface.getTextStampPref());
        alertDialog.setView(dialog_view);
        alertDialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                if( MyDebug.LOG )
                    Log.d(TAG, "custom text stamp clicked okay");

                String custom_text = editText.getText().toString();
                SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString(PreferenceKeys.TextStampPreferenceKey, custom_text);
                editor.apply();

                mainUI.updateTextStampIcon();
            }
        });
        alertDialog.setNegativeButton(android.R.string.cancel, null);

        final AlertDialog alert = alertDialog.create();
        alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface arg0) {
                if( MyDebug.LOG )
                    Log.d(TAG, "custom stamp text dialog dismissed");
                setWindowFlagsForCamera();
                showPreview(true);
            }
        });

        showPreview(false);
        setWindowFlagsForSettings();
        showAlert(alert);
    }

    public void clickedStamp(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedStamp");

        this.closePopup();

        boolean value = applicationInterface.getStampPref().equals("preference_stamp_yes");
        value = !value;
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(PreferenceKeys.StampPreferenceKey, value ? "preference_stamp_yes" : "preference_stamp_no");
        editor.apply();

        mainUI.updateStampIcon();
        applicationInterface.getDrawPreview().updateSettings();
        preview.showToast(stamp_toast, value ? R.string.stamp_enabled : R.string.stamp_disabled, true);
    }

    public void clickedFocusPeaking(View view) {
        clickedFocusPeaking();
    }

    public void clickedFocusPeaking() {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedFocusPeaking");
        boolean value = applicationInterface.getFocusPeakingPref();
        value = !value;

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(PreferenceKeys.FocusPeakingPreferenceKey, value ? "preference_focus_peaking_on" : "preference_focus_peaking_off");
        editor.apply();

        mainUI.updateFocusPeakingIcon();
        applicationInterface.getDrawPreview().updateSettings(); // needed to update focus peaking
    }

    public void clickedAutoLevel(View view) {
        clickedAutoLevel();
    }

    public void clickedAutoLevel() {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedAutoLevel");
        boolean value = applicationInterface.getAutoStabilisePref();
        value = !value;

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(PreferenceKeys.AutoStabilisePreferenceKey, value);
        editor.apply();

        boolean done_dialog = false;
        if( value ) {
            boolean done_auto_stabilise_info = sharedPreferences.contains(PreferenceKeys.AutoStabiliseInfoPreferenceKey);
            if( !done_auto_stabilise_info ) {
                mainUI.showInfoDialog(R.string.preference_auto_stabilise, R.string.auto_stabilise_info, PreferenceKeys.AutoStabiliseInfoPreferenceKey);
                done_dialog = true;
            }
        }

        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, true);
        }

        mainUI.updateAutoLevelIcon();
        applicationInterface.getDrawPreview().updateSettings(); // because we cache the auto-stabilise setting
        this.closePopup();
    }

    public void clickedCycleFlash(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedCycleFlash");

        preview.cycleFlash(true, true);
        mainUI.updateCycleFlashIcon();
    }

    public void clickedFaceDetection(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedFaceDetection");

        this.closePopup();

        boolean value = applicationInterface.getFaceDetectionPref();
        value = !value;
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(PreferenceKeys.FaceDetectionPreferenceKey, value);
        editor.apply();

        mainUI.updateFaceDetectionIcon();
        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();
    }

    public void clickedAudioControl(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedAudioControl");
        // check hasAudioControl just in case!
        if( !hasAudioControl() ) {
            if( MyDebug.LOG )
                Log.e(TAG, "clickedAudioControl, but hasAudioControl returns false!");
            return;
        }
        this.closePopup();
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String audio_control = sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none");
        /*if( audio_control.equals("voice") && speechControl.hasSpeechRecognition() ) {
            if( speechControl.isStarted() ) {
                speechControl.stopListening();
            }
            else {
                boolean has_audio_permission = true;
                if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
                    // we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener()
                    if( MyDebug.LOG )
                        Log.d(TAG, "check for record audio permission");
                    if( ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "record audio permission not available");
                        applicationInterface.requestRecordAudioPermission();
                        has_audio_permission = false;
                    }
                }
                if( has_audio_permission ) {
                    speechControl.showToast(true);
                    speechControl.startSpeechRecognizerIntent();
                    speechControl.speechRecognizerStarted();
                }
            }
        }
        else*/ if( audio_control.equals("noise") ){
            if( audio_listener != null ) {
                freeAudioListener(false);
            }
            else {
                startAudioListener();
            }
        }
    }

    /* Returns the cameraId that the "Switch camera" button will switch to.
     * Note that this may not necessarily be the next camera ID, on multi camera devices (if
     * isMultiCamEnabled() returns true).
     */
    public int getNextCameraId() {
        if( MyDebug.LOG )
            Log.d(TAG, "getNextCameraId");
        int cameraId = getActualCameraId();
        if( MyDebug.LOG )
            Log.d(TAG, "current cameraId: " + cameraId);
        if( this.preview.canSwitchCamera() ) {
            if( isMultiCamEnabled() ) {
                // don't use preview.getCameraController(), as it may be null if user quickly switches between cameras
                switch( preview.getCameraControllerManager().getFacing(cameraId) ) {
                    case FACING_BACK:
                        if( !front_camera_ids.isEmpty() )
                            cameraId = front_camera_ids.get(0);
                        else if( !other_camera_ids.isEmpty() )
                            cameraId = other_camera_ids.get(0);
                        break;
                    case FACING_FRONT:
                        if( !other_camera_ids.isEmpty() )
                            cameraId = other_camera_ids.get(0);
                        else if( !back_camera_ids.isEmpty() )
                            cameraId = back_camera_ids.get(0);
                        break;
                    default:
                        if( !back_camera_ids.isEmpty() )
                            cameraId = back_camera_ids.get(0);
                        else if( !front_camera_ids.isEmpty() )
                            cameraId = front_camera_ids.get(0);
                        break;
                }
            }
            else {
                int n_cameras = preview.getCameraControllerManager().getNumberOfCameras();
                cameraId = (cameraId+1) % n_cameras;
            }
        }
        if( MyDebug.LOG )
            Log.d(TAG, "next cameraId: " + cameraId);
        return cameraId;
    }

    /* Returns the next cameraId with the same-facing as current camera.
     * Should only be called if isMultiCamEnabled() returns true.
     * Only used for testing, now that we bring up a menu instead of cycling.
     */
    /*public int testGetNextMultiCameraId() {
        if( MyDebug.LOG )
            Log.d(TAG, "testGetNextMultiCameraId");
        if( !isMultiCamEnabled() ) {
            Log.e(TAG, "testGetNextMultiCameraId() called but not in multi-cam mode");
            throw new RuntimeException("testGetNextMultiCameraId() called but not in multi-cam mode");
        }
        List<Integer> camera_set;
        // don't use preview.getCameraController(), as it may be null if user quickly switches between cameras
        int currCameraId = getActualCameraId();
        switch( preview.getCameraControllerManager().getFacing(currCameraId) ) {
            case FACING_BACK:
                camera_set = back_camera_ids;
                break;
            case FACING_FRONT:
                camera_set = front_camera_ids;
                break;
            default:
                camera_set = other_camera_ids;
                break;
        }
        int cameraId;
        int indx = camera_set.indexOf(currCameraId);
        if( indx == -1 ) {
            Log.e(TAG, "camera id not in current camera set");
            // this shouldn't happen, but if it does, revert to the first camera id in the set
            // update: oddly had reports of IndexOutOfBoundsException crashes from Google Play from camera_set.get(0)
            // because of camera_set having length 0, so stick with currCameraId in such cases
            if( camera_set.size() == 0 ) {
                Log.e(TAG, "camera_set is empty");
                cameraId = currCameraId;
            }
            else
                cameraId = camera_set.get(0);
        }
        else {
            indx = (indx+1) % camera_set.size();
            cameraId = camera_set.get(indx);
        }
        if( MyDebug.LOG )
            Log.d(TAG, "next multi cameraId: " + cameraId);
        return cameraId;
    }*/

    private void pushCameraIdToast(int cameraId, String cameraIdSPhysical) {
        if( MyDebug.LOG )
            Log.d(TAG, "pushCameraIdToast: " + cameraId);
        if( preview.getCameraControllerManager().getNumberOfCameras() > 2 || cameraIdSPhysical != null ) {
            // telling the user which camera is pointless for only two cameras, but on devices that now
            // expose many cameras it can be confusing, so show a toast to at least display the id
            // similarly we want to show a toast if using a physical camera, so user doesn't forget
            String description = cameraIdSPhysical != null ?
                    preview.getCameraControllerManager().getDescription(null, this, cameraIdSPhysical, true, true) :
                    preview.getCameraControllerManager().getDescription(this, cameraId);
            if( description != null ) {
                String toast_string = description;
                if( cameraIdSPhysical == null ) // only add the ID if not a physical camera
                    toast_string += ": " + getResources().getString(R.string.camera_id) + " " + cameraId;
                //preview.showToast(null, toast_string);
                this.push_info_toast_text = toast_string;
            }
        }
    }

    public void userSwitchToCamera(int cameraId, String cameraIdSPhysical) {
        if( MyDebug.LOG )
            Log.d(TAG, "userSwitchToCamera: " + cameraId + " / " + cameraIdSPhysical);
        View switchCameraButton = findViewById(R.id.switch_camera);
        View switchMultiCameraButton = findViewById(R.id.switch_multi_camera);
        // prevent slowdown if user repeatedly clicks:
        switchCameraButton.setEnabled(false);
        switchMultiCameraButton.setEnabled(false);
        applicationInterface.reset(true);
        this.getApplicationInterface().getDrawPreview().setDimPreview(true);
        if( this.switch_multi_camera_dialog != null ) {
            // only clear if switching to a different camera ID (switching between lenses is fine)
            int curr_camera_id = getActualCameraId();
            if( MyDebug.LOG )
                Log.d(TAG, "curr_camera_id: " + curr_camera_id);
            if( cameraId != curr_camera_id ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "clear switch_multi_camera_dialog");
                this.switch_multi_camera_dialog = null;
            }
            else {
                if( MyDebug.LOG )
                    Log.d(TAG, "keep switch_multi_camera_dialog");
            }
        }
        this.preview.setCamera(cameraId, cameraIdSPhysical);
        switchCameraButton.setEnabled(true);
        switchMultiCameraButton.setEnabled(true);
        // no need to call mainUI.setSwitchCameraContentDescription - this will be called from Preview.cameraSetup when the
        // new camera is opened
        this.announceCameraForAccessibility(cameraId, cameraIdSPhysical);
    }

    /**
     * Selects the next camera on the phone - in practice, switches between
     * front and back cameras
     */
    public void clickedSwitchCamera(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedSwitchCamera");
        if( preview.isOpeningCamera() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "already opening camera in background thread");
            return;
        }
        this.closePopup();
        if( this.preview.canSwitchCamera() ) {
            int cameraId = getNextCameraId();
            if( !isMultiCamEnabled() ) {
                pushCameraIdToast(cameraId, null);
            }
            else {
                // In multi-cam mode, no need to show the toast when just switching between front and back cameras.
                // But it is useful to clear an active fake toast, otherwise have issue if the user uses
                // clickedSwitchMultiCamera() (which displays a fake toast for the camera via the info toast), then
                // immediately uses clickedSwitchCamera() - the toast for the wrong camera will still be lingering
                // until it expires, which looks a bit strange.
                // (If using non-fake toasts, this isn't an issue, at least on Android 10+, as now toasts seem to
                // disappear when the user touches the screen anyway.)
                preview.clearActiveFakeToast();
            }
            userSwitchToCamera(cameraId, null);

            push_switched_camera = true;
        }
    }

    /** Returns list of logical cameras with same facing as the supplied camera_id.
     */
    public List<Integer> getSameFacingLogicalCameras(int camera_id) {
        List<Integer> logical_camera_ids = new ArrayList<>();
        CameraController.Facing this_facing = preview.getCameraControllerManager().getFacing(camera_id);
        for(int i=0;i<preview.getCameraControllerManager().getNumberOfCameras();i++) {
            if( preview.getCameraControllerManager().getFacing(i) != this_facing ) {
                // only show cameras with same facing
                continue;
            }
            logical_camera_ids.add(i);
        }
        return logical_camera_ids;
    }

    private AlertDialog switch_multi_camera_dialog;

    private AlertDialog createSwitchMultiCameraDialog() {
        if( MyDebug.LOG )
            Log.d(TAG, "createSwitchMultiCameraDialog");
        long debug_time = 0;
        if( MyDebug.LOG ) {
            debug_time = System.currentTimeMillis();
        }

        AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
        alertDialog.setTitle(R.string.choose_camera);

        int curr_camera_id = getActualCameraId();
        List<Integer> logical_camera_ids = getSameFacingLogicalCameras(curr_camera_id);
        if( MyDebug.LOG )
            Log.d(TAG, "createSwitchMultiCameraDialog: time after logical_camera_ids: " + (System.currentTimeMillis() - debug_time));

        int n_logical_cameras = logical_camera_ids.size();
        int n_cameras = n_logical_cameras;
        if( preview.hasPhysicalCameras() ) {
            n_cameras += preview.getPhysicalCameras().size();
            //n_cameras++; // for the info message
        }
        CharSequence [] items = new CharSequence[n_cameras];
        int [] items_logical_camera_id = new int[n_cameras];
        String [] items_physical_camera_id = new String[n_cameras];
        int index=0;
        int selected=-1;
        String curr_physical_camera_id = applicationInterface.getCameraIdSPhysicalPref();
        for(int i=0;i<n_logical_cameras;i++) {
            int logical_camera_id = logical_camera_ids.get(i);
            if( MyDebug.LOG )
                Log.d(TAG, "createSwitchMultiCameraDialog: time before getDescription: " + (System.currentTimeMillis() - debug_time));
            String camera_name = logical_camera_id + ": " + preview.getCameraControllerManager().getDescription(this, logical_camera_id);
            if( MyDebug.LOG )
                Log.d(TAG, "createSwitchMultiCameraDialog: time after getDescription: " + (System.currentTimeMillis() - debug_time));
            if( logical_camera_id == curr_camera_id ) {
                // this is the current logical camera
                if( preview.hasPhysicalCameras() ) {
                    camera_name +=  " (" + getResources().getString(R.string.auto_lens) + ")";
                }
                if( curr_physical_camera_id == null ) {
                    // the logical camera is being used directly
                    selected = index;
                    //String html_camera_name = "<b>[" + camera_name + "]</b>";
                    String html_camera_name = "<b>" + camera_name + "</b>";
                    if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
                        items[index] = Html.fromHtml(html_camera_name, Html.FROM_HTML_MODE_LEGACY);
                    }
                    else {
                        items[index] = Html.fromHtml(html_camera_name);
                    }
                }
                else {
                    // a physical camera is in use, so don't bold this entry
                    items[index] = camera_name;
                }
                items_logical_camera_id[index] = logical_camera_id;
                items_physical_camera_id[index] = null;
                index++;

                if( preview.hasPhysicalCameras() ) {
                    // also add the physical cameras that underlie the current logical camera
                    Set<String> physical_camera_ids = preview.getPhysicalCameras();

                    // sort by view angle
                    class PhysicalCamera {
                        private final String id;
                        private final String description;
                        private final SizeF view_angle;

                        private PhysicalCamera(String id) {
                            this.id = id;
                            CameraControllerManager.CameraInfo info = new CameraControllerManager.CameraInfo();
                            this.description = preview.getCameraControllerManager().getDescription(info, MainActivity.this, id, false, true);
                            this.view_angle = info.view_angle;
                        }
                    }
                    ArrayList<PhysicalCamera> physical_cameras = new ArrayList<>();
                    for(String physical_id : physical_camera_ids) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "createSwitchMultiCameraDialog: time before getDescription: " + (System.currentTimeMillis() - debug_time));
                        physical_cameras.add(new PhysicalCamera(physical_id));
                        if( MyDebug.LOG )
                            Log.d(TAG, "createSwitchMultiCameraDialog: time after getDescription: " + (System.currentTimeMillis() - debug_time));
                    }
                    {
                        Collections.sort(physical_cameras, new Comparator<>() {
                            @Override
                            public int compare(PhysicalCamera o1, PhysicalCamera o2) {
                                float diff = o2.view_angle.getWidth() - o1.view_angle.getWidth();
                                if( Math.abs(diff) < 1.0e-5f )
                                    return 0;
                                else if( diff > 0.0f )
                                    return 1;
                                else
                                    return -1;
                            }
                        });
                    }

                    int j=0;
                    String indent = "&nbsp;&nbsp;&nbsp;&nbsp;";
                    for(PhysicalCamera physical_camera : physical_cameras) {
                        String physical_id = physical_camera.id;
                        camera_name = getResources().getString(R.string.lens) + " " + j + ": " + physical_camera.description;
                        String html_camera_name;
                        if( curr_physical_camera_id != null && curr_physical_camera_id.equals(physical_id) ) {
                            // this is the current physical camera
                            selected = index;
                            //html_camera_name = indent + "<b>[" + camera_name + "]</b>";
                            html_camera_name = indent + "<b>" + camera_name + "</b>";
                        }
                        else {
                            html_camera_name = indent + camera_name;
                        }
                        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
                            items[index] = Html.fromHtml(html_camera_name, Html.FROM_HTML_MODE_LEGACY);
                        }
                        else {
                            items[index] = Html.fromHtml(html_camera_name);
                        }
                        items_logical_camera_id[index] = logical_camera_id;
                        items_physical_camera_id[index] = physical_id;
                        index++;

                        j++;
                    }
                }
            }
            else {
                items[index] = camera_name;
                items_logical_camera_id[index] = logical_camera_id;
                items_physical_camera_id[index] = null;
                index++;
            }
        }
        /*if( preview.hasPhysicalCameras() ) {
            items[index] = getResources().getString(R.string.physical_cameras_info);
            items_logical_camera_id[index] = -1;
            items_physical_camera_id[index] = null;
            //index++;
        }*/
        if( MyDebug.LOG )
            Log.d(TAG, "createSwitchMultiCameraDialog: time after building menu: " + (System.currentTimeMillis() - debug_time));

        //alertDialog.setItems(items, new DialogInterface.OnClickListener() {
        alertDialog.setSingleChoiceItems(items, selected, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if( MyDebug.LOG )
                    Log.d(TAG, "selected: " + which);
                int logical_camera = items_logical_camera_id[which];
                String physical_camera = items_physical_camera_id[which];
                if( MyDebug.LOG ) {
                    Log.d(TAG, "logical_camera: " + logical_camera);
                    Log.d(TAG, "physical_camera: " + physical_camera);
                }
                int n_cameras = preview.getCameraControllerManager().getNumberOfCameras();
                if( logical_camera >= 0 && logical_camera < n_cameras ) {
                    if( preview.isOpeningCamera() ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "already opening camera in background thread");
                        return;
                    }
                    MainActivity.this.closePopup();
                    if( MainActivity.this.preview.canSwitchCamera() ) {
                        pushCameraIdToast(logical_camera, physical_camera);
                        userSwitchToCamera(logical_camera, physical_camera);
                    }
                }
                //setWindowFlagsForCamera();
                //showPreview(true);
                dialog.dismiss(); // need to explicitly dismiss for setSingleChoiceItems
            }
        });
        if( MyDebug.LOG )
            Log.d(TAG, "createSwitchMultiCameraDialog: time after setting items: " + (System.currentTimeMillis() - debug_time));
        /*alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface arg0) {
                setWindowFlagsForCamera();
                showPreview(true);
            }
        });*/
        //setWindowFlagsForSettings(false); // set set_lock_protect to false - no need to protect this dialog with lock screen (fine to run above lock screen if that option is set)
        //showAlert(alertDialog.create());
        AlertDialog dialog = alertDialog.create();
        if( MyDebug.LOG )
            Log.d(TAG, "createSwitchMultiCameraDialog: time after dialog create: " + (System.currentTimeMillis() - debug_time));
        if( preview.hasPhysicalCameras() ) {
            TextView footer = new TextView(this);
            footer.setText(R.string.physical_cameras_info);
            final float scale = getResources().getDisplayMetrics().density;
            final int padding = (int) (5 * scale + 0.5f); // convert dps to pixels
            footer.setPadding(padding, padding, padding, padding);
            dialog.getListView().addFooterView(footer, null, false);
            if( MyDebug.LOG )
                Log.d(TAG, "createSwitchMultiCameraDialog: time after adding footer: " + (System.currentTimeMillis() - debug_time));
        }
        if( dialog.getWindow() != null ) {
            dialog.getWindow().setWindowAnimations(R.style.DialogAnimation);
        }
        return dialog;
    }

    /** User can long-click on switch multi cam icon to bring up a menu to switch to any camera.
     *  Update: from v1.53 onwards with support for exposing physical lens, we always call this with
     *  a regular click on the switch multi cam icon.
     */
    public void clickedSwitchMultiCamera(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedSwitchMultiCamera");

        long debug_time = 0;
        if( MyDebug.LOG ) {
            debug_time = System.currentTimeMillis();
        }
        //showPreview(false);
        //AlertDialog dialog = createSwitchMultiCameraDialog();
        if( switch_multi_camera_dialog == null ) {
            switch_multi_camera_dialog = createSwitchMultiCameraDialog();
        }
        AlertDialog dialog = switch_multi_camera_dialog;
        if( MyDebug.LOG )
            Log.d(TAG, "clickedSwitchMultiCamera: time before showing dialog: " + (System.currentTimeMillis() - debug_time));
        dialog.show();
        if( MyDebug.LOG )
            Log.d(TAG, "clickedSwitchMultiCamera: total time: " + (System.currentTimeMillis() - debug_time));
    }

    /**
     * Toggles Photo/Video mode
     */
    public void clickedSwitchVideo(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedSwitchVideo");
        this.closePopup();
        mainUI.destroyPopup(); // important as we don't want to use a cached popup, as we can show different options depending on whether we're in photo or video mode

        // In practice stopping the gyro sensor shouldn't be needed as (a) we don't show the switch
        // photo/video icon when recording, (b) at the time of writing switching to video mode
        // reopens the camera, which will stop panorama recording anyway, but we do this just to be
        // safe.
        applicationInterface.stopPanorama(true);

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

        mainUI.setTakePhotoIcon();
        mainUI.setPopupIcon(); // needed as turning to video mode or back can turn flash mode off or back on

        // ensure icons invisible if they're affected by being in video mode or not (e.g., on-screen RAW icon)
        // (if enabling them, we'll make the icon visible later on)
        checkDisableGUIIcons();

        if( !block_startup_toast ) {
            this.showPhotoVideoToast(true);
        }
    }

    public void clickedWhiteBalanceLock(View view) {
        if( MyDebug.LOG )
            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, true);
    }

    public void clickedExposureLock(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedExposureLock");
        this.preview.toggleExposureLock();
        mainUI.updateExposureLockIcon();
        preview.showToast(exposure_lock_toast, preview.isExposureLocked() ? R.string.exposure_locked : R.string.exposure_unlocked, true);
    }

    public void clickedExposure(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedExposure");
        mainUI.toggleExposureUI();
    }

    public void clickedSettings(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedSettings");
        KeyguardUtils.requireKeyguard(this, this::openSettings);
    }

    public boolean popupIsOpen() {
        return mainUI.popupIsOpen();
    }

    // for testing
    public View getUIButton(String key) {
        return mainUI.getUIButton(key);
    }

    public void closePopup() {
        mainUI.closePopup();
    }

    public Bitmap getPreloadedBitmap(int resource) {
        return this.preloaded_bitmap_resources.get(resource);
    }

    public void clickedPopupSettings(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedPopupSettings");
        mainUI.togglePopupSettings();
    }

    private final PreferencesListener preferencesListener = new PreferencesListener();

    /** Keeps track of changes to SharedPreferences.
     */
    class PreferencesListener implements SharedPreferences.OnSharedPreferenceChangeListener {
        private static final String TAG = "PreferencesListener";

        private boolean any_significant_change; // whether any changes that require updateForSettings have been made since startListening()
        private boolean any_change; // whether any changes have been made since startListening()

        void startListening() {
            if( MyDebug.LOG )
                Log.d(TAG, "startListening");
            any_significant_change = false;
            any_change = false;

            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
            // n.b., registerOnSharedPreferenceChangeListener warns that we must keep a reference to the listener (which
            // is this class) as long as we want to listen for changes, otherwise the listener may be garbage collected!
            sharedPreferences.registerOnSharedPreferenceChangeListener(this);
        }

        void stopListening() {
            if( MyDebug.LOG )
                Log.d(TAG, "stopListening");
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
            sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
        }

        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            if( MyDebug.LOG )
                Log.d(TAG, "onSharedPreferenceChanged: " + key);

            if( key == null ) {
                // on Android 11+, when targetting Android 11+, this method is called with key==null
                // if preferences are cleared (see testSettings(), or when doing "Reset settings")
                return;
            }

            any_change = true;

            switch( key ) {
                // we whitelist preferences where we're sure that we don't need to call updateForSettings() if they've changed
                //case "preference_face_detection": // need to update camera controller
                case "preference_timer":
                case "preference_burst_mode":
                case "preference_burst_interval":
                case "preference_touch_capture":
                case "preference_pause_preview":
                case "preference_shutter_sound":
                case "preference_timer_beep":
                case "preference_timer_speak":
                case "preference_volume_keys":
                    //case "preference_audio_control": // need to update the UI
                case "preference_audio_noise_control_sensitivity":
                    //case "preference_enable_remote": // handled below
                    //case "preference_remote_type":
                    //case "preference_remote_device_name": // handled below
                    //case "preference_remote_disconnect_screen_dim":
                    //case "preference_water_type": // handled below
                case "preference_lock_orientation":
                    //case "preference_save_location": // we could probably whitelist this, but accessed it a lot of places...
                case "preference_using_saf":
                case "preference_save_photo_prefix":
                case "preference_save_video_prefix":
                case "preference_save_zulu_time":
                case "preference_show_when_locked":
                case "preference_startup_focus":
                    //case "preference_preview_size": // need to update preview
                    //case "preference_ghost_image": // don't whitelist this, as may need to reload ghost image (at fullscreen resolution) if "last" is enabled
                case "ghost_image_alpha":
                case "preference_focus_assist":
                case "preference_show_zoom":
                case "preference_show_angle":
                case "preference_show_angle_line":
                case "preference_show_pitch_lines":
                case "preference_angle_highlight_color":
                    //case "preference_show_geo_direction": // don't whitelist these, as if enabled we need to call checkMagneticAccuracy()
                    //case "preference_show_geo_direction_lines": // as above
                case "preference_show_battery":
                case "preference_show_time":
                case "preference_free_memory":
                case "preference_show_iso":
                case "preference_histogram":
                case "preference_zebra_stripes":
                case "preference_zebra_stripes_foreground_color":
                case "preference_zebra_stripes_background_color":
                case "preference_focus_peaking":
                case "preference_focus_peaking_color":
                case "preference_show_video_max_amp":
                case "preference_grid":
                case "preference_crop_guide":
                case "preference_thumbnail_animation":
                case "preference_take_photo_border":
                    //case "preference_rotate_preview": // need to update the Preview
                    //case "preference_ui_placement": // need to update the UI
                    //case "preference_immersive_mode": // probably could whitelist?
                    //case "preference_show_face_detection": // need to update the UI
                    //case "preference_show_cycle_flash": // need to update the UI
                    //case "preference_show_auto_level": // need to update the UI
                    //case "preference_show_stamp": // need to update the UI
                    //case "preference_show_textstamp": // need to update the UI
                    //case "preference_show_store_location": // need to update the UI
                    //case "preference_show_cycle_raw": // need to update the UI
                    //case "preference_show_white_balance_lock": // need to update the UI
                    //case "preference_show_exposure_lock": // need to update the UI
                    //case "preference_show_zoom_slider_controls": // need to update the UI
                    //case "preference_show_take_photo": // need to update the UI
                case "preference_show_toasts":
                case "preference_show_whats_new":
                //case "preference_multi_cam_button": // need to update the UI
                case "preference_keep_display_on":
                case "preference_max_brightness":
                    //case "preference_resolution": // need to set up camera controller and preview
                    //case "preference_quality": // need to set up camera controller
                    //case "preference_image_format": // need to set up camera controller (as it can affect the image quality that we set)
                    //case "preference_raw": // need to update as it affects how we set up camera controller
                    //case "preference_raw_expo_bracketing": // as above
                    //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
                case "preference_panorama_crop":
                    //case "preference_panorama_save": // we could probably whitelist this, but have not done so in case in future we allow RAW to be saved for the base images
                case "preference_front_camera_mirror":
                case "preference_exif_artist":
                case "preference_exif_copyright":
                case "preference_stamp":
                case "preference_stamp_dateformat":
                case "preference_stamp_timeformat":
                case "preference_stamp_gpsformat":
                case "preference_stamp_geo_address":
                case "preference_units_distance":
                case "preference_textstamp":
                case "preference_stamp_fontsize":
                case "preference_stamp_font_color":
                case "preference_stamp_style":
                    //case "preference_camera2_fake_flash": // need to update camera controller
                    //case "preference_camera2_fast_burst": // could probably whitelist?
                    //case "preference_camera2_photo_video_recording": // need to update camera controller
                case "preference_background_photo_saving":
                    //case "preference_video_quality": // need to update camera controller and preview
                    //case "preference_video_stabilization": // need to update camera controller
                    //case "preference_video_output_format": // could probably whitelist, but safest to restart camera
                    //case "preference_video_log": // need to update camera controller
                    //case "preference_video_profile_gamma": // as above
                    //case "preference_video_max_duration": // could probably whitelist, but safest to restart camera
                    //case "preference_video_restart": // could probably whitelist, but safest to restart camera
                    //case "preference_video_max_filesize": // could probably whitelist, but safest to restart camera
                    //case "preference_video_restart_max_filesize": // could probably whitelist, but safest to restart camera
                case "preference_record_audio":
                case "preference_record_audio_src":
                case "preference_record_audio_channels":
                case "preference_lock_video":
                case "preference_video_subtitle":
                    //case "preference_video_bitrate": // could probably whitelist, but safest to restart camera
                    //case "preference_video_fps": // could probably whitelist, but safest to restart camera
                    //case "preference_force_video_4k": // could probably whitelist, but safest to restart camera
                case "preference_video_low_power_check":
                case "preference_video_flash":
                    //case "preference_location": // need to enable/disable gps listeners etc
                    //case "preference_gps_direction": // need to update listeners
                case "preference_require_location":
                    //case "preference_antibanding": // need to set up camera controller
                    //case "preference_edge_mode": // need to set up camera controller
                    //case "preference_noise_reduction_mode": // need to set up camera controller
                    //case "preference_camera_api": // no point whitelisting as we restart anyway
                    if( MyDebug.LOG )
                        Log.d(TAG, "this change doesn't require update");
                    break;
                case PreferenceKeys.EnableRemote:
                    bluetoothRemoteControl.startRemoteControl();
                    break;
                case PreferenceKeys.RemoteName:
                    // The remote address changed, restart the service
                    if (bluetoothRemoteControl.remoteEnabled())
                        bluetoothRemoteControl.stopRemoteControl();
                    bluetoothRemoteControl.startRemoteControl();
                    break;
                case PreferenceKeys.WaterType:
                    boolean wt = sharedPreferences.getBoolean(PreferenceKeys.WaterType, true);
                    mWaterDensity = wt ? WATER_DENSITY_SALTWATER : WATER_DENSITY_FRESHWATER;
                    break;
                default:
                    if( MyDebug.LOG )
                        Log.d(TAG, "this change does require update");
                    any_significant_change = true;
                    break;
            }
        }

        boolean anyChange() {
            return any_change;
        }

        boolean anySignificantChange() {
            return any_significant_change;
        }
    }

    public void openSettings() {
        if( MyDebug.LOG )
            Log.d(TAG, "openSettings");
        closePopup(); // important to close the popup to avoid confusing with back button callbacks
        preview.cancelTimer(); // best to cancel any timer, in case we take a photo while settings window is open, or when changing settings
        preview.cancelRepeat(); // similarly cancel the auto-repeat mode!
        preview.stopVideo(false); // important to stop video, as we'll be changing camera parameters when the settings window closes
        applicationInterface.stopPanorama(true); // important to stop panorama recording, as we might end up as we'll be changing camera parameters when the settings window closes
        stopAudioListeners();
        // close back handler callbacks (so back button is enabled again when going to settings) - in theory shouldn't be needed as all of these should
        // be disabled now, but just in case:
        this.enablePopupOnBackPressedCallback(false);
        this.enablePausePreviewOnBackPressedCallback(false);
        this.enableScreenLockOnBackPressedCallback(false);

        Bundle bundle = new Bundle();
        bundle.putBoolean("edge_to_edge_mode", edge_to_edge_mode);
        bundle.putInt("cameraId", this.preview.getCameraId());
        bundle.putString("cameraIdSPhysical", this.applicationInterface.getCameraIdSPhysicalPref());
        bundle.putInt("nCameras", preview.getCameraControllerManager().getNumberOfCameras());
        bundle.putBoolean("camera_open", this.preview.getCameraController() != null);
        bundle.putString("camera_api", this.preview.getCameraAPI());
        bundle.putBoolean("using_android_l", this.preview.usingCamera2API());
        if( this.preview.getCameraController() != null ) {
            bundle.putInt("camera_orientation", this.preview.getCameraController().getCameraOrientation());
        }
        bundle.putString("photo_mode_string", getPhotoModeString(applicationInterface.getPhotoMode(), true));
        bundle.putBoolean("supports_auto_stabilise", this.supports_auto_stabilise);
        bundle.putBoolean("supports_flash", this.preview.supportsFlash());
        bundle.putBoolean("supports_force_video_4k", this.supports_force_video_4k);
        bundle.putBoolean("supports_camera2", this.supports_camera2);
        bundle.putBoolean("supports_face_detection", this.preview.supportsFaceDetection());
        bundle.putBoolean("supports_jpeg_r", this.preview.supportsJpegR());
        bundle.putBoolean("supports_raw", this.preview.supportsRaw());
        bundle.putBoolean("supports_burst_raw", this.supportsBurstRaw());
        bundle.putBoolean("supports_optimise_focus_latency", this.supportsOptimiseFocusLatency());
        bundle.putBoolean("supports_preshots", this.supportsPreShots());
        bundle.putBoolean("supports_hdr", this.supportsHDR());
        bundle.putBoolean("supports_nr", this.supportsNoiseReduction());
        bundle.putBoolean("supports_panorama", this.supportsPanorama());
        bundle.putBoolean("has_gyro_sensors", applicationInterface.getGyroSensor().hasSensors());
        bundle.putBoolean("supports_expo_bracketing", this.supportsExpoBracketing());
        bundle.putBoolean("supports_preview_bitmaps", this.supportsPreviewBitmaps());
        bundle.putInt("max_expo_bracketing_n_images", this.maxExpoBracketingNImages());
        bundle.putBoolean("supports_exposure_compensation", this.preview.supportsExposures());
        bundle.putInt("exposure_compensation_min", this.preview.getMinimumExposure());
        bundle.putInt("exposure_compensation_max", this.preview.getMaximumExposure());
        bundle.putBoolean("supports_iso_range", this.preview.supportsISORange());
        bundle.putInt("iso_range_min", this.preview.getMinimumISO());
        bundle.putInt("iso_range_max", this.preview.getMaximumISO());
        bundle.putBoolean("supports_exposure_time", this.preview.supportsExposureTime());
        bundle.putBoolean("supports_exposure_lock", this.preview.supportsExposureLock());
        bundle.putBoolean("supports_white_balance_lock", this.preview.supportsWhiteBalanceLock());
        bundle.putLong("exposure_time_min", this.preview.getMinimumExposureTime());
        bundle.putLong("exposure_time_max", this.preview.getMaximumExposureTime());
        bundle.putBoolean("supports_white_balance_temperature", this.preview.supportsWhiteBalanceTemperature());
        bundle.putInt("white_balance_temperature_min", this.preview.getMinimumWhiteBalanceTemperature());
        bundle.putInt("white_balance_temperature_max", this.preview.getMaximumWhiteBalanceTemperature());
        bundle.putBoolean("is_multi_cam", this.is_multi_cam);
        bundle.putBoolean("has_physical_cameras", this.preview.hasPhysicalCameras());
        bundle.putBoolean("supports_optical_stabilization", this.preview.supportsOpticalStabilization());
        bundle.putBoolean("optical_stabilization_enabled", this.preview.getOpticalStabilization());
        bundle.putBoolean("supports_video_stabilization", this.preview.supportsVideoStabilization());
        bundle.putBoolean("video_stabilization_enabled", this.preview.getVideoStabilization());
        bundle.putBoolean("can_disable_shutter_sound", this.preview.canDisableShutterSound());
        bundle.putInt("tonemap_max_curve_points", this.preview.getTonemapMaxCurvePoints());
        bundle.putBoolean("supports_tonemap_curve", this.preview.supportsTonemapCurve());
        bundle.putBoolean("supports_photo_video_recording", this.preview.supportsPhotoVideoRecording());
        bundle.putFloat("camera_view_angle_x", preview.getViewAngleX(false));
        bundle.putFloat("camera_view_angle_y", preview.getViewAngleY(false));
        bundle.putFloat("min_zoom_factor", preview.getMinZoomRatio());
        bundle.putFloat("max_zoom_factor", preview.getMaxZoomRatio());

        putBundleExtra(bundle, "color_effects", this.preview.getSupportedColorEffects());
        putBundleExtra(bundle, "scene_modes", this.preview.getSupportedSceneModes());
        putBundleExtra(bundle, "white_balances", this.preview.getSupportedWhiteBalances());
        putBundleExtra(bundle, "isos", this.preview.getSupportedISOs());
        bundle.putInt("magnetic_accuracy", magneticSensor.getMagneticAccuracy());
        bundle.putString("iso_key", this.preview.getISOKey());
        if( this.preview.getCameraController() != null ) {
            bundle.putString("parameters_string", preview.getCameraController().getParametersString());
        }
        List<String> antibanding = this.preview.getSupportedAntiBanding();
        putBundleExtra(bundle, "antibanding", antibanding);
        if( antibanding != null ) {
            String [] entries_arr = new String[antibanding.size()];
            int i=0;
            for(String value: antibanding) {
                entries_arr[i] = getMainUI().getEntryForAntiBanding(value);
                i++;
            }
            bundle.putStringArray("antibanding_entries", entries_arr);
        }
        List<String> edge_modes = this.preview.getSupportedEdgeModes();
        putBundleExtra(bundle, "edge_modes", edge_modes);
        if( edge_modes != null ) {
            String [] entries_arr = new String[edge_modes.size()];
            int i=0;
            for(String value: edge_modes) {
                entries_arr[i] = getMainUI().getEntryForNoiseReductionMode(value);
                i++;
            }
            bundle.putStringArray("edge_modes_entries", entries_arr);
        }
        List<String> noise_reduction_modes = this.preview.getSupportedNoiseReductionModes();
        putBundleExtra(bundle, "noise_reduction_modes", noise_reduction_modes);
        if( noise_reduction_modes != null ) {
            String [] entries_arr = new String[noise_reduction_modes.size()];
            int i=0;
            for(String value: noise_reduction_modes) {
                entries_arr[i] = getMainUI().getEntryForNoiseReductionMode(value);
                i++;
            }
            bundle.putStringArray("noise_reduction_modes_entries", entries_arr);
        }

        List<CameraController.Size> preview_sizes = this.preview.getSupportedPreviewSizes();
        if( preview_sizes != null ) {
            int [] widths = new int[preview_sizes.size()];
            int [] heights = new int[preview_sizes.size()];
            int i=0;
            for(CameraController.Size size: preview_sizes) {
                widths[i] = size.width;
                heights[i] = size.height;
                i++;
            }
            bundle.putIntArray("preview_widths", widths);
            bundle.putIntArray("preview_heights", heights);
        }
        bundle.putInt("preview_width", preview.getCurrentPreviewSize().width);
        bundle.putInt("preview_height", preview.getCurrentPreviewSize().height);

        // Note that we set check_burst to false, as the Settings always displays all supported resolutions (along with the "saved"
        // resolution preference, even if that doesn't support burst and we're in a burst mode).
        // This is to be consistent with other preferences, e.g., we still show RAW settings even though that might not be supported
        // for the current photo mode.
        List<CameraController.Size> sizes = this.preview.getSupportedPictureSizes(false);
        if( sizes != null ) {
            int [] widths = new int[sizes.size()];
            int [] heights = new int[sizes.size()];
            boolean [] supports_burst = new boolean[sizes.size()];
            int i=0;
            for(CameraController.Size size: sizes) {
                widths[i] = size.width;
                heights[i] = size.height;
                supports_burst[i] = size.supports_burst;
                i++;
            }
            bundle.putIntArray("resolution_widths", widths);
            bundle.putIntArray("resolution_heights", heights);
            bundle.putBooleanArray("resolution_supports_burst", supports_burst);
        }
        if( preview.getCurrentPictureSize() != null ) {
            bundle.putInt("resolution_width", preview.getCurrentPictureSize().width);
            bundle.putInt("resolution_height", preview.getCurrentPictureSize().height);
        }

        //List<String> video_quality = this.preview.getVideoQualityHander().getSupportedVideoQuality();
        String fps_value = applicationInterface.getVideoFPSPref(); // n.b., this takes into account slow motion mode putting us into a high frame rate
        if( MyDebug.LOG )
            Log.d(TAG, "fps_value: " + fps_value);
        List<String> video_quality = this.preview.getSupportedVideoQuality(fps_value);
        if( video_quality == null || video_quality.isEmpty() ) {
            Log.e(TAG, "can't find any supported video sizes for current fps!");
            // fall back to unfiltered list
            video_quality = this.preview.getVideoQualityHander().getSupportedVideoQuality();
        }
        if( video_quality != null && this.preview.getCameraController() != null ) {
            String [] video_quality_arr = new String[video_quality.size()];
            String [] video_quality_string_arr = new String[video_quality.size()];
            int i=0;
            for(String value: video_quality) {
                video_quality_arr[i] = value;
                video_quality_string_arr[i] = this.preview.getCamcorderProfileDescription(value);
                i++;
            }
            bundle.putStringArray("video_quality", video_quality_arr);
            bundle.putStringArray("video_quality_string", video_quality_string_arr);

            boolean is_high_speed = this.preview.fpsIsHighSpeed(fps_value);
            bundle.putBoolean("video_is_high_speed", is_high_speed);
            String video_quality_preference_key = PreferenceKeys.getVideoQualityPreferenceKey(this.preview.getCameraId(), applicationInterface.getCameraIdSPhysicalPref(), is_high_speed);
            if( MyDebug.LOG )
                Log.d(TAG, "video_quality_preference_key: " + video_quality_preference_key);
            bundle.putString("video_quality_preference_key", video_quality_preference_key);
        }

        if( preview.getVideoQualityHander().getCurrentVideoQuality() != null ) {
            bundle.putString("current_video_quality", preview.getVideoQualityHander().getCurrentVideoQuality());
        }
        VideoProfile camcorder_profile = preview.getVideoProfile();
        bundle.putInt("video_frame_width", camcorder_profile.videoFrameWidth);
        bundle.putInt("video_frame_height", camcorder_profile.videoFrameHeight);
        bundle.putInt("video_bit_rate", camcorder_profile.videoBitRate);
        bundle.putInt("video_frame_rate", camcorder_profile.videoFrameRate);
        bundle.putDouble("video_capture_rate", camcorder_profile.videoCaptureRate);
        bundle.putBoolean("video_high_speed", preview.isVideoHighSpeed());
        bundle.putFloat("video_capture_rate_factor", applicationInterface.getVideoCaptureRateFactor());

        List<CameraController.Size> video_sizes = this.preview.getVideoQualityHander().getSupportedVideoSizes();
        if( video_sizes != null ) {
            int [] widths = new int[video_sizes.size()];
            int [] heights = new int[video_sizes.size()];
            int i=0;
            for(CameraController.Size size: video_sizes) {
                widths[i] = size.width;
                heights[i] = size.height;
                i++;
            }
            bundle.putIntArray("video_widths", widths);
            bundle.putIntArray("video_heights", heights);
        }

        // set up supported fps values
        if( preview.usingCamera2API() ) {
            // with Camera2, we know what frame rates are supported
            int [] candidate_fps = {15, 24, 25, 30, 60, 96, 100, 120, 240};
            List<Integer> video_fps = new ArrayList<>();
            List<Boolean> video_fps_high_speed = new ArrayList<>();
            for(int fps : candidate_fps) {
                if( preview.fpsIsHighSpeed(String.valueOf(fps)) ) {
                    video_fps.add(fps);
                    video_fps_high_speed.add(true);
                }
                else if( this.preview.getVideoQualityHander().videoSupportsFrameRate(fps) ) {
                    video_fps.add(fps);
                    video_fps_high_speed.add(false);
                }
            }
            int [] video_fps_array = new int[video_fps.size()];
            for(int i=0;i<video_fps.size();i++) {
                video_fps_array[i] = video_fps.get(i);
            }
            bundle.putIntArray("video_fps", video_fps_array);
            boolean [] video_fps_high_speed_array = new boolean[video_fps_high_speed.size()];
            for(int i=0;i<video_fps_high_speed.size();i++) {
                video_fps_high_speed_array[i] = video_fps_high_speed.get(i);
            }
            bundle.putBooleanArray("video_fps_high_speed", video_fps_high_speed_array);
        }
        else {
            // with old API, we don't know what frame rates are supported, so we make it up and let the user try
            // probably shouldn't allow 120fps, but we did in the past, and there may be some devices where this did work?
            int [] video_fps = {15, 24, 25, 30, 60, 96, 100, 120};
            bundle.putIntArray("video_fps", video_fps);
            boolean [] video_fps_high_speed_array = new boolean[video_fps.length];
            for(int i=0;i<video_fps.length;i++) {
                video_fps_high_speed_array[i] = false; // no concept of high speed frame rates in old API
            }
            bundle.putBooleanArray("video_fps_high_speed", video_fps_high_speed_array);
        }

        putBundleExtra(bundle, "flash_values", this.preview.getSupportedFlashValues());
        putBundleExtra(bundle, "focus_values", this.preview.getSupportedFocusValues());

        preferencesListener.startListening();

        showPreview(false);
        setWindowFlagsForSettings(); // important to do after passing camera info into bundle, since this will close the camera
        MyPreferenceFragment fragment = new MyPreferenceFragment();
        fragment.setArguments(bundle);
        // use commitAllowingStateLoss() instead of commit(), does to "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState" crash seen on Google Play
        // see http://stackoverflow.com/questions/7575921/illegalstateexception-can-not-perform-this-action-after-onsaveinstancestate-wit
        getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "PREFERENCE_FRAGMENT").addToBackStack(null).commitAllowingStateLoss();
    }

    @Override
    public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
        if( MyDebug.LOG ) {
            Log.d(TAG, "onPreferenceStartFragment");
            Log.d(TAG, "pref: " + pref.getFragment());
        }

        // instantiate the new fragment
        //final Bundle args = pref.getExtras();
        // we want to pass the caller preference fragment's bundle to the new sub-screen (this will be a
        // copy of the bundle originally created in openSettings()
        final Bundle args = new Bundle(caller.getArguments());

        final Fragment fragment = Fragment.instantiate(this, pref.getFragment(), args);
        fragment.setTargetFragment(caller, 0);
        if( MyDebug.LOG )
            Log.d(TAG, "replace fragment");
        /*getFragmentManager().beginTransaction()
                .replace(R.id.content, fragment)
                .addToBackStack(null)
                .commit();*/
        getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "PREFERENCE_FRAGMENT_"+pref.getFragment()).addToBackStack(null).commitAllowingStateLoss();

        /*
        // AndroidX version:
        final Fragment fragment = getSupportFragmentManager().getFragmentFactory().instantiate(
                getClassLoader(),
                pref.getFragment());
        fragment.setArguments(args);
        fragment.setTargetFragment(caller, 0);
        // replace the existing fragment with the new fragment:
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.content, fragment)
                .addToBackStack(null)
                .commit();
         */
        return true;
    }

    public void updateForSettings(boolean update_camera) {
        updateForSettings(update_camera, null);
    }

    public void updateForSettings(boolean update_camera, String toast_message) {
        updateForSettings(update_camera, toast_message, false, false);
    }

    /** Must be called when an settings (as stored in SharedPreferences) are made, so we can update the
     *  camera, and make any other necessary changes.
     * @param update_camera Whether the camera needs to be updated. Can be set to false if we know changes
     *                      haven't been made to the camera settings, or we already reopened it.
     * @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 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, boolean allow_dim) {
        if( MyDebug.LOG ) {
            Log.d(TAG, "updateForSettings()");
            if( toast_message != null ) {
                Log.d(TAG, "toast_message: " + toast_message);
            }
        }
        long debug_time = 0;
        if( MyDebug.LOG ) {
            debug_time = System.currentTimeMillis();
        }
        // make sure we're into continuous video mode
        // workaround for bug on Samsung Galaxy S5 with UHD, where if the user switches to another (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the video is corrupted
        // so to be safe, we always reset to continuous video mode, and then reset it afterwards
    	/*String saved_focus_value = preview.updateFocusForVideo(); // n.b., may be null if focus mode not changed
		if( MyDebug.LOG )
			Log.d(TAG, "saved_focus_value: " + saved_focus_value);*/

        if( MyDebug.LOG )
            Log.d(TAG, "update folder history");
        save_location_history.updateFolderHistory(getStorageUtils().getSaveLocation(), true); // this also updates the last icon for ghost image, if that pref has changed
        // no need to update save_location_history_saf, as we always do this in onActivityResult()
        if( MyDebug.LOG ) {
            Log.d(TAG, "updateForSettings: time after update folder history: " + (System.currentTimeMillis() - debug_time));
        }

        imageQueueChanged(); // needed at least for changing photo mode, but might as well call it always

        if( !keep_popup ) {
            mainUI.destroyPopup(); // important as we don't want to use a cached popup
            if( MyDebug.LOG ) {
                Log.d(TAG, "updateForSettings: time after destroy popup: " + (System.currentTimeMillis() - debug_time));
            }
        }

        // update camera for changes made in prefs - do this without closing and reopening the camera app if possible for speed!
        // but need workaround for Nexus 7 bug on old camera API, where scene mode doesn't take effect unless the camera is restarted - I can reproduce this with other 3rd party camera apps, so may be a Nexus 7 issue...
        // doesn't happen if we allow using Camera2 API on Nexus 7, but reopen for consistency (and changing scene modes via
        // popup menu no longer should be calling updateForSettings() for Camera2, anyway)
        boolean need_reopen = false;
        if( update_camera && preview.getCameraController() != null ) {
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            String scene_mode = preview.getCameraController().getSceneMode();
            if( MyDebug.LOG )
                Log.d(TAG, "scene mode was: " + scene_mode);
            String key = PreferenceKeys.SceneModePreferenceKey;
            String value = sharedPreferences.getString(key, CameraController.SCENE_MODE_DEFAULT);
            // n.b., on Android 4.3 emulator, scene mode is returned as null (this may be because it doesn't support
            // scene modes at all) - treat this the same as auto
            if( scene_mode == null )
                scene_mode = CameraController.SCENE_MODE_DEFAULT;
            if( !value.equals(scene_mode) ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "scene mode changed to: " + value);
                need_reopen = true;
            }
            else {
                if( applicationInterface.useCamera2() ) {
                    // need to reopen if fake flash mode changed, as it changes the available camera features, and we can only set this after opening the camera
                    boolean camera2_fake_flash = preview.getCameraController().getUseCamera2FakeFlash();
                    if( MyDebug.LOG )
                        Log.d(TAG, "camera2_fake_flash was: " + camera2_fake_flash);
                    if( applicationInterface.useCamera2FakeFlash() != camera2_fake_flash ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "camera2_fake_flash changed");
                        need_reopen = true;
                    }
                }
            }

            if( !need_reopen ) {
                CameraController.TonemapProfile old_tonemap_profile = preview.getCameraController().getTonemapProfile();
                if( old_tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF ) {
                    CameraController.TonemapProfile new_tonemap_profile = applicationInterface.getVideoTonemapProfile();
                    if( new_tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && new_tonemap_profile != old_tonemap_profile ) {
                        // needed for Galaxy S10e when changing from TONEMAP_MODE_CONTRAST_CURVE to TONEMAP_MODE_PRESET_CURVE,
                        // otherwise the contrast curve remains active!
                        if( MyDebug.LOG )
                            Log.d(TAG, "switching between tonemap profiles");
                        need_reopen = true;
                    }
                }
            }

            if( !need_reopen ) {
                boolean old_is_extension = preview.getCameraController().isCameraExtension();
                boolean new_is_extension = applicationInterface.isCameraExtensionPref();
                if( old_is_extension || new_is_extension ) {
                    // At least on Galaxy S10e, we have problems stopping and starting a camera extension session,
                    // e.g., when changing resolutions whilst in an extension mode (XHDR or bokeh) or switching
                    // from XHDR to other modes (including non-extension modes like STD). Problems such as preview
                    // no longer receiving frames, or the call to createExtensionSession() (or createCaptureSession)
                    // hanging. So therefore we should reopen the camera if at least
                    // old_is_extension==true.
                    // This isn't required if old_is_extension==false but new_is_extension==true,
                    // but we still do so since reopening the camera occurs on a background thread
                    // (opening an extension session seems to take longer, so better not to block
                    // the UI thread).
                    if( MyDebug.LOG )
                        Log.d(TAG, "need to reopen camera for changes to extension session");
                    need_reopen = true;
                }
            }
        }
        if( MyDebug.LOG ) {
            Log.d(TAG, "need_reopen: " + need_reopen);
            Log.d(TAG, "updateForSettings: time after check need_reopen: " + (System.currentTimeMillis() - debug_time));
        }

        mainUI.layoutUI(); // needed in case we've changed UI placement; or in "top" mode, if we've enabled/disabled on-screen UI icons
        if( MyDebug.LOG ) {
            Log.d(TAG, "updateForSettings: time after layoutUI: " + (System.currentTimeMillis() - debug_time));
        }

        // ensure icons invisible if disabling them from showing from the Settings
        // (if enabling them, we'll make the icon visible later on)
        checkDisableGUIIcons();

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String audio_control = sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none");
        // better to only display the audio control icon if it matches specific known supported types
        // (important now that "voice" is no longer supported)
        //if( !audio_control.equals("voice") && !audio_control.equals("noise") ) {
        if( !audio_control.equals("noise") ) {
            View speechRecognizerButton = findViewById(R.id.audio_control);
            speechRecognizerButton.setVisibility(View.GONE);
        }

        //speechControl.initSpeechRecognizer(); // in case we've enabled or disabled speech recognizer

        // we no longer call initLocation() here (for having enabled or disabled geotagging), as that's
        // done in setWindowFlagsForCamera() - important not to call it here as well, otherwise if
        // permission wasn't granted, we'll ask for permission twice in a row (on Android 9 or earlier
        // at least)
        //initLocation(); // in case we've enabled or disabled GPS

        initGyroSensors(); // in case we've entered or left panorama
        if( MyDebug.LOG ) {
            Log.d(TAG, "updateForSettings: time after init speech and location: " + (System.currentTimeMillis() - debug_time));
        }
        if( toast_message != null )
            block_startup_toast = true;
        if( !update_camera ) {
            // don't try to update camera
        }
        else if( preview.isPreviewStarting() ) {
            // ideally we should avoid getting into this state - we shouldn't allow changing settings if preview is still opening (e.g., we prevent opening
            // the popup menu if preview is not started, and we close camera when going to settings)
            Log.e(TAG, "updateForSettings: preview is still starting");
        }
        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));
            }
        }
        else {
            preview.setCameraDisplayOrientation(); // need to call in case the preview rotation option was changed
            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));
            }

            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() {
                    // if we ever support calling this with wait_until_started==true, beware of trying to change resolution repeatedly when the popup menu
                    // is open - as currently we'll have problems if trying to calling updateForSettings when preview is already starting on background
                    // thread (see above)
                    preview.setupCamera(false, true, null);
                }
            }, 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.isEmpty() )
            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 ) {
			if( MyDebug.LOG )
				Log.d(TAG, "switch focus back to: " + saved_focus_value);
    		preview.updateFocus(saved_focus_value, true, false);
    	}*/

        magneticSensor.registerMagneticListener(mSensorManager); // check whether we need to register or unregister the magnetic listener
        magneticSensor.checkMagneticAccuracy();

        if( MyDebug.LOG ) {
            Log.d(TAG, "updateForSettings: done: " + (System.currentTimeMillis() - debug_time));
        }
    }

    /** Disables the optional on-screen icons if either user doesn't want to enable them, or not
     *  supported). Note that displaying icons is done via MainUI.showGUI.
     * @return Whether an icon's visibility was changed.
     */
    private boolean checkDisableGUIIcons() {
        if( MyDebug.LOG )
            Log.d(TAG, "checkDisableGUIIcons");
        boolean changed = false;
        if( !supportsExposureButton() ) {
            View button = findViewById(R.id.exposure);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showExposureLockIcon() ) {
            View button = findViewById(R.id.exposure_lock);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showWhiteBalanceLockIcon() ) {
            View button = findViewById(R.id.white_balance_lock);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showCycleRawIcon() ) {
            View button = findViewById(R.id.cycle_raw);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showStoreLocationIcon() ) {
            View button = findViewById(R.id.store_location);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showTextStampIcon() ) {
            View button = findViewById(R.id.text_stamp);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showStampIcon() ) {
            View button = findViewById(R.id.stamp);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showFocusPeakingIcon() ) {
            View button = findViewById(R.id.focus_peaking);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showAutoLevelIcon() ) {
            View button = findViewById(R.id.auto_level);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showCycleFlashIcon() ) {
            View button = findViewById(R.id.cycle_flash);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !mainUI.showFaceDetectionIcon() ) {
            View button = findViewById(R.id.face_detection);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( !showSwitchMultiCamIcon() ) {
            // also handle the multi-cam icon here, as this can change when switching between front/back cameras
            // (e.g., if say a device only has multiple back cameras)
            View button = findViewById(R.id.switch_multi_camera);
            changed = changed || (button.getVisibility() != View.GONE);
            button.setVisibility(View.GONE);
        }
        if( MyDebug.LOG )
            Log.d(TAG, "checkDisableGUIIcons: " + changed);
        return changed;
    }

    public MyPreferenceFragment getPreferenceFragment() {
        return (MyPreferenceFragment)getFragmentManager().findFragmentByTag("PREFERENCE_FRAGMENT");
    }

    private boolean settingsIsOpen() {
        return getPreferenceFragment() != null;
    }

    /** Call when the settings is going to be closed.
     */
    void settingsClosing() {
        if( MyDebug.LOG )
            Log.d(TAG, "close settings");
        setWindowFlagsForCamera();
        showPreview(true);

        preferencesListener.stopListening();

        // Update the cached settings in DrawPreview
        // Note that some GUI related settings won't trigger preferencesListener.anyChange(), so
        // we always call this. Perhaps we could add more classifications to PreferencesListener
        // to mark settings that need us to update DrawPreview but not call updateForSettings().
        // However, DrawPreview.updateSettings() should be a quick function (the main point is
        // 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();
        }

        if( preferencesListener.anySignificantChange() ) {
            // don't need to update camera, as we now pause/resume camera when going to settings
            updateForSettings(false);
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "no need to call updateForSettings() for changes made to preferences");
            if( preferencesListener.anyChange() ) {
                // however we should still destroy cached popup, in case UI settings need to be kept in
                // sync (e.g., changing the Repeat Mode)
                mainUI.destroyPopup();
            }
        }
    }

    private PopupOnBackPressedCallback popupOnBackPressedCallback;

    private class PopupOnBackPressedCallback extends OnBackPressedCallback {
        public PopupOnBackPressedCallback(boolean enabled) {
            super(enabled);
        }

        @Override
        public void handleOnBackPressed() {
            if( MyDebug.LOG )
                Log.d(TAG, "PopupOnBackPressedCallback.handleOnBackPressed");
            if( popupIsOpen() ) {
                // close popup will disable the PopupOnBackPressedCallback, so no need to do it here
                closePopup();
            }
            else {
                // shouldn't be here (if popup isn't open, this callback shouldn't be enabled), but just in case
                if( MyDebug.LOG )
                    Log.e(TAG, "PopupOnBackPressedCallback was enabled but popup menu not open?!");
                this.setEnabled(false);
                MainActivity.this.onBackPressed();
            }
        }
    }

    public void enablePopupOnBackPressedCallback(boolean enabled) {
        if( MyDebug.LOG )
            Log.d(TAG, "enablePopupOnBackPressedCallback: " + enabled);
        if( popupOnBackPressedCallback != null ) {
            popupOnBackPressedCallback.setEnabled(enabled);
        }
    }

    private PausePreviewOnBackPressedCallback pausePreviewOnBackPressedCallback;

    private class PausePreviewOnBackPressedCallback extends OnBackPressedCallback {
        public PausePreviewOnBackPressedCallback(boolean enabled) {
            super(enabled);
        }

        @Override
        public void handleOnBackPressed() {
            if( MyDebug.LOG )
                Log.d(TAG, "PausePreviewOnBackPressedCallback.handleOnBackPressed");

            if( preview != null && preview.isPreviewPaused() ) {
                // starting the preview will disable the PausePreviewOnBackPressedCallback, so no need to do it here
                if( MyDebug.LOG )
                    Log.d(TAG, "preview was paused, so unpause it");
                preview.startCameraPreview(true, null);
            }
            else {
                // shouldn't be here (if preview isn't paused, this callback shouldn't be enabled), but just in case
                if( MyDebug.LOG )
                    Log.e(TAG, "PausePreviewOnBackPressedCallback was enabled but preview not paused?!");
                this.setEnabled(false);
                MainActivity.this.onBackPressed();
            }
        }
    }

    public void enablePausePreviewOnBackPressedCallback(boolean enabled) {
        if( MyDebug.LOG )
            Log.d(TAG, "enablePausePreviewOnBackPressedCallback: " + enabled);
        if( pausePreviewOnBackPressedCallback != null ) {
            pausePreviewOnBackPressedCallback.setEnabled(enabled);
        }
    }

    private ScreenLockOnBackPressedCallback screenLockOnBackPressedCallback;

    private class ScreenLockOnBackPressedCallback extends OnBackPressedCallback {
        public ScreenLockOnBackPressedCallback(boolean enabled) {
            super(enabled);
        }

        @Override
        public void handleOnBackPressed() {
            if( MyDebug.LOG )
                Log.d(TAG, "ScreenLockOnBackPressedCallback.handleOnBackPressed");

            if( screen_is_locked ) {
                preview.showToast(screen_locked_toast, R.string.screen_is_locked);
            }
            else {
                // shouldn't be here (if screen isn't locked, this callback shouldn't be enabled), but just in case
                if( MyDebug.LOG )
                    Log.e(TAG, "ScreenLockOnBackPressedCallback was enabled but screen isn't locked?!");
                this.setEnabled(false);
                MainActivity.this.onBackPressed();
            }
        }
    }

    private void enableScreenLockOnBackPressedCallback(boolean enabled) {
        if( MyDebug.LOG )
            Log.d(TAG, "enableScreenLockOnBackPressedCallback: " + enabled);
        if( screenLockOnBackPressedCallback != null ) {
            screenLockOnBackPressedCallback.setEnabled(enabled);
        }
    }

    // should no longer use onBackPressed() - instead use OnBackPressedCallback, for upcoming changes in Android 14+ (predictive back gestures)
    /*@Override
    public void onBackPressed() {
        if( MyDebug.LOG )
            Log.d(TAG, "onBackPressed");
        if( screen_is_locked ) {
            preview.showToast(screen_locked_toast, R.string.screen_is_locked);
            return;
        }

        if( settingsIsOpen() ) {
            settingsClosing();
        }
        else if( preview != null && preview.isPreviewPaused() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "preview was paused, so unpause it");
            preview.startCameraPreview();
            return;
        }
        else {
            if( popupIsOpen() ) {
                closePopup();
                return;
            }
        }
        super.onBackPressed();
    }*/

    /** Returns whether we are always running in edge-to-edge mode. (If false, we may still sometimes
     *  run edge-to-edge.)
     */
    public boolean getEdgeToEdgeMode() {
        return this.edge_to_edge_mode;
    }

    /** Whether to allow the application to show under the navigation bar, or not.
     *  Arguably we could enable this all the time, but in practice we only enable for cases when
     *  want_no_limits==true and navigation_gap!=0 (if want_no_limits==false, there's no need to
     *  show under the navigation bar; if navigation_gap==0, there is no navigation bar).
     */
    private void showUnderNavigation(boolean enable) {
        if( MyDebug.LOG )
            Log.d(TAG, "showUnderNavigation: " + enable);

        if( edge_to_edge_mode ) {
            // we are already always in edge-to-edge mode
            return;
        }

        {
            // We used to use window flag FLAG_LAYOUT_NO_LIMITS, but this didn't work properly on
            // Android 11 (didn't take effect until orientation changed or application paused/resumed).
            // Although system ui visibility flags are deprecated on Android 11, this still works better
            // than the FLAG_LAYOUT_NO_LIMITS flag (which was not well documented anyway).
            // Update, now using WindowCompat.setDecorFitsSystemWindows. This is non-deprecated, and
            // documented at https://developer.android.com/develop/ui/views/layout/edge-to-edge-manually .
            /*int flags = getWindow().getDecorView().getSystemUiVisibility();
            if( enable ) {
                getWindow().getDecorView().setSystemUiVisibility(flags | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
            }
            else {
                getWindow().getDecorView().setSystemUiVisibility(flags & ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
            }*/
            test_set_show_under_navigation = enable;
            // in theory the VANILLA_ICE_CREAM is redundant as we shouldn't be here on Android 15+ anyway (since edge_to_edge_mode==true), but
            // wrapping in case this helps Google Play recommendation to avoid deprecated APIs for edge-to-edge
            if( Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM ) {
                WindowCompat.setDecorFitsSystemWindows(getWindow(), !enable);
            }
        }

        // in theory the VANILLA_ICE_CREAM is redundant as we shouldn't be here on Android 15+ anyway (since edge_to_edge_mode==true), but
        // wrapping in case this helps Google Play recommendation to avoid deprecated APIs for edge-to-edge
        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM ) {
            getWindow().setNavigationBarColor(enable ? Color.TRANSPARENT : Color.BLACK);
        }
    }

    public int getNavigationGap() {
        return (want_no_limits || edge_to_edge_mode) ? navigation_gap : 0;
    }

    public int getNavigationGapLandscape() {
        return edge_to_edge_mode ? navigation_gap_landscape : 0;
    }

    public int getNavigationGapReverseLandscape() {
        return edge_to_edge_mode ? navigation_gap_reverse_landscape : 0;
    }

    /** The system is now such that we have entered or exited immersive mode. If visible is true,
     *  system UI is now visible such that we should exit immersive mode. If visible is false, the
     *  system has entered immersive mode.
     */
    private void immersiveModeChanged(boolean visible) {
        if( MyDebug.LOG )
            Log.d(TAG, "immersiveModeChanged: " + visible);
        if( !usingKitKatImmersiveMode() )
            return;

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
        String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_off");
        boolean hide_ui = immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything");

        if( visible ) {
            if( MyDebug.LOG )
                Log.d(TAG, "system bars now visible");
            // change UI due to having exited immersive mode
            if( hide_ui )
                mainUI.setImmersiveMode(false);
            setImmersiveTimer();
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "system bars now NOT visible");
            // change UI due to having entered immersive mode
            if( hide_ui )
                mainUI.setImmersiveMode(true);
        }
    }

    /** Set up listener to handle listening for system ui changes (for immersive mode), and setting
      * a WindowsInsetsListener to find the navigation_gap.
      */
    private void setupSystemUiVisibilityListener() {
        View decorView = getWindow().getDecorView();

        {
            // set a window insets listener to find the navigation_gap
            if( MyDebug.LOG )
                Log.d(TAG, "set a window insets listener");
            this.set_window_insets_listener = true;
            decorView.getRootView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
                private boolean has_last_system_orientation;
                private SystemOrientation last_system_orientation;
                @Override
                public @NonNull WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets windowInsets) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "onApplyWindowInsets");
                    int inset_left;
                    int inset_top;
                    int inset_right;
                    int inset_bottom;
                    if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) {
                        // take opportunity to use non-deprecated versions; also for edge_to_edge_mode==true, we need to use getInsetsIgnoringVisibility for
                        // immersive mode (since for edge_to_edge_mode==true, we are not using setSystemUiVisibility() / SYSTEM_UI_FLAG_LAYOUT_STABLE in setImmersiveMode())
                        // also compare with MyApplicationInterface.getDisplaySize() - in particular we don't care about caption/system bar that is returned on e.g.
                        // OnePlus Pad for insets.top when in landscape orientation (since the system bar isn't shown); however we also need to subtract any from the cutout -
                        // since this code is for finding what margins we need to set to avoid navigation bars; avoiding the cutout is done below for the entire
                        // Open Camera view
                        Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout());
                        Insets cutout_insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.displayCutout());
                        if( test_force_window_insets ) {
                            insets = test_insets;
                            cutout_insets = test_cutout_insets;
                        }
                        inset_left = insets.left - cutout_insets.left;
                        inset_top = insets.top - cutout_insets.top;
                        inset_right = insets.right - cutout_insets.right;
                        inset_bottom = insets.bottom - cutout_insets.bottom;
                    }
                    else {
                        inset_left = windowInsets.getSystemWindowInsetLeft();
                        inset_top = windowInsets.getSystemWindowInsetTop();
                        inset_right = windowInsets.getSystemWindowInsetRight();
                        inset_bottom = windowInsets.getSystemWindowInsetBottom();
                    }
                    if( MyDebug.LOG ) {
                        Log.d(TAG, "inset left: " + inset_left);
                        Log.d(TAG, "inset top: " + inset_top);
                        Log.d(TAG, "inset right: " + inset_right);
                        Log.d(TAG, "inset bottom: " + inset_bottom);
                    }

                    if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) {
                        // easier to ensure the entire activity avoids display cutouts - for the preview, we still support
                        // it showing under the navigation bar
                        Insets insets = windowInsets.getInsets(WindowInsets.Type.displayCutout());
                        if( test_force_window_insets ) {
                            insets = test_cutout_insets;
                        }
                        v.setPadding(insets.left, insets.top, insets.right, insets.bottom);

                        // also handle change of immersive mode (instead of using deprecated setOnSystemUiVisibilityChangeListener below
                        immersiveModeChanged( windowInsets.isVisible(WindowInsets.Type.navigationBars()) );
                    }

                    resetCachedSystemOrientation(); // don't want to get cached result - this can sometimes happen e.g. on Pixel 6 Pro when switching between landscape and reverse landscape
                    SystemOrientation system_orientation = getSystemOrientation();
                    int new_navigation_gap, new_navigation_gap_landscape, new_navigation_gap_reverse_landscape;
                    switch ( system_orientation ) {
                        case PORTRAIT:
                            if( MyDebug.LOG )
                                Log.d(TAG, "portrait");
                            new_navigation_gap = inset_bottom;
                            new_navigation_gap_landscape = inset_left;
                            new_navigation_gap_reverse_landscape = inset_right;
                            break;
                        case LANDSCAPE:
                            if( MyDebug.LOG )
                                Log.d(TAG, "landscape");
                            new_navigation_gap = inset_right;
                            new_navigation_gap_landscape = inset_bottom;
                            new_navigation_gap_reverse_landscape = inset_top;
                            break;
                        case REVERSE_LANDSCAPE:
                            if( MyDebug.LOG )
                                Log.d(TAG, "reverse landscape");
                            new_navigation_gap = inset_left;
                            new_navigation_gap_landscape = inset_top;
                            new_navigation_gap_reverse_landscape = inset_bottom;
                            break;
                        default:
                            Log.e(TAG, "unknown system_orientation?!: " + system_orientation);
                            new_navigation_gap = 0;
                            new_navigation_gap_landscape = 0;
                            new_navigation_gap_reverse_landscape = 0;
                            break;
                    }
                    if( !edge_to_edge_mode ) {
                        // we only care about avoiding a landscape navigation bar (e.g., large tablets in landscape orientation) for edge_to_edge_mode==true
                        // in theory this could be useful when edge_to_edge_mode==false, but in practice we will never enter edge-to-edge-mode if the
                        // navigation bar is along the landscape-edge, so restrict behaviour change to edge_to_edge_mode==true
                        new_navigation_gap_landscape = 0;
                        new_navigation_gap_reverse_landscape = 0;
                    }

                    // for edge_to_edge_mode==false, we only enter this case if system orientation changes, due to issues where this callback may be called first with 0 navigation gap
                    // (see notes below)
                    // for edge_to_edge_mode==true, simpler to always react to updated insets - in particular, in split-window mode, the navigation gaps can
                    // change when device rotates, even though the application remains in the same orientation
                    if( (edge_to_edge_mode || (has_last_system_orientation && system_orientation != last_system_orientation)) && (new_navigation_gap != navigation_gap || new_navigation_gap_landscape != navigation_gap_landscape || new_navigation_gap_reverse_landscape != navigation_gap_reverse_landscape ) ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "navigation_gap changed from " + navigation_gap + " to " + new_navigation_gap);

                        navigation_gap = new_navigation_gap;
                        navigation_gap_landscape = new_navigation_gap_landscape;
                        navigation_gap_reverse_landscape = new_navigation_gap_reverse_landscape;

                        if( MyDebug.LOG )
                            Log.d(TAG, "want_no_limits: " + want_no_limits);
                        if( want_no_limits || edge_to_edge_mode ) {
                            // If we want no_limits mode, then need to take care in case of device orientation
                            // in cases where that changes the navigation_gap:
                            // - Need to set showUnderNavigation() (in case navigation_gap when from zero to non-zero or vice versa).
                            // - Need to call layoutUI() (for different value of navigation_gap)

                            // Need to call showUnderNavigation() from handler for it to take effect.
                            // Similarly we have problems if we call layoutUI without post-ing it -
                            // sometimes when rotating a device, we get a call to OnApplyWindowInsetsListener
                            // with 0 navigation_gap followed by the call with the correct non-zero values -
                            // posting the call to layoutUI means it runs after the second call, so we have the
                            // correct navigation_gap.
                            Handler handler = new Handler();
                            handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    if( MyDebug.LOG )
                                        Log.d(TAG, "runnable for change in navigation_gap due to orientation change");
                                    if( navigation_gap != 0 ) {
                                        if( MyDebug.LOG )
                                            Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
                                        showUnderNavigation(true);
                                    }
                                    else {
                                        if( MyDebug.LOG )
                                            Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
                                        showUnderNavigation(false);
                                    }
                                    // needed for OnePlus Pad when rotating, to avoid delay in updating last_take_photo_top_time (affects placement of on-screen text e.g. zoom)
                                    // need to do this from handler for this to take effect (otherwise last_take_photo_top_time won't update to new value)
                                    applicationInterface.getDrawPreview().onNavigationGapChanged();

                                    if( MyDebug.LOG )
                                        Log.d(TAG, "layout UI due to changing navigation_gap");
                                    mainUI.layoutUI();
                                }
                            });
                        }
                    }
                    else if( !edge_to_edge_mode && navigation_gap == 0 ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "navigation_gap changed from zero to " + new_navigation_gap);
                        navigation_gap = new_navigation_gap;
                        // Sometimes when this callback is called, the navigation_gap may still be 0 even if
                        // the device doesn't have physical navigation buttons - we need to wait
                        // until we have found a non-zero value before switching to no limits.
                        // On devices with physical navigation bar, navigation_gap should remain 0
                        // (and there's no point setting FLAG_LAYOUT_NO_LIMITS)
                        if( want_no_limits && navigation_gap != 0 ) {
                            if( MyDebug.LOG )
                                Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
                            showUnderNavigation(true);
                        }
                    }

                    if( has_last_system_orientation && (
                            ( system_orientation == SystemOrientation.LANDSCAPE && last_system_orientation == SystemOrientation.REVERSE_LANDSCAPE ) ||
                            ( system_orientation == SystemOrientation.REVERSE_LANDSCAPE && last_system_orientation == SystemOrientation.LANDSCAPE )
                    ) ) {
                        // hack - this should be done via MyDisplayListener.onDisplayChanged(), but that doesn't work on Galaxy S24+ (either MyDisplayListener.onDisplayChanged()
                        // isn't called, or getDefaultDisplay().getRotation() is still returning the old rotation)
                        if( MyDebug.LOG )
                            Log.d(TAG, "onApplyWindowInsets: switched between landscape and reverse orientation");
                        onSystemOrientationChanged();
                    }

                    has_last_system_orientation = true;
                    last_system_orientation = system_orientation;

                    // see comments in MainUI.layoutUI() for why we don't use this
                    /*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSystemOrientation() == SystemOrientation.LANDSCAPE ) {
                        Rect privacy_indicator_rect = windowInsets.getPrivacyIndicatorBounds();
                        if( privacy_indicator_rect != null ) {
                            Rect window_bounds = getWindowManager().getCurrentWindowMetrics().getBounds();
                            if( MyDebug.LOG ) {
                                Log.d(TAG, "privacy_indicator_rect: " + privacy_indicator_rect);
                                Log.d(TAG, "window_bounds: " + window_bounds);
                            }
                            privacy_indicator_gap = window_bounds.right - privacy_indicator_rect.left;
                            if( privacy_indicator_gap < 0 )
                                privacy_indicator_gap = 0; // just in case??
                            if( MyDebug.LOG )
                                Log.d(TAG, "privacy_indicator_gap: " + privacy_indicator_gap);
                        }
                    }
                    else {
                        privacy_indicator_gap = 0;
                    }*/
                    return getWindow().getDecorView().getRootView().onApplyWindowInsets(windowInsets);
                }
            });
        }

        if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) {
            // already handled by the setOnApplyWindowInsetsListener above
        }
        else {
            decorView.setOnSystemUiVisibilityChangeListener
                (new View.OnSystemUiVisibilityChangeListener() {
                    @Override
                    public void onSystemUiVisibilityChange(int visibility) {
                        // Note that system bars will only be "visible" if none of the
                        // LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.

                        if( MyDebug.LOG )
                            Log.d(TAG, "onSystemUiVisibilityChange: " + visibility);

                        // Note that Android example code says to test against SYSTEM_UI_FLAG_FULLSCREEN,
                        // but this stopped working on Android 11, as when calling setSystemUiVisibility(0)
                        // to exit immersive mode, when we arrive here the flag SYSTEM_UI_FLAG_FULLSCREEN
                        // is still set. Fixed by checking for SYSTEM_UI_FLAG_HIDE_NAVIGATION instead -
                        // which makes some sense since we run in fullscreen mode all the time anyway.
                        //if( (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 ) {
                        if( (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0 ) {
                            immersiveModeChanged(true);
                        }
                        else {
                            immersiveModeChanged(false);
                        }
                    }
                });
        }
    }

    public boolean usingKitKatImmersiveMode() {
        // whether we are using a Kit Kat style immersive mode (either hiding navigation bar, GUI, or everything)
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_off");
        if( immersive_mode.equals("immersive_mode_navigation") || immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything") )
            return true;
        return false;
    }

    public boolean usingKitKatImmersiveModeEverything() {
        // whether we are using a Kit Kat style immersive mode for everything
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_off");
        if( immersive_mode.equals("immersive_mode_everything") )
            return true;
        return false;
    }


    private Handler immersive_timer_handler = null;
    private Runnable immersive_timer_runnable = null;

    private void cancelImmersiveTimer() {
        if( immersive_timer_handler != null && immersive_timer_runnable != null ) {
            immersive_timer_handler.removeCallbacks(immersive_timer_runnable);
            immersive_timer_handler = null;
            immersive_timer_runnable = null;
        }
    }

    private void setImmersiveTimer() {
        cancelImmersiveTimer();
        if( app_is_paused ) {
            // don't want to enter immersive mode from background
            // problem that even after onPause, we can end up here via various callbacks
            return;
        }
        immersive_timer_handler = new Handler();
        immersive_timer_handler.postDelayed(immersive_timer_runnable = new Runnable(){
            @Override
            public void run(){
                if( MyDebug.LOG )
                    Log.d(TAG, "setImmersiveTimer: run");
                // even though timer should have been cancelled when in background, check app_is_paused just in case
                if( !app_is_paused && !camera_in_background && !popupIsOpen() && usingKitKatImmersiveMode() )
                    setImmersiveMode(true);
            }
        }, 5000);
    }

    public void initImmersiveMode() {
        if( !usingKitKatImmersiveMode() ) {
            setImmersiveMode(true);
        }
        else {
            // don't start in immersive mode, only after a timer
            setImmersiveTimer();
        }
    }

    void setImmersiveMode(boolean on) {
        if( MyDebug.LOG )
            Log.d(TAG, "setImmersiveMode: " + on);
        // n.b., preview.setImmersiveMode() is called from onSystemUiVisibilityChange()

        // don't allow the kitkat-style immersive mode for panorama mode (problem that in "full" immersive mode, the gyro spot can't be seen - we could fix this, but simplest to just disallow)
        boolean enable_immersive = on && usingKitKatImmersiveMode() && applicationInterface.getPhotoMode() != MyApplicationInterface.PhotoMode.Panorama;
        if( MyDebug.LOG )
            Log.d(TAG, "enable_immersive?: " + enable_immersive);

        if( edge_to_edge_mode ) {
            // take opportunity to avoid deprecated setSystemUiVisibility
            WindowInsetsControllerCompat windowInsetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
            int type = WindowInsetsCompat.Type.navigationBars(); // only show/hide navigation bars, as we run with system bars always hidden
            if( enable_immersive ) {
                windowInsetsController.hide(type);
            }
            else {
                windowInsetsController.show(type);
            }
        }
        else {
            // save whether we set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - since this flag might be enabled for showUnderNavigation(true), at least indirectly by setDecorFitsSystemWindows() on old versions of Android
            int saved_flags = getWindow().getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
            if( MyDebug.LOG )
                Log.d(TAG, "saved_flags?: " + saved_flags);
            if( enable_immersive ) {
                getWindow().getDecorView().setSystemUiVisibility(saved_flags | View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
            }
            else {
                getWindow().getDecorView().setSystemUiVisibility(saved_flags);
            }
        }
    }

    /** Sets the brightness level for normal operation (when camera preview is visible).
     *  If force_max is true, this always forces maximum brightness; otherwise this depends on user preference.
     */
    public void setBrightnessForCamera(boolean force_max) {
        if( MyDebug.LOG )
            Log.d(TAG, "setBrightnessForCamera");
        // set screen to max brightness - see http://stackoverflow.com/questions/11978042/android-screen-brightness-max-value
        // done here rather than onCreate, so that changing it in preferences takes effect without restarting app
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        final WindowManager.LayoutParams layout = getWindow().getAttributes();
        if( force_max || sharedPreferences.getBoolean(PreferenceKeys.MaxBrightnessPreferenceKey, false) ) {
            layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL;
        }
        else {
            layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
        }

        // this must be called from the ui thread
        // sometimes this method may be called not on UI thread, e.g., Preview.takePhotoWhenFocused->CameraController2.takePicture
        // ->CameraController2.runFakePrecapture->Preview/onFrontScreenTurnOn->MyApplicationInterface.turnFrontScreenFlashOn
        // -> this.setBrightnessForCamera
        this.runOnUiThread(new Runnable() {
            public void run() {
                getWindow().setAttributes(layout);
            }
        });
    }

    /**
     * Set the brightness to minimal in case the preference key is set to do it
     */
    public void setBrightnessToMinimumIfWanted() {
        if( MyDebug.LOG )
            Log.d(TAG, "setBrightnessToMinimum");
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        final WindowManager.LayoutParams layout = getWindow().getAttributes();
        if( sharedPreferences.getBoolean(PreferenceKeys.DimWhenDisconnectedPreferenceKey, false) ) {
            layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
        }
        else {
            layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
        }

        this.runOnUiThread(new Runnable() {
            public void run() {
                getWindow().setAttributes(layout);
            }
        });

    }

    /** Sets the window flags for normal operation (when camera preview is visible).
     */
    public void setWindowFlagsForCamera() {
        if( MyDebug.LOG )
            Log.d(TAG, "setWindowFlagsForCamera");
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ) {
            // we set this to prevent what's on the preview being used to show under the "recent apps" view - potentially useful
            // for privacy reasons
            setRecentsScreenshotEnabled(false);
        }

        if( lock_to_landscape ) {
            // force to landscape mode
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            //setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); // testing for devices with unusual sensor orientation (e.g., Nexus 5X)
        }
        else {
            // allow orientation to change for camera, even if user has locked orientation
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
        }
        if( preview != null ) {
            // also need to call preview.setCameraDisplayOrientation, as this handles if the user switched from portrait to reverse landscape whilst in settings/etc
            // as switching from reverse landscape back to landscape isn't detected in onConfigurationChanged
            // update: now probably irrelevant now that we close/reopen the camera, but keep it here anyway
            preview.setCameraDisplayOrientation();
        }
        if( preview != null && mainUI != null ) {
            // layoutUI() is needed because even though we call layoutUI from MainUI.onOrientationChanged(), certain things
            // (ui_rotation) depend on the system orientation too.
            // Without this, going to Settings, then changing orientation, then exiting settings, would show the icons with the
            // wrong orientation.
            // We put this here instead of onConfigurationChanged() as onConfigurationChanged() isn't called when switching from
            // reverse landscape to landscape orientation: so it's needed to fix if the user starts in portrait, goes to settings
            // or a dialog, then switches to reverse landscape, then exits settings/dialog - the system orientation will switch
            // to landscape (which Open Camera is forced to).
            mainUI.layoutUI();
        }


        // keep screen active - see http://stackoverflow.com/questions/2131948/force-screen-on
        if( sharedPreferences.getBoolean(PreferenceKeys.KeepDisplayOnPreferenceKey, true) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "do keep screen on");
            this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "don't keep screen on");
            this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
        if( sharedPreferences.getBoolean(PreferenceKeys.ShowWhenLockedPreferenceKey, false) ) {
            if( MyDebug.LOG )
                Log.d(TAG, "do show when locked");
            // keep Open Camera on top of screen-lock (will still need to unlock when going to gallery or settings)
            showWhenLocked(true);
        }
        else {
            if( MyDebug.LOG )
                Log.d(TAG, "don't show when locked");
            showWhenLocked(false);
        }

        if( want_no_limits && navigation_gap != 0 ) {
            if( MyDebug.LOG )
                Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
            showUnderNavigation(true);
        }

        setBrightnessForCamera(false);

        initImmersiveMode();
        camera_in_background = false;

        magneticSensor.clearDialog(); // if the magnetic accuracy was opened, it must have been closed now
        if( !app_is_paused ) {
            // Needs to be called after camera_in_background is set to false.
            // Note that the app_is_paused guard is in some sense unnecessary, as initLocation tests for that too,
            // but useful for error tracking - ideally we want to make sure that initLocation is never called when
            // app is paused. It can happen here because setWindowFlagsForCamera() is called from
            // onCreate()
            initLocation();

            // Similarly only want to reopen the camera if no longer paused
            if( preview != null ) {
                preview.onResume();
            }
        }
    }

    private void setWindowFlagsForSettings() {
        setWindowFlagsForSettings(true);
    }

    /** Sets the window flags for when the settings window is open.
     * @param set_lock_protect If true, then window flags will be set to protect by screen lock, no
     *                         matter what the preference setting
     *                         PreferenceKeys.getShowWhenLockedPreferenceKey() is set to. This
     *                         should be true for the Settings window, and anything else that might
     *                         need protecting. But some callers use this method for opening other
     *                         things (such as info dialogs).
     */
    public void setWindowFlagsForSettings(boolean set_lock_protect) {
        if( MyDebug.LOG )
            Log.d(TAG, "setWindowFlagsForSettings: " + set_lock_protect);

        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ) {
            // in settings mode, okay to revert to default behaviour for using a screenshot for "recent apps" view
            setRecentsScreenshotEnabled(true);
        }

        // allow screen rotation
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);

        // revert to standard screen blank behaviour
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        if( want_no_limits && navigation_gap != 0 ) {
            if( MyDebug.LOG )
                Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
            showUnderNavigation(false);
        }
        if( set_lock_protect ) {
            // settings should still be protected by screen lock
            showWhenLocked(false);
        }

        {
            WindowManager.LayoutParams layout = getWindow().getAttributes();
            layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
            getWindow().setAttributes(layout);
        }

        setImmersiveMode(false);
        camera_in_background = true;

        // we disable location listening when showing settings or a dialog etc - saves battery life, also better for privacy
        applicationInterface.getLocationSupplier().freeLocationListeners();

        // similarly we close the camera
        preview.onPause(false);

        push_switched_camera = false; // just in case
    }

    private void showWhenLocked(boolean show) {
        if( MyDebug.LOG )
            Log.d(TAG, "showWhenLocked: " + show);
        // although FLAG_SHOW_WHEN_LOCKED is deprecated, setShowWhenLocked(false) does not work
        // correctly: if we turn screen off and on when camera is open (so we're now running above
        // the lock screen), going to settings does not show the lock screen, i.e.,
        // setShowWhenLocked(false) does not take effect!
		/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
			if( MyDebug.LOG )
				Log.d(TAG, "use setShowWhenLocked");
			setShowWhenLocked(show);
		}
		else*/ {
            if( show ) {
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
            }
            else {
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
            }
        }
    }

    /** Use this is place of simply alert.show(), if the orientation has just been set to allow
     *  rotation via setWindowFlagsForSettings(). On some devices (e.g., OnePlus 3T with Android 8),
     *  the dialog doesn't show properly if the phone is held in portrait. A workaround seems to be
     *  to use postDelayed. Note that postOnAnimation() doesn't work.
     */
    public void showAlert(final AlertDialog alert) {
        if( MyDebug.LOG )
            Log.d(TAG, "showAlert");
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            public void run() {
                alert.show();
            }
        }, 20);
        // note that 1ms usually fixes the problem, but not always; 10ms seems fine, have set 20ms
        // just in case
    }

    public void showPreview(boolean show) {
        if( MyDebug.LOG )
            Log.d(TAG, "showPreview: " + show);
        final ViewGroup container = findViewById(R.id.hide_container);
        container.setVisibility(show ? View.GONE : View.VISIBLE);
    }

    /** Rotates the supplied bitmap according to the orientation tag stored in the exif data. If no
     *  rotation is required, the input bitmap is returned. If rotation is required, the input
     *  bitmap is recycled.
     * @param uri Uri containing the JPEG with Exif information to use.
     */
    public Bitmap rotateForExif(Bitmap bitmap, Uri uri) throws IOException {
        ExifInterface exif;
        InputStream inputStream = null;
        try {
            inputStream = this.getContentResolver().openInputStream(uri);
            exif = new ExifInterface(inputStream);
        }
        finally {
            if( inputStream != null )
                inputStream.close();
        }

        if( exif != null ) {
            int exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
            boolean needs_tf = false;
            int exif_orientation = 0;
            // see http://jpegclub.org/exif_orientation.html
            // and http://stackoverflow.com/questions/20478765/how-to-get-the-correct-orientation-of-the-image-selected-from-the-default-image
            if( exif_orientation_s == ExifInterface.ORIENTATION_UNDEFINED || exif_orientation_s == ExifInterface.ORIENTATION_NORMAL ) {
                // leave unchanged
            }
            else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_180 ) {
                needs_tf = true;
                exif_orientation = 180;
            }
            else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_90 ) {
                needs_tf = true;
                exif_orientation = 90;
            }
            else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_270 ) {
                needs_tf = true;
                exif_orientation = 270;
            }
            else {
                // just leave unchanged for now
                if( MyDebug.LOG )
                    Log.e(TAG, "    unsupported exif orientation: " + exif_orientation_s);
            }
            if( MyDebug.LOG )
                Log.d(TAG, "    exif orientation: " + exif_orientation);

            if( needs_tf ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "    need to rotate bitmap due to exif orientation tag");
                Matrix m = new Matrix();
                m.setRotate(exif_orientation, bitmap.getWidth() * 0.5f, bitmap.getHeight() * 0.5f);
                Bitmap rotated_bitmap = Bitmap.createBitmap(bitmap, 0, 0,bitmap.getWidth(), bitmap.getHeight(), m, true);
                if( rotated_bitmap != bitmap ) {
                    bitmap.recycle();
                    bitmap = rotated_bitmap;
                }
            }
        }
        return bitmap;
    }

    /** Loads a thumbnail from the supplied image uri (not videos). Note this loads from the bitmap
     *  rather than reading from MediaStore. Therefore this works with SAF uris as well as
     *  MediaStore uris, as well as allowing control over the resolution of the thumbnail.
     *  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.
     *  We now use this for photos in preference to APIs like
     *  MediaStore.Images.Thumbnails.getThumbnail(). Advantages are simplifying the code, reducing
     *  number of different codepaths, but also seems to help against device specific bugs
     *  in getThumbnail() e.g. Pixel 6 Pro with x-night in portrait.
     */
    private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor) {
        Bitmap thumbnail = null;
        try {
            //thumbnail = MediaStore.Images.Media.getBitmap(getContentResolver(), media.uri);
            // only need to load a bitmap as large as the screen size
            BitmapFactory.Options options = new BitmapFactory.Options();
            InputStream is = getContentResolver().openInputStream(uri);
            // get dimensions
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, options);
            int bitmap_width = options.outWidth;
            int bitmap_height = options.outHeight;
            Point display_size = new Point();
            applicationInterface.getDisplaySize(display_size, true);
            if( MyDebug.LOG ) {
                Log.d(TAG, "bitmap_width: " + bitmap_width);
                Log.d(TAG, "bitmap_height: " + bitmap_height);
                Log.d(TAG, "display width: " + display_size.x);
                Log.d(TAG, "display height: " + display_size.y);
            }
            // align dimensions
            if( display_size.x < display_size.y ) {
                //noinspection SuspiciousNameCombination
                display_size.set(display_size.y, display_size.x);
            }
            if( bitmap_width < bitmap_height ) {
                int dummy = bitmap_width;
                //noinspection SuspiciousNameCombination
                bitmap_width = bitmap_height;
                bitmap_height = dummy;
            }
            if( MyDebug.LOG ) {
                Log.d(TAG, "bitmap_width: " + bitmap_width);
                Log.d(TAG, "bitmap_height: " + bitmap_height);
                Log.d(TAG, "display width: " + display_size.x);
                Log.d(TAG, "display height: " + display_size.y);
            }
            // only care about height, to save worrying about different aspect ratios
            options.inSampleSize = 1;
            while( bitmap_height / (2*options.inSampleSize) >= display_size.y ) {
                options.inSampleSize *= 2;
            }
            options.inSampleSize *= sample_factor;
            if( MyDebug.LOG ) {
                Log.d(TAG, "inSampleSize: " + options.inSampleSize);
            }
            options.inJustDecodeBounds = false;
            // need a new inputstream, see https://stackoverflow.com/questions/2503628/bitmapfactory-decodestream-returning-null-when-options-are-set
            is.close();
            is = getContentResolver().openInputStream(uri);
            thumbnail = BitmapFactory.decodeStream(is, null, options);
            if( thumbnail == null ) {
                Log.e(TAG, "decodeStream returned null bitmap for ghost image last");
            }
            is.close();

            thumbnail = rotateForExif(thumbnail, uri);
        }
        catch(IOException e) {
            MyDebug.logStackTrace(TAG, "failed to load bitmap for ghost image last", e);
        }
        return thumbnail;
    }

    /** Shows the default "blank" gallery icon, when we don't have a thumbnail available.
     */
    private void updateGalleryIconToBlank() {
        if( MyDebug.LOG )
            Log.d(TAG, "updateGalleryIconToBlank");
        ImageButton galleryButton = this.findViewById(R.id.gallery);
        int bottom = galleryButton.getPaddingBottom();
        int top = galleryButton.getPaddingTop();
        int right = galleryButton.getPaddingRight();
        int left = galleryButton.getPaddingLeft();
	    /*if( MyDebug.LOG )
			Log.d(TAG, "padding: " + bottom);*/
        galleryButton.setImageBitmap(null);
        galleryButton.setImageResource(R.drawable.baseline_photo_library_white_48);
        // workaround for setImageResource also resetting padding, Android bug
        galleryButton.setPadding(left, top, right, bottom);
        gallery_bitmap = null;
    }

    /** Shows a thumbnail for the gallery icon.
     */
    void updateGalleryIcon(Bitmap thumbnail) {
        if( MyDebug.LOG )
            Log.d(TAG, "updateGalleryIcon: " + thumbnail);
        // If we're currently running the background task to update the gallery (see updateGalleryIcon()), we should cancel that!
        // Otherwise if user takes a photo whilst the background task is still running, the thumbnail from the latest photo will
        // be overridden when the background task completes. This is more likely when using SAF on Android 10+ with scoped storage,
        // due to SAF's poor performance for folders with large number of files.
        if( update_gallery_future != null ) {
            if( MyDebug.LOG )
                Log.d(TAG, "cancel update_gallery_future");
            update_gallery_future.cancel(true);
        }
        ImageButton galleryButton = this.findViewById(R.id.gallery);
        galleryButton.setImageBitmap(thumbnail);
        gallery_bitmap = thumbnail;
    }

    /** Updates the gallery icon by searching for the most recent photo.
     *  Launches the task in a separate thread.
     */
    public void updateGalleryIcon() {
        long debug_time = 0;
        if( MyDebug.LOG ) {
            Log.d(TAG, "updateGalleryIcon");
            debug_time = System.currentTimeMillis();
        }
        if( update_gallery_future != null ) {
            Log.d(TAG, "previous updateGalleryIcon task already running");
            return;
        }

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String ghost_image_pref = sharedPreferences.getString(PreferenceKeys.GhostImagePreferenceKey, "preference_ghost_image_off");
        final boolean ghost_image_last = ghost_image_pref.equals("preference_ghost_image_last");

        final Handler handler = new Handler(Looper.getMainLooper());

        //new AsyncTask<Void, Void, Bitmap>() {
        Runnable runnable = new Runnable() {
            private static final String TAG = "updateGalleryIcon";
            private Uri uri;
            private boolean is_raw;
            private boolean is_video;

            @Override
            //protected Bitmap doInBackground(Void... params) {
            public void run() {
                if( MyDebug.LOG )
                    Log.d(TAG, "doInBackground");
                StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia();
                Bitmap thumbnail = null;
                KeyguardManager keyguard_manager = (KeyguardManager)MainActivity.this.getSystemService(Context.KEYGUARD_SERVICE);
                boolean is_locked = keyguard_manager != null && keyguard_manager.inKeyguardRestrictedInputMode();
                if( MyDebug.LOG )
                    Log.d(TAG, "is_locked?: " + is_locked);
                if( media != null && getContentResolver() != null && !is_locked ) {
                    // check for getContentResolver() != null, as have had reported Google Play crashes

                    uri = media.getMediaStoreUri(MainActivity.this);
                    is_raw = media.filename != null && StorageUtils.filenameIsRaw(media.filename);
                    is_video = media.video;

                    if( ghost_image_last && !media.video ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "load full size bitmap for ghost image last photo");
                        // use sample factor of 1 so that it's full size for ghost image
                        thumbnail = loadThumbnailFromUri(media.uri, 1);
                    }
                    if( thumbnail == null ) {
                        try {
                            if( !media.video ) {
                                if( MyDebug.LOG )
                                    Log.d(TAG, "load thumbnail for photo");
                                // use sample factor as this image is only used for thumbnail; and
                                // unlike code in MyApplicationInterface.saveImage() we don't need to
                                // worry about the thumbnail animation when taking/saving a photo
                                thumbnail = loadThumbnailFromUri(media.uri, 8);
                            }
                            else if( !media.mediastore ) {
                                if( MyDebug.LOG )
                                    Log.d(TAG, "load thumbnail for video from SAF uri");
                                ParcelFileDescriptor pfd_saf = null; // keep a reference to this as long as retriever, to avoid risk of pfd_saf being garbage collected
                                MediaMetadataRetriever retriever = new MediaMetadataRetriever();
                                try {
                                    pfd_saf = getContentResolver().openFileDescriptor(media.uri, "r");
                                    retriever.setDataSource(pfd_saf.getFileDescriptor());
                                    thumbnail = retriever.getFrameAtTime(-1);
                                }
                                catch(Exception e) {
                                    MyDebug.logStackTrace(TAG, "failed to load video thumbnail", e);
                                }
                                finally {
                                    try {
                                        retriever.release();
                                    }
                                    catch(RuntimeException ex) {
                                        // ignore
                                    }
                                    try {
                                        if( pfd_saf != null ) {
                                            pfd_saf.close();
                                        }
                                    }
                                    catch(IOException e) {
                                        MyDebug.logStackTrace(TAG, "failed to close pfd_saf", e);
                                    }
                                }
                            }
                            else {
                                if( MyDebug.LOG )
                                    Log.d(TAG, "load thumbnail for video");
                                if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) {
                                    final Size size = new Size(512, 384); // same as MediaStore.ThumbnailConstants.MINI_SIZE, which is used for MediaStore.Video.Thumbnails.MINI_KIND
                                    thumbnail = getContentResolver().loadThumbnail(media.uri, size, new CancellationSignal());
                                }
                                else {
                                    // non-deprecated getContentResolver().loadThumbnail requires Android Q
                                    //noinspection deprecation
                                    thumbnail = MediaStore.Video.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Video.Thumbnails.MINI_KIND, null);
                                }
                            }
                        }
                        catch(Throwable e) {
                            // have had Google Play NoClassDefFoundError crashes from getThumbnail() for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn)
                            // also NegativeArraySizeException - best to catch everything
                            if( MyDebug.LOG )
                                Log.e(TAG, "thumbnail exception");
                            MyDebug.logStackTrace(TAG, "thumbnail exception", e);
                        }
                    }
                }
                //return thumbnail;

                final Bitmap thumbnail_f = thumbnail;
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        onPostExecute(thumbnail_f);
                    }
                });
            }

            /** Runs on UI thread, after background work is complete.
             */
            private void onPostExecute(Bitmap thumbnail) {
                if( MyDebug.LOG )
                    Log.d(TAG, "onPostExecute");
                if( update_gallery_future != null && update_gallery_future.isCancelled() ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "was cancelled");
                    update_gallery_future = null;
                    return;
                }
                // since we're now setting the thumbnail to the latest media on disk, we need to make sure clicking the Gallery goes to this
                applicationInterface.getStorageUtils().clearLastMediaScanned();
                if( uri != null ) {
                    if( MyDebug.LOG ) {
                        Log.d(TAG, "found media uri: " + uri);
                        Log.d(TAG, "    is_raw?: " + is_raw);
                    }
                    applicationInterface.getStorageUtils().setLastMediaScanned(uri, is_raw, false, null);
                }
                if( thumbnail != null ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "set gallery button to thumbnail");
                    updateGalleryIcon(thumbnail);
                    applicationInterface.getDrawPreview().updateThumbnail(thumbnail, is_video, false); // needed in case last ghost image is enabled
                }
                else {
                    if( MyDebug.LOG )
                        Log.d(TAG, "set gallery button to blank");
                    updateGalleryIconToBlank();
                }

                update_gallery_future = null;
            }
        //}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        };

        ExecutorService executor = Executors.newSingleThreadExecutor();
        //executor.execute(runnable);
        update_gallery_future = executor.submit(runnable);

        if( MyDebug.LOG )
            Log.d(TAG, "updateGalleryIcon: total time to update gallery icon: " + (System.currentTimeMillis() - debug_time));
    }

    void savingImage(final boolean started) {
        if( MyDebug.LOG )
            Log.d(TAG, "savingImage: " + started);

        this.runOnUiThread(new Runnable() {
            public void run() {
                final ImageButton galleryButton = findViewById(R.id.gallery);
                if( started ) {
                    //galleryButton.setColorFilter(0x80ffffff, PorterDuff.Mode.MULTIPLY);
                    if( gallery_save_anim == null ) {
                        gallery_save_anim = ValueAnimator.ofInt(Color.argb(200, 255, 255, 255), Color.argb(63, 255, 255, 255));
                        gallery_save_anim.setEvaluator(new ArgbEvaluator());
                        gallery_save_anim.setRepeatCount(ValueAnimator.INFINITE);
                        gallery_save_anim.setRepeatMode(ValueAnimator.REVERSE);
                        gallery_save_anim.setDuration(500);
                    }
                    gallery_save_anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                            galleryButton.setColorFilter((Integer)animation.getAnimatedValue(), PorterDuff.Mode.MULTIPLY);
                        }
                    });
                    gallery_save_anim.start();
                }
                else
                if( gallery_save_anim != null ) {
                    gallery_save_anim.cancel();
                }
                galleryButton.setColorFilter(null);
            }
        });
    }

    /** Called when the number of images being saved in ImageSaver changes (or otherwise something
     *  that changes our calculation of whether we can take a new photo, e.g., changing photo mode).
     */
    void imageQueueChanged() {
        if( MyDebug.LOG )
            Log.d(TAG, "imageQueueChanged");
        applicationInterface.getDrawPreview().setImageQueueFull( !applicationInterface.canTakeNewPhoto() );

        /*if( applicationInterface.getImageSaver().getNImagesToSave() == 0) {
            cancelImageSavingNotification();
        }
        else if( has_notification ) {
            // call again to update the text of remaining images
            createImageSavingNotification();
        }*/
    }

    /** Creates a notification to indicate still saving images (or updates an existing one).
      * Update: notifications now removed due to needing permissions on Android 13+.
     */
    private void createImageSavingNotification() {
        if( MyDebug.LOG )
            Log.d(TAG, "createImageSavingNotification");
        /*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
            int n_images_to_save = applicationInterface.getImageSaver().getNRealImagesToSave();
            Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
                    .setSmallIcon(R.drawable.ic_stat_notify_take_photo)
                    .setContentTitle(getString(R.string.app_name))
                    .setContentText(getString(R.string.image_saving_notification) + " " + n_images_to_save + " " + getString(R.string.remaining))
                    //.setStyle(new Notification.BigTextStyle()
                    //        .bigText("Much longer text that cannot fit one line..."))
                    //.setPriority(Notification.PRIORITY_DEFAULT)
                    ;
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.notify(image_saving_notification_id, builder.build());
            has_notification = true;
        }*/
    }

    /** Cancels the notification for saving images.
     *  Update: notifications now removed due to needing permissions on Android 13+.
     */
    private void cancelImageSavingNotification() {
        if( MyDebug.LOG )
            Log.d(TAG, "cancelImageSavingNotification");
        /*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.cancel(image_saving_notification_id);
            has_notification = false;
        }*/
    }

    public void clickedGallery(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedGallery");
        openGallery();
    }

    private void openGallery() {
        if( MyDebug.LOG )
            Log.d(TAG, "openGallery");
        //Intent intent = new Intent(Intent.ACTION_VIEW, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        Uri uri = applicationInterface.getStorageUtils().getLastMediaScanned();
        boolean is_raw = uri != null && applicationInterface.getStorageUtils().getLastMediaScannedIsRaw();
        if( MyDebug.LOG && uri != null ) {
            Log.d(TAG, "found cached most recent uri: " + uri);
            Log.d(TAG, "    is_raw: " + is_raw);
        }
        if( uri == null ) {
            if( MyDebug.LOG )
                Log.d(TAG, "go to latest media");
            StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia();
            if( media != null ) {
                if( MyDebug.LOG ) {
                    Log.d(TAG, "latest uri:" + media.uri);
                    Log.d(TAG, "filename: " + media.filename);
                }
                uri = media.getMediaStoreUri(this);
                if( MyDebug.LOG )
                    Log.d(TAG, "media uri:" + uri);
                is_raw = media.filename != null && StorageUtils.filenameIsRaw(media.filename);
                if( MyDebug.LOG )
                    Log.d(TAG, "is_raw:" + is_raw);
            }
        }

        if( uri != null && !MainActivity.useScopedStorage() ) {
            // check uri exists
            // note, with scoped storage this isn't reliable when using SAF - since we don't actually have permission to access mediastore URIs that
            // were created via Storage Access Framework, even though Open Camera was the application that saved them(!)
            try {
                ContentResolver cr = getContentResolver();
                ParcelFileDescriptor pfd = cr.openFileDescriptor(uri, "r");
                if( pfd == null ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "uri no longer exists (1): " + uri);
                    uri = null;
                    is_raw = false;
                }
                else {
                    pfd.close();
                }
            }
            catch(IOException e) {
                if( MyDebug.LOG )
                    Log.d(TAG, "uri no longer exists (2): " + uri);
                uri = null;
                is_raw = false;
            }
        }
        if( uri == null ) {
            uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            is_raw = false;
        }
        if( !is_test ) {
            // don't do if testing, as unclear how to exit activity to finish test (for testGallery())
            if( MyDebug.LOG )
                Log.d(TAG, "launch uri:" + uri);
            final String REVIEW_ACTION = "com.android.camera.action.REVIEW";
            boolean done = false;
            if( !is_raw ) {
                // REVIEW_ACTION means we can view video files without autoplaying.
                // However, Google Photos at least has problems with going to a RAW photo (in RAW only mode),
                // unless we first pause and resume Open Camera.
                // Update: on Galaxy S10e with Android 11 at least, no longer seem to have problems, but leave
                // the check for is_raw just in case for older devices.
                if( MyDebug.LOG )
                    Log.d(TAG, "try REVIEW_ACTION");
                try {
                    Intent intent = new Intent(REVIEW_ACTION, uri);
                    this.startActivity(intent);
                    done = true;
                }
                catch(ActivityNotFoundException e) {
                    MyDebug.logStackTrace(TAG, "failed to start REVIEW_ACTION intent", e);
                }
            }
            if( !done ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "try ACTION_VIEW");
                try {
                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                    this.startActivity(intent);
                }
                catch(ActivityNotFoundException e) {
                    MyDebug.logStackTrace(TAG, "failed to start ACTION_VIEW intent", e);
                    preview.showToast(null, R.string.no_gallery_app);
                }
                catch(SecurityException e) {
                    // have received this crash from Google Play - don't display a toast, simply do nothing
                    MyDebug.logStackTrace(TAG, "SecurityException from ACTION_VIEW startActivity", e);
                }
            }
        }
    }

    /** Opens the Storage Access Framework dialog to select a folder for save location.
     * @param from_preferences Whether called from the Preferences
     */
    void openFolderChooserDialogSAF(boolean from_preferences) {
        if( MyDebug.LOG )
            Log.d(TAG, "openFolderChooserDialogSAF: " + from_preferences);
        this.saf_dialog_from_preferences = from_preferences;
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        //Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        //intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent, CHOOSE_SAVE_FOLDER_SAF_CODE);
    }

    /** Opens the Storage Access Framework dialog to select a file for ghost image.
     * @param from_preferences Whether called from the Preferences
     */
    void openGhostImageChooserDialogSAF(boolean from_preferences) {
        if( MyDebug.LOG )
            Log.d(TAG, "openGhostImageChooserDialogSAF: " + from_preferences);
        this.saf_dialog_from_preferences = from_preferences;
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        try {
            startActivityForResult(intent, CHOOSE_GHOST_IMAGE_SAF_CODE);
        }
        catch(ActivityNotFoundException e) {
            // see https://stackoverflow.com/questions/34021039/action-open-document-not-working-on-miui/34045627
            preview.showToast(null, R.string.open_files_saf_exception_ghost);
            MyDebug.logStackTrace(TAG, "ActivityNotFoundException from startActivityForResult", e);
        }
    }

    /** Opens the Storage Access Framework dialog to select a file for loading settings.
     * @param from_preferences Whether called from the Preferences
     */
    void openLoadSettingsChooserDialogSAF(boolean from_preferences) {
        if( MyDebug.LOG )
            Log.d(TAG, "openLoadSettingsChooserDialogSAF: " + from_preferences);
        this.saf_dialog_from_preferences = from_preferences;
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("text/xml"); // note that application/xml doesn't work (can't select the xml files)!
        try {
            startActivityForResult(intent, CHOOSE_LOAD_SETTINGS_SAF_CODE);
        }
        catch(ActivityNotFoundException e) {
            // see https://stackoverflow.com/questions/34021039/action-open-document-not-working-on-miui/34045627
            preview.showToast(null, R.string.open_files_saf_exception_generic);
            MyDebug.logStackTrace(TAG, "ActivityNotFoundException from startActivityForResult", e);
        }
    }

    /** Call when the SAF save history has been updated.
     *  This is only public so we can call from testing.
     * @param save_folder The new SAF save folder Uri.
     */
    public void updateFolderHistorySAF(String save_folder) {
        if( MyDebug.LOG )
            Log.d(TAG, "updateSaveHistorySAF");
        if( save_location_history_saf == null ) {
            save_location_history_saf = new SaveLocationHistory(this, "save_location_history_saf", save_folder);
        }
        save_location_history_saf.updateFolderHistory(save_folder, true);
    }

    /** Listens for the response from the Storage Access Framework dialog to select a folder
     *  (as opened with openFolderChooserDialogSAF()).
     */
    public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
        if( MyDebug.LOG )
            Log.d(TAG, "onActivityResult: " + requestCode);

        super.onActivityResult(requestCode, resultCode, resultData);

        switch( requestCode ) {
            case CHOOSE_SAVE_FOLDER_SAF_CODE:
                if( resultCode == RESULT_OK && resultData != null ) {
                    Uri treeUri = resultData.getData();
                    if( MyDebug.LOG )
                        Log.d(TAG, "returned treeUri: " + treeUri);
                    // see https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions :
                    final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                    try {
					/*if( true )
						throw new SecurityException(); // test*/
                        getContentResolver().takePersistableUriPermission(treeUri, takeFlags);

                        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
                        SharedPreferences.Editor editor = sharedPreferences.edit();
                        editor.putString(PreferenceKeys.SaveLocationSAFPreferenceKey, treeUri.toString());
                        editor.apply();

                        if( MyDebug.LOG )
                            Log.d(TAG, "update folder history for saf");
                        updateFolderHistorySAF(treeUri.toString());

                        String file = applicationInterface.getStorageUtils().getImageFolderPath();
                        if( file != null ) {
                            preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + file);
                        }
                    }
                    catch(SecurityException e) {
                        MyDebug.logStackTrace(TAG, "SecurityException failed to take permission", e);
                        preview.showToast(null, R.string.saf_permission_failed);
                        // failed - if the user had yet to set a save location, make sure we switch SAF back off
                        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
                        String uri = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, "");
                        if( uri.isEmpty() ) {
                            if( MyDebug.LOG )
                                Log.d(TAG, "no SAF save location was set");
                            SharedPreferences.Editor editor = sharedPreferences.edit();
                            editor.putBoolean(PreferenceKeys.UsingSAFPreferenceKey, false);
                            editor.apply();
                        }
                    }
                }
                else {
                    if( MyDebug.LOG )
                        Log.d(TAG, "SAF dialog cancelled");
                    // cancelled - if the user had yet to set a save location, make sure we switch SAF back off
                    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
                    String uri = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, "");
                    if( uri.isEmpty() ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "no SAF save location was set");
                        SharedPreferences.Editor editor = sharedPreferences.edit();
                        editor.putBoolean(PreferenceKeys.UsingSAFPreferenceKey, false);
                        editor.apply();
                        preview.showToast(null, R.string.saf_cancelled);
                    }
                }

                if( !saf_dialog_from_preferences ) {
                    setWindowFlagsForCamera();
                    showPreview(true);
                }
                break;
            case CHOOSE_GHOST_IMAGE_SAF_CODE:
                if( resultCode == RESULT_OK && resultData != null ) {
                    Uri fileUri = resultData.getData();
                    if( MyDebug.LOG )
                        Log.d(TAG, "returned single fileUri: " + fileUri);
                    // persist permission just in case?
                    final int takeFlags = resultData.getFlags()
                            & (Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    try {
					/*if( true )
						throw new SecurityException(); // test*/
                        // Check for the freshest data.
                        getContentResolver().takePersistableUriPermission(fileUri, takeFlags);

                        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
                        SharedPreferences.Editor editor = sharedPreferences.edit();
                        editor.putString(PreferenceKeys.GhostSelectedImageSAFPreferenceKey, fileUri.toString());
                        editor.apply();
                    }
                    catch(SecurityException e) {
                        MyDebug.logStackTrace(TAG, "SecurityException failed to take permission", e);
                        preview.showToast(null, R.string.saf_permission_failed_open_image);
                        // failed - if the user had yet to set a ghost image
                        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
                        String uri = sharedPreferences.getString(PreferenceKeys.GhostSelectedImageSAFPreferenceKey, "");
                        if( uri.isEmpty() ) {
                            if( MyDebug.LOG )
                                Log.d(TAG, "no SAF ghost image was set");
                            SharedPreferences.Editor editor = sharedPreferences.edit();
                            editor.putString(PreferenceKeys.GhostImagePreferenceKey, "preference_ghost_image_off");
                            editor.apply();
                        }
                    }
                }
                else {
                    if( MyDebug.LOG )
                        Log.d(TAG, "SAF dialog cancelled");
                    // cancelled - if the user had yet to set a ghost image, make sure we switch the option back off
                    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
                    String uri = sharedPreferences.getString(PreferenceKeys.GhostSelectedImageSAFPreferenceKey, "");
                    if( uri.isEmpty() ) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "no SAF ghost image was set");
                        SharedPreferences.Editor editor = sharedPreferences.edit();
                        editor.putString(PreferenceKeys.GhostImagePreferenceKey, "preference_ghost_image_off");
                        editor.apply();
                    }
                }

                if( !saf_dialog_from_preferences ) {
                    setWindowFlagsForCamera();
                    showPreview(true);
                }
                break;
            case CHOOSE_LOAD_SETTINGS_SAF_CODE:
                if( resultCode == RESULT_OK && resultData != null ) {
                    Uri fileUri = resultData.getData();
                    if( MyDebug.LOG )
                        Log.d(TAG, "returned single fileUri: " + fileUri);
                    // persist permission just in case?
                    final int takeFlags = resultData.getFlags()
                            & (Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    try {
					/*if( true )
						throw new SecurityException(); // test*/
                        // Check for the freshest data.
                        getContentResolver().takePersistableUriPermission(fileUri, takeFlags);

                        settingsManager.loadSettings(fileUri);
                    }
                    catch(SecurityException e) {
                        MyDebug.logStackTrace(TAG, "SecurityException failed to take permission", e);
                        preview.showToast(null, R.string.restore_settings_failed);
                    }
                }
                else {
                    if( MyDebug.LOG )
                        Log.d(TAG, "SAF dialog cancelled");
                }

                if( !saf_dialog_from_preferences ) {
                    setWindowFlagsForCamera();
                    showPreview(true);
                }
                break;
        }
    }

    /** Update the save folder (for non-SAF methods).
     */
    void updateSaveFolder(String new_save_location) {
        if( MyDebug.LOG )
            Log.d(TAG, "updateSaveFolder: " + new_save_location);
        if( new_save_location != null ) {
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            String orig_save_location = this.applicationInterface.getStorageUtils().getSaveLocation();

            if( !orig_save_location.equals(new_save_location) ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "changed save_folder to: " + this.applicationInterface.getStorageUtils().getSaveLocation());
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString(PreferenceKeys.SaveLocationPreferenceKey, new_save_location);
                editor.apply();

                this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), true);
                String save_folder_name = getHumanReadableSaveFolder(this.applicationInterface.getStorageUtils().getSaveLocation());
                this.preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name);
            }
        }
    }

    public static class MyFolderChooserDialog extends FolderChooserDialog {
        @Override
        public void onDismiss(DialogInterface dialog) {
            if( MyDebug.LOG )
                Log.d(TAG, "FolderChooserDialog dismissed");
            // n.b., fragments have to be static (as they might be inserted into a new Activity - see http://stackoverflow.com/questions/15571010/fragment-inner-class-should-be-static),
            // so we access the MainActivity via the fragment's getActivity().
            MainActivity main_activity = (MainActivity)this.getActivity();
            // activity may be null, see https://stackoverflow.com/questions/13116104/best-practice-to-reference-the-parent-activity-of-a-fragment
            // have had Google Play crashes from this
            if( main_activity != null ) {
                main_activity.setWindowFlagsForCamera();
                main_activity.showPreview(true);
                String new_save_location = this.getChosenFolder();
                main_activity.updateSaveFolder(new_save_location);
            }
            else {
                if( MyDebug.LOG )
                    Log.e(TAG, "activity no longer exists!");
            }
            super.onDismiss(dialog);
        }
    }

    /** Processes a user specified save folder. This should be used with the non-SAF scoped storage
     *  method, where the user types a folder directly.
     */
    public static String processUserSaveLocation(String folder) {
        // filter repeated '/', e.g., replace // with /:
        String strip = "//";
        while( !folder.isEmpty() && folder.contains(strip) ) {
            folder = folder.replaceAll(strip, "/");
        }

        if( !folder.isEmpty() && folder.charAt(0) == '/' ) {
            // strip '/' as first character - as absolute paths not allowed with scoped storage
            // whilst we do block entering a '/' as first character in the InputFilter, users could
            // get around this (e.g., put a '/' as second character, then delete the first character)
            folder = folder.substring(1);
        }

        if( !folder.isEmpty() && folder.charAt(folder.length()-1) == '/' ) {
            // strip '/' as last character - MediaStore will ignore it, but seems cleaner to strip it out anyway
            // (we still need to allow '/' as last character in the InputFilter, otherwise users won't be able to type it whilst writing a subfolder)
            folder = folder.substring(0, folder.length()-1);
        }

        return folder;
    }

    /** Creates a dialog builder for specifying a save folder dialog (used when not using SAF,
     *  and on scoped storage, as an alternative to using FolderChooserDialog).
     */
    public AlertDialog.Builder createSaveFolderDialog() {
        final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
        alertDialog.setTitle(R.string.preference_save_location);

        final View dialog_view = LayoutInflater.from(this).inflate(R.layout.alertdialog_edittext, null);
        final EditText editText = dialog_view.findViewById(R.id.edit_text);

        // set hint instead of content description for EditText, see https://support.google.com/accessibility/android/answer/6378120
        editText.setHint(getResources().getString(R.string.preference_save_location));
        editText.setInputType(InputType.TYPE_CLASS_TEXT);
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        editText.setText(sharedPreferences.getString(PreferenceKeys.SaveLocationPreferenceKey, "OpenCamera"));
        InputFilter filter = new InputFilter() {
            // whilst Android seems to allow any characters on internal memory, SD cards are typically formatted with FAT32
            final String disallowed = "|\\?*<\":>";
            public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
                for(int i=start;i<end;i++) {
                    if( disallowed.indexOf( source.charAt(i) ) != -1 ) {
                        return "";
                    }
                }
                // also check for '/', not allowed at start
                if( dstart == 0 && start < source.length() && source.charAt(start) == '/' ) {
                    return "";
                }
                return null;
            }
        };
        editText.setFilters(new InputFilter[]{filter});

        alertDialog.setView(dialog_view);

        alertDialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                if( MyDebug.LOG )
                    Log.d(TAG, "save location clicked okay");

                String folder = editText.getText().toString();
                folder = processUserSaveLocation(folder);

                updateSaveFolder(folder);
            }
        });
        alertDialog.setNegativeButton(android.R.string.cancel, null);

        return alertDialog;
    }

    /** Opens Open Camera's own (non-Storage Access Framework) dialog to select a folder.
     */
    private void openFolderChooserDialog() {
        if( MyDebug.LOG )
            Log.d(TAG, "openFolderChooserDialog");
        showPreview(false);
        setWindowFlagsForSettings();

        if( MainActivity.useScopedStorage() ) {
            AlertDialog.Builder alertDialog = createSaveFolderDialog();
            final AlertDialog alert = alertDialog.create();
            // AlertDialog.Builder.setOnDismissListener() requires API level 17, so do it this way instead
            alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
                @Override
                public void onDismiss(DialogInterface arg0) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "save folder dialog dismissed");
                    setWindowFlagsForCamera();
                    showPreview(true);
                }
            });
            alert.show();
        }
        else {
            File start_folder = getStorageUtils().getImageFolder();

            FolderChooserDialog fragment = new MyFolderChooserDialog();
            fragment.setStartFolder(start_folder);
            // use commitAllowingStateLoss() instead of fragment.show(), does to "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState" crash seen on Google Play
            // see https://stackoverflow.com/questions/14262312/java-lang-illegalstateexception-can-not-perform-this-action-after-onsaveinstanc
            //fragment.show(getFragmentManager(), "FOLDER_FRAGMENT");
            getFragmentManager().beginTransaction().add(fragment, "FOLDER_FRAGMENT").commitAllowingStateLoss();
        }
    }

    /** Returns a human readable string for the save_folder (as stored in the preferences).
     */
    String getHumanReadableSaveFolder(String save_folder) {
        if( applicationInterface.getStorageUtils().isUsingSAF() ) {
            // try to get human readable form if possible
            String file_name = applicationInterface.getStorageUtils().getFilePathFromDocumentUriSAF(Uri.parse(save_folder), true);
            if( file_name != null ) {
                save_folder = file_name;
            }
        }
        else {
            // The strings can either be a sub-folder of DCIM, or (pre-scoped-storage) a full path, so normally either can be displayed.
            // But with scoped storage, an empty string is used to mean DCIM, so seems clearer to say that instead of displaying a blank line!
            if( MainActivity.useScopedStorage() && save_folder.isEmpty() ) {
                save_folder = "DCIM";
            }
        }
        return save_folder;
    }

    /** User can long-click on gallery to select a recent save location from the history, of if not available,
     *  go straight to the file dialog to pick a folder.
     */
    private void longClickedGallery() {
        if( MyDebug.LOG )
            Log.d(TAG, "longClickedGallery");
        if( applicationInterface.getStorageUtils().isUsingSAF() ) {
            if( save_location_history_saf == null || save_location_history_saf.size() <= 1 ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "go straight to choose folder dialog for SAF");
                openFolderChooserDialogSAF(false);
                return;
            }
        }
        else {
            if( save_location_history.size() <= 1 ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "go straight to choose folder dialog");
                openFolderChooserDialog();
                return;
            }
        }

        final SaveLocationHistory history = applicationInterface.getStorageUtils().isUsingSAF() ? save_location_history_saf : save_location_history;
        showPreview(false);
        AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
        alertDialog.setTitle(R.string.choose_save_location);
        CharSequence [] items = new CharSequence[history.size()+2];
        int index=0;
        // history is stored in order most-recent-last
        for(int i=0;i<history.size();i++) {
            String folder_name = history.get(history.size() - 1 - i);
            folder_name = getHumanReadableSaveFolder(folder_name);
            items[index++] = folder_name;
        }
        final int clear_index = index;
        items[index++] = getResources().getString(R.string.clear_folder_history);
        final int new_index = index;
        //noinspection UnusedAssignment
        items[index++] = getResources().getString(R.string.choose_another_folder);
        //alertDialog.setItems(items, new DialogInterface.OnClickListener() {
        alertDialog.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if( which == clear_index ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "selected clear save history");
                    new AlertDialog.Builder(MainActivity.this)
                            .setIcon(android.R.drawable.ic_dialog_alert)
                            .setTitle(R.string.clear_folder_history)
                            .setMessage(R.string.clear_folder_history_question)
                            .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    if( MyDebug.LOG )
                                        Log.d(TAG, "confirmed clear save history");
                                    if( applicationInterface.getStorageUtils().isUsingSAF() )
                                        clearFolderHistorySAF();
                                    else
                                        clearFolderHistory();
                                    setWindowFlagsForCamera();
                                    showPreview(true);
                                }
                            })
                            .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    if( MyDebug.LOG )
                                        Log.d(TAG, "don't clear save history");
                                    setWindowFlagsForCamera();
                                    showPreview(true);
                                }
                            })
                            .setOnCancelListener(new DialogInterface.OnCancelListener() {
                                @Override
                                public void onCancel(DialogInterface arg0) {
                                    if( MyDebug.LOG )
                                        Log.d(TAG, "cancelled clear save history");
                                    setWindowFlagsForCamera();
                                    showPreview(true);
                                }
                            })
                            .show();
                }
                else if( which == new_index ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "selected choose new folder");
                    if( applicationInterface.getStorageUtils().isUsingSAF() ) {
                        openFolderChooserDialogSAF(false);
                    }
                    else {
                        openFolderChooserDialog();
                    }
                }
                else {
                    if( MyDebug.LOG )
                        Log.d(TAG, "selected: " + which);
                    if( which >= 0 && which < history.size() ) {
                        String save_folder = history.get(history.size() - 1 - which);
                        if( MyDebug.LOG )
                            Log.d(TAG, "changed save_folder from history to: " + save_folder);
                        String save_folder_name = getHumanReadableSaveFolder(save_folder);
                        preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name);
                        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
                        SharedPreferences.Editor editor = sharedPreferences.edit();
                        if( applicationInterface.getStorageUtils().isUsingSAF() )
                            editor.putString(PreferenceKeys.SaveLocationSAFPreferenceKey, save_folder);
                        else
                            editor.putString(PreferenceKeys.SaveLocationPreferenceKey, save_folder);
                        editor.apply();
                        history.updateFolderHistory(save_folder, true); // to move new selection to most recent
                    }
                    setWindowFlagsForCamera();
                    showPreview(true);
                }

                dialog.dismiss(); // need to explicitly dismiss for setSingleChoiceItems
            }
        });
        alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface arg0) {
                setWindowFlagsForCamera();
                showPreview(true);
            }
        });
        //getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
        setWindowFlagsForSettings();
        showAlert(alertDialog.create());
    }

    /** Clears the non-SAF folder history.
     */
    public void clearFolderHistory() {
        if( MyDebug.LOG )
            Log.d(TAG, "clearFolderHistory");
        save_location_history.clearFolderHistory(getStorageUtils().getSaveLocation());
    }

    /** Clears the SAF folder history.
     */
    public void clearFolderHistorySAF() {
        if( MyDebug.LOG )
            Log.d(TAG, "clearFolderHistorySAF");
        save_location_history_saf.clearFolderHistory(getStorageUtils().getSaveLocationSAF());
    }

    static private void putBundleExtra(Bundle bundle, String key, List<String> values) {
        if( values != null ) {
            String [] values_arr = new String[values.size()];
            int i=0;
            for(String value: values) {
                values_arr[i] = value;
                i++;
            }
            bundle.putStringArray(key, values_arr);
        }
    }

    public void clickedShare(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedShare");
        applicationInterface.shareLastImage();
    }

    public void clickedTrash(View view) {
        if( MyDebug.LOG )
            Log.d(TAG, "clickedTrash");
        applicationInterface.trashLastImage();
    }

    /** User has pressed the take picture button, or done an equivalent action to request this (e.g.,
     *  volume buttons, audio trigger).
     * @param photo_snapshot If true, then the user has requested taking a photo whilst video
     *                       recording. If false, either take a photo or start/stop video depending
     *                       on the current mode.
     */
    public void takePicture(boolean photo_snapshot) {
        if( MyDebug.LOG )
            Log.d(TAG, "takePicture");

        if( applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama ) {
            if( preview.isTakingPhoto() ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "ignore whilst taking panorama photo");
            }
            else if( applicationInterface.getGyroSensor().isRecording() ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "panorama complete");
                applicationInterface.finishPanorama();
                return;
            }
            else if( !applicationInterface.canTakeNewPhoto() ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "can't start new panoroma, still saving in background");
                // we need to test here, otherwise the Preview won't take a new photo - but we'll think we've
                // started the panorama!
            }
            else {
                if( MyDebug.LOG )
                    Log.d(TAG, "start panorama");
                applicationInterface.startPanorama();
            }
        }

        this.takePicturePressed(photo_snapshot, false);
    }

    /** Returns whether the last photo operation was a continuous fast burst.
     */
    boolean lastContinuousFastBurst() {
        return this.last_continuous_fast_burst;
    }

    /**
     * @param photo_snapshot If true, then the user has requested taking a photo whilst video
     *                       recording. If false, either take a photo or start/stop video depending
     *                       on the current mode.
     * @param continuous_fast_burst If true, then start a continuous fast burst.
     */
    void takePicturePressed(boolean photo_snapshot, boolean continuous_fast_burst) {
        if( MyDebug.LOG )
            Log.d(TAG, "takePicturePressed");

        closePopup();

        this.last_continuous_fast_burst = continuous_fast_burst;
        this.preview.takePicturePressed(photo_snapshot, continuous_fast_burst);
    }

    /** Lock the screen - this is Open Camera's own lock to guard against accidental presses,
     *  not the standard Android lock.
     */
    void lockScreen() {
        findViewById(R.id.locker).setOnTouchListener(new View.OnTouchListener() {
            @SuppressLint("ClickableViewAccessibility") @Override
            public boolean onTouch(View arg0, MotionEvent event) {
                return gestureDetector.onTouchEvent(event);
                //return true;
            }
        });
        screen_is_locked = true;
        this.enableScreenLockOnBackPressedCallback(true); // also disable back button
    }

    /** Unlock the screen (see lockScreen()).
     */
    void unlockScreen() {
        findViewById(R.id.locker).setOnTouchListener(null);
        screen_is_locked = false;
        this.enableScreenLockOnBackPressedCallback(false); // reenable back button
    }

    /** Whether the screen is locked (see lockScreen()).
     */
    public boolean isScreenLocked() {
        return screen_is_locked;
    }

    /** Listen for gestures.
     *  Doing a swipe will unlock the screen (see lockScreen()).
     */
    private class MyGestureDetector extends SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
            try {
                if( MyDebug.LOG )
                    Log.d(TAG, "from " + e1.getX() + " , " + e1.getY() + " to " + e2.getX() + " , " + e2.getY());
                final ViewConfiguration vc = ViewConfiguration.get(MainActivity.this);
                //final int swipeMinDistance = 4*vc.getScaledPagingTouchSlop();
                final float scale = getResources().getDisplayMetrics().density;
                final int swipeMinDistance = (int) (160 * scale + 0.5f); // convert dps to pixels
                final int swipeThresholdVelocity = vc.getScaledMinimumFlingVelocity();
                if( MyDebug.LOG ) {
                    Log.d(TAG, "from " + e1.getX() + " , " + e1.getY() + " to " + e2.getX() + " , " + e2.getY());
                    Log.d(TAG, "swipeMinDistance: " + swipeMinDistance);
                }
                float xdist = e1.getX() - e2.getX();
                float ydist = e1.getY() - e2.getY();
                float dist2 = xdist*xdist + ydist*ydist;
                float vel2 = velocityX*velocityX + velocityY*velocityY;
                if( dist2 > swipeMinDistance*swipeMinDistance && vel2 > swipeThresholdVelocity*swipeThresholdVelocity ) {
                    preview.showToast(screen_locked_toast, R.string.unlocked);
                    unlockScreen();
                }
            }
            catch(Exception e) {
                MyDebug.logStackTrace(TAG, "onFling failed", e);
            }
            return false;
        }

        @Override
        public boolean onDown(@NonNull MotionEvent e) {
            preview.showToast(screen_locked_toast, R.string.screen_is_locked);
            return true;
        }
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle state) {
        if( MyDebug.LOG )
            Log.d(TAG, "onSaveInstanceState");
        super.onSaveInstanceState(state);
        if( this.preview != null ) {
            preview.onSaveInstanceState(state);
        }
        if( this.applicationInterface != null ) {
            applicationInterface.onSaveInstanceState(state);
        }
    }

    public boolean supportsExposureButton() {
        if( preview.isVideoHighSpeed() ) {
            // manual ISO/exposure not supported for high speed video mode
            // it's safer not to allow opening the panel at all (otherwise the user could open it, and switch to manual)
            return false;
        }
        if( applicationInterface.isCameraExtensionPref() ) {
            // nothing in this UI (exposure compensation, manual ISO/exposure, manual white balance) is supported for camera extensions
            return false;
        }
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String iso_value = sharedPreferences.getString(PreferenceKeys.ISOPreferenceKey, CameraController.ISO_DEFAULT);
        boolean manual_iso = !iso_value.equals(CameraController.ISO_DEFAULT);
        return preview.supportsExposures() || (manual_iso && preview.supportsISORange() );
    }

    void cameraSetup() {
        long debug_time = 0;
        if( MyDebug.LOG ) {
            Log.d(TAG, "cameraSetup");
            debug_time = System.currentTimeMillis();
        }
        if( 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;
        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "multi-window mode");
            // don't support want_no_limits mode in multi-window mode - extra complexity that the
            // preview size could change from simply resizing the window; also problem that the
            // navigation_gap, and whether we'd want want_no_limits, can both change depending on
            // device orientation (because application can e.g. be in landscape mode even if device
            // has switched to portrait)
        }
        else if( set_window_insets_listener && !edge_to_edge_mode ) {
            Point display_size = new Point();
            applicationInterface.getDisplaySize(display_size, true);
            int display_width = Math.max(display_size.x, display_size.y);
            int display_height = Math.min(display_size.x, display_size.y);
            double display_aspect_ratio = ((double)display_width)/(double)display_height;
            double preview_aspect_ratio = preview.getCurrentPreviewAspectRatio();
            if( MyDebug.LOG ) {
                Log.d(TAG, "display_aspect_ratio: " + display_aspect_ratio);
                Log.d(TAG, "preview_aspect_ratio: " + preview_aspect_ratio);
            }
            boolean preview_is_wide = preview_aspect_ratio > display_aspect_ratio + 1.0e-5f;
            if( test_preview_want_no_limits ) {
                preview_is_wide = test_preview_want_no_limits_value;
            }
            if( preview_is_wide ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "preview is wide, set want_no_limits");
                this.want_no_limits = true;

                if( !old_want_no_limits ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "need to change to FLAG_LAYOUT_NO_LIMITS");
                    // Ideally we'd just go straight to FLAG_LAYOUT_NO_LIMITS mode, but then all calls to onApplyWindowInsets()
                    // end up returning a value of 0 for the navigation_gap! So we need to wait until we know the navigation_gap.
                    if( navigation_gap != 0 ) {
                        // already have navigation gap, can go straight into no limits mode
                        if( MyDebug.LOG )
                            Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
                        showUnderNavigation(true);
                        // need to layout the UI again due to now taking the navigation gap into account
                        if( MyDebug.LOG )
                            Log.d(TAG, "layout UI due to changing want_no_limits behaviour");
                        mainUI.layoutUI();
                    }
                    else {
                        if( MyDebug.LOG )
                            Log.d(TAG, "but navigation_gap is 0");
                    }
                }
            }
            else if( old_want_no_limits && navigation_gap != 0 ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
                showUnderNavigation(false);
                // need to layout the UI again due to no longer taking the navigation gap into account
                if( MyDebug.LOG )
                    Log.d(TAG, "layout UI due to changing want_no_limits behaviour");
                mainUI.layoutUI();
            }
        }

        if( this.supportsForceVideo4K() && preview.usingCamera2API() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "using Camera2 API, so can disable the force 4K option");
            this.disableForceVideo4K();
        }
        if( this.supportsForceVideo4K() && preview.getVideoQualityHander().getSupportedVideoSizes() != null ) {
            for(CameraController.Size size : preview.getVideoQualityHander().getSupportedVideoSizes()) {
                if( size.width >= 3840 && size.height >= 2160 ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "camera natively supports 4K, so can disable the force option");
                    this.disableForceVideo4K();
                }
            }
        }
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: time after handling Force 4K option: " + (System.currentTimeMillis() - debug_time));

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        {
            if( MyDebug.LOG )
                Log.d(TAG, "set up zoom");
            if( MyDebug.LOG )
                Log.d(TAG, "has_zoom? " + preview.supportsZoom());
            SeekBar zoomSeekBar = findViewById(R.id.zoom_seekbar);

            if( preview.supportsZoom() ) {
                zoomSeekBar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
                zoomSeekBar.setMax(preview.getMaxZoom());
                zoomSeekBar.setProgress(preview.getMaxZoom()-preview.getCameraController().getZoom());
                zoomSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
                    private long last_haptic_time;

                    @Override
                    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "zoom onProgressChanged: " + progress);
                        // note we zoom even if !fromUser, as various other UI controls (multitouch, volume key zoom)
                        // indirectly set zoom via this method, from setting the zoom slider
                        // if hasSmoothZoom()==true, then the preview already handled zooming to the current value
                        if( !preview.hasSmoothZoom() ) {
                            int new_zoom_factor = preview.getMaxZoom() - progress;
                            if( fromUser && preview.getCameraController() != null ) {
                                float old_zoom_ratio = preview.getZoomRatio();
                                float new_zoom_ratio = preview.getZoomRatio(new_zoom_factor);
                                if( new_zoom_ratio != old_zoom_ratio ) {
                                    last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
                                }
                            }
                            preview.zoomTo(new_zoom_factor, false, true);
                        }
                    }

                    @Override
                    public void onStartTrackingTouch(SeekBar seekBar) {
                    }

                    @Override
                    public void onStopTrackingTouch(SeekBar seekBar) {
                    }
                });

                if( sharedPreferences.getBoolean(PreferenceKeys.ShowZoomSliderControlsPreferenceKey, true) ) {
                    if( !mainUI.inImmersiveMode() ) {
                        zoomSeekBar.setVisibility(View.VISIBLE);
                    }
                }
                else {
                    zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this; in future we might want this similarly for exposure panel
                }
            }
            else {
                zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this; in future we might want this similarly for the exposure panel
            }
            if( MyDebug.LOG )
                Log.d(TAG, "cameraSetup: time after setting up zoom: " + (System.currentTimeMillis() - debug_time));

            View takePhotoButton = findViewById(R.id.take_photo);
            if( sharedPreferences.getBoolean(PreferenceKeys.ShowTakePhotoPreferenceKey, true) ) {
                if( !mainUI.inImmersiveMode() ) {
                    takePhotoButton.setVisibility(View.VISIBLE);
                }
            }
            else {
                takePhotoButton.setVisibility(View.INVISIBLE);
            }
        }
        {
            if( MyDebug.LOG )
                Log.d(TAG, "set up manual focus");
            setManualFocusSeekbar(false);
            setManualFocusSeekbar(true);
        }
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: time after setting up manual focus: " + (System.currentTimeMillis() - debug_time));
        {
            if( preview.supportsISORange()) {
                if( MyDebug.LOG )
                    Log.d(TAG, "set up iso");
                final SeekBar iso_seek_bar = findViewById(R.id.iso_seekbar);
                iso_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
                //setProgressSeekbarExponential(iso_seek_bar, preview.getMinimumISO(), preview.getMaximumISO(), preview.getCameraController().getISO());
                manualSeekbars.setProgressSeekbarISO(iso_seek_bar, preview.getMinimumISO(), preview.getMaximumISO(), preview.getCameraController().getISO());
                iso_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
                    private long last_haptic_time;

                    @Override
                    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "iso seekbar onProgressChanged: " + progress);
						/*double frac = progress/(double)iso_seek_bar.getMax();
						if( MyDebug.LOG )
							Log.d(TAG, "exposure_time frac: " + frac);
						double scaling = MainActivity.seekbarScaling(frac);
						if( MyDebug.LOG )
							Log.d(TAG, "exposure_time scaling: " + scaling);
						int min_iso = preview.getMinimumISO();
						int max_iso = preview.getMaximumISO();
						int iso = min_iso + (int)(scaling * (max_iso - min_iso));*/
						/*int min_iso = preview.getMinimumISO();
						int max_iso = preview.getMaximumISO();
						int iso = (int)exponentialScaling(frac, min_iso, max_iso);*/
                        // n.b., important to update even if fromUser==false (e.g., so this works when user changes ISO via clicking
                        // the ISO buttons rather than moving the slider directly, see MainUI.setupExposureUI())
                        preview.setISO( manualSeekbars.getISO(progress) );
                        mainUI.updateSelectedISOButton();
                        if( fromUser ) {
                            last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
                        }
                    }

                    @Override
                    public void onStartTrackingTouch(SeekBar seekBar) {
                    }

                    @Override
                    public void onStopTrackingTouch(SeekBar seekBar) {
                    }
                });
                if( preview.supportsExposureTime() ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "set up exposure time");
                    final SeekBar exposure_time_seek_bar = findViewById(R.id.exposure_time_seekbar);
                    exposure_time_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
                    //setProgressSeekbarExponential(exposure_time_seek_bar, preview.getMinimumExposureTime(), preview.getMaximumExposureTime(), preview.getCameraController().getExposureTime());
                    manualSeekbars.setProgressSeekbarShutterSpeed(exposure_time_seek_bar, preview.getMinimumExposureTime(), preview.getMaximumExposureTime(), preview.getCameraController().getExposureTime());
                    exposure_time_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
                        private long last_haptic_time;

                        @Override
                        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                            if( MyDebug.LOG )
                                Log.d(TAG, "exposure_time seekbar onProgressChanged: " + progress);
							/*double frac = progress/(double)exposure_time_seek_bar.getMax();
							if( MyDebug.LOG )
								Log.d(TAG, "exposure_time frac: " + frac);
							long min_exposure_time = preview.getMinimumExposureTime();
							long max_exposure_time = preview.getMaximumExposureTime();
							long exposure_time = exponentialScaling(frac, min_exposure_time, max_exposure_time);*/
                            preview.setExposureTime( manualSeekbars.getExposureTime(progress) );
                            if( fromUser ) {
                                last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
                            }
                        }

                        @Override
                        public void onStartTrackingTouch(SeekBar seekBar) {
                        }

                        @Override
                        public void onStopTrackingTouch(SeekBar seekBar) {
                        }
                    });
                }
            }
        }
        setManualWBSeekbar();
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: time after setting up iso: " + (System.currentTimeMillis() - debug_time));
        {
            exposure_seekbar_values = null;
            if( preview.supportsExposures() ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "set up exposure compensation");
                final int min_exposure = preview.getMinimumExposure();
                SeekBar exposure_seek_bar = findViewById(R.id.exposure_seekbar);
                exposure_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state

                final int exposure_seekbar_n_repeated_zero = 3; // how many times to repeat 0 for R.id.exposure_seekbar, so that it "sticks" to zero when changing seekbar

                //exposure_seek_bar.setMax( preview.getMaximumExposure() - min_exposure + exposure_seekbar_n_repeated_zero-1 );
                //exposure_seek_bar.setProgress( preview.getCurrentExposure() - min_exposure );

                exposure_seekbar_values = new ArrayList<>();
                int current_exposure = preview.getCurrentExposure();
                int current_progress = 0;
                for(int i=min_exposure;i<=preview.getMaximumExposure();i++) {
                    exposure_seekbar_values.add(i);
                    if( i == 0 ) {
                        exposure_seekbar_values_zero = exposure_seekbar_values.size()-1;
                        exposure_seekbar_values_zero += (exposure_seekbar_n_repeated_zero-1)/2; // centre within the region of zeroes
                        for(int j=0;j<exposure_seekbar_n_repeated_zero-1;j++) {
                            exposure_seekbar_values.add(i);
                        }
                    }
                    if( i == current_exposure ) {
                        if( i == 0 ) {
                            current_progress += exposure_seekbar_values_zero;
                        }
                        else {
                            current_progress = exposure_seekbar_values.size()-1;
                        }
                    }
                }
                exposure_seek_bar.setMax( exposure_seekbar_values.size()-1 );
                exposure_seek_bar.setProgress( current_progress );
                exposure_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
                    private long last_haptic_time;

                    @Override
                    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                        if( MyDebug.LOG )
                            Log.d(TAG, "exposure seekbar onProgressChanged: " + progress);
                        if( exposure_seekbar_values == null ) {
                            Log.e(TAG, "exposure_seekbar_values is null");
                            return;
                        }
                        int new_exposure = getExposureSeekbarValue(progress);
                        if( fromUser ) {
                            // check if not scrolling past the repeated zeroes
                            if( preview.getCurrentExposure() != new_exposure ) {
                                last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
                            }
                        }
                        preview.setExposure(new_exposure);
                    }

                    @Override
                    public void onStartTrackingTouch(SeekBar seekBar) {
                    }

                    @Override
                    public void onStopTrackingTouch(SeekBar seekBar) {
                    }
                });
            }
        }
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: time after setting up exposure: " + (System.currentTimeMillis() - debug_time));

        // On-screen icons such as exposure lock, white balance lock, face detection etc are made visible if necessary in
        // MainUI.showGUI()
        // However still need to enable visibility of icons where visibility depends on camera setup - e.g., exposure button
        // not supported for high speed video frame rates - see testTakeVideoFPSHighSpeedManual().
        // (Disabling is done in checkDisableGUIIcons(), called below.)
        View exposureButton = findViewById(R.id.exposure);
        //exposureButton.setVisibility(supportsExposureButton() && !mainUI.inImmersiveMode() ? View.VISIBLE : View.GONE);
        if( supportsExposureButton() && !mainUI.inImmersiveMode() )
            exposureButton.setVisibility(View.VISIBLE);

        // needed as availability of some icons is per-camera (e.g., flash, RAW)
        // for making icons visible, this is done elsewhere in call to MainUI.showGUI()
        if( checkDisableGUIIcons() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "cameraSetup: need to layoutUI as we hid some icons");
            mainUI.layoutUI();
        }

        // need to update some icons, e.g., white balance and exposure lock due to them being turned off when pause/resuming
        mainUI.updateOnScreenIcons();

        mainUI.setPopupIcon(); // needed so that the icon is set right even if no flash mode is set when starting up camera (e.g., switching to front camera with no flash)
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: time after setting popup icon: " + (System.currentTimeMillis() - debug_time));

        mainUI.setTakePhotoIcon();
        mainUI.setSwitchCameraContentDescription();
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: time after setting take photo icon: " + (System.currentTimeMillis() - debug_time));

        if( !block_startup_toast ) {
            this.showPhotoVideoToast(false);
        }
        block_startup_toast = false;
        if( MyDebug.LOG )
            Log.d(TAG, "cameraSetup: total time for cameraSetup: " + (System.currentTimeMillis() - debug_time));

        this.getApplicationInterface().getDrawPreview().setDimPreview(false);

        if( push_switched_camera ) {
            push_switched_camera = false;
            View switchCameraButton = findViewById(R.id.switch_camera);
            switchCameraButton.animate().rotationBy(180).setDuration(250).setInterpolator(new AccelerateDecelerateInterpolator()).start();
        }
    }

    public static long performHapticFeedback(SeekBar seekBar, long last_haptic_time) {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(seekBar.getContext());
        if( sharedPreferences.getBoolean(PreferenceKeys.AllowHapticFeedbackPreferenceKey, true) ) {
            long time_ms = System.currentTimeMillis();
            if( time_ms > last_haptic_time + 16 ) {
                last_haptic_time = time_ms;
                // SEGMENT_TICK or SEGMENT_TICK doesn't work on Galaxy S24+ at least, even though on Android 14!
                /*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ) {
                    seekBar.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK);
                }
                else*/ {
                    seekBar.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
                }
            }
        }
        return last_haptic_time;
    }

    public void setManualFocusSeekbarProgress(final boolean is_target_distance, float focus_distance) {
        final SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar);
        ManualSeekbars.setProgressSeekbarScaled(focusSeekBar, 0.0, preview.getMinimumFocusDistance(), focus_distance);
    }

    private void setManualFocusSeekbar(final boolean is_target_distance) {
        if( MyDebug.LOG )
            Log.d(TAG, "setManualFocusSeekbar");
        final SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar);
        focusSeekBar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
        setManualFocusSeekbarProgress(is_target_distance, is_target_distance ? preview.getCameraController().getFocusBracketingTargetDistance() : preview.getCameraController().getFocusDistance());
        focusSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            private boolean has_saved_zoom;
            private int saved_zoom_factor;

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if( !is_target_distance && applicationInterface.isFocusBracketingSourceAutoPref() ) {
                    // source is set from continuous focus, not by changing the seekbar
                    if( fromUser ) {
                        // but if user has manually changed, then exit auto mode
                        applicationInterface.setFocusBracketingSourceAutoPref(false);
                        mainUI.destroyPopup(); // need to recreate popup
                    }
                    else {
                        return;
                    }
                }
                double frac = progress/(double)focusSeekBar.getMax();
                double scaling = ManualSeekbars.seekbarScaling(frac);
                float focus_distance = (float)(scaling * preview.getMinimumFocusDistance());
                preview.setFocusDistance(focus_distance, is_target_distance, true);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                if( MyDebug.LOG )
                    Log.d(TAG, "manual focus seekbar: onStartTrackingTouch");
                has_saved_zoom = false;
                if( preview.supportsZoom() ) {
                    int focus_assist = applicationInterface.getFocusAssistPref();
                    if( focus_assist > 0 && preview.getCameraController() != null ) {
                        has_saved_zoom = true;
                        saved_zoom_factor = preview.getCameraController().getZoom();
                        if( MyDebug.LOG )
                            Log.d(TAG, "zoom by " + focus_assist + " for focus assist, zoom factor was: " + saved_zoom_factor);
                        int new_zoom_factor = preview.getScaledZoomFactor(focus_assist);
                        preview.getCameraController().setZoom(new_zoom_factor);
                    }
                }
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                if( MyDebug.LOG )
                    Log.d(TAG, "manual focus seekbar: onStopTrackingTouch");
                if( has_saved_zoom && preview.getCameraController() != null ) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "unzoom for focus assist, zoom factor was: " + saved_zoom_factor);
                    preview.getCameraController().setZoom(saved_zoom_factor);
                }
                preview.stoppedSettingFocusDistance(is_target_distance);
            }
        });
        setManualFocusSeekBarVisibility(is_target_distance);
    }

    public boolean showManualFocusSeekbar(final boolean is_target_distance) {
        if( (applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.FocusBracketing) && !preview.isVideo() ) {
            return true; // both seekbars shown in focus bracketing mode
        }
        if( is_target_distance ) {
            return false; // target seekbar only shown in focus bracketing mode
        }
        boolean is_visible = preview.getCurrentFocusValue() != null && this.getPreview().getCurrentFocusValue().equals("focus_mode_manual2");
        return is_visible;
    }

    void setManualFocusSeekBarVisibility(final boolean is_target_distance) {
        boolean is_visible = showManualFocusSeekbar(is_target_distance);
        SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar);
        final int visibility = is_visible ? View.VISIBLE : View.GONE;
        focusSeekBar.setVisibility(visibility);
        if( is_visible ) {
            applicationInterface.getDrawPreview().updateSettings(); // needed so that we reset focus_seekbars_margin_left, as the focus seekbars can only be updated when visible
        }
    }

    public void setManualWBSeekbar() {
        if( MyDebug.LOG )
            Log.d(TAG, "setManualWBSeekbar");
        if( preview.getSupportedWhiteBalances() != null && preview.supportsWhiteBalanceTemperature() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "set up manual white balance");
            SeekBar white_balance_seek_bar = findViewById(R.id.white_balance_seekbar);
            white_balance_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
            final int minimum_temperature = preview.getMinimumWhiteBalanceTemperature();
            final int maximum_temperature = preview.getMaximumWhiteBalanceTemperature();
			/*
			// white balance should use linear scaling
			white_balance_seek_bar.setMax(maximum_temperature - minimum_temperature);
			white_balance_seek_bar.setProgress(preview.getCameraController().getWhiteBalanceTemperature() - minimum_temperature);
			*/
            manualSeekbars.setProgressSeekbarWhiteBalance(white_balance_seek_bar, minimum_temperature, maximum_temperature, preview.getCameraController().getWhiteBalanceTemperature());
            white_balance_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
                private long last_haptic_time;

                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    if( MyDebug.LOG )
                        Log.d(TAG, "white balance seekbar onProgressChanged: " + progress);
                    //int temperature = minimum_temperature + progress;
                    //preview.setWhiteBalanceTemperature(temperature);
                    preview.setWhiteBalanceTemperature( manualSeekbars.getWhiteBalanceTemperature(progress) );
                    if( fromUser ) {
                        last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
                    }
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
        }
    }

    public boolean supportsAutoStabilise() {
        if( applicationInterface.isRawOnly() )
            return false; // if not saving JPEGs, no point having auto-stabilise mode, as it won't affect the RAW images
        if( applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama )
            return false; // not supported in panorama mode
        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
        return true;
    }

    public boolean supportsHDR() {
        // we also require the device have sufficient memory to do the processing
        return large_heap_memory >= 128 && preview.supportsExpoBracketing();
    }

    public boolean supportsExpoBracketing() {
        if( applicationInterface.isImageCaptureIntent() )
            return false; // don't support expo bracketing mode if called from image capture intent
        return preview.supportsExpoBracketing();
    }

    public boolean supportsFocusBracketing() {
        if( applicationInterface.isImageCaptureIntent() )
            return false; // don't support focus bracketing mode if called from image capture intent
        return preview.supportsFocusBracketing();
    }

    /** Whether we support the auto mode for setting source focus distance for focus bracketing mode.
     *  Note the caller should still separately call supportsFocusBracketing() to see if focus
     *  bracketing is supported in the first place.
     */
    public boolean supportsFocusBracketingSourceAuto() {
        return preview.supportsFocus() && preview.getSupportedFocusValues().contains("focus_mode_continuous_picture");
    }

    public boolean supportsPanorama() {
        // don't support panorama mode if called from image capture intent
        // in theory this works, but problem that currently we'd end up doing the processing on the UI thread, so risk ANR
        if( applicationInterface.isImageCaptureIntent() )
            return false;
        // require 256MB just to be safe, due to the large number of images that may be created
        // remember to update the FAQ "Why isn't Panorama supported on my device?" if this changes
        return large_heap_memory >= 256 && applicationInterface.getGyroSensor().hasSensors();
        //return false; // currently blocked for release
    }

    public boolean supportsFastBurst() {
        if( applicationInterface.isImageCaptureIntent() )
            return false; // don't support burst mode if called from image capture intent
        // require 512MB just to be safe, due to the large number of images that may be created
        return( preview.usingCamera2API() && large_heap_memory >= 512 && preview.supportsBurst() );
    }

    public boolean supportsNoiseReduction() {
        // we require Android 7 to limit to more modern devices (for performance reasons)
        return( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && preview.usingCamera2API() && large_heap_memory >= 512 && preview.supportsBurst() && preview.supportsExposureTime() );
        //return false; // currently blocked for release
    }

    /** Whether the Camera vendor extension is supported (see
     * https://developer.android.com/reference/android/hardware/camera2/CameraExtensionCharacteristics ).
     */
    public boolean supportsCameraExtension(int extension) {
        return preview.supportsCameraExtension(extension);
    }

    /** Whether RAW mode would be supported for various burst modes (expo bracketing etc).
     *  Note that caller should still separately check preview.supportsRaw() if required.
     */
    public boolean supportsBurstRaw() {
        return( large_heap_memory >= 512 );
    }

    public boolean supportsOptimiseFocusLatency() {
        // whether to support optimising focus for latency
        // in theory this works on any device, as well as old or Camera2 API, but restricting this for now to avoid risk of poor default behaviour
        // on older devices
        return( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preview.usingCamera2API() );
    }

    public boolean supportsPreviewBitmaps() {
        // In practice we only use TextureView on Android 5+ (with Camera2 API enabled) anyway, but have put an explicit check here -
        return preview.getView() instanceof TextureView && large_heap_memory >= 128;
    }

    public boolean supportsPreShots() {
        // Need at least Android 5+ for TextureView
        // Need at least Android 8+ for video encoding classes
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && preview.getView() instanceof TextureView && large_heap_memory >= 512;
    }

    private int maxExpoBracketingNImages() {
        return preview.maxExpoBracketingNImages();
    }

    public boolean supportsForceVideo4K() {
        return this.supports_force_video_4k;
    }

    public boolean supportsCamera2() {
        return this.supports_camera2;
    }

    private void disableForceVideo4K() {
        this.supports_force_video_4k = false;
    }

    public Preview getPreview() {
        return this.preview;
    }

    public boolean isCameraInBackground() {
        return this.camera_in_background;
    }

    public boolean isAppPaused() {
        return this.app_is_paused;
    }

    public BluetoothRemoteControl getBluetoothRemoteControl() {
        return bluetoothRemoteControl;
    }

    public PermissionHandler getPermissionHandler() {
        return permissionHandler;
    }

    public SettingsManager getSettingsManager() {
        return settingsManager;
    }

    public MainUI getMainUI() {
        return this.mainUI;
    }

    public ManualSeekbars getManualSeekbars() {
        return this.manualSeekbars;
    }

    public MyApplicationInterface getApplicationInterface() {
        return this.applicationInterface;
    }

    public TextFormatter getTextFormatter() {
        return this.textFormatter;
    }

    SoundPoolManager getSoundPoolManager() {
        return this.soundPoolManager;
    }

    public LocationSupplier getLocationSupplier() {
        return this.applicationInterface.getLocationSupplier();
    }

    public StorageUtils getStorageUtils() {
        return this.applicationInterface.getStorageUtils();
    }

    public File getImageFolder() {
        return this.applicationInterface.getStorageUtils().getImageFolder();
    }

    public ToastBoxer getChangedAutoStabiliseToastBoxer() {
        return changed_auto_stabilise_toast;
    }

    private String getPhotoModeString(MyApplicationInterface.PhotoMode photo_mode, boolean string_for_std) {
        String photo_mode_string = null;
        switch( photo_mode ) {
            case Standard:
                if( string_for_std )
                    photo_mode_string = getResources().getString(R.string.photo_mode_standard_full);
                break;
            case DRO:
                photo_mode_string = getResources().getString(R.string.photo_mode_dro);
                break;
            case HDR:
                photo_mode_string = getResources().getString(R.string.photo_mode_hdr);
                break;
            case ExpoBracketing:
                photo_mode_string = getResources().getString(R.string.photo_mode_expo_bracketing_full);
                break;
            case FocusBracketing: {
                photo_mode_string = getResources().getString(R.string.photo_mode_focus_bracketing_full);
                int n_images = applicationInterface.getFocusBracketingNImagesPref();
                photo_mode_string += " (" + n_images + ")";
                break;
            }
            case FastBurst: {
                photo_mode_string = getResources().getString(R.string.photo_mode_fast_burst_full);
                int n_images = applicationInterface.getBurstNImages();
                photo_mode_string += " (" + n_images + ")";
                break;
            }
            case NoiseReduction:
                photo_mode_string = getResources().getString(R.string.photo_mode_noise_reduction_full);
                break;
            case Panorama:
                photo_mode_string = getResources().getString(R.string.photo_mode_panorama_full);
                break;
            case X_Auto:
                photo_mode_string = getResources().getString(R.string.photo_mode_x_auto_full);
                break;
            case X_HDR:
                photo_mode_string = getResources().getString(R.string.photo_mode_x_hdr_full);
                break;
            case X_Night:
                photo_mode_string = getResources().getString(R.string.photo_mode_x_night_full);
                break;
            case X_Bokeh:
                photo_mode_string = getResources().getString(R.string.photo_mode_x_bokeh_full);
                break;
            case X_Beauty:
                photo_mode_string = getResources().getString(R.string.photo_mode_x_beauty_full);
                break;
        }
        return photo_mode_string;
    }

    /** Displays a toast with information about the current preferences.
     *  If always_show is true, the toast is always displayed; otherwise, we only display
     *  a toast if it's important to notify the user (i.e., unusual non-default settings are
     *  set). We want a balance between not pestering the user too much, whilst also reminding
     *  them if certain settings are on.
     */
    private void showPhotoVideoToast(boolean always_show) {
        if( MyDebug.LOG ) {
            Log.d(TAG, "showPhotoVideoToast");
            Log.d(TAG, "always_show? " + always_show);
        }
        CameraController camera_controller = preview.getCameraController();
        if( camera_controller == null || this.camera_in_background ) {
            if( MyDebug.LOG )
                Log.d(TAG, "camera not open or in background");
            return;
        }
        String toast_string;
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        boolean simple = true;
        boolean video_high_speed = preview.isVideoHighSpeed();
        MyApplicationInterface.PhotoMode photo_mode = applicationInterface.getPhotoMode();
        if( preview.isVideo() ) {
            VideoProfile profile = preview.getVideoProfile();

            String extension_string = profile.fileExtension;
            if( !profile.fileExtension.equals("mp4") ) {
                simple = false;
            }

            String bitrate_string;
            if( profile.videoBitRate >= 10000000 )
                bitrate_string = profile.videoBitRate/1000000 + "Mbps";
            else if( profile.videoBitRate >= 10000 )
                bitrate_string = profile.videoBitRate/1000 + "Kbps";
            else
                bitrate_string = profile.videoBitRate + "bps";
            String bitrate_value = applicationInterface.getVideoBitratePref();
            if( !bitrate_value.equals("default") ) {
                simple = false;
            }

            double capture_rate = profile.videoCaptureRate;
            String capture_rate_string = (capture_rate < 9.5f) ? new DecimalFormat("#0.###").format(capture_rate) : String.valueOf((int) (profile.videoCaptureRate + 0.5));
            toast_string = getResources().getString(R.string.video) + ": " + profile.videoFrameWidth + "x" + profile.videoFrameHeight + "\n" +
                    capture_rate_string + getResources().getString(R.string.fps) + (video_high_speed ? " [" + getResources().getString(R.string.high_speed) + "]" : "") + ", " + bitrate_string + " (" + extension_string + ")";

            String fps_value = applicationInterface.getVideoFPSPref();
            if( !fps_value.equals("default") || video_high_speed ) {
                simple = false;
            }

            float capture_rate_factor = applicationInterface.getVideoCaptureRateFactor();
            if( Math.abs(capture_rate_factor - 1.0f) > 1.0e-5 ) {
                toast_string += "\n" + getResources().getString(R.string.preference_video_capture_rate) + ": " + capture_rate_factor + "x";
                simple = false;
            }

            {
                CameraController.TonemapProfile tonemap_profile = applicationInterface.getVideoTonemapProfile();
                if( tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && preview.supportsTonemapCurve() ) {
                    if( applicationInterface.getVideoTonemapProfile() != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && preview.supportsTonemapCurve() ) {
                        int string_id = 0;
                        switch( tonemap_profile ) {
                            case TONEMAPPROFILE_REC709:
                                string_id = R.string.preference_video_rec709;
                                break;
                            case TONEMAPPROFILE_SRGB:
                                string_id = R.string.preference_video_srgb;
                                break;
                            case TONEMAPPROFILE_LOG:
                                string_id = R.string.video_log;
                                break;
                            case TONEMAPPROFILE_GAMMA:
                                string_id = R.string.preference_video_gamma;
                                break;
                            case TONEMAPPROFILE_JTVIDEO:
                                string_id = R.string.preference_video_jtvideo;
                                break;
                            case TONEMAPPROFILE_JTLOG:
                                string_id = R.string.preference_video_jtlog;
                                break;
                            case TONEMAPPROFILE_JTLOG2:
                                string_id = R.string.preference_video_jtlog2;
                                break;
                        }
                        if( string_id != 0 ) {
                            simple = false;
                            toast_string += "\n" + getResources().getString(string_id);
                            if( tonemap_profile == CameraController.TonemapProfile.TONEMAPPROFILE_GAMMA ) {
                                toast_string += " " + applicationInterface.getVideoProfileGamma();
                            }
                        }
                        else {
                            Log.e(TAG, "unknown tonemap_profile: " + tonemap_profile);
                        }
                    }
                }
            }

            boolean record_audio = applicationInterface.getRecordAudioPref();
            if( !record_audio ) {
                toast_string += "\n" + getResources().getString(R.string.audio_disabled);
                simple = false;
            }
            String max_duration_value = sharedPreferences.getString(PreferenceKeys.VideoMaxDurationPreferenceKey, "0");
            if( !max_duration_value.isEmpty() && !max_duration_value.equals("0") ) {
                String [] entries_array = getResources().getStringArray(R.array.preference_video_max_duration_entries);
                String [] values_array = getResources().getStringArray(R.array.preference_video_max_duration_values);
                int index = Arrays.asList(values_array).indexOf(max_duration_value);
                if( index != -1 ) { // just in case!
                    String entry = entries_array[index];
                    toast_string += "\n" + getResources().getString(R.string.max_duration) +": " + entry;
                    simple = false;
                }
            }
            long max_filesize = applicationInterface.getVideoMaxFileSizeUserPref();
            if( max_filesize != 0 ) {
                toast_string += "\n" + getResources().getString(R.string.max_filesize) +": ";
                if( max_filesize >= 1024*1024*1024 ) {
                    long max_filesize_gb = max_filesize/(1024*1024*1024);
                    toast_string += max_filesize_gb + getResources().getString(R.string.gb_abbreviation);
                }
                else {
                    long max_filesize_mb = max_filesize/(1024*1024);
                    toast_string += max_filesize_mb + getResources().getString(R.string.mb_abbreviation);
                }
                simple = false;
            }
            if( applicationInterface.getVideoFlashPref() && preview.supportsFlash() ) {
                toast_string += "\n" + getResources().getString(R.string.preference_video_flash);
                simple = false;
            }
        }
        else {
            if( photo_mode == MyApplicationInterface.PhotoMode.Panorama ) {
                // don't show resolution in panorama mode
                toast_string = "";
            }
            else {
                toast_string = getResources().getString(R.string.photo);
                CameraController.Size current_size = preview.getCurrentPictureSize();
                toast_string += " " + current_size.width + "x" + current_size.height;
            }

            String photo_mode_string = getPhotoModeString(photo_mode, false);
            if( photo_mode_string != null ) {
                toast_string += (toast_string.isEmpty() ? "" : "\n") + getResources().getString(R.string.photo_mode) + ": " + photo_mode_string;
                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 ) {
                String focus_value = preview.getCurrentFocusValue();
                if( focus_value != null && !focus_value.equals("focus_mode_auto") && !focus_value.equals("focus_mode_continuous_picture") ) {
                    String focus_entry = preview.findFocusEntryForValue(focus_value);
                    if( focus_entry != null ) {
                        toast_string += "\n" + focus_entry;
                    }
                }
            }

            if( applicationInterface.getAutoStabilisePref() ) {
                // important as users are sometimes confused at the behaviour if they don't realise the option is on
                toast_string += (toast_string.isEmpty() ? "" : "\n") + getResources().getString(R.string.preference_auto_stabilise);
                simple = false;
            }
        }
        if( applicationInterface.getFaceDetectionPref() ) {
            // important so that the user realises why touching for focus/metering areas won't work - easy to forget that face detection has been turned on!
            toast_string += "\n" + getResources().getString(R.string.preference_face_detection);
            simple = false;
        }
        if( !video_high_speed ) {
            //manual ISO only supported for high speed video
            String iso_value = applicationInterface.getISOPref();
            if( !iso_value.equals(CameraController.ISO_DEFAULT) ) {
                toast_string += "\nISO: " + iso_value;
                if( preview.supportsExposureTime() ) {
                    long exposure_time_value = applicationInterface.getExposureTimePref();
                    toast_string += " " + preview.getExposureTimeString(exposure_time_value);
                }
                simple = false;
            }
            int current_exposure = camera_controller.getExposureCompensation();
            if( current_exposure != 0 ) {
                toast_string += "\n" + preview.getExposureCompensationString(current_exposure);
                simple = false;
            }
        }
        try {
            String scene_mode = camera_controller.getSceneMode();
            String white_balance = camera_controller.getWhiteBalance();
            String color_effect = camera_controller.getColorEffect();
            if( scene_mode != null && !scene_mode.equals(CameraController.SCENE_MODE_DEFAULT) ) {
                toast_string += "\n" + getResources().getString(R.string.scene_mode) + ": " + mainUI.getEntryForSceneMode(scene_mode);
                simple = false;
            }
            if( white_balance != null && !white_balance.equals(CameraController.WHITE_BALANCE_DEFAULT) ) {
                toast_string += "\n" + getResources().getString(R.string.white_balance) + ": " + mainUI.getEntryForWhiteBalance(white_balance);
                if( white_balance.equals("manual") && preview.supportsWhiteBalanceTemperature() ) {
                    toast_string += " " + camera_controller.getWhiteBalanceTemperature();
                }
                simple = false;
            }
            if( color_effect != null && !color_effect.equals(CameraController.COLOR_EFFECT_DEFAULT) ) {
                toast_string += "\n" + getResources().getString(R.string.color_effect) + ": " + mainUI.getEntryForColorEffect(color_effect);
                simple = false;
            }
        }
        catch(RuntimeException e) {
            // catch runtime error from camera_controller old API from camera.getParameters()
            MyDebug.logStackTrace(TAG, "failed to get info from camera controller", e);
        }
        String lock_orientation = applicationInterface.getLockOrientationPref();
        if( !lock_orientation.equals("none") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) {
            // panorama locks to portrait, but don't want to display that in the toast
            String [] entries_array = getResources().getStringArray(R.array.preference_lock_orientation_entries);
            String [] values_array = getResources().getStringArray(R.array.preference_lock_orientation_values);
            int index = Arrays.asList(values_array).indexOf(lock_orientation);
            if( index != -1 ) { // just in case!
                String entry = entries_array[index];
                toast_string += "\n" + entry;
                simple = false;
            }
        }
        String timer = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0");
        if( !timer.equals("0") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) {
            String [] entries_array = getResources().getStringArray(R.array.preference_timer_entries);
            String [] values_array = getResources().getStringArray(R.array.preference_timer_values);
            int index = Arrays.asList(values_array).indexOf(timer);
            if( index != -1 ) { // just in case!
                String entry = entries_array[index];
                toast_string += "\n" + getResources().getString(R.string.preference_timer) + ": " + entry;
                simple = false;
            }
        }
        String repeat = applicationInterface.getRepeatPref();
        if( !repeat.equals("1") ) {
            String [] entries_array = getResources().getStringArray(R.array.preference_burst_mode_entries);
            String [] values_array = getResources().getStringArray(R.array.preference_burst_mode_values);
            int index = Arrays.asList(values_array).indexOf(repeat);
            if( index != -1 ) { // just in case!
                String entry = entries_array[index];
                toast_string += "\n" + getResources().getString(R.string.preference_burst_mode) + ": " + entry;
                simple = false;
            }
        }
		/*if( audio_listener != null ) {
			toast_string += "\n" + getResources().getString(R.string.preference_audio_noise_control);
		}*/

        if( MyDebug.LOG ) {
            Log.d(TAG, "toast_string: " + toast_string);
            Log.d(TAG, "simple?: " + simple);
            Log.d(TAG, "push_info_toast_text: " + push_info_toast_text);
        }
        final boolean use_fake_toast = true;
        if( !simple || always_show ) {
            if( push_info_toast_text != null ) {
                toast_string = push_info_toast_text + "\n" + toast_string;
            }
            preview.showToast(switch_video_toast, toast_string, use_fake_toast);
        }
        else if( push_info_toast_text != null ) {
            preview.showToast(switch_video_toast, push_info_toast_text, use_fake_toast);
        }
        push_info_toast_text = null; // reset
    }

    private void freeAudioListener(boolean wait_until_done) {
        if( MyDebug.LOG )
            Log.d(TAG, "freeAudioListener");
        if( audio_listener != null ) {
            audio_listener.release(wait_until_done);
            audio_listener = null;
        }
        mainUI.audioControlStopped();
    }

    private void startAudioListener() {
        if( MyDebug.LOG )
            Log.d(TAG, "startAudioListener");
        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
            // we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener()
            if( MyDebug.LOG )
                Log.d(TAG, "check for record audio permission");
            if( ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) {
                if( MyDebug.LOG )
                    Log.d(TAG, "record audio permission not available");
                applicationInterface.requestRecordAudioPermission();
                return;
            }
        }

        MyAudioTriggerListenerCallback callback = new MyAudioTriggerListenerCallback(this);
        audio_listener = new AudioListener(callback);
        if( audio_listener.status() ) {
            preview.showToast(audio_control_toast, R.string.audio_listener_started, true);

            audio_listener.start();
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            String sensitivity_pref = sharedPreferences.getString(PreferenceKeys.AudioNoiseControlSensitivityPreferenceKey, "0");
            int audio_noise_sensitivity;
            switch(sensitivity_pref) {
                case "3":
                    audio_noise_sensitivity = 50;
                    break;
                case "2":
                    audio_noise_sensitivity = 75;
                    break;
                case "1":
                    audio_noise_sensitivity = 125;
                    break;
                case "-1":
                    audio_noise_sensitivity = 150;
                    break;
                case "-2":
                    audio_noise_sensitivity = 200;
                    break;
                case "-3":
                    audio_noise_sensitivity = 400;
                    break;
                default:
                    // default
                    audio_noise_sensitivity = 100;
                    break;
            }
            callback.setAudioNoiseSensitivity(audio_noise_sensitivity);
            mainUI.audioControlStarted();
        }
        else {
            audio_listener.release(true); // shouldn't be needed, but just to be safe
            audio_listener = null;
            preview.showToast(null, R.string.audio_listener_failed);
        }
    }

    public boolean hasAudioControl() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String audio_control = sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none");
        /*if( audio_control.equals("voice") ) {
            return speechControl.hasSpeechRecognition();
        }
        else*/ if( audio_control.equals("noise") ) {
            return true;
        }
        return false;
    }

	/*void startAudioListeners() {
		initAudioListener();
		// no need to restart speech recognizer, as we didn't free it in stopAudioListeners(), and it's controlled by a user button
	}*/

    public void stopAudioListeners() {
        freeAudioListener(true);
        /*if( speechControl.hasSpeechRecognition() ) {
            // no need to free the speech recognizer, just stop it
            speechControl.stopListening();
        }*/
    }

    public void initLocation() {
        if( MyDebug.LOG )
            Log.d(TAG, "initLocation");
        if( app_is_paused ) {
            if( MyDebug.LOG )
                Log.d(TAG, "initLocation: app is paused!");
            // we shouldn't need this (as we only call initLocation() when active), but just in case we end up here after onPause...
            // in fact this happens when we need to grant permission for location - the call to initLocation() from
            // MainActivity.onRequestPermissionsResult()->PermissionsHandler.onRequestPermissionsResult() will be when the application
            // is still paused - so we won't do anything here, but instead initLocation() will be called after when resuming.
        }
        else if( camera_in_background ) {
            if( MyDebug.LOG )
                Log.d(TAG, "initLocation: camera in background!");
            // we will end up here if app is pause/resumed when camera in background (settings, dialog, etc)
        }
        else if( !applicationInterface.getLocationSupplier().setupLocationListener() ) {
            if( MyDebug.LOG )
                Log.d(TAG, "location permission not available, so request permission");
            permissionHandler.requestLocationPermission();
        }
    }

    private void initGyroSensors() {
        if( MyDebug.LOG )
            Log.d(TAG, "initGyroSensors");
        if( applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama ) {
            applicationInterface.getGyroSensor().enableSensors();
        }
        else {
            applicationInterface.getGyroSensor().disableSensors();
        }
    }

    void speak(String text) {
        if( textToSpeech != null && textToSpeechSuccess ) {
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if( MyDebug.LOG )
            Log.d(TAG, "onRequestPermissionsResult: requestCode " + requestCode);
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        permissionHandler.onRequestPermissionsResult(requestCode, grantResults);
    }

    public void restartOpenCamera() {
        if( MyDebug.LOG )
            Log.d(TAG, "restartOpenCamera");
        this.waitUntilImageQueueEmpty();
        // see http://stackoverflow.com/questions/2470870/force-application-to-restart-on-first-activity
        Intent intent = this.getBaseContext().getPackageManager().getLaunchIntentForPackage( this.getBaseContext().getPackageName() );
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        this.startActivity(intent);
    }

    public void takePhotoButtonLongClickCancelled() {
        if( MyDebug.LOG )
            Log.d(TAG, "takePhotoButtonLongClickCancelled");
        if( preview.getCameraController() != null && preview.getCameraController().isContinuousBurstInProgress() ) {
            preview.getCameraController().stopContinuousBurst();
        }
    }

    // for testing:
    public SaveLocationHistory getSaveLocationHistory() {
        return this.save_location_history;
    }

    public SaveLocationHistory getSaveLocationHistorySAF() {
        return this.save_location_history_saf;
    }

    public void usedFolderPicker() {
        if( applicationInterface.getStorageUtils().isUsingSAF() ) {
            save_location_history_saf.updateFolderHistory(getStorageUtils().getSaveLocationSAF(), true);
        }
        else {
            save_location_history.updateFolderHistory(getStorageUtils().getSaveLocation(), true);
        }
    }

    public boolean hasThumbnailAnimation() {
        return this.applicationInterface.hasThumbnailAnimation();
    }

    /*public boolean testHasNotification() {
        return has_notification;
    }*/
}
