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 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 back_camera_ids; private List front_camera_ids; private List 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 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= 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= 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=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 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 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 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 getSameFacingLogicalCameras(int camera_id) { List logical_camera_ids = new ArrayList<>(); CameraController.Facing this_facing = preview.getCameraControllerManager().getFacing(camera_id); for(int i=0;i 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"; String html_camera_name = "" + 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); } } 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 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 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 = "    "; 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 + "[" + camera_name + "]"; html_camera_name = indent + "" + camera_name + ""; } 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 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 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 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 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 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 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 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 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 video_fps = new ArrayList<>(); List 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= 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() { 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= 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 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 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; }*/ }