open-camera/app/src/main/java/net/sourceforge/opencamera/MainActivity.java
2025-11-22 13:52:14 +01:00

6963 lines
344 KiB
Java

package net.sourceforge.opencamera;
import net.sourceforge.opencamera.cameracontroller.CameraController;
import net.sourceforge.opencamera.cameracontroller.CameraControllerManager;
import net.sourceforge.opencamera.cameracontroller.CameraControllerManager2;
import net.sourceforge.opencamera.preview.Preview;
import net.sourceforge.opencamera.preview.VideoProfile;
import net.sourceforge.opencamera.remotecontrol.BluetoothRemoteControl;
import net.sourceforge.opencamera.ui.DrawPreview;
import net.sourceforge.opencamera.ui.FolderChooserDialog;
import net.sourceforge.opencamera.ui.MainUI;
import net.sourceforge.opencamera.ui.ManualSeekbars;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import android.Manifest;
import android.app.Fragment;
import android.content.pm.PackageInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.speech.tts.TextToSpeech;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.exifinterface.media.ExifInterface;
import android.text.Html;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spanned;
import android.util.Log;
import android.util.Size;
import android.util.SizeF;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
/** The main Activity for Open Camera.
*/
public class MainActivity extends AppCompatActivity implements PreferenceFragment.OnPreferenceStartFragmentCallback {
private static final String TAG = "MainActivity";
private static int activity_count = 0;
private boolean app_is_paused = true;
private SensorManager mSensorManager;
private Sensor mSensorAccelerometer;
// components: always non-null (after onCreate())
private BluetoothRemoteControl bluetoothRemoteControl;
private PermissionHandler permissionHandler;
private SettingsManager settingsManager;
private MainUI mainUI;
private ManualSeekbars manualSeekbars;
private MyApplicationInterface applicationInterface;
private TextFormatter textFormatter;
private SoundPoolManager soundPoolManager;
private MagneticSensor magneticSensor;
//private SpeechControl speechControl;
private Preview preview;
private OrientationEventListener orientationEventListener;
private View.OnLayoutChangeListener layoutChangeListener;
private int large_heap_memory;
private boolean supports_auto_stabilise;
private boolean supports_force_video_4k;
private boolean supports_camera2;
private SaveLocationHistory save_location_history; // save location for non-SAF
private SaveLocationHistory save_location_history_saf; // save location for SAF (only initialised when SAF is used)
private boolean saf_dialog_from_preferences; // if a SAF dialog is opened, this records whether we opened it from the Preferences
private boolean camera_in_background; // whether the camera is covered by a fragment/dialog (such as settings or folder picker)
private GestureDetector gestureDetector;
private boolean screen_is_locked; // whether screen is "locked" - this is Open Camera's own lock to guard against accidental presses, not the standard Android lock
private final Map<Integer, Bitmap> preloaded_bitmap_resources = new Hashtable<>();
private ValueAnimator gallery_save_anim;
private boolean last_continuous_fast_burst; // whether the last photo operation was a continuous_fast_burst
private Future<?> update_gallery_future;
private TextToSpeech textToSpeech;
private boolean textToSpeechSuccess;
private AudioListener audio_listener; // may be null - created when needed
//private boolean ui_placement_right = true;
//private final boolean edge_to_edge_mode = false; // whether running always in edge-to-edge mode
//private final boolean edge_to_edge_mode = true; // whether running always in edge-to-edge mode
private final boolean edge_to_edge_mode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM; // whether running always in edge-to-edge mode
private boolean want_no_limits; // whether we want to run with FLAG_LAYOUT_NO_LIMITS
private boolean set_window_insets_listener; // whether we've enabled a setOnApplyWindowInsetsListener()
private int navigation_gap; // gap for navigation bar along bottom (portrait) or right (landscape)
private int navigation_gap_landscape; // gap for navigation bar along left (portrait) or bottom (landscape); only set for edge_to_edge_mode==true
private int navigation_gap_reverse_landscape; // gap for navigation bar along right (portrait) or top (landscape); only set for edge_to_edge_mode==true
public static volatile boolean test_preview_want_no_limits; // test flag, if set to true then instead use test_preview_want_no_limits_value; needs to be static, as it needs to be set before activity is created to take effect
public static volatile boolean test_preview_want_no_limits_value;
public volatile boolean test_set_show_under_navigation; // test flag, the value of enable for the last call of showUnderNavigation() (or false if not yet called)
public static volatile boolean test_force_system_orientation; // test flag, if set to true, that getSystemOrientation() returns test_system_orientation
public static volatile SystemOrientation test_system_orientation = SystemOrientation.PORTRAIT;
public static volatile boolean test_force_window_insets; // test flag, if set to true, then the OnApplyWindowInsetsListener will read from the following flags
public static volatile Insets test_insets; // test insets for WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()
public static volatile Insets test_cutout_insets; // test insets for WindowInsets.Type.displayCutout()
// whether this is a multi-camera device (note, this isn't simply having more than 1 camera, but also having more than one with the same facing)
// note that in most cases, code should check the MultiCamButtonPreferenceKey preference as well as the is_multi_cam flag,
// this can be done via isMultiCamEnabled().
private boolean is_multi_cam;
// These lists are lists of camera IDs with the same "facing" (front, back or external).
// Only initialised if is_multi_cam==true.
private List<Integer> back_camera_ids;
private List<Integer> front_camera_ids;
private List<Integer> other_camera_ids;
private final ToastBoxer switch_video_toast = new ToastBoxer();
private final ToastBoxer screen_locked_toast = new ToastBoxer();
private final ToastBoxer stamp_toast = new ToastBoxer();
private final ToastBoxer changed_auto_stabilise_toast = new ToastBoxer();
private final ToastBoxer white_balance_lock_toast = new ToastBoxer();
private final ToastBoxer exposure_lock_toast = new ToastBoxer();
private final ToastBoxer audio_control_toast = new ToastBoxer();
private final ToastBoxer store_location_toast = new ToastBoxer();
private boolean block_startup_toast = false; // used when returning from Settings/Popup - if we're displaying a toast anyway, don't want to display the info toast too
private String push_info_toast_text; // can be used to "push" extra text to the info text for showPhotoVideoToast()
private boolean push_switched_camera = false; // whether to display animation for switching front/back cameras
// application shortcuts:
static private final String ACTION_SHORTCUT_CAMERA = "net.sourceforge.opencamera.SHORTCUT_CAMERA";
static private final String ACTION_SHORTCUT_SELFIE = "net.sourceforge.opencamera.SHORTCUT_SELFIE";
static private final String ACTION_SHORTCUT_VIDEO = "net.sourceforge.opencamera.SHORTCUT_VIDEO";
static private final String ACTION_SHORTCUT_GALLERY = "net.sourceforge.opencamera.SHORTCUT_GALLERY";
static private final String ACTION_SHORTCUT_SETTINGS = "net.sourceforge.opencamera.SHORTCUT_SETTINGS";
private static final int CHOOSE_SAVE_FOLDER_SAF_CODE = 42;
private static final int CHOOSE_GHOST_IMAGE_SAF_CODE = 43;
private static final int CHOOSE_LOAD_SETTINGS_SAF_CODE = 44;
// for testing; must be volatile for test project reading the state
// n.b., avoid using static, as static variables are shared between different instances of an application,
// and won't be reset in subsequent tests in a suite!
public boolean is_test; // whether called from OpenCamera.test testing
public volatile Bitmap gallery_bitmap;
public volatile boolean test_low_memory;
public volatile boolean test_have_angle;
public volatile float test_angle;
public volatile Uri test_last_saved_imageuri; // uri of last image; set if using scoped storage OR using SAF
public volatile String test_last_saved_image; // filename (including full path) of last image; set if not using scoped storage nor using SAF (i.e., writing using File API)
public static boolean test_force_supports_camera2; // okay to be static, as this is set for an entire test suite
public volatile String test_save_settings_file;
// update: notifications now removed due to needing permissions on Android 13+
//private boolean has_notification;
//private final String CHANNEL_ID = "open_camera_channel";
//private final int image_saving_notification_id = 1;
private static final float WATER_DENSITY_FRESHWATER = 1.0f;
private static final float WATER_DENSITY_SALTWATER = 1.03f;
private float mWaterDensity = 1.0f;
// whether to lock to landscape orientation, or allow switching between portrait and landscape orientations
//public static final boolean lock_to_landscape = true;
public static final boolean lock_to_landscape = false;
// handling for lock_to_landscape==false:
public enum SystemOrientation {
LANDSCAPE,
PORTRAIT,
REVERSE_LANDSCAPE
}
private MyDisplayListener displayListener;
private boolean has_cached_system_orientation;
private SystemOrientation cached_system_orientation;
private boolean hasOldSystemOrientation;
private SystemOrientation oldSystemOrientation;
private boolean has_cached_display_rotation;
private long cached_display_rotation_time_ms;
private int cached_display_rotation;
List<Integer> exposure_seekbar_values; // mapping from exposure_seekbar progress value to preview exposure compensation
private int exposure_seekbar_values_zero; // index in exposure_seekbar_values that maps to zero preview exposure compensation
@Override
protected void onCreate(Bundle savedInstanceState) {
long debug_time = 0;
if( MyDebug.LOG ) {
Log.d(TAG, "onCreate: " + this);
debug_time = System.currentTimeMillis();
}
activity_count++;
if( MyDebug.LOG )
Log.d(TAG, "activity_count: " + activity_count);
//EdgeToEdge.enable(this, SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), SystemBarStyle.dark(Color.TRANSPARENT)); // test edge-to-edge on pre-Android 15
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // initialise any unset preferences to their default values
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting default preference values: " + (System.currentTimeMillis() - debug_time));
if( getIntent() != null && getIntent().getExtras() != null ) {
// whether called from testing
is_test = getIntent().getExtras().getBoolean("test_project");
if( MyDebug.LOG )
Log.d(TAG, "is_test: " + is_test);
}
/*if( getIntent() != null && getIntent().getExtras() != null ) {
// whether called from Take Photo widget
if( MyDebug.LOG )
Log.d(TAG, "take_photo?: " + getIntent().getExtras().getBoolean(TakePhoto.TAKE_PHOTO));
}*/
if( MyDebug.LOG ) {
// whether called from Take Photo widget
Log.d(TAG, "take_photo?: " + TakePhoto.TAKE_PHOTO);
}
if( getIntent() != null && getIntent().getAction() != null ) {
// invoked via the manifest shortcut?
if( MyDebug.LOG )
Log.d(TAG, "shortcut: " + getIntent().getAction());
}
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
// determine whether we should support "auto stabilise" feature
// risk of running out of memory on lower end devices, due to manipulation of large bitmaps
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
if( MyDebug.LOG ) {
Log.d(TAG, "large max memory = " + activityManager.getLargeMemoryClass() + "MB");
}
large_heap_memory = activityManager.getLargeMemoryClass();
if( large_heap_memory >= 128 ) {
supports_auto_stabilise = true;
}
if( MyDebug.LOG )
Log.d(TAG, "supports_auto_stabilise? " + supports_auto_stabilise);
// hack to rule out phones unlikely to have 4K video, so no point even offering the option!
// both S5 and Note 3 have 128MB standard and 512MB large heap (tested via Samsung RTL), as does Galaxy K Zoom
if( activityManager.getLargeMemoryClass() >= 512 ) {
supports_force_video_4k = true;
}
if( MyDebug.LOG )
Log.d(TAG, "supports_force_video_4k? " + supports_force_video_4k);
// set up components
bluetoothRemoteControl = new BluetoothRemoteControl(this);
permissionHandler = new PermissionHandler(this);
settingsManager = new SettingsManager(this);
mainUI = new MainUI(this);
manualSeekbars = new ManualSeekbars();
applicationInterface = new MyApplicationInterface(this, savedInstanceState);
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after creating application interface: " + (System.currentTimeMillis() - debug_time));
textFormatter = new TextFormatter(this);
soundPoolManager = new SoundPoolManager(this);
magneticSensor = new MagneticSensor(this);
//speechControl = new SpeechControl(this);
// determine whether we support Camera2 API
// must be done before setDeviceDefaults()
initCamera2Support();
// set some per-device defaults
// must be done before creating the Preview (as setDeviceDefaults() may set Camera2 API)
boolean has_done_first_time = sharedPreferences.contains(PreferenceKeys.FirstTimePreferenceKey);
if( MyDebug.LOG )
Log.d(TAG, "has_done_first_time: " + has_done_first_time);
if( !has_done_first_time ) {
// must be done after initCamera2Support()
setDeviceDefaults();
}
boolean settings_is_open = settingsIsOpen();
if( MyDebug.LOG )
Log.d(TAG, "settings_is_open?: " + settings_is_open);
// settings_is_open==true can happen if application is recreated when settings is open
// to reproduce: go to settings, then turn screen off and on (and unlock)
if( !settings_is_open ) {
// set up window flags for normal operation
setWindowFlagsForCamera();
}
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting window flags: " + (System.currentTimeMillis() - debug_time));
save_location_history = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistoryBasePreferenceKey, getStorageUtils().getSaveLocation());
checkSaveLocations();
if( applicationInterface.getStorageUtils().isUsingSAF() ) {
if( MyDebug.LOG )
Log.d(TAG, "create new SaveLocationHistory for SAF");
save_location_history_saf = new SaveLocationHistory(this, PreferenceKeys.SaveLocationHistorySAFBasePreferenceKey, getStorageUtils().getSaveLocationSAF());
}
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after updating folder history: " + (System.currentTimeMillis() - debug_time));
// set up sensors
mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
// accelerometer sensor (for device orientation)
if( mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ) {
if( MyDebug.LOG )
Log.d(TAG, "found accelerometer");
mSensorAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
else {
if( MyDebug.LOG )
Log.d(TAG, "no support for accelerometer");
}
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after creating accelerometer sensor: " + (System.currentTimeMillis() - debug_time));
// magnetic sensor (for compass direction)
magneticSensor.initSensor(mSensorManager);
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after creating magnetic sensor: " + (System.currentTimeMillis() - debug_time));
// clear any seek bars (just in case??)
mainUI.closeExposureUI();
// set up the camera and its preview
preview = new Preview(applicationInterface, (this.findViewById(R.id.preview)));
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after creating preview: " + (System.currentTimeMillis() - debug_time));
if( settings_is_open ) {
// must be done after creating preview
setWindowFlagsForSettings();
}
{
// don't show orientation animations
// must be done after creating Preview (so we know if Camera2 API or not)
WindowManager.LayoutParams layout = getWindow().getAttributes();
// If locked to landscape, ROTATION_ANIMATION_SEAMLESS/JUMPCUT has the problem that when going to
// Settings in portrait, we briefly see the UI change - this is because we set the flag
// to no longer lock to landscape, and that change happens too quickly.
// This isn't a problem when lock_to_landscape==false, and we want
// ROTATION_ANIMATION_SEAMLESS so that there is no/minimal pause from the preview when
// rotating the device. However if using old camera API, we get an ugly transition with
// ROTATION_ANIMATION_SEAMLESS (probably related to not using TextureView?)
if( lock_to_landscape || !preview.usingCamera2API() )
layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
else if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O )
layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
else
layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
getWindow().setAttributes(layout);
}
// Setup multi-camera buttons (must be done after creating preview so we know which Camera API is being used,
// and before initialising on-screen visibility).
// We only allow the separate icon for switching cameras if:
// - there are at least 2 types of "facing" camera, and
// - there are at least 2 cameras with the same "facing".
// If there are multiple cameras but all with different "facing", then the switch camera
// icon is used to iterate over all cameras.
// If there are more than two cameras, but all cameras have the same "facing, we still stick
// with using the switch camera icon to iterate over all cameras.
int n_cameras = preview.getCameraControllerManager().getNumberOfCameras();
if( n_cameras > 2 ) {
this.back_camera_ids = new ArrayList<>();
this.front_camera_ids = new ArrayList<>();
this.other_camera_ids = new ArrayList<>();
for(int i=0;i<n_cameras;i++) {
switch( preview.getCameraControllerManager().getFacing(i) ) {
case FACING_BACK:
back_camera_ids.add(i);
break;
case FACING_FRONT:
front_camera_ids.add(i);
break;
default:
// we assume any unknown cameras are also external
other_camera_ids.add(i);
break;
}
}
boolean multi_same_facing = back_camera_ids.size() >= 2 || front_camera_ids.size() >= 2 || other_camera_ids.size() >= 2;
int n_facing = 0;
if( !back_camera_ids.isEmpty() )
n_facing++;
if( !front_camera_ids.isEmpty() )
n_facing++;
if( !other_camera_ids.isEmpty() )
n_facing++;
this.is_multi_cam = multi_same_facing && n_facing >= 2;
//this.is_multi_cam = false; // test
if( MyDebug.LOG ) {
Log.d(TAG, "multi_same_facing: " + multi_same_facing);
Log.d(TAG, "n_facing: " + n_facing);
Log.d(TAG, "is_multi_cam: " + is_multi_cam);
}
if( !is_multi_cam ) {
this.back_camera_ids = null;
this.front_camera_ids = null;
this.other_camera_ids = null;
}
}
// initialise on-screen button visibility
View switchCameraButton = findViewById(R.id.switch_camera);
switchCameraButton.setVisibility(n_cameras > 1 ? View.VISIBLE : View.GONE);
// switchMultiCameraButton visibility updated below in mainUI.updateOnScreenIcons(), as it also depends on user preference
View speechRecognizerButton = findViewById(R.id.audio_control);
speechRecognizerButton.setVisibility(View.GONE); // disabled by default, until the speech recognizer is created
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting button visibility: " + (System.currentTimeMillis() - debug_time));
View pauseVideoButton = findViewById(R.id.pause_video);
pauseVideoButton.setVisibility(View.GONE);
View takePhotoVideoButton = findViewById(R.id.take_photo_when_video_recording);
takePhotoVideoButton.setVisibility(View.GONE);
View cancelPanoramaButton = findViewById(R.id.cancel_panorama);
cancelPanoramaButton.setVisibility(View.GONE);
// We initialise optional controls to invisible/gone, so they don't show while the camera is opening - the actual visibility is
// set in cameraSetup().
// Note that ideally we'd set this in the xml, but doing so for R.id.zoom causes a crash on Galaxy Nexus startup beneath
// setContentView()!
// To be safe, we also do so for take_photo and zoom_seekbar (we already know we've had no reported crashes for focus_seekbar,
// however).
View takePhotoButton = findViewById(R.id.take_photo);
takePhotoButton.setVisibility(View.INVISIBLE);
View zoomSeekbar = findViewById(R.id.zoom_seekbar);
zoomSeekbar.setVisibility(View.INVISIBLE);
// initialise state of on-screen icons
mainUI.updateOnScreenIcons();
if( MainActivity.lock_to_landscape ) {
// listen for orientation event change (only required if lock_to_landscape==true
// (MainUI.onOrientationChanged() does nothing if lock_to_landscape==false)
orientationEventListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int orientation) {
MainActivity.this.mainUI.onOrientationChanged(orientation);
}
};
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting orientation event listener: " + (System.currentTimeMillis() - debug_time));
}
layoutChangeListener = new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if( MyDebug.LOG )
Log.d(TAG, "onLayoutChange");
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode() ) {
Point display_size = new Point();
applicationInterface.getDisplaySize(display_size, true);
if( MyDebug.LOG ) {
Log.d(TAG, " display width: " + display_size.x);
Log.d(TAG, " display height: " + display_size.y);
Log.d(TAG, " layoutUI display width: " + mainUI.layoutUI_display_w);
Log.d(TAG, " layoutUI display height: " + mainUI.layoutUI_display_h);
}
// We need to call layoutUI when the window is resized without an orientation change -
// this can happen in split-screen or multi-window mode, where onConfigurationChanged
// is not guaranteed to be called.
// We check against the size of when layoutUI was last called, to avoid repeated calls
// when the resize is due to the device rotating and onConfigurationChanged is called -
// in fact we'd have a problem of repeatedly calling layoutUI, since doing layoutUI
// causes onLayoutChange() to be called again.
if( display_size.x != mainUI.layoutUI_display_w || display_size.y != mainUI.layoutUI_display_h ) {
if( MyDebug.LOG )
Log.d(TAG, "call layoutUI due to resize");
mainUI.layoutUI();
}
}
}
};
// set up take photo long click
takePhotoButton.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if( !allowLongPress() ) {
// return false, so a regular click will still be triggered when the user releases the touch
return false;
}
return longClickedTakePhoto();
}
});
// set up on touch listener so we can detect if we've released from a long click
takePhotoButton.setOnTouchListener(new View.OnTouchListener() {
// the suppressed warning ClickableViewAccessibility suggests calling view.performClick for ACTION_UP, but this
// results in an additional call to clickedTakePhoto() - that is, if there is no long press, we get two calls to
// clickedTakePhoto instead one one; and if there is a long press, we get one call to clickedTakePhoto where
// there should be none.
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if( motionEvent.getAction() == MotionEvent.ACTION_UP ) {
if( MyDebug.LOG )
Log.d(TAG, "takePhotoButton ACTION_UP");
takePhotoButtonLongClickCancelled();
if( MyDebug.LOG )
Log.d(TAG, "takePhotoButton ACTION_UP done");
}
return false;
}
});
// set up gallery button long click
View galleryButton = findViewById(R.id.gallery);
galleryButton.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if( !allowLongPress() ) {
// return false, so a regular click will still be triggered when the user releases the touch
return false;
}
//preview.showToast(null, "Long click");
longClickedGallery();
return true;
}
});
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting long click listeners: " + (System.currentTimeMillis() - debug_time));
// listen for gestures
gestureDetector = new GestureDetector(this, new MyGestureDetector());
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after creating gesture detector: " + (System.currentTimeMillis() - debug_time));
setupSystemUiVisibilityListener();
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting system ui visibility listener: " + (System.currentTimeMillis() - debug_time));
// show "about" dialog for first time use
if( !has_done_first_time ) {
if( !is_test ) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.app_name);
alertDialog.setMessage(R.string.intro_text);
alertDialog.setPositiveButton(android.R.string.ok, null);
alertDialog.setNegativeButton(R.string.preference_online_help, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( MyDebug.LOG )
Log.d(TAG, "online help");
launchOnlineHelp();
}
});
alertDialog.show();
}
setFirstTimeFlag();
}
{
// handle What's New dialog
int version_code = -1;
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
version_code = pInfo.versionCode;
}
catch(PackageManager.NameNotFoundException e) {
MyDebug.logStackTrace(TAG, "NameNotFoundException exception trying to get version number", e);
}
if( version_code != -1 ) {
int latest_version = sharedPreferences.getInt(PreferenceKeys.LatestVersionPreferenceKey, 0);
if( MyDebug.LOG ) {
Log.d(TAG, "version_code: " + version_code);
Log.d(TAG, "latest_version: " + latest_version);
}
//final boolean whats_new_enabled = false;
final boolean whats_new_enabled = true;
if( whats_new_enabled ) {
// whats_new_version is the version code that the What's New text is written for. Normally it will equal the
// current release (version_code), but it some cases we may want to leave it unchanged.
// E.g., we have a "What's New" for 1.44 (64), but then push out a quick fix for 1.44.1 (65). We don't want to
// show the dialog again to people who already received 1.44 (64), but we still want to show the dialog to people
// upgrading from earlier versions.
int whats_new_version = 93; // 1.55
whats_new_version = Math.min(whats_new_version, version_code); // whats_new_version should always be <= version_code, but just in case!
if( MyDebug.LOG ) {
Log.d(TAG, "whats_new_version: " + whats_new_version);
}
final boolean force_whats_new = false;
//final boolean force_whats_new = true; // for testing
boolean allow_show_whats_new = sharedPreferences.getBoolean(PreferenceKeys.ShowWhatsNewPreferenceKey, true);
if( MyDebug.LOG )
Log.d(TAG, "allow_show_whats_new: " + allow_show_whats_new);
// don't show What's New if this is the first time the user has run
if( has_done_first_time && allow_show_whats_new && ( force_whats_new || whats_new_version > latest_version ) ) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.whats_new);
alertDialog.setMessage(R.string.whats_new_text);
alertDialog.setPositiveButton(android.R.string.ok, null);
alertDialog.show();
}
}
// We set the latest_version whether or not the dialog is shown - if we showed the first time dialog, we don't
// want to then show the What's New dialog next time we run! Similarly if the user had disabled showing the dialog,
// but then enables it, we still shouldn't show the dialog until the new time Open Camera upgrades.
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt(PreferenceKeys.LatestVersionPreferenceKey, version_code);
editor.apply();
}
}
setModeFromIntents(savedInstanceState);
// load icons
preloadIcons(R.array.flash_icons);
preloadIcons(R.array.focus_mode_icons);
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after preloading icons: " + (System.currentTimeMillis() - debug_time));
// initialise text to speech engine
textToSpeechSuccess = false;
// run in separate thread so as to not delay startup time
new Thread(new Runnable() {
public void run() {
textToSpeech = new TextToSpeech(MainActivity.this, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if( MyDebug.LOG )
Log.d(TAG, "TextToSpeech initialised");
if( status == TextToSpeech.SUCCESS ) {
textToSpeechSuccess = true;
if( MyDebug.LOG )
Log.d(TAG, "TextToSpeech succeeded");
}
else {
if( MyDebug.LOG )
Log.d(TAG, "TextToSpeech failed");
}
}
});
}
}).start();
// handle on back behaviour
popupOnBackPressedCallback = new PopupOnBackPressedCallback(false);
this.getOnBackPressedDispatcher().addCallback(this, popupOnBackPressedCallback);
pausePreviewOnBackPressedCallback = new PausePreviewOnBackPressedCallback(false);
this.getOnBackPressedDispatcher().addCallback(this, pausePreviewOnBackPressedCallback);
screenLockOnBackPressedCallback = new ScreenLockOnBackPressedCallback(false);
this.getOnBackPressedDispatcher().addCallback(this, screenLockOnBackPressedCallback);
// create notification channel - only needed on Android 8+
// update: notifications now removed due to needing permissions on Android 13+
/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
CharSequence name = "Open Camera Image Saving";
String description = "Notification channel for processing and saving images in the background";
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}*/
// so we get the icons rotation even when rotating for the first time - see onSystemOrientationChanged
this.hasOldSystemOrientation = true;
this.oldSystemOrientation = getSystemOrientation();
if( MyDebug.LOG )
Log.d(TAG, "onCreate: total time for Activity startup: " + (System.currentTimeMillis() - debug_time));
}
/** Whether to use codepaths that are compatible with scoped storage.
*/
public static boolean useScopedStorage() {
//return false;
//return true;
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
/** Whether this is a multi camera device, and the user preference is set to enable the multi-camera button.
*/
public boolean isMultiCamEnabled() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
return is_multi_cam && sharedPreferences.getBoolean(PreferenceKeys.MultiCamButtonPreferenceKey, true);
}
/** Whether this is a multi camera device, whether or not the user preference is set to enable
* the multi-camera button.
*/
public boolean isMultiCam() {
return is_multi_cam;
}
/* Returns the camera Id in use by the preview - or the one we requested, if the camera failed
* to open.
* Needed as Preview.getCameraId() returns 0 if camera_controller==null, but if the camera
* fails to open, we want the switch camera icons to still work as expected!
*/
private int getActualCameraId() {
if( preview.getCameraController() == null )
return applicationInterface.getCameraIdPref();
else
return preview.getCameraId();
}
/** Whether the icon switch_multi_camera should be displayed. This is if the following are all
* true:
* - The device is a multi camera device (MainActivity.is_multi_cam==true).
* - The user preference for using the separate icons is enabled
* (PreferenceKeys.MultiCamButtonPreferenceKey).
* - For the current camera ID, there are at least two cameras with the same front/back/external
* "facing" (e.g., imagine a device with two back cameras, but only one front camera - no point
* showing the multi-cam icon for just a single logical front camera).
* OR there are physical cameras for the current camera, and again the user preference
* PreferenceKeys.MultiCamButtonPreferenceKey is enabled.
*/
public boolean showSwitchMultiCamIcon() {
if( preview.hasPhysicalCameras() ) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if( sharedPreferences.getBoolean(PreferenceKeys.MultiCamButtonPreferenceKey, true) )
return true;
}
if( isMultiCamEnabled() ) {
int cameraId = getActualCameraId();
switch( preview.getCameraControllerManager().getFacing(cameraId) ) {
case FACING_BACK:
if( back_camera_ids.size() > 1 )
return true;
break;
case FACING_FRONT:
if( front_camera_ids.size() > 1 )
return true;
break;
default:
if( other_camera_ids.size() > 1 )
return true;
break;
}
}
return false;
}
/** Whether user preference is set to allow long press actions.
*/
private boolean allowLongPress() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
return sharedPreferences.getBoolean(PreferenceKeys.AllowLongPressPreferenceKey, true);
}
/* This method sets the preference defaults which are set specific for a particular device.
* This method should be called when Open Camera is run for the very first time after installation,
* or when the user has requested to "Reset settings".
*/
void setDeviceDefaults() {
if( MyDebug.LOG )
Log.d(TAG, "setDeviceDefaults");
boolean is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung");
//SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
//boolean is_samsung = Build.MANUFACTURER.toLowerCase(Locale.US).contains("samsung");
//boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus");
//boolean is_nexus = Build.MODEL.toLowerCase(Locale.US).contains("nexus");
//boolean is_nexus6 = Build.MODEL.toLowerCase(Locale.US).contains("nexus 6");
//boolean is_pixel_phone = Build.DEVICE != null && Build.DEVICE.equals("sailfish");
//boolean is_pixel_xl_phone = Build.DEVICE != null && Build.DEVICE.equals("marlin");
/*if( MyDebug.LOG ) {
//Log.d(TAG, "is_samsung? " + is_samsung);
//Log.d(TAG, "is_oneplus? " + is_oneplus);
//Log.d(TAG, "is_nexus? " + is_nexus);
//Log.d(TAG, "is_nexus6? " + is_nexus6);
//Log.d(TAG, "is_pixel_phone? " + is_pixel_phone);
//Log.d(TAG, "is_pixel_xl_phone? " + is_pixel_xl_phone);
}*/
/*if( is_samsung || is_oneplus ) {
// The problems we used to have on Samsung Galaxy devices are now fixed, by setting
// TEMPLATE_PREVIEW for the precaptureBuilder in CameraController2. This also fixes the
// problems with OnePlus 3T having blue tinge if flash is on, and the scene is bright
// enough not to need it
if( MyDebug.LOG )
Log.d(TAG, "set fake flash for camera2");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.Camera2FakeFlashPreferenceKey, true);
editor.apply();
}*/
/*if( is_nexus6 ) {
// Nexus 6 captureBurst() started having problems with Android 7 upgrade - images appeared in wrong order (and with wrong order of shutter speeds in exif info), as well as problems with the camera failing with serious errors
// we set this even for Nexus 6 devices not on Android 7, as at some point they'll likely be upgraded to Android 7
// Update: now fixed in v1.37, this was due to bug where we set RequestTag.CAPTURE for all captures in takePictureBurstExpoBracketing(), rather than just the last!
if( MyDebug.LOG )
Log.d(TAG, "disable fast burst for camera2");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.Camera2FastBurstPreferenceKey, false);
editor.apply();
}*/
if( is_samsung && !is_test ) {
// Samsung Galaxy devices (including S10e, S24) have problems with HDR/expo - base images come out with wrong exposures.
// This can be fixed by not using fast bast, allowing us to adjust the preview exposure to match.
if( MyDebug.LOG )
Log.d(TAG, "disable fast burst for camera2");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.Camera2FastBurstPreferenceKey, false);
editor.apply();
}
if( supports_camera2 && !is_test ) {
// n.b., when testing, we explicitly decide whether to run with Camera2 API or not
CameraControllerManager2 manager2 = new CameraControllerManager2(this);
int n_cameras = manager2.getNumberOfCameras();
boolean all_supports_camera2 = true; // whether all cameras have at least LIMITED support for Camera2 (risky to default to Camera2 if any cameras are LEGACY, as not easy to test such devices)
for(int i=0;i<n_cameras && all_supports_camera2;i++) {
if( !manager2.allowCamera2Support(i) ) {
if( MyDebug.LOG )
Log.d(TAG, "camera " + i + " doesn't have at least LIMITED support for Camera2 API");
all_supports_camera2 = false;
}
}
if( all_supports_camera2 ) {
boolean default_to_camera2 = false;
boolean is_google = Build.MANUFACTURER.toLowerCase(Locale.US).contains("google");
boolean is_nokia = Build.MANUFACTURER.toLowerCase(Locale.US).contains("hmd global");
boolean is_oneplus = Build.MANUFACTURER.toLowerCase(Locale.US).contains("oneplus");
if( is_google && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S )
default_to_camera2 = true;
else if( is_nokia && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
default_to_camera2 = true;
else if( is_samsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S )
default_to_camera2 = true;
else if( is_oneplus && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE )
default_to_camera2 = true;
if( default_to_camera2 ) {
if( MyDebug.LOG )
Log.d(TAG, "default to camera2 API");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.CameraAPIPreferenceKey, "preference_camera_api_camera2");
editor.apply();
}
}
}
}
/** Switches modes if required, if called from a relevant intent/tile.
*/
private void setModeFromIntents(Bundle savedInstanceState) {
if( MyDebug.LOG )
Log.d(TAG, "setModeFromIntents");
if( savedInstanceState != null ) {
// If we're restoring from a saved state, we shouldn't be resetting any modes
if( MyDebug.LOG )
Log.d(TAG, "restoring from saved state");
return;
}
boolean done_facing = false;
String action = this.getIntent().getAction();
if( MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(action) || MediaStore.ACTION_VIDEO_CAPTURE.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from video intent");
applicationInterface.setVideoPref(true);
}
else if( MediaStore.ACTION_IMAGE_CAPTURE.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(action) || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from photo intent");
applicationInterface.setVideoPref(false);
}
else if( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && MyTileService.TILE_ID.equals(action)) || ACTION_SHORTCUT_CAMERA.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from quick settings tile or application shortcut for Open Camera: photo mode");
applicationInterface.setVideoPref(false);
}
else if( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && MyTileServiceVideo.TILE_ID.equals(action)) || ACTION_SHORTCUT_VIDEO.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from quick settings tile or application shortcut for Open Camera: video mode");
applicationInterface.setVideoPref(true);
}
else if( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && MyTileServiceFrontCamera.TILE_ID.equals(action)) || ACTION_SHORTCUT_SELFIE.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from quick settings tile or application shortcut for Open Camera: selfie mode");
done_facing = true;
applicationInterface.switchToCamera(true);
}
else if( ACTION_SHORTCUT_GALLERY.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from application shortcut for Open Camera: gallery");
openGallery();
}
else if( ACTION_SHORTCUT_SETTINGS.equals(action) ) {
if( MyDebug.LOG )
Log.d(TAG, "launching from application shortcut for Open Camera: settings");
openSettings();
}
Bundle extras = this.getIntent().getExtras();
if( extras != null ) {
if( MyDebug.LOG )
Log.d(TAG, "handle intent extra information");
if( !done_facing ) {
int camera_facing = extras.getInt("android.intent.extras.CAMERA_FACING", -1);
if( camera_facing == 0 || camera_facing == 1 ) {
if( MyDebug.LOG )
Log.d(TAG, "found android.intent.extras.CAMERA_FACING: " + camera_facing);
applicationInterface.switchToCamera(camera_facing==1);
done_facing = true;
}
}
if( !done_facing ) {
if( extras.getInt("android.intent.extras.LENS_FACING_FRONT", -1) == 1 ) {
if( MyDebug.LOG )
Log.d(TAG, "found android.intent.extras.LENS_FACING_FRONT");
applicationInterface.switchToCamera(true);
done_facing = true;
}
}
if( !done_facing ) {
if( extras.getInt("android.intent.extras.LENS_FACING_BACK", -1) == 1 ) {
if( MyDebug.LOG )
Log.d(TAG, "found android.intent.extras.LENS_FACING_BACK");
applicationInterface.switchToCamera(false);
done_facing = true;
}
}
if( !done_facing ) {
if( extras.getBoolean("android.intent.extra.USE_FRONT_CAMERA", false) ) {
if( MyDebug.LOG )
Log.d(TAG, "found android.intent.extra.USE_FRONT_CAMERA");
applicationInterface.switchToCamera(true);
done_facing = true;
}
}
}
// N.B., in practice the hasSetCameraId() check is pointless as we don't save the camera ID in shared preferences, so it will always
// be false when application is started from onCreate(), unless resuming from saved instance (in which case we shouldn't be here anyway)
if( !done_facing && !applicationInterface.hasSetCameraId() ) {
if( MyDebug.LOG )
Log.d(TAG, "initialise to back camera");
// most devices have first camera as back camera anyway so this wouldn't be needed, but some (e.g., LG G6) have first camera
// as front camera, so we should explicitly switch to back camera
applicationInterface.switchToCamera(false);
}
}
/** Determine whether we support Camera2 API.
*/
private void initCamera2Support() {
if( MyDebug.LOG )
Log.d(TAG, "initCamera2Support");
supports_camera2 = false;
{
// originally we allowed Camera2 if all cameras support at least LIMITED
// as of 1.45, we allow Camera2 if at least one camera supports at least LIMITED - this
// is to support devices that might have a camera with LIMITED or better support, but
// also a LEGACY camera
CameraControllerManager2 manager2 = new CameraControllerManager2(this);
supports_camera2 = false;
int n_cameras = manager2.getNumberOfCameras();
if( n_cameras == 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "Camera2 reports 0 cameras");
supports_camera2 = false;
}
for(int i=0;i<n_cameras && !supports_camera2;i++) {
if( manager2.allowCamera2Support(i) ) {
if( MyDebug.LOG )
Log.d(TAG, "camera " + i + " has at least limited support for Camera2 API");
supports_camera2 = true;
}
}
}
//test_force_supports_camera2 = true; // test
if( test_force_supports_camera2 ) {
if( MyDebug.LOG )
Log.d(TAG, "forcing supports_camera2");
supports_camera2 = true;
}
if( MyDebug.LOG )
Log.d(TAG, "supports_camera2? " + supports_camera2);
// handle the switch from a boolean preference_use_camera2 to String preference_camera_api
// that occurred in v1.48
if( supports_camera2 ) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if( !sharedPreferences.contains(PreferenceKeys.CameraAPIPreferenceKey) // doesn't have the new key set yet
&& sharedPreferences.contains("preference_use_camera2") // has the old key set
&& sharedPreferences.getBoolean("preference_use_camera2", false) // and camera2 was enabled
) {
if( MyDebug.LOG )
Log.d(TAG, "transfer legacy camera2 boolean preference to new api option");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.CameraAPIPreferenceKey, "preference_camera_api_camera2");
editor.remove("preference_use_camera2"); // remove the old key, just in case
editor.apply();
}
}
}
/** Handles users updating to a version with scoped storage (this could be Android 10 users upgrading
* to the version of Open Camera with scoped storage; or users who later upgrade to Android 10).
* With scoped storage, we no longer support saving outside of DCIM/ when not using SAF.
* This updates if necessary both the current save location, and the save folder history.
*/
private void checkSaveLocations() {
if( MyDebug.LOG )
Log.d(TAG, "checkSaveLocations");
if( useScopedStorage() ) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean any_changes = false;
String save_location = getStorageUtils().getSaveLocation();
CheckSaveLocationResult res = checkSaveLocation(save_location);
if( !res.res ) {
if( MyDebug.LOG )
Log.d(TAG, "save_location not valid with scoped storage: " + save_location);
String new_folder;
if( res.alt == null ) {
// no alternative, fall back to default
new_folder = "OpenCamera";
}
else {
// replace with the alternative
if( MyDebug.LOG )
Log.d(TAG, "alternative: " + res.alt);
new_folder = res.alt;
}
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.SaveLocationPreferenceKey, new_folder);
editor.apply();
any_changes = true;
}
// now check history
// go backwards so we can remove easily
for(int i=save_location_history.size()-1;i>=0;i--) {
String this_location = save_location_history.get(i);
res = checkSaveLocation(this_location);
if( !res.res ) {
if( MyDebug.LOG )
Log.d(TAG, "save_location in history " + i + " not valid with scoped storage: " + this_location);
if( res.alt == null ) {
// no alternative, remove
save_location_history.remove(i);
}
else {
// replace with the alternative
if( MyDebug.LOG )
Log.d(TAG, "alternative: " + res.alt);
save_location_history.set(i, res.alt);
}
any_changes = true;
}
}
if( any_changes ) {
this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), false);
}
}
}
/** Result from checkSaveLocation. Ideally we'd just use android.util.Pair, but that's not mocked
* for use in unit tests.
* See checkSaveLocation() for documentation.
*/
public static class CheckSaveLocationResult {
final boolean res;
final String alt;
public CheckSaveLocationResult(boolean res, String alt) {
this.res = res;
this.alt = alt;
}
@Override
public boolean equals(Object o) {
if( !(o instanceof CheckSaveLocationResult) ) {
return false;
}
CheckSaveLocationResult that = (CheckSaveLocationResult)o;
// stop dumb inspection that suggests replacing warning with an error(!) (Objects class is not available on all API versions)
// and the other inspection suggests replacing with code that would cause a nullpointerexception
//noinspection EqualsReplaceableByObjectsCall,StringEquality
return that.res == this.res && ( (that.alt == this.alt) || (that.alt != null && that.alt.equals(this.alt) ) );
//return that.res == this.res && ( (that.alt == this.alt) || (that.alt != null && that.alt.equals(this.alt) ) );
}
@Override
public int hashCode() {
return (res ? 1249 : 1259) ^ (alt == null ? 0 : alt.hashCode());
}
@NonNull
@Override
public String toString() {
return "CheckSaveLocationResult{" + res + " , " + alt + "}";
}
}
public static CheckSaveLocationResult checkSaveLocation(final String folder) {
return checkSaveLocation(folder, null);
}
/** Checks to see if the supplied folder (in the format as used by our preferences) is supported
* with scoped storage.
* @return The Boolean is always non-null, and returns whether the save location is valid.
* If the return is false, then if the String is non-null, this stores an alternative
* form that is valid. If null, there is no valid alternative.
* @param base_folder This should normally be null, but can be used to specify manually the
* folder instead of using StorageUtils.getBaseFolder() - needed for unit
* tests as Environment class (for Environment.getExternalStoragePublicDirectory())
* is not mocked.
*/
public static CheckSaveLocationResult checkSaveLocation(final String folder, String base_folder) {
/*if( MyDebug.LOG )
Log.d(TAG, "DCIM path: " + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());*/
if( StorageUtils.saveFolderIsFull(folder) ) {
if( MyDebug.LOG )
Log.d(TAG, "checkSaveLocation for full path: " + folder);
// But still check to see if the full path is part of DCIM. Since when using the
// file dialog method with non-scoped storage, if the user specifies multiple subfolders
// e.g. DCIM/blah_a/blah_b, we don't spot that in FolderChooserDialog.useFolder(), and
// instead still store that as the full path.
if( base_folder == null )
base_folder = StorageUtils.getBaseFolder().getAbsolutePath();
// strip '/' as last character - makes it easier to also spot cases where the folder is the
// DCIM folder, but doesn't have a '/' last character
if( !base_folder.isEmpty() && base_folder.charAt(base_folder.length()-1) == '/' )
base_folder = base_folder.substring(0, base_folder.length()-1);
if( MyDebug.LOG )
Log.d(TAG, " compare to base_folder: " + base_folder);
String alt_folder = null;
if( folder.startsWith(base_folder) ) {
alt_folder = folder.substring(base_folder.length());
// also need to strip the first '/' if it exists
if( !alt_folder.isEmpty() && alt_folder.charAt(0) == '/' )
alt_folder = alt_folder.substring(1);
}
return new CheckSaveLocationResult(false, alt_folder);
}
else {
// already in expected format (indicates a sub-folder of DCIM)
return new CheckSaveLocationResult(true, null);
}
}
private void preloadIcons(int icons_id) {
long debug_time = 0;
if( MyDebug.LOG ) {
Log.d(TAG, "preloadIcons: " + icons_id);
debug_time = System.currentTimeMillis();
}
String [] icons = getResources().getStringArray(icons_id);
for(String icon : icons) {
int resource = getResources().getIdentifier(icon, null, this.getApplicationContext().getPackageName());
if( MyDebug.LOG )
Log.d(TAG, "load resource: " + resource);
Bitmap bm = BitmapFactory.decodeResource(getResources(), resource);
this.preloaded_bitmap_resources.put(resource, bm);
}
if( MyDebug.LOG ) {
Log.d(TAG, "preloadIcons: total time for preloadIcons: " + (System.currentTimeMillis() - debug_time));
Log.d(TAG, "size of preloaded_bitmap_resources: " + preloaded_bitmap_resources.size());
}
}
@Override
protected void onStop() {
if( MyDebug.LOG )
Log.d(TAG, "onStop");
super.onStop();
// we stop location listening in onPause, but done here again just to be certain!
applicationInterface.getLocationSupplier().freeLocationListeners();
}
@Override
protected void onDestroy() {
if( MyDebug.LOG ) {
Log.d(TAG, "onDestroy");
Log.d(TAG, "size of preloaded_bitmap_resources: " + preloaded_bitmap_resources.size());
}
activity_count--;
if( MyDebug.LOG )
Log.d(TAG, "activity_count: " + activity_count);
// should do asap before waiting for images to be saved - as risk the application will be killed whilst waiting for that to happen,
// and we want to avoid notifications hanging around
cancelImageSavingNotification();
if( want_no_limits && navigation_gap != 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
// it's unclear why this matters - but there is a bug when exiting split-screen mode, if the split-screen mode had set want_no_limits:
// even though the application is created when leaving split-screen mode, we still end up with the window flags for showing
// under the navigation bar!
// update: this issue is also fixed by not allowing want_no_limits mode in multi-window mode, but still good to reset things here
// just in case
showUnderNavigation(false);
}
// reduce risk of losing any images
// we don't do this in onPause or onStop, due to risk of ANRs
// note that even if we did call this earlier in onPause or onStop, we'd still want to wait again here: as it can happen
// that a new image appears after onPause/onStop is called, in which case we want to wait until images are saved,
waitUntilImageQueueEmpty();
preview.onDestroy();
if( applicationInterface != null ) {
applicationInterface.onDestroy();
}
// Need to recycle to avoid out of memory when running tests - probably good practice to do anyway
for(Map.Entry<Integer, Bitmap> entry : preloaded_bitmap_resources.entrySet()) {
if( MyDebug.LOG )
Log.d(TAG, "recycle: " + entry.getKey());
entry.getValue().recycle();
}
preloaded_bitmap_resources.clear();
if( textToSpeech != null ) {
// http://stackoverflow.com/questions/4242401/tts-error-leaked-serviceconnection-android-speech-tts-texttospeech-solved
if( MyDebug.LOG )
Log.d(TAG, "free textToSpeech");
textToSpeech.stop();
textToSpeech.shutdown();
textToSpeech = null;
}
// we stop location listening in onPause, but done here again just to be certain!
applicationInterface.getLocationSupplier().freeLocationListeners();
super.onDestroy();
if( MyDebug.LOG )
Log.d(TAG, "onDestroy done");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
private void setFirstTimeFlag() {
if( MyDebug.LOG )
Log.d(TAG, "setFirstTimeFlag");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.FirstTimePreferenceKey, true);
editor.apply();
}
private static String getOnlineHelpUrl(String append) {
if( MyDebug.LOG )
Log.d(TAG, "getOnlineHelpUrl: " + append);
// if we change this, remember that any page linked to must abide by Google Play developer policies!
// also if we change this method name or where it's located, remember to update the mention in
// opencamera_source.txt
//return "https://opencamera.sourceforge.io/" + append;
return "https://opencamera.org.uk/" + append;
}
void launchOnlineHelp() {
if( MyDebug.LOG )
Log.d(TAG, "launchOnlineHelp");
// if we change this, remember that any page linked to must abide by Google Play developer policies!
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("")));
startActivity(browserIntent);
}
void launchOnlinePrivacyPolicy() {
if( MyDebug.LOG )
Log.d(TAG, "launchOnlinePrivacyPolicy");
// if we change this, remember that any page linked to must abide by Google Play developer policies!
//Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("index.html#privacy")));
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("privacy_oc.html")));
startActivity(browserIntent);
}
void launchOnlineLicences() {
if( MyDebug.LOG )
Log.d(TAG, "launchOnlineLicences");
// if we change this, remember that any page linked to must abide by Google Play developer policies!
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getOnlineHelpUrl("#licence")));
startActivity(browserIntent);
}
/* Audio trigger - either loud sound, or speech recognition.
* This performs some additional checks before taking a photo.
*/
void audioTrigger() {
if( MyDebug.LOG )
Log.d(TAG, "ignore audio trigger due to popup open");
if( popupIsOpen() ) {
if( MyDebug.LOG )
Log.d(TAG, "ignore audio trigger due to popup open");
}
else if( camera_in_background ) {
if( MyDebug.LOG )
Log.d(TAG, "ignore audio trigger due to camera in background");
}
else if( preview.isTakingPhotoOrOnTimer() ) {
if( MyDebug.LOG )
Log.d(TAG, "ignore audio trigger due to already taking photo or on timer");
}
else if( preview.isVideoRecording() ) {
if( MyDebug.LOG )
Log.d(TAG, "ignore audio trigger due to already recording video");
}
else {
if( MyDebug.LOG )
Log.d(TAG, "schedule take picture due to loud noise");
//takePicture();
this.runOnUiThread(new Runnable() {
public void run() {
if( MyDebug.LOG )
Log.d(TAG, "taking picture due to audio trigger");
takePicture(false);
}
});
}
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
if( MyDebug.LOG )
Log.d(TAG, "onKeyDown: " + keyCode);
if( camera_in_background ) {
// don't allow keys such as volume keys for taking photo when camera in background!
if( MyDebug.LOG )
Log.d(TAG, "camera is in background");
}
else {
boolean handled = mainUI.onKeyDown(keyCode, event);
if( handled )
return true;
}
return super.onKeyDown(keyCode, event);
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if( MyDebug.LOG )
Log.d(TAG, "onKeyUp: " + keyCode);
if( camera_in_background ) {
// don't allow keys such as volume keys for taking photo when camera in background!
if( MyDebug.LOG )
Log.d(TAG, "camera is in background");
}
else {
mainUI.onKeyUp(keyCode, event);
}
return super.onKeyUp(keyCode, event);
}
private void zoomByStep(int change) {
if( MyDebug.LOG )
Log.d(TAG, "zoomByStep: " + change);
if( preview.supportsZoom() && change != 0 ) {
if( preview.getCameraController() != null ) {
// If the minimum zoom is < 1.0, the seekbar will have repeated entries for 1x zoom
// (so it's easier for the user to zoom to exactly 1.0x). But if using the -/+ buttons,
// volume keys etc to zoom, we want to skip over these repeated values.
int zoom_factor = preview.getCameraController().getZoom();
int new_zoom_factor = zoom_factor + change;
if( MyDebug.LOG )
Log.d(TAG, "new_zoom_factor: " + new_zoom_factor);
while( new_zoom_factor > 0 && new_zoom_factor < preview.getMaxZoom() && preview.getZoomRatio(new_zoom_factor) == preview.getZoomRatio() ) {
if( change > 0 )
change++;
else
change--;
new_zoom_factor = zoom_factor + change;
if( MyDebug.LOG )
Log.d(TAG, "skip over constant region: " + new_zoom_factor);
}
}
mainUI.changeSeekbar(R.id.zoom_seekbar, -change); // seekbar is opposite direction to zoom array
}
}
public void zoomIn() {
zoomByStep(1);
}
public void zoomOut() {
zoomByStep(-1);
}
public void changeExposure(int change) {
if( preview.supportsExposures() ) {
if( exposure_seekbar_values != null ) {
SeekBar seekBar = this.findViewById(R.id.exposure_seekbar);
int progress = seekBar.getProgress();
int new_progress = progress + change;
int current_exposure = getExposureSeekbarValue(progress);
if( new_progress < 0 || new_progress > exposure_seekbar_values.size()-1 ) {
// skip
}
else if( getExposureSeekbarValue(new_progress) == 0 && current_exposure != 0 ) {
// snap to the central repeated zero
new_progress = exposure_seekbar_values_zero;
change = new_progress - progress;
}
else {
// skip over the repeated zeroes
while( new_progress > 0 && new_progress < exposure_seekbar_values.size()-1 && getExposureSeekbarValue(new_progress) == current_exposure ) {
if( change > 0 )
change++;
else
change--;
new_progress = progress + change;
if( MyDebug.LOG )
Log.d(TAG, "skip over constant region: " + new_progress);
}
}
}
mainUI.changeSeekbar(R.id.exposure_seekbar, change);
}
}
public int getExposureSeekbarProgressZero() {
return exposure_seekbar_values_zero;
}
/** Returns the exposure compensation corresponding to a progress on the seekbar.
* Caller is responsible for checking that progress is within valid range.
*/
public int getExposureSeekbarValue(int progress) {
return exposure_seekbar_values.get(progress);
}
public void changeISO(int change) {
if( preview.supportsISORange() ) {
mainUI.changeSeekbar(R.id.iso_seekbar, change);
}
}
public void changeFocusDistance(int change, boolean is_target_distance) {
mainUI.changeSeekbar(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar, change);
}
private final SensorEventListener accelerometerListener = new SensorEventListener() {
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
@Override
public void onSensorChanged(SensorEvent event) {
preview.onAccelerometerSensorChanged(event);
}
};
public float getWaterDensity() {
return this.mWaterDensity;
}
@Override
protected void onResume() {
long debug_time = 0;
if( MyDebug.LOG ) {
Log.d(TAG, "onResume");
debug_time = System.currentTimeMillis();
}
super.onResume();
this.app_is_paused = false; // must be set before initLocation() at least
// this is intentionally true, not false, as the uncovering happens in DrawPreview when we receive frames from the camera after it's opened
// (this should already have been set from the call in onPause(), but we set it here again just in case)
applicationInterface.getDrawPreview().setCoverPreview(true);
applicationInterface.getDrawPreview().clearDimPreview(); // shouldn't be needed, but just in case the dim preview flag got set somewhere
cancelImageSavingNotification();
// Set black window background; also needed if we hide the virtual buttons in immersive mode
// Note that we do it here rather than customising the theme's android:windowBackground, so this doesn't affect other views - in particular, the MyPreferenceFragment settings
getWindow().getDecorView().getRootView().setBackgroundColor(Color.BLACK);
if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM ) {
// needed on Android 15, otherwise the navigation bar is not transparent
getWindow().setNavigationBarContrastEnforced(false);
}
registerDisplayListener();
mSensorManager.registerListener(accelerometerListener, mSensorAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
magneticSensor.registerMagneticListener(mSensorManager);
if( orientationEventListener != null ) {
orientationEventListener.enable();
}
getWindow().getDecorView().addOnLayoutChangeListener(layoutChangeListener);
// if BLE remote control is enabled, then start the background BLE service
bluetoothRemoteControl.startRemoteControl();
//speechControl.initSpeechRecognizer();
initLocation();
initGyroSensors();
applicationInterface.getImageSaver().onResume();
soundPoolManager.initSound();
soundPoolManager.loadSound(R.raw.mybeep);
soundPoolManager.loadSound(R.raw.mybeep_hi);
resetCachedSystemOrientation(); // just in case?
mainUI.layoutUI();
// If the cached last media has exif datetime info, it's fine to just call updateGalleryIcon(),
// which will find the most recent media (and takes care of if the cached last image may have
// been deleted).
// If it doesn't have exif datetime tags, updateGalleryIcon() may not be able to find the most
// recent media, so we stick with the cached uri if we can test that it's still accessible.
if( !getStorageUtils().getLastMediaScannedHasNoExifDateTime() ) {
updateGalleryIcon();
}
else {
if( MyDebug.LOG )
Log.d(TAG, "last media has no exif datetime, so check it still exists");
boolean uri_exists = false;
InputStream inputStream = null;
Uri check_uri = getStorageUtils().getLastMediaScannedCheckUri();
if( MyDebug.LOG )
Log.d(TAG, "check_uri: " + check_uri);
try {
inputStream = this.getContentResolver().openInputStream(check_uri);
if( inputStream != null )
uri_exists = true;
}
catch(Exception ignored) {
}
finally {
if( inputStream != null ) {
try {
inputStream.close();
}
catch(IOException e) {
MyDebug.logStackTrace(TAG, "failed to close inputStream", e);
}
}
}
if( uri_exists ) {
if( MyDebug.LOG )
Log.d(TAG, " most recent uri exists");
// also re-allow ghost image again in case that option is set (since we won't be
// doing this via updateGalleryIcon())
applicationInterface.getDrawPreview().allowGhostImage();
}
else {
if( MyDebug.LOG )
Log.d(TAG, " most recent uri no longer valid");
updateGalleryIcon();
}
}
applicationInterface.reset(false); // should be called before opening the camera in preview.onResume()
if( !camera_in_background ) {
// don't restart camera if we're showing a dialog or settings
preview.onResume();
}
{
// show a toast for the camera if it's not the first for front of back facing (otherwise on multi-front/back camera
// devices, it's easy to forget if set to a different camera)
// but we only show this when resuming, not every time the camera opens
// OR show the toast for the camera if it's a physical camera
int cameraId = applicationInterface.getCameraIdPref();
String cameraIdSPhysical = applicationInterface.getCameraIdSPhysicalPref();
if( cameraId > 0 || cameraIdSPhysical != null ) {
CameraControllerManager camera_controller_manager = preview.getCameraControllerManager();
CameraController.Facing front_facing = camera_controller_manager.getFacing(cameraId);
if( MyDebug.LOG )
Log.d(TAG, "front_facing: " + front_facing);
if( camera_controller_manager.getNumberOfCameras() > 2 || cameraIdSPhysical != null ) {
boolean camera_is_default = true;
if( cameraIdSPhysical != null )
camera_is_default = false;
for(int i=0;i<cameraId && camera_is_default;i++) {
CameraController.Facing that_front_facing = camera_controller_manager.getFacing(i);
if( MyDebug.LOG )
Log.d(TAG, "camera " + i + " that_front_facing: " + that_front_facing);
if( that_front_facing == front_facing ) {
// found an earlier camera with same front/back facing
camera_is_default = false;
}
}
if( MyDebug.LOG )
Log.d(TAG, "camera_is_default: " + camera_is_default);
if( !camera_is_default ) {
this.pushCameraIdToast(cameraId, cameraIdSPhysical);
}
}
}
this.announceCameraForAccessibility(cameraId, cameraIdSPhysical);
}
push_switched_camera = false; // just in case
if( MyDebug.LOG ) {
Log.d(TAG, "onResume: total time to resume: " + (System.currentTimeMillis() - debug_time));
}
}
/** Give details on the camera for talkback. Should be called when resuming (but not every time the
* camera is reopened), or if switching camera.
*/
private void announceCameraForAccessibility(int cameraId, String cameraIdSPhysical) {
String description = cameraIdSPhysical != null ?
preview.getCameraControllerManager().getDescription(null, this, cameraIdSPhysical, true, false) :
preview.getCameraControllerManager().getDescription(this, cameraId);
if( description != null ) {
String talkback_string = description;
if( cameraIdSPhysical == null )
talkback_string += " " + getResources().getString(R.string.camera_id) + " " + cameraId;
else
talkback_string += " " + getResources().getString(R.string.lens) + " " + cameraIdSPhysical;
if( MyDebug.LOG )
Log.d(TAG, "talkback_string: " + talkback_string);
this.preview.getView().announceForAccessibility(talkback_string);
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if( MyDebug.LOG )
Log.d(TAG, "onWindowFocusChanged: " + hasFocus);
super.onWindowFocusChanged(hasFocus);
if( !this.camera_in_background && hasFocus ) {
// low profile mode is cleared when app goes into background
// and for Kit Kat immersive mode, we want to set up the timer
// we do in onWindowFocusChanged rather than onResume(), to also catch when window lost focus due to notification bar being dragged down (which prevents resetting of immersive mode)
initImmersiveMode();
}
}
@Override
protected void onPause() {
long debug_time = 0;
if( MyDebug.LOG ) {
Log.d(TAG, "onPause");
debug_time = System.currentTimeMillis();
}
super.onPause(); // docs say to call this before freeing other things
this.app_is_paused = true;
mainUI.destroyPopup(); // important as user could change/reset settings from Android settings when pausing
if( this.switch_multi_camera_dialog != null ) {
// to be safe - again, camera ID could reset
if( MyDebug.LOG )
Log.d(TAG, "clear switch_multi_camera_dialog");
this.switch_multi_camera_dialog = null;
}
unregisterDisplayListener();
mSensorManager.unregisterListener(accelerometerListener);
magneticSensor.unregisterMagneticListener(mSensorManager);
if( orientationEventListener != null ) {
orientationEventListener.disable();
}
getWindow().getDecorView().removeOnLayoutChangeListener(layoutChangeListener);
bluetoothRemoteControl.stopRemoteControl();
freeAudioListener(false);
//speechControl.stopSpeechRecognizer();
applicationInterface.getLocationSupplier().freeLocationListeners();
applicationInterface.stopPanorama(true); // in practice not needed as we should stop panorama when camera is closed, but good to do it explicitly here, before disabling the gyro sensors
applicationInterface.getGyroSensor().disableSensors();
applicationInterface.getImageSaver().onPause();
soundPoolManager.releaseSound();
applicationInterface.clearLastImages(); // this should happen when pausing the preview, but call explicitly just to be safe
applicationInterface.getDrawPreview().clearGhostImage();
preview.onPause();
applicationInterface.getDrawPreview().setCoverPreview(true); // must be after we've closed the preview (otherwise risk that further frames from preview will unset the cover_preview flag in DrawPreview)
if( applicationInterface.getImageSaver().getNImagesToSave() > 0) {
createImageSavingNotification();
}
if( update_gallery_future != null ) {
update_gallery_future.cancel(true);
}
// intentionally do this again, just in case something turned location on since - keep this right at the end:
applicationInterface.getLocationSupplier().freeLocationListeners();
// don't want to enter immersive mode when in background
// needs to be last in case anything above indirectly called initImmersiveMode()
cancelImmersiveTimer();
if( MyDebug.LOG ) {
Log.d(TAG, "onPause: total time to pause: " + (System.currentTimeMillis() - debug_time));
}
}
private class MyDisplayListener implements DisplayManager.DisplayListener {
private int old_rotation;
private MyDisplayListener() {
int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation();
if( MyDebug.LOG ) {
Log.d(TAG, "MyDisplayListener");
Log.d(TAG, "rotation: " + rotation);
}
old_rotation = rotation;
}
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation();
if( MyDebug.LOG ) {
Log.d(TAG, "onDisplayChanged: " + displayId);
Log.d(TAG, "rotation: " + rotation);
Log.d(TAG, "old_rotation: " + old_rotation);
}
if( ( rotation == Surface.ROTATION_0 && old_rotation == Surface.ROTATION_180 ) ||
( rotation == Surface.ROTATION_180 && old_rotation == Surface.ROTATION_0 ) ||
( rotation == Surface.ROTATION_90 && old_rotation == Surface.ROTATION_270 ) ||
( rotation == Surface.ROTATION_270 && old_rotation == Surface.ROTATION_90 )
) {
if( MyDebug.LOG )
Log.d(TAG, "onDisplayChanged: switched between landscape and reverse orientation");
onSystemOrientationChanged();
}
old_rotation = rotation;
}
}
/** Creates and registers a display listener, needed to handle switches between landscape and
* reverse landscape (without going via portrait) when lock_to_landscape==false.
*/
private void registerDisplayListener() {
if( MyDebug.LOG )
Log.d(TAG, "registerDisplayListener");
if( !lock_to_landscape ) {
displayListener = new MyDisplayListener();
DisplayManager displayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE);
displayManager.registerDisplayListener(displayListener, null);
}
}
private void unregisterDisplayListener() {
if( MyDebug.LOG )
Log.d(TAG, "unregisterDisplayListener");
if( displayListener != null ) {
DisplayManager displayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE);
displayManager.unregisterDisplayListener(displayListener);
displayListener = null;
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
if( MyDebug.LOG )
Log.d(TAG, "onConfigurationChanged(): " + newConfig.orientation);
// configuration change can include screen orientation (landscape/portrait) when not locked (when settings is open)
// needed if app is paused/resumed when settings is open and device is in portrait mode
// update: need this all the time when lock_to_landscape==false
onSystemOrientationChanged();
super.onConfigurationChanged(newConfig);
}
private void onSystemOrientationChanged() {
if( MyDebug.LOG )
Log.d(TAG, "onSystemOrientationChanged");
// n.b., need to call this first, before preview.setCameraDisplayOrientation(), since
// preview.setCameraDisplayOrientation() will call getDisplayRotation() and we don't want
// to be using the outdated cached value now that the rotation has changed!
// update: no longer relevant, as preview.setCameraDisplayOrientation() now sets
// prefer_later to true to avoid using cached value. But might as well call it first anyway.
resetCachedSystemOrientation();
preview.setCameraDisplayOrientation();
if( !lock_to_landscape ) {
SystemOrientation newSystemOrientation = getSystemOrientation();
if( hasOldSystemOrientation && oldSystemOrientation == newSystemOrientation ) {
if( MyDebug.LOG )
Log.d(TAG, "onSystemOrientationChanged: orientation hasn't changed");
}
else {
if( hasOldSystemOrientation ) {
// handle rotation animation
int start_rotation = getRotationFromSystemOrientation(oldSystemOrientation) - getRotationFromSystemOrientation(newSystemOrientation);
if( MyDebug.LOG )
Log.d(TAG, "start_rotation: " + start_rotation);
if( start_rotation < -180 )
start_rotation += 360;
else if( start_rotation > 180 )
start_rotation -= 360;
mainUI.layoutUIWithRotation(start_rotation);
}
else {
mainUI.layoutUI();
}
applicationInterface.getDrawPreview().updateSettings();
hasOldSystemOrientation = true;
oldSystemOrientation = newSystemOrientation;
}
}
}
/** Returns the current system orientation.
* Note if lock_to_landscape is true, this always returns LANDSCAPE even if called when we're
* allowing configuration changes (e.g., in Settings or a dialog is showing). (This method,
* and hence calls to it, were added to support lock_to_landscape==false behaviour, and we
* want to avoid changing behaviour for lock_to_landscape==true behaviour.)
* Note that this also caches the orientation: firstly for performance (as this is called from
* DrawPreview), secondly to support REVERSE_LANDSCAPE, we don't want a sudden change if
* getDefaultDisplay().getRotation() changes after the configuration changes.
*/
public SystemOrientation getSystemOrientation() {
if( test_force_system_orientation ) {
return test_system_orientation;
}
if( lock_to_landscape ) {
return SystemOrientation.LANDSCAPE;
}
if( has_cached_system_orientation ) {
return cached_system_orientation;
}
SystemOrientation result;
int system_orientation = getResources().getConfiguration().orientation;
if( MyDebug.LOG )
Log.d(TAG, "system orientation: " + system_orientation);
switch( system_orientation ) {
case Configuration.ORIENTATION_LANDSCAPE:
result = SystemOrientation.LANDSCAPE;
// now try to distinguish between landscape and reverse landscape
{
int rotation = getWindowManager().getDefaultDisplay().getRotation();
if( MyDebug.LOG )
Log.d(TAG, "rotation: " + rotation);
switch( rotation ) {
case Surface.ROTATION_0:
case Surface.ROTATION_90:
// landscape
if( MyDebug.LOG )
Log.d(TAG, "landscape");
break;
case Surface.ROTATION_180:
case Surface.ROTATION_270:
// reverse landscape
if( MyDebug.LOG )
Log.d(TAG, "reverse landscape");
result = SystemOrientation.REVERSE_LANDSCAPE;
break;
default:
if( MyDebug.LOG )
Log.e(TAG, "unknown rotation: " + rotation);
break;
}
}
break;
case Configuration.ORIENTATION_PORTRAIT:
result = SystemOrientation.PORTRAIT;
break;
case Configuration.ORIENTATION_UNDEFINED:
default:
if( MyDebug.LOG )
Log.e(TAG, "unknown system orientation: " + system_orientation);
result = SystemOrientation.LANDSCAPE;
break;
}
if( MyDebug.LOG )
Log.d(TAG, "system orientation is now: " + result);
this.has_cached_system_orientation = true;
this.cached_system_orientation = result;
return result;
}
/** Returns rotation in degrees (as a multiple of 90 degrees) corresponding to the supplied
* system orientation.
*/
public static int getRotationFromSystemOrientation(SystemOrientation system_orientation) {
int rotation;
if( system_orientation == MainActivity.SystemOrientation.PORTRAIT )
rotation = 270;
else if( system_orientation == MainActivity.SystemOrientation.REVERSE_LANDSCAPE )
rotation = 180;
else
rotation = 0;
return rotation;
}
private void resetCachedSystemOrientation() {
this.has_cached_system_orientation = false;
this.has_cached_display_rotation = false;
}
/** A wrapper for getWindowManager().getDefaultDisplay().getRotation(), except if
* lock_to_landscape==false && prefer_later==false, this uses a cached value.
*/
public int getDisplayRotation(boolean prefer_later) {
/*if( MyDebug.LOG ) {
Log.d(TAG, "getDisplayRotationDegrees");
Log.d(TAG, "prefer_later: " + prefer_later);
}*/
if( lock_to_landscape || prefer_later ) {
return getWindowManager().getDefaultDisplay().getRotation();
}
// we cache to reduce effect of annoying problem where rotation changes shortly before the
// configuration actually changes (several frames), so on-screen elements would briefly show
// in wrong location when device rotates from/to portrait and landscape; also not a bad idea
// to cache for performance anyway, to avoid calling
// getWindowManager().getDefaultDisplay().getRotation() every frame
long time_ms = System.currentTimeMillis();
if( has_cached_display_rotation && time_ms < cached_display_rotation_time_ms + 1000 ) {
return cached_display_rotation;
}
has_cached_display_rotation = true;
int rotation = getWindowManager().getDefaultDisplay().getRotation();
cached_display_rotation = rotation;
cached_display_rotation_time_ms = time_ms;
return rotation;
}
public void waitUntilImageQueueEmpty() {
if( MyDebug.LOG )
Log.d(TAG, "waitUntilImageQueueEmpty");
applicationInterface.getImageSaver().waitUntilDone();
}
/**
* @return True if the long-click is handled, otherwise return false to indicate that regular
* click should still be triggered when the user releases the touch.
*/
private boolean longClickedTakePhoto() {
if( MyDebug.LOG )
Log.d(TAG, "longClickedTakePhoto");
if( preview.isVideo() ) {
// no long-click action for video mode
}
else if( supportsFastBurst() ) {
// need to check whether fast burst is supported (including for the current resolution),
// in case we're in Standard photo mode
CameraController.Size current_size = preview.getCurrentPictureSize();
if( current_size != null && current_size.supports_burst ) {
MyApplicationInterface.PhotoMode photo_mode = applicationInterface.getPhotoMode();
if( photo_mode == MyApplicationInterface.PhotoMode.Standard &&
applicationInterface.isRawOnly(photo_mode) ) {
if( MyDebug.LOG )
Log.d(TAG, "fast burst not supported in RAW-only mode");
// in JPEG+RAW mode, a continuous fast burst will only produce JPEGs which is fine; but in RAW only mode,
// no images at all would be saved! (Or we could switch to produce JPEGs anyway, but this seems misleading
// in RAW only mode.)
}
else if( photo_mode == MyApplicationInterface.PhotoMode.Standard ||
photo_mode == MyApplicationInterface.PhotoMode.FastBurst ) {
this.takePicturePressed(false, true);
return true;
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "fast burst not supported for this resolution");
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "fast burst not supported");
}
// return false, so a regular click will still be triggered when the user releases the touch
return false;
}
public void clickedTakePhoto(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedTakePhoto");
this.takePicture(false);
}
/** User has clicked button to take a photo snapshot whilst video recording.
*/
public void clickedTakePhotoVideoSnapshot(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedTakePhotoVideoSnapshot");
this.takePicture(true);
}
public void clickedPauseVideo(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedPauseVideo");
pauseVideo();
}
public void pauseVideo() {
if( MyDebug.LOG )
Log.d(TAG, "pauseVideo");
if( preview.isVideoRecording() ) { // just in case
preview.pauseVideo();
mainUI.setPauseVideoContentDescription();
}
}
public void clickedCancelPanorama(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedCancelPanorama");
applicationInterface.stopPanorama(true);
}
public void clickedCycleRaw(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedCycleRaw");
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String new_value = null;
switch( sharedPreferences.getString(PreferenceKeys.RawPreferenceKey, "preference_raw_no") ) {
case "preference_raw_no":
new_value = "preference_raw_yes";
break;
case "preference_raw_yes":
new_value = "preference_raw_only";
break;
case "preference_raw_only":
new_value = "preference_raw_no";
break;
default:
Log.e(TAG, "unrecognised raw preference");
break;
}
if( new_value != null ) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.RawPreferenceKey, new_value);
editor.apply();
mainUI.updateCycleRawIcon();
applicationInterface.getDrawPreview().updateSettings();
preview.reopenCamera(); // needed for RAW options to take effect
}
}
public void clickedStoreLocation(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedStoreLocation");
boolean value = applicationInterface.getGeotaggingPref();
value = !value;
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.LocationPreferenceKey, value);
editor.apply();
mainUI.updateStoreLocationIcon();
applicationInterface.getDrawPreview().updateSettings(); // because we cache the geotagging setting
initLocation(); // required to enable or disable GPS, also requests permission if necessary
this.closePopup();
String message = getResources().getString(R.string.preference_location) + ": " + getResources().getString(value ? R.string.on : R.string.off);
preview.showToast(store_location_toast, message, true);
}
public void clickedTextStamp(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedTextStamp");
this.closePopup();
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.preference_textstamp);
final View dialog_view = LayoutInflater.from(this).inflate(R.layout.alertdialog_edittext, null);
final EditText editText = dialog_view.findViewById(R.id.edit_text);
// set hint instead of content description for EditText, see https://support.google.com/accessibility/android/answer/6378120
editText.setHint(getResources().getString(R.string.preference_textstamp));
editText.setText(applicationInterface.getTextStampPref());
alertDialog.setView(dialog_view);
alertDialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if( MyDebug.LOG )
Log.d(TAG, "custom text stamp clicked okay");
String custom_text = editText.getText().toString();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.TextStampPreferenceKey, custom_text);
editor.apply();
mainUI.updateTextStampIcon();
}
});
alertDialog.setNegativeButton(android.R.string.cancel, null);
final AlertDialog alert = alertDialog.create();
alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface arg0) {
if( MyDebug.LOG )
Log.d(TAG, "custom stamp text dialog dismissed");
setWindowFlagsForCamera();
showPreview(true);
}
});
showPreview(false);
setWindowFlagsForSettings();
showAlert(alert);
}
public void clickedStamp(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedStamp");
this.closePopup();
boolean value = applicationInterface.getStampPref().equals("preference_stamp_yes");
value = !value;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.StampPreferenceKey, value ? "preference_stamp_yes" : "preference_stamp_no");
editor.apply();
mainUI.updateStampIcon();
applicationInterface.getDrawPreview().updateSettings();
preview.showToast(stamp_toast, value ? R.string.stamp_enabled : R.string.stamp_disabled, true);
}
public void clickedFocusPeaking(View view) {
clickedFocusPeaking();
}
public void clickedFocusPeaking() {
if( MyDebug.LOG )
Log.d(TAG, "clickedFocusPeaking");
boolean value = applicationInterface.getFocusPeakingPref();
value = !value;
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.FocusPeakingPreferenceKey, value ? "preference_focus_peaking_on" : "preference_focus_peaking_off");
editor.apply();
mainUI.updateFocusPeakingIcon();
applicationInterface.getDrawPreview().updateSettings(); // needed to update focus peaking
}
public void clickedAutoLevel(View view) {
clickedAutoLevel();
}
public void clickedAutoLevel() {
if( MyDebug.LOG )
Log.d(TAG, "clickedAutoLevel");
boolean value = applicationInterface.getAutoStabilisePref();
value = !value;
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.AutoStabilisePreferenceKey, value);
editor.apply();
boolean done_dialog = false;
if( value ) {
boolean done_auto_stabilise_info = sharedPreferences.contains(PreferenceKeys.AutoStabiliseInfoPreferenceKey);
if( !done_auto_stabilise_info ) {
mainUI.showInfoDialog(R.string.preference_auto_stabilise, R.string.auto_stabilise_info, PreferenceKeys.AutoStabiliseInfoPreferenceKey);
done_dialog = true;
}
}
if( !done_dialog ) {
String message = getResources().getString(R.string.preference_auto_stabilise) + ": " + getResources().getString(value ? R.string.on : R.string.off);
preview.showToast(this.getChangedAutoStabiliseToastBoxer(), message, true);
}
mainUI.updateAutoLevelIcon();
applicationInterface.getDrawPreview().updateSettings(); // because we cache the auto-stabilise setting
this.closePopup();
}
public void clickedCycleFlash(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedCycleFlash");
preview.cycleFlash(true, true);
mainUI.updateCycleFlashIcon();
}
public void clickedFaceDetection(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedFaceDetection");
this.closePopup();
boolean value = applicationInterface.getFaceDetectionPref();
value = !value;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.FaceDetectionPreferenceKey, value);
editor.apply();
mainUI.updateFaceDetectionIcon();
preview.showToast(stamp_toast, value ? R.string.face_detection_enabled : R.string.face_detection_disabled, true);
block_startup_toast = true; // so the toast from reopening camera is suppressed, otherwise it conflicts with the face detection toast
preview.reopenCamera();
}
public void clickedAudioControl(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedAudioControl");
// check hasAudioControl just in case!
if( !hasAudioControl() ) {
if( MyDebug.LOG )
Log.e(TAG, "clickedAudioControl, but hasAudioControl returns false!");
return;
}
this.closePopup();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String audio_control = sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none");
/*if( audio_control.equals("voice") && speechControl.hasSpeechRecognition() ) {
if( speechControl.isStarted() ) {
speechControl.stopListening();
}
else {
boolean has_audio_permission = true;
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
// we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener()
if( MyDebug.LOG )
Log.d(TAG, "check for record audio permission");
if( ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) {
if( MyDebug.LOG )
Log.d(TAG, "record audio permission not available");
applicationInterface.requestRecordAudioPermission();
has_audio_permission = false;
}
}
if( has_audio_permission ) {
speechControl.showToast(true);
speechControl.startSpeechRecognizerIntent();
speechControl.speechRecognizerStarted();
}
}
}
else*/ if( audio_control.equals("noise") ){
if( audio_listener != null ) {
freeAudioListener(false);
}
else {
startAudioListener();
}
}
}
/* Returns the cameraId that the "Switch camera" button will switch to.
* Note that this may not necessarily be the next camera ID, on multi camera devices (if
* isMultiCamEnabled() returns true).
*/
public int getNextCameraId() {
if( MyDebug.LOG )
Log.d(TAG, "getNextCameraId");
int cameraId = getActualCameraId();
if( MyDebug.LOG )
Log.d(TAG, "current cameraId: " + cameraId);
if( this.preview.canSwitchCamera() ) {
if( isMultiCamEnabled() ) {
// don't use preview.getCameraController(), as it may be null if user quickly switches between cameras
switch( preview.getCameraControllerManager().getFacing(cameraId) ) {
case FACING_BACK:
if( !front_camera_ids.isEmpty() )
cameraId = front_camera_ids.get(0);
else if( !other_camera_ids.isEmpty() )
cameraId = other_camera_ids.get(0);
break;
case FACING_FRONT:
if( !other_camera_ids.isEmpty() )
cameraId = other_camera_ids.get(0);
else if( !back_camera_ids.isEmpty() )
cameraId = back_camera_ids.get(0);
break;
default:
if( !back_camera_ids.isEmpty() )
cameraId = back_camera_ids.get(0);
else if( !front_camera_ids.isEmpty() )
cameraId = front_camera_ids.get(0);
break;
}
}
else {
int n_cameras = preview.getCameraControllerManager().getNumberOfCameras();
cameraId = (cameraId+1) % n_cameras;
}
}
if( MyDebug.LOG )
Log.d(TAG, "next cameraId: " + cameraId);
return cameraId;
}
/* Returns the next cameraId with the same-facing as current camera.
* Should only be called if isMultiCamEnabled() returns true.
* Only used for testing, now that we bring up a menu instead of cycling.
*/
/*public int testGetNextMultiCameraId() {
if( MyDebug.LOG )
Log.d(TAG, "testGetNextMultiCameraId");
if( !isMultiCamEnabled() ) {
Log.e(TAG, "testGetNextMultiCameraId() called but not in multi-cam mode");
throw new RuntimeException("testGetNextMultiCameraId() called but not in multi-cam mode");
}
List<Integer> camera_set;
// don't use preview.getCameraController(), as it may be null if user quickly switches between cameras
int currCameraId = getActualCameraId();
switch( preview.getCameraControllerManager().getFacing(currCameraId) ) {
case FACING_BACK:
camera_set = back_camera_ids;
break;
case FACING_FRONT:
camera_set = front_camera_ids;
break;
default:
camera_set = other_camera_ids;
break;
}
int cameraId;
int indx = camera_set.indexOf(currCameraId);
if( indx == -1 ) {
Log.e(TAG, "camera id not in current camera set");
// this shouldn't happen, but if it does, revert to the first camera id in the set
// update: oddly had reports of IndexOutOfBoundsException crashes from Google Play from camera_set.get(0)
// because of camera_set having length 0, so stick with currCameraId in such cases
if( camera_set.size() == 0 ) {
Log.e(TAG, "camera_set is empty");
cameraId = currCameraId;
}
else
cameraId = camera_set.get(0);
}
else {
indx = (indx+1) % camera_set.size();
cameraId = camera_set.get(indx);
}
if( MyDebug.LOG )
Log.d(TAG, "next multi cameraId: " + cameraId);
return cameraId;
}*/
private void pushCameraIdToast(int cameraId, String cameraIdSPhysical) {
if( MyDebug.LOG )
Log.d(TAG, "pushCameraIdToast: " + cameraId);
if( preview.getCameraControllerManager().getNumberOfCameras() > 2 || cameraIdSPhysical != null ) {
// telling the user which camera is pointless for only two cameras, but on devices that now
// expose many cameras it can be confusing, so show a toast to at least display the id
// similarly we want to show a toast if using a physical camera, so user doesn't forget
String description = cameraIdSPhysical != null ?
preview.getCameraControllerManager().getDescription(null, this, cameraIdSPhysical, true, true) :
preview.getCameraControllerManager().getDescription(this, cameraId);
if( description != null ) {
String toast_string = description;
if( cameraIdSPhysical == null ) // only add the ID if not a physical camera
toast_string += ": " + getResources().getString(R.string.camera_id) + " " + cameraId;
//preview.showToast(null, toast_string);
this.push_info_toast_text = toast_string;
}
}
}
public void userSwitchToCamera(int cameraId, String cameraIdSPhysical) {
if( MyDebug.LOG )
Log.d(TAG, "userSwitchToCamera: " + cameraId + " / " + cameraIdSPhysical);
View switchCameraButton = findViewById(R.id.switch_camera);
View switchMultiCameraButton = findViewById(R.id.switch_multi_camera);
// prevent slowdown if user repeatedly clicks:
switchCameraButton.setEnabled(false);
switchMultiCameraButton.setEnabled(false);
applicationInterface.reset(true);
this.getApplicationInterface().getDrawPreview().setDimPreview(true);
if( this.switch_multi_camera_dialog != null ) {
// only clear if switching to a different camera ID (switching between lenses is fine)
int curr_camera_id = getActualCameraId();
if( MyDebug.LOG )
Log.d(TAG, "curr_camera_id: " + curr_camera_id);
if( cameraId != curr_camera_id ) {
if( MyDebug.LOG )
Log.d(TAG, "clear switch_multi_camera_dialog");
this.switch_multi_camera_dialog = null;
}
else {
if( MyDebug.LOG )
Log.d(TAG, "keep switch_multi_camera_dialog");
}
}
this.preview.setCamera(cameraId, cameraIdSPhysical);
switchCameraButton.setEnabled(true);
switchMultiCameraButton.setEnabled(true);
// no need to call mainUI.setSwitchCameraContentDescription - this will be called from Preview.cameraSetup when the
// new camera is opened
this.announceCameraForAccessibility(cameraId, cameraIdSPhysical);
}
/**
* Selects the next camera on the phone - in practice, switches between
* front and back cameras
*/
public void clickedSwitchCamera(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedSwitchCamera");
if( preview.isOpeningCamera() ) {
if( MyDebug.LOG )
Log.d(TAG, "already opening camera in background thread");
return;
}
this.closePopup();
if( this.preview.canSwitchCamera() ) {
int cameraId = getNextCameraId();
if( !isMultiCamEnabled() ) {
pushCameraIdToast(cameraId, null);
}
else {
// In multi-cam mode, no need to show the toast when just switching between front and back cameras.
// But it is useful to clear an active fake toast, otherwise have issue if the user uses
// clickedSwitchMultiCamera() (which displays a fake toast for the camera via the info toast), then
// immediately uses clickedSwitchCamera() - the toast for the wrong camera will still be lingering
// until it expires, which looks a bit strange.
// (If using non-fake toasts, this isn't an issue, at least on Android 10+, as now toasts seem to
// disappear when the user touches the screen anyway.)
preview.clearActiveFakeToast();
}
userSwitchToCamera(cameraId, null);
push_switched_camera = true;
}
}
/** Returns list of logical cameras with same facing as the supplied camera_id.
*/
public List<Integer> getSameFacingLogicalCameras(int camera_id) {
List<Integer> logical_camera_ids = new ArrayList<>();
CameraController.Facing this_facing = preview.getCameraControllerManager().getFacing(camera_id);
for(int i=0;i<preview.getCameraControllerManager().getNumberOfCameras();i++) {
if( preview.getCameraControllerManager().getFacing(i) != this_facing ) {
// only show cameras with same facing
continue;
}
logical_camera_ids.add(i);
}
return logical_camera_ids;
}
private AlertDialog switch_multi_camera_dialog;
private AlertDialog createSwitchMultiCameraDialog() {
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog");
long debug_time = 0;
if( MyDebug.LOG ) {
debug_time = System.currentTimeMillis();
}
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.choose_camera);
int curr_camera_id = getActualCameraId();
List<Integer> logical_camera_ids = getSameFacingLogicalCameras(curr_camera_id);
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after logical_camera_ids: " + (System.currentTimeMillis() - debug_time));
int n_logical_cameras = logical_camera_ids.size();
int n_cameras = n_logical_cameras;
if( preview.hasPhysicalCameras() ) {
n_cameras += preview.getPhysicalCameras().size();
//n_cameras++; // for the info message
}
CharSequence [] items = new CharSequence[n_cameras];
int [] items_logical_camera_id = new int[n_cameras];
String [] items_physical_camera_id = new String[n_cameras];
int index=0;
int selected=-1;
String curr_physical_camera_id = applicationInterface.getCameraIdSPhysicalPref();
for(int i=0;i<n_logical_cameras;i++) {
int logical_camera_id = logical_camera_ids.get(i);
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time before getDescription: " + (System.currentTimeMillis() - debug_time));
String camera_name = logical_camera_id + ": " + preview.getCameraControllerManager().getDescription(this, logical_camera_id);
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after getDescription: " + (System.currentTimeMillis() - debug_time));
if( logical_camera_id == curr_camera_id ) {
// this is the current logical camera
if( preview.hasPhysicalCameras() ) {
camera_name += " (" + getResources().getString(R.string.auto_lens) + ")";
}
if( curr_physical_camera_id == null ) {
// the logical camera is being used directly
selected = index;
//String html_camera_name = "<b>[" + camera_name + "]</b>";
String html_camera_name = "<b>" + camera_name + "</b>";
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
items[index] = Html.fromHtml(html_camera_name, Html.FROM_HTML_MODE_LEGACY);
}
else {
items[index] = Html.fromHtml(html_camera_name);
}
}
else {
// a physical camera is in use, so don't bold this entry
items[index] = camera_name;
}
items_logical_camera_id[index] = logical_camera_id;
items_physical_camera_id[index] = null;
index++;
if( preview.hasPhysicalCameras() ) {
// also add the physical cameras that underlie the current logical camera
Set<String> physical_camera_ids = preview.getPhysicalCameras();
// sort by view angle
class PhysicalCamera {
private final String id;
private final String description;
private final SizeF view_angle;
private PhysicalCamera(String id) {
this.id = id;
CameraControllerManager.CameraInfo info = new CameraControllerManager.CameraInfo();
this.description = preview.getCameraControllerManager().getDescription(info, MainActivity.this, id, false, true);
this.view_angle = info.view_angle;
}
}
ArrayList<PhysicalCamera> physical_cameras = new ArrayList<>();
for(String physical_id : physical_camera_ids) {
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time before getDescription: " + (System.currentTimeMillis() - debug_time));
physical_cameras.add(new PhysicalCamera(physical_id));
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after getDescription: " + (System.currentTimeMillis() - debug_time));
}
{
Collections.sort(physical_cameras, new Comparator<>() {
@Override
public int compare(PhysicalCamera o1, PhysicalCamera o2) {
float diff = o2.view_angle.getWidth() - o1.view_angle.getWidth();
if( Math.abs(diff) < 1.0e-5f )
return 0;
else if( diff > 0.0f )
return 1;
else
return -1;
}
});
}
int j=0;
String indent = "&nbsp;&nbsp;&nbsp;&nbsp;";
for(PhysicalCamera physical_camera : physical_cameras) {
String physical_id = physical_camera.id;
camera_name = getResources().getString(R.string.lens) + " " + j + ": " + physical_camera.description;
String html_camera_name;
if( curr_physical_camera_id != null && curr_physical_camera_id.equals(physical_id) ) {
// this is the current physical camera
selected = index;
//html_camera_name = indent + "<b>[" + camera_name + "]</b>";
html_camera_name = indent + "<b>" + camera_name + "</b>";
}
else {
html_camera_name = indent + camera_name;
}
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
items[index] = Html.fromHtml(html_camera_name, Html.FROM_HTML_MODE_LEGACY);
}
else {
items[index] = Html.fromHtml(html_camera_name);
}
items_logical_camera_id[index] = logical_camera_id;
items_physical_camera_id[index] = physical_id;
index++;
j++;
}
}
}
else {
items[index] = camera_name;
items_logical_camera_id[index] = logical_camera_id;
items_physical_camera_id[index] = null;
index++;
}
}
/*if( preview.hasPhysicalCameras() ) {
items[index] = getResources().getString(R.string.physical_cameras_info);
items_logical_camera_id[index] = -1;
items_physical_camera_id[index] = null;
//index++;
}*/
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after building menu: " + (System.currentTimeMillis() - debug_time));
//alertDialog.setItems(items, new DialogInterface.OnClickListener() {
alertDialog.setSingleChoiceItems(items, selected, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( MyDebug.LOG )
Log.d(TAG, "selected: " + which);
int logical_camera = items_logical_camera_id[which];
String physical_camera = items_physical_camera_id[which];
if( MyDebug.LOG ) {
Log.d(TAG, "logical_camera: " + logical_camera);
Log.d(TAG, "physical_camera: " + physical_camera);
}
int n_cameras = preview.getCameraControllerManager().getNumberOfCameras();
if( logical_camera >= 0 && logical_camera < n_cameras ) {
if( preview.isOpeningCamera() ) {
if( MyDebug.LOG )
Log.d(TAG, "already opening camera in background thread");
return;
}
MainActivity.this.closePopup();
if( MainActivity.this.preview.canSwitchCamera() ) {
pushCameraIdToast(logical_camera, physical_camera);
userSwitchToCamera(logical_camera, physical_camera);
}
}
//setWindowFlagsForCamera();
//showPreview(true);
dialog.dismiss(); // need to explicitly dismiss for setSingleChoiceItems
}
});
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after setting items: " + (System.currentTimeMillis() - debug_time));
/*alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
setWindowFlagsForCamera();
showPreview(true);
}
});*/
//setWindowFlagsForSettings(false); // set set_lock_protect to false - no need to protect this dialog with lock screen (fine to run above lock screen if that option is set)
//showAlert(alertDialog.create());
AlertDialog dialog = alertDialog.create();
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after dialog create: " + (System.currentTimeMillis() - debug_time));
if( preview.hasPhysicalCameras() ) {
TextView footer = new TextView(this);
footer.setText(R.string.physical_cameras_info);
final float scale = getResources().getDisplayMetrics().density;
final int padding = (int) (5 * scale + 0.5f); // convert dps to pixels
footer.setPadding(padding, padding, padding, padding);
dialog.getListView().addFooterView(footer, null, false);
if( MyDebug.LOG )
Log.d(TAG, "createSwitchMultiCameraDialog: time after adding footer: " + (System.currentTimeMillis() - debug_time));
}
if( dialog.getWindow() != null ) {
dialog.getWindow().setWindowAnimations(R.style.DialogAnimation);
}
return dialog;
}
/** User can long-click on switch multi cam icon to bring up a menu to switch to any camera.
* Update: from v1.53 onwards with support for exposing physical lens, we always call this with
* a regular click on the switch multi cam icon.
*/
public void clickedSwitchMultiCamera(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedSwitchMultiCamera");
long debug_time = 0;
if( MyDebug.LOG ) {
debug_time = System.currentTimeMillis();
}
//showPreview(false);
//AlertDialog dialog = createSwitchMultiCameraDialog();
if( switch_multi_camera_dialog == null ) {
switch_multi_camera_dialog = createSwitchMultiCameraDialog();
}
AlertDialog dialog = switch_multi_camera_dialog;
if( MyDebug.LOG )
Log.d(TAG, "clickedSwitchMultiCamera: time before showing dialog: " + (System.currentTimeMillis() - debug_time));
dialog.show();
if( MyDebug.LOG )
Log.d(TAG, "clickedSwitchMultiCamera: total time: " + (System.currentTimeMillis() - debug_time));
}
/**
* Toggles Photo/Video mode
*/
public void clickedSwitchVideo(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedSwitchVideo");
this.closePopup();
mainUI.destroyPopup(); // important as we don't want to use a cached popup, as we can show different options depending on whether we're in photo or video mode
// In practice stopping the gyro sensor shouldn't be needed as (a) we don't show the switch
// photo/video icon when recording, (b) at the time of writing switching to video mode
// reopens the camera, which will stop panorama recording anyway, but we do this just to be
// safe.
applicationInterface.stopPanorama(true);
View switchVideoButton = findViewById(R.id.switch_video);
switchVideoButton.setEnabled(false); // prevent slowdown if user repeatedly clicks
applicationInterface.reset(false);
this.getApplicationInterface().getDrawPreview().setDimPreview(true);
this.preview.switchVideo(false, true);
switchVideoButton.setEnabled(true);
mainUI.setTakePhotoIcon();
mainUI.setPopupIcon(); // needed as turning to video mode or back can turn flash mode off or back on
// ensure icons invisible if they're affected by being in video mode or not (e.g., on-screen RAW icon)
// (if enabling them, we'll make the icon visible later on)
checkDisableGUIIcons();
if( !block_startup_toast ) {
this.showPhotoVideoToast(true);
}
}
public void clickedWhiteBalanceLock(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedWhiteBalanceLock");
this.preview.toggleWhiteBalanceLock();
mainUI.updateWhiteBalanceLockIcon();
preview.showToast(white_balance_lock_toast, preview.isWhiteBalanceLocked() ? R.string.white_balance_locked : R.string.white_balance_unlocked, true);
}
public void clickedExposureLock(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedExposureLock");
this.preview.toggleExposureLock();
mainUI.updateExposureLockIcon();
preview.showToast(exposure_lock_toast, preview.isExposureLocked() ? R.string.exposure_locked : R.string.exposure_unlocked, true);
}
public void clickedExposure(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedExposure");
mainUI.toggleExposureUI();
}
public void clickedSettings(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedSettings");
KeyguardUtils.requireKeyguard(this, this::openSettings);
}
public boolean popupIsOpen() {
return mainUI.popupIsOpen();
}
// for testing
public View getUIButton(String key) {
return mainUI.getUIButton(key);
}
public void closePopup() {
mainUI.closePopup();
}
public Bitmap getPreloadedBitmap(int resource) {
return this.preloaded_bitmap_resources.get(resource);
}
public void clickedPopupSettings(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedPopupSettings");
mainUI.togglePopupSettings();
}
private final PreferencesListener preferencesListener = new PreferencesListener();
/** Keeps track of changes to SharedPreferences.
*/
class PreferencesListener implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "PreferencesListener";
private boolean any_significant_change; // whether any changes that require updateForSettings have been made since startListening()
private boolean any_change; // whether any changes have been made since startListening()
void startListening() {
if( MyDebug.LOG )
Log.d(TAG, "startListening");
any_significant_change = false;
any_change = false;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
// n.b., registerOnSharedPreferenceChangeListener warns that we must keep a reference to the listener (which
// is this class) as long as we want to listen for changes, otherwise the listener may be garbage collected!
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
}
void stopListening() {
if( MyDebug.LOG )
Log.d(TAG, "stopListening");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if( MyDebug.LOG )
Log.d(TAG, "onSharedPreferenceChanged: " + key);
if( key == null ) {
// on Android 11+, when targetting Android 11+, this method is called with key==null
// if preferences are cleared (see testSettings(), or when doing "Reset settings")
return;
}
any_change = true;
switch( key ) {
// we whitelist preferences where we're sure that we don't need to call updateForSettings() if they've changed
//case "preference_face_detection": // need to update camera controller
case "preference_timer":
case "preference_burst_mode":
case "preference_burst_interval":
case "preference_touch_capture":
case "preference_pause_preview":
case "preference_shutter_sound":
case "preference_timer_beep":
case "preference_timer_speak":
case "preference_volume_keys":
//case "preference_audio_control": // need to update the UI
case "preference_audio_noise_control_sensitivity":
//case "preference_enable_remote": // handled below
//case "preference_remote_type":
//case "preference_remote_device_name": // handled below
//case "preference_remote_disconnect_screen_dim":
//case "preference_water_type": // handled below
case "preference_lock_orientation":
//case "preference_save_location": // we could probably whitelist this, but accessed it a lot of places...
case "preference_using_saf":
case "preference_save_photo_prefix":
case "preference_save_video_prefix":
case "preference_save_zulu_time":
case "preference_show_when_locked":
case "preference_startup_focus":
//case "preference_preview_size": // need to update preview
//case "preference_ghost_image": // don't whitelist this, as may need to reload ghost image (at fullscreen resolution) if "last" is enabled
case "ghost_image_alpha":
case "preference_focus_assist":
case "preference_show_zoom":
case "preference_show_angle":
case "preference_show_angle_line":
case "preference_show_pitch_lines":
case "preference_angle_highlight_color":
//case "preference_show_geo_direction": // don't whitelist these, as if enabled we need to call checkMagneticAccuracy()
//case "preference_show_geo_direction_lines": // as above
case "preference_show_battery":
case "preference_show_time":
case "preference_free_memory":
case "preference_show_iso":
case "preference_histogram":
case "preference_zebra_stripes":
case "preference_zebra_stripes_foreground_color":
case "preference_zebra_stripes_background_color":
case "preference_focus_peaking":
case "preference_focus_peaking_color":
case "preference_show_video_max_amp":
case "preference_grid":
case "preference_crop_guide":
case "preference_thumbnail_animation":
case "preference_take_photo_border":
//case "preference_rotate_preview": // need to update the Preview
//case "preference_ui_placement": // need to update the UI
//case "preference_immersive_mode": // probably could whitelist?
//case "preference_show_face_detection": // need to update the UI
//case "preference_show_cycle_flash": // need to update the UI
//case "preference_show_auto_level": // need to update the UI
//case "preference_show_stamp": // need to update the UI
//case "preference_show_textstamp": // need to update the UI
//case "preference_show_store_location": // need to update the UI
//case "preference_show_cycle_raw": // need to update the UI
//case "preference_show_white_balance_lock": // need to update the UI
//case "preference_show_exposure_lock": // need to update the UI
//case "preference_show_zoom_slider_controls": // need to update the UI
//case "preference_show_take_photo": // need to update the UI
case "preference_show_toasts":
case "preference_show_whats_new":
//case "preference_multi_cam_button": // need to update the UI
case "preference_keep_display_on":
case "preference_max_brightness":
//case "preference_resolution": // need to set up camera controller and preview
//case "preference_quality": // need to set up camera controller
//case "preference_image_format": // need to set up camera controller (as it can affect the image quality that we set)
//case "preference_raw": // need to update as it affects how we set up camera controller
//case "preference_raw_expo_bracketing": // as above
//case "preference_raw_focus_bracketing": // as above
//case "preference_nr_save": // we could probably whitelist this, but have not done so in case in future we allow RAW to be saved for the base image
//case "preference_hdr_save_expo": // we need to update if this is changed, as it affects whether we request RAW or not in HDR mode when RAW is enabled
case "preference_hdr_tonemapping":
case "preference_hdr_contrast_enhancement":
//case "preference_expo_bracketing_n_images": // need to set up camera controller
//case "preference_expo_bracketing_stops": // need to set up camera controller
case "preference_panorama_crop":
//case "preference_panorama_save": // we could probably whitelist this, but have not done so in case in future we allow RAW to be saved for the base images
case "preference_front_camera_mirror":
case "preference_exif_artist":
case "preference_exif_copyright":
case "preference_stamp":
case "preference_stamp_dateformat":
case "preference_stamp_timeformat":
case "preference_stamp_gpsformat":
case "preference_stamp_geo_address":
case "preference_units_distance":
case "preference_textstamp":
case "preference_stamp_fontsize":
case "preference_stamp_font_color":
case "preference_stamp_style":
//case "preference_camera2_fake_flash": // need to update camera controller
//case "preference_camera2_fast_burst": // could probably whitelist?
//case "preference_camera2_photo_video_recording": // need to update camera controller
case "preference_background_photo_saving":
//case "preference_video_quality": // need to update camera controller and preview
//case "preference_video_stabilization": // need to update camera controller
//case "preference_video_output_format": // could probably whitelist, but safest to restart camera
//case "preference_video_log": // need to update camera controller
//case "preference_video_profile_gamma": // as above
//case "preference_video_max_duration": // could probably whitelist, but safest to restart camera
//case "preference_video_restart": // could probably whitelist, but safest to restart camera
//case "preference_video_max_filesize": // could probably whitelist, but safest to restart camera
//case "preference_video_restart_max_filesize": // could probably whitelist, but safest to restart camera
case "preference_record_audio":
case "preference_record_audio_src":
case "preference_record_audio_channels":
case "preference_lock_video":
case "preference_video_subtitle":
//case "preference_video_bitrate": // could probably whitelist, but safest to restart camera
//case "preference_video_fps": // could probably whitelist, but safest to restart camera
//case "preference_force_video_4k": // could probably whitelist, but safest to restart camera
case "preference_video_low_power_check":
case "preference_video_flash":
//case "preference_location": // need to enable/disable gps listeners etc
//case "preference_gps_direction": // need to update listeners
case "preference_require_location":
//case "preference_antibanding": // need to set up camera controller
//case "preference_edge_mode": // need to set up camera controller
//case "preference_noise_reduction_mode": // need to set up camera controller
//case "preference_camera_api": // no point whitelisting as we restart anyway
if( MyDebug.LOG )
Log.d(TAG, "this change doesn't require update");
break;
case PreferenceKeys.EnableRemote:
bluetoothRemoteControl.startRemoteControl();
break;
case PreferenceKeys.RemoteName:
// The remote address changed, restart the service
if (bluetoothRemoteControl.remoteEnabled())
bluetoothRemoteControl.stopRemoteControl();
bluetoothRemoteControl.startRemoteControl();
break;
case PreferenceKeys.WaterType:
boolean wt = sharedPreferences.getBoolean(PreferenceKeys.WaterType, true);
mWaterDensity = wt ? WATER_DENSITY_SALTWATER : WATER_DENSITY_FRESHWATER;
break;
default:
if( MyDebug.LOG )
Log.d(TAG, "this change does require update");
any_significant_change = true;
break;
}
}
boolean anyChange() {
return any_change;
}
boolean anySignificantChange() {
return any_significant_change;
}
}
public void openSettings() {
if( MyDebug.LOG )
Log.d(TAG, "openSettings");
closePopup(); // important to close the popup to avoid confusing with back button callbacks
preview.cancelTimer(); // best to cancel any timer, in case we take a photo while settings window is open, or when changing settings
preview.cancelRepeat(); // similarly cancel the auto-repeat mode!
preview.stopVideo(false); // important to stop video, as we'll be changing camera parameters when the settings window closes
applicationInterface.stopPanorama(true); // important to stop panorama recording, as we might end up as we'll be changing camera parameters when the settings window closes
stopAudioListeners();
// close back handler callbacks (so back button is enabled again when going to settings) - in theory shouldn't be needed as all of these should
// be disabled now, but just in case:
this.enablePopupOnBackPressedCallback(false);
this.enablePausePreviewOnBackPressedCallback(false);
this.enableScreenLockOnBackPressedCallback(false);
Bundle bundle = new Bundle();
bundle.putBoolean("edge_to_edge_mode", edge_to_edge_mode);
bundle.putInt("cameraId", this.preview.getCameraId());
bundle.putString("cameraIdSPhysical", this.applicationInterface.getCameraIdSPhysicalPref());
bundle.putInt("nCameras", preview.getCameraControllerManager().getNumberOfCameras());
bundle.putBoolean("camera_open", this.preview.getCameraController() != null);
bundle.putString("camera_api", this.preview.getCameraAPI());
bundle.putBoolean("using_android_l", this.preview.usingCamera2API());
if( this.preview.getCameraController() != null ) {
bundle.putInt("camera_orientation", this.preview.getCameraController().getCameraOrientation());
}
bundle.putString("photo_mode_string", getPhotoModeString(applicationInterface.getPhotoMode(), true));
bundle.putBoolean("supports_auto_stabilise", this.supports_auto_stabilise);
bundle.putBoolean("supports_flash", this.preview.supportsFlash());
bundle.putBoolean("supports_force_video_4k", this.supports_force_video_4k);
bundle.putBoolean("supports_camera2", this.supports_camera2);
bundle.putBoolean("supports_face_detection", this.preview.supportsFaceDetection());
bundle.putBoolean("supports_jpeg_r", this.preview.supportsJpegR());
bundle.putBoolean("supports_raw", this.preview.supportsRaw());
bundle.putBoolean("supports_burst_raw", this.supportsBurstRaw());
bundle.putBoolean("supports_optimise_focus_latency", this.supportsOptimiseFocusLatency());
bundle.putBoolean("supports_preshots", this.supportsPreShots());
bundle.putBoolean("supports_hdr", this.supportsHDR());
bundle.putBoolean("supports_nr", this.supportsNoiseReduction());
bundle.putBoolean("supports_panorama", this.supportsPanorama());
bundle.putBoolean("has_gyro_sensors", applicationInterface.getGyroSensor().hasSensors());
bundle.putBoolean("supports_expo_bracketing", this.supportsExpoBracketing());
bundle.putBoolean("supports_preview_bitmaps", this.supportsPreviewBitmaps());
bundle.putInt("max_expo_bracketing_n_images", this.maxExpoBracketingNImages());
bundle.putBoolean("supports_exposure_compensation", this.preview.supportsExposures());
bundle.putInt("exposure_compensation_min", this.preview.getMinimumExposure());
bundle.putInt("exposure_compensation_max", this.preview.getMaximumExposure());
bundle.putBoolean("supports_iso_range", this.preview.supportsISORange());
bundle.putInt("iso_range_min", this.preview.getMinimumISO());
bundle.putInt("iso_range_max", this.preview.getMaximumISO());
bundle.putBoolean("supports_exposure_time", this.preview.supportsExposureTime());
bundle.putBoolean("supports_exposure_lock", this.preview.supportsExposureLock());
bundle.putBoolean("supports_white_balance_lock", this.preview.supportsWhiteBalanceLock());
bundle.putLong("exposure_time_min", this.preview.getMinimumExposureTime());
bundle.putLong("exposure_time_max", this.preview.getMaximumExposureTime());
bundle.putBoolean("supports_white_balance_temperature", this.preview.supportsWhiteBalanceTemperature());
bundle.putInt("white_balance_temperature_min", this.preview.getMinimumWhiteBalanceTemperature());
bundle.putInt("white_balance_temperature_max", this.preview.getMaximumWhiteBalanceTemperature());
bundle.putBoolean("is_multi_cam", this.is_multi_cam);
bundle.putBoolean("has_physical_cameras", this.preview.hasPhysicalCameras());
bundle.putBoolean("supports_optical_stabilization", this.preview.supportsOpticalStabilization());
bundle.putBoolean("optical_stabilization_enabled", this.preview.getOpticalStabilization());
bundle.putBoolean("supports_video_stabilization", this.preview.supportsVideoStabilization());
bundle.putBoolean("video_stabilization_enabled", this.preview.getVideoStabilization());
bundle.putBoolean("can_disable_shutter_sound", this.preview.canDisableShutterSound());
bundle.putInt("tonemap_max_curve_points", this.preview.getTonemapMaxCurvePoints());
bundle.putBoolean("supports_tonemap_curve", this.preview.supportsTonemapCurve());
bundle.putBoolean("supports_photo_video_recording", this.preview.supportsPhotoVideoRecording());
bundle.putFloat("camera_view_angle_x", preview.getViewAngleX(false));
bundle.putFloat("camera_view_angle_y", preview.getViewAngleY(false));
bundle.putFloat("min_zoom_factor", preview.getMinZoomRatio());
bundle.putFloat("max_zoom_factor", preview.getMaxZoomRatio());
putBundleExtra(bundle, "color_effects", this.preview.getSupportedColorEffects());
putBundleExtra(bundle, "scene_modes", this.preview.getSupportedSceneModes());
putBundleExtra(bundle, "white_balances", this.preview.getSupportedWhiteBalances());
putBundleExtra(bundle, "isos", this.preview.getSupportedISOs());
bundle.putInt("magnetic_accuracy", magneticSensor.getMagneticAccuracy());
bundle.putString("iso_key", this.preview.getISOKey());
if( this.preview.getCameraController() != null ) {
bundle.putString("parameters_string", preview.getCameraController().getParametersString());
}
List<String> antibanding = this.preview.getSupportedAntiBanding();
putBundleExtra(bundle, "antibanding", antibanding);
if( antibanding != null ) {
String [] entries_arr = new String[antibanding.size()];
int i=0;
for(String value: antibanding) {
entries_arr[i] = getMainUI().getEntryForAntiBanding(value);
i++;
}
bundle.putStringArray("antibanding_entries", entries_arr);
}
List<String> edge_modes = this.preview.getSupportedEdgeModes();
putBundleExtra(bundle, "edge_modes", edge_modes);
if( edge_modes != null ) {
String [] entries_arr = new String[edge_modes.size()];
int i=0;
for(String value: edge_modes) {
entries_arr[i] = getMainUI().getEntryForNoiseReductionMode(value);
i++;
}
bundle.putStringArray("edge_modes_entries", entries_arr);
}
List<String> noise_reduction_modes = this.preview.getSupportedNoiseReductionModes();
putBundleExtra(bundle, "noise_reduction_modes", noise_reduction_modes);
if( noise_reduction_modes != null ) {
String [] entries_arr = new String[noise_reduction_modes.size()];
int i=0;
for(String value: noise_reduction_modes) {
entries_arr[i] = getMainUI().getEntryForNoiseReductionMode(value);
i++;
}
bundle.putStringArray("noise_reduction_modes_entries", entries_arr);
}
List<CameraController.Size> preview_sizes = this.preview.getSupportedPreviewSizes();
if( preview_sizes != null ) {
int [] widths = new int[preview_sizes.size()];
int [] heights = new int[preview_sizes.size()];
int i=0;
for(CameraController.Size size: preview_sizes) {
widths[i] = size.width;
heights[i] = size.height;
i++;
}
bundle.putIntArray("preview_widths", widths);
bundle.putIntArray("preview_heights", heights);
}
bundle.putInt("preview_width", preview.getCurrentPreviewSize().width);
bundle.putInt("preview_height", preview.getCurrentPreviewSize().height);
// Note that we set check_burst to false, as the Settings always displays all supported resolutions (along with the "saved"
// resolution preference, even if that doesn't support burst and we're in a burst mode).
// This is to be consistent with other preferences, e.g., we still show RAW settings even though that might not be supported
// for the current photo mode.
List<CameraController.Size> sizes = this.preview.getSupportedPictureSizes(false);
if( sizes != null ) {
int [] widths = new int[sizes.size()];
int [] heights = new int[sizes.size()];
boolean [] supports_burst = new boolean[sizes.size()];
int i=0;
for(CameraController.Size size: sizes) {
widths[i] = size.width;
heights[i] = size.height;
supports_burst[i] = size.supports_burst;
i++;
}
bundle.putIntArray("resolution_widths", widths);
bundle.putIntArray("resolution_heights", heights);
bundle.putBooleanArray("resolution_supports_burst", supports_burst);
}
if( preview.getCurrentPictureSize() != null ) {
bundle.putInt("resolution_width", preview.getCurrentPictureSize().width);
bundle.putInt("resolution_height", preview.getCurrentPictureSize().height);
}
//List<String> video_quality = this.preview.getVideoQualityHander().getSupportedVideoQuality();
String fps_value = applicationInterface.getVideoFPSPref(); // n.b., this takes into account slow motion mode putting us into a high frame rate
if( MyDebug.LOG )
Log.d(TAG, "fps_value: " + fps_value);
List<String> video_quality = this.preview.getSupportedVideoQuality(fps_value);
if( video_quality == null || video_quality.isEmpty() ) {
Log.e(TAG, "can't find any supported video sizes for current fps!");
// fall back to unfiltered list
video_quality = this.preview.getVideoQualityHander().getSupportedVideoQuality();
}
if( video_quality != null && this.preview.getCameraController() != null ) {
String [] video_quality_arr = new String[video_quality.size()];
String [] video_quality_string_arr = new String[video_quality.size()];
int i=0;
for(String value: video_quality) {
video_quality_arr[i] = value;
video_quality_string_arr[i] = this.preview.getCamcorderProfileDescription(value);
i++;
}
bundle.putStringArray("video_quality", video_quality_arr);
bundle.putStringArray("video_quality_string", video_quality_string_arr);
boolean is_high_speed = this.preview.fpsIsHighSpeed(fps_value);
bundle.putBoolean("video_is_high_speed", is_high_speed);
String video_quality_preference_key = PreferenceKeys.getVideoQualityPreferenceKey(this.preview.getCameraId(), applicationInterface.getCameraIdSPhysicalPref(), is_high_speed);
if( MyDebug.LOG )
Log.d(TAG, "video_quality_preference_key: " + video_quality_preference_key);
bundle.putString("video_quality_preference_key", video_quality_preference_key);
}
if( preview.getVideoQualityHander().getCurrentVideoQuality() != null ) {
bundle.putString("current_video_quality", preview.getVideoQualityHander().getCurrentVideoQuality());
}
VideoProfile camcorder_profile = preview.getVideoProfile();
bundle.putInt("video_frame_width", camcorder_profile.videoFrameWidth);
bundle.putInt("video_frame_height", camcorder_profile.videoFrameHeight);
bundle.putInt("video_bit_rate", camcorder_profile.videoBitRate);
bundle.putInt("video_frame_rate", camcorder_profile.videoFrameRate);
bundle.putDouble("video_capture_rate", camcorder_profile.videoCaptureRate);
bundle.putBoolean("video_high_speed", preview.isVideoHighSpeed());
bundle.putFloat("video_capture_rate_factor", applicationInterface.getVideoCaptureRateFactor());
List<CameraController.Size> video_sizes = this.preview.getVideoQualityHander().getSupportedVideoSizes();
if( video_sizes != null ) {
int [] widths = new int[video_sizes.size()];
int [] heights = new int[video_sizes.size()];
int i=0;
for(CameraController.Size size: video_sizes) {
widths[i] = size.width;
heights[i] = size.height;
i++;
}
bundle.putIntArray("video_widths", widths);
bundle.putIntArray("video_heights", heights);
}
// set up supported fps values
if( preview.usingCamera2API() ) {
// with Camera2, we know what frame rates are supported
int [] candidate_fps = {15, 24, 25, 30, 60, 96, 100, 120, 240};
List<Integer> video_fps = new ArrayList<>();
List<Boolean> video_fps_high_speed = new ArrayList<>();
for(int fps : candidate_fps) {
if( preview.fpsIsHighSpeed(String.valueOf(fps)) ) {
video_fps.add(fps);
video_fps_high_speed.add(true);
}
else if( this.preview.getVideoQualityHander().videoSupportsFrameRate(fps) ) {
video_fps.add(fps);
video_fps_high_speed.add(false);
}
}
int [] video_fps_array = new int[video_fps.size()];
for(int i=0;i<video_fps.size();i++) {
video_fps_array[i] = video_fps.get(i);
}
bundle.putIntArray("video_fps", video_fps_array);
boolean [] video_fps_high_speed_array = new boolean[video_fps_high_speed.size()];
for(int i=0;i<video_fps_high_speed.size();i++) {
video_fps_high_speed_array[i] = video_fps_high_speed.get(i);
}
bundle.putBooleanArray("video_fps_high_speed", video_fps_high_speed_array);
}
else {
// with old API, we don't know what frame rates are supported, so we make it up and let the user try
// probably shouldn't allow 120fps, but we did in the past, and there may be some devices where this did work?
int [] video_fps = {15, 24, 25, 30, 60, 96, 100, 120};
bundle.putIntArray("video_fps", video_fps);
boolean [] video_fps_high_speed_array = new boolean[video_fps.length];
for(int i=0;i<video_fps.length;i++) {
video_fps_high_speed_array[i] = false; // no concept of high speed frame rates in old API
}
bundle.putBooleanArray("video_fps_high_speed", video_fps_high_speed_array);
}
putBundleExtra(bundle, "flash_values", this.preview.getSupportedFlashValues());
putBundleExtra(bundle, "focus_values", this.preview.getSupportedFocusValues());
preferencesListener.startListening();
showPreview(false);
setWindowFlagsForSettings(); // important to do after passing camera info into bundle, since this will close the camera
MyPreferenceFragment fragment = new MyPreferenceFragment();
fragment.setArguments(bundle);
// use commitAllowingStateLoss() instead of commit(), does to "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState" crash seen on Google Play
// see http://stackoverflow.com/questions/7575921/illegalstateexception-can-not-perform-this-action-after-onsaveinstancestate-wit
getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "PREFERENCE_FRAGMENT").addToBackStack(null).commitAllowingStateLoss();
}
@Override
public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
if( MyDebug.LOG ) {
Log.d(TAG, "onPreferenceStartFragment");
Log.d(TAG, "pref: " + pref.getFragment());
}
// instantiate the new fragment
//final Bundle args = pref.getExtras();
// we want to pass the caller preference fragment's bundle to the new sub-screen (this will be a
// copy of the bundle originally created in openSettings()
final Bundle args = new Bundle(caller.getArguments());
final Fragment fragment = Fragment.instantiate(this, pref.getFragment(), args);
fragment.setTargetFragment(caller, 0);
if( MyDebug.LOG )
Log.d(TAG, "replace fragment");
/*getFragmentManager().beginTransaction()
.replace(R.id.content, fragment)
.addToBackStack(null)
.commit();*/
getFragmentManager().beginTransaction().add(android.R.id.content, fragment, "PREFERENCE_FRAGMENT_"+pref.getFragment()).addToBackStack(null).commitAllowingStateLoss();
/*
// AndroidX version:
final Fragment fragment = getSupportFragmentManager().getFragmentFactory().instantiate(
getClassLoader(),
pref.getFragment());
fragment.setArguments(args);
fragment.setTargetFragment(caller, 0);
// replace the existing fragment with the new fragment:
getSupportFragmentManager().beginTransaction()
.replace(R.id.content, fragment)
.addToBackStack(null)
.commit();
*/
return true;
}
public void updateForSettings(boolean update_camera) {
updateForSettings(update_camera, null);
}
public void updateForSettings(boolean update_camera, String toast_message) {
updateForSettings(update_camera, toast_message, false, false);
}
/** Must be called when an settings (as stored in SharedPreferences) are made, so we can update the
* camera, and make any other necessary changes.
* @param update_camera Whether the camera needs to be updated. Can be set to false if we know changes
* haven't been made to the camera settings, or we already reopened it.
* @param toast_message If non-null, display this toast instead of the usual camera "startup" toast
* that's shown in showPhotoVideoToast(). If non-null but an empty string, then
* this means no toast is shown at all.
* @param keep_popup If false, the popup will be closed and destroyed. Set to true if you're sure
* that the changed setting isn't one that requires the PopupView to be recreated
* @param allow_dim If true, for Camera2 API a dimming effect will be applied if updating the
* camera.
*/
public void updateForSettings(boolean update_camera, String toast_message, boolean keep_popup, boolean allow_dim) {
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings()");
if( toast_message != null ) {
Log.d(TAG, "toast_message: " + toast_message);
}
}
long debug_time = 0;
if( MyDebug.LOG ) {
debug_time = System.currentTimeMillis();
}
// make sure we're into continuous video mode
// workaround for bug on Samsung Galaxy S5 with UHD, where if the user switches to another (non-continuous-video) focus mode, then goes to Settings, then returns and records video, the preview freezes and the video is corrupted
// so to be safe, we always reset to continuous video mode, and then reset it afterwards
/*String saved_focus_value = preview.updateFocusForVideo(); // n.b., may be null if focus mode not changed
if( MyDebug.LOG )
Log.d(TAG, "saved_focus_value: " + saved_focus_value);*/
if( MyDebug.LOG )
Log.d(TAG, "update folder history");
save_location_history.updateFolderHistory(getStorageUtils().getSaveLocation(), true); // this also updates the last icon for ghost image, if that pref has changed
// no need to update save_location_history_saf, as we always do this in onActivityResult()
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after update folder history: " + (System.currentTimeMillis() - debug_time));
}
imageQueueChanged(); // needed at least for changing photo mode, but might as well call it always
if( !keep_popup ) {
mainUI.destroyPopup(); // important as we don't want to use a cached popup
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after destroy popup: " + (System.currentTimeMillis() - debug_time));
}
}
// update camera for changes made in prefs - do this without closing and reopening the camera app if possible for speed!
// but need workaround for Nexus 7 bug on old camera API, where scene mode doesn't take effect unless the camera is restarted - I can reproduce this with other 3rd party camera apps, so may be a Nexus 7 issue...
// doesn't happen if we allow using Camera2 API on Nexus 7, but reopen for consistency (and changing scene modes via
// popup menu no longer should be calling updateForSettings() for Camera2, anyway)
boolean need_reopen = false;
if( update_camera && preview.getCameraController() != null ) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String scene_mode = preview.getCameraController().getSceneMode();
if( MyDebug.LOG )
Log.d(TAG, "scene mode was: " + scene_mode);
String key = PreferenceKeys.SceneModePreferenceKey;
String value = sharedPreferences.getString(key, CameraController.SCENE_MODE_DEFAULT);
// n.b., on Android 4.3 emulator, scene mode is returned as null (this may be because it doesn't support
// scene modes at all) - treat this the same as auto
if( scene_mode == null )
scene_mode = CameraController.SCENE_MODE_DEFAULT;
if( !value.equals(scene_mode) ) {
if( MyDebug.LOG )
Log.d(TAG, "scene mode changed to: " + value);
need_reopen = true;
}
else {
if( applicationInterface.useCamera2() ) {
// need to reopen if fake flash mode changed, as it changes the available camera features, and we can only set this after opening the camera
boolean camera2_fake_flash = preview.getCameraController().getUseCamera2FakeFlash();
if( MyDebug.LOG )
Log.d(TAG, "camera2_fake_flash was: " + camera2_fake_flash);
if( applicationInterface.useCamera2FakeFlash() != camera2_fake_flash ) {
if( MyDebug.LOG )
Log.d(TAG, "camera2_fake_flash changed");
need_reopen = true;
}
}
}
if( !need_reopen ) {
CameraController.TonemapProfile old_tonemap_profile = preview.getCameraController().getTonemapProfile();
if( old_tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF ) {
CameraController.TonemapProfile new_tonemap_profile = applicationInterface.getVideoTonemapProfile();
if( new_tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && new_tonemap_profile != old_tonemap_profile ) {
// needed for Galaxy S10e when changing from TONEMAP_MODE_CONTRAST_CURVE to TONEMAP_MODE_PRESET_CURVE,
// otherwise the contrast curve remains active!
if( MyDebug.LOG )
Log.d(TAG, "switching between tonemap profiles");
need_reopen = true;
}
}
}
if( !need_reopen ) {
boolean old_is_extension = preview.getCameraController().isCameraExtension();
boolean new_is_extension = applicationInterface.isCameraExtensionPref();
if( old_is_extension || new_is_extension ) {
// At least on Galaxy S10e, we have problems stopping and starting a camera extension session,
// e.g., when changing resolutions whilst in an extension mode (XHDR or bokeh) or switching
// from XHDR to other modes (including non-extension modes like STD). Problems such as preview
// no longer receiving frames, or the call to createExtensionSession() (or createCaptureSession)
// hanging. So therefore we should reopen the camera if at least
// old_is_extension==true.
// This isn't required if old_is_extension==false but new_is_extension==true,
// but we still do so since reopening the camera occurs on a background thread
// (opening an extension session seems to take longer, so better not to block
// the UI thread).
if( MyDebug.LOG )
Log.d(TAG, "need to reopen camera for changes to extension session");
need_reopen = true;
}
}
}
if( MyDebug.LOG ) {
Log.d(TAG, "need_reopen: " + need_reopen);
Log.d(TAG, "updateForSettings: time after check need_reopen: " + (System.currentTimeMillis() - debug_time));
}
mainUI.layoutUI(); // needed in case we've changed UI placement; or in "top" mode, if we've enabled/disabled on-screen UI icons
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after layoutUI: " + (System.currentTimeMillis() - debug_time));
}
// ensure icons invisible if disabling them from showing from the Settings
// (if enabling them, we'll make the icon visible later on)
checkDisableGUIIcons();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String audio_control = sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none");
// better to only display the audio control icon if it matches specific known supported types
// (important now that "voice" is no longer supported)
//if( !audio_control.equals("voice") && !audio_control.equals("noise") ) {
if( !audio_control.equals("noise") ) {
View speechRecognizerButton = findViewById(R.id.audio_control);
speechRecognizerButton.setVisibility(View.GONE);
}
//speechControl.initSpeechRecognizer(); // in case we've enabled or disabled speech recognizer
// we no longer call initLocation() here (for having enabled or disabled geotagging), as that's
// done in setWindowFlagsForCamera() - important not to call it here as well, otherwise if
// permission wasn't granted, we'll ask for permission twice in a row (on Android 9 or earlier
// at least)
//initLocation(); // in case we've enabled or disabled GPS
initGyroSensors(); // in case we've entered or left panorama
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after init speech and location: " + (System.currentTimeMillis() - debug_time));
}
if( toast_message != null )
block_startup_toast = true;
if( !update_camera ) {
// don't try to update camera
}
else if( preview.isPreviewStarting() ) {
// ideally we should avoid getting into this state - we shouldn't allow changing settings if preview is still opening (e.g., we prevent opening
// the popup menu if preview is not started, and we close camera when going to settings)
Log.e(TAG, "updateForSettings: preview is still starting");
}
else if( need_reopen || preview.getCameraController() == null ) { // if camera couldn't be opened before, might as well try again
if( allow_dim )
applicationInterface.getDrawPreview().setDimPreview(true);
preview.reopenCamera();
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after reopen: " + (System.currentTimeMillis() - debug_time));
}
}
else {
preview.setCameraDisplayOrientation(); // need to call in case the preview rotation option was changed
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after set display orientation: " + (System.currentTimeMillis() - debug_time));
}
if( allow_dim )
applicationInterface.getDrawPreview().setDimPreview(true);
preview.pausePreview(true);
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: time after pause: " + (System.currentTimeMillis() - debug_time));
}
Handler handler = new Handler();
// We run setupCamera on the UI thread, but we do it on a post-delayed so that the dimming effect (for Camera2 API) has a chance to run.
// Even if allow_dim==false, still run as a postDelayed (a) for consistency, (b) to allow UI to run for a bit (to avoid risk of slow frames).
handler.postDelayed(new Runnable() {
public void run() {
// if we ever support calling this with wait_until_started==true, beware of trying to change resolution repeatedly when the popup menu
// is open - as currently we'll have problems if trying to calling updateForSettings when preview is already starting on background
// thread (see above)
preview.setupCamera(false, true, null);
}
}, DrawPreview.dim_effect_time_c+16); // +16 to allow time for a frame update to run
}
// don't set block_startup_toast to false yet, as camera might be closing/opening on background thread
if( toast_message != null && !toast_message.isEmpty() )
preview.showToast(null, toast_message, true);
// don't need to reset to saved_focus_value, as we'll have done this when setting up the camera (or will do so when the camera is reopened, if need_reopen)
/*if( saved_focus_value != null ) {
if( MyDebug.LOG )
Log.d(TAG, "switch focus back to: " + saved_focus_value);
preview.updateFocus(saved_focus_value, true, false);
}*/
magneticSensor.registerMagneticListener(mSensorManager); // check whether we need to register or unregister the magnetic listener
magneticSensor.checkMagneticAccuracy();
if( MyDebug.LOG ) {
Log.d(TAG, "updateForSettings: done: " + (System.currentTimeMillis() - debug_time));
}
}
/** Disables the optional on-screen icons if either user doesn't want to enable them, or not
* supported). Note that displaying icons is done via MainUI.showGUI.
* @return Whether an icon's visibility was changed.
*/
private boolean checkDisableGUIIcons() {
if( MyDebug.LOG )
Log.d(TAG, "checkDisableGUIIcons");
boolean changed = false;
if( !supportsExposureButton() ) {
View button = findViewById(R.id.exposure);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showExposureLockIcon() ) {
View button = findViewById(R.id.exposure_lock);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showWhiteBalanceLockIcon() ) {
View button = findViewById(R.id.white_balance_lock);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showCycleRawIcon() ) {
View button = findViewById(R.id.cycle_raw);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showStoreLocationIcon() ) {
View button = findViewById(R.id.store_location);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showTextStampIcon() ) {
View button = findViewById(R.id.text_stamp);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showStampIcon() ) {
View button = findViewById(R.id.stamp);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showFocusPeakingIcon() ) {
View button = findViewById(R.id.focus_peaking);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showAutoLevelIcon() ) {
View button = findViewById(R.id.auto_level);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showCycleFlashIcon() ) {
View button = findViewById(R.id.cycle_flash);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !mainUI.showFaceDetectionIcon() ) {
View button = findViewById(R.id.face_detection);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( !showSwitchMultiCamIcon() ) {
// also handle the multi-cam icon here, as this can change when switching between front/back cameras
// (e.g., if say a device only has multiple back cameras)
View button = findViewById(R.id.switch_multi_camera);
changed = changed || (button.getVisibility() != View.GONE);
button.setVisibility(View.GONE);
}
if( MyDebug.LOG )
Log.d(TAG, "checkDisableGUIIcons: " + changed);
return changed;
}
public MyPreferenceFragment getPreferenceFragment() {
return (MyPreferenceFragment)getFragmentManager().findFragmentByTag("PREFERENCE_FRAGMENT");
}
private boolean settingsIsOpen() {
return getPreferenceFragment() != null;
}
/** Call when the settings is going to be closed.
*/
void settingsClosing() {
if( MyDebug.LOG )
Log.d(TAG, "close settings");
setWindowFlagsForCamera();
showPreview(true);
preferencesListener.stopListening();
// Update the cached settings in DrawPreview
// Note that some GUI related settings won't trigger preferencesListener.anyChange(), so
// we always call this. Perhaps we could add more classifications to PreferencesListener
// to mark settings that need us to update DrawPreview but not call updateForSettings().
// However, DrawPreview.updateSettings() should be a quick function (the main point is
// to avoid reading the preferences in every single frame).
applicationInterface.getDrawPreview().updateSettings();
// Set the flag to cover the preview until the camera is open and receiving frames again
// (for Camera2 API) - avoids showing a flash of the preview from before the user went to
// the settings.
applicationInterface.getDrawPreview().setCoverPreview(true);
if( preferencesListener.anyChange() ) {
mainUI.updateOnScreenIcons();
}
if( preferencesListener.anySignificantChange() ) {
// don't need to update camera, as we now pause/resume camera when going to settings
updateForSettings(false);
}
else {
if( MyDebug.LOG )
Log.d(TAG, "no need to call updateForSettings() for changes made to preferences");
if( preferencesListener.anyChange() ) {
// however we should still destroy cached popup, in case UI settings need to be kept in
// sync (e.g., changing the Repeat Mode)
mainUI.destroyPopup();
}
}
}
private PopupOnBackPressedCallback popupOnBackPressedCallback;
private class PopupOnBackPressedCallback extends OnBackPressedCallback {
public PopupOnBackPressedCallback(boolean enabled) {
super(enabled);
}
@Override
public void handleOnBackPressed() {
if( MyDebug.LOG )
Log.d(TAG, "PopupOnBackPressedCallback.handleOnBackPressed");
if( popupIsOpen() ) {
// close popup will disable the PopupOnBackPressedCallback, so no need to do it here
closePopup();
}
else {
// shouldn't be here (if popup isn't open, this callback shouldn't be enabled), but just in case
if( MyDebug.LOG )
Log.e(TAG, "PopupOnBackPressedCallback was enabled but popup menu not open?!");
this.setEnabled(false);
MainActivity.this.onBackPressed();
}
}
}
public void enablePopupOnBackPressedCallback(boolean enabled) {
if( MyDebug.LOG )
Log.d(TAG, "enablePopupOnBackPressedCallback: " + enabled);
if( popupOnBackPressedCallback != null ) {
popupOnBackPressedCallback.setEnabled(enabled);
}
}
private PausePreviewOnBackPressedCallback pausePreviewOnBackPressedCallback;
private class PausePreviewOnBackPressedCallback extends OnBackPressedCallback {
public PausePreviewOnBackPressedCallback(boolean enabled) {
super(enabled);
}
@Override
public void handleOnBackPressed() {
if( MyDebug.LOG )
Log.d(TAG, "PausePreviewOnBackPressedCallback.handleOnBackPressed");
if( preview != null && preview.isPreviewPaused() ) {
// starting the preview will disable the PausePreviewOnBackPressedCallback, so no need to do it here
if( MyDebug.LOG )
Log.d(TAG, "preview was paused, so unpause it");
preview.startCameraPreview(true, null);
}
else {
// shouldn't be here (if preview isn't paused, this callback shouldn't be enabled), but just in case
if( MyDebug.LOG )
Log.e(TAG, "PausePreviewOnBackPressedCallback was enabled but preview not paused?!");
this.setEnabled(false);
MainActivity.this.onBackPressed();
}
}
}
public void enablePausePreviewOnBackPressedCallback(boolean enabled) {
if( MyDebug.LOG )
Log.d(TAG, "enablePausePreviewOnBackPressedCallback: " + enabled);
if( pausePreviewOnBackPressedCallback != null ) {
pausePreviewOnBackPressedCallback.setEnabled(enabled);
}
}
private ScreenLockOnBackPressedCallback screenLockOnBackPressedCallback;
private class ScreenLockOnBackPressedCallback extends OnBackPressedCallback {
public ScreenLockOnBackPressedCallback(boolean enabled) {
super(enabled);
}
@Override
public void handleOnBackPressed() {
if( MyDebug.LOG )
Log.d(TAG, "ScreenLockOnBackPressedCallback.handleOnBackPressed");
if( screen_is_locked ) {
preview.showToast(screen_locked_toast, R.string.screen_is_locked);
}
else {
// shouldn't be here (if screen isn't locked, this callback shouldn't be enabled), but just in case
if( MyDebug.LOG )
Log.e(TAG, "ScreenLockOnBackPressedCallback was enabled but screen isn't locked?!");
this.setEnabled(false);
MainActivity.this.onBackPressed();
}
}
}
private void enableScreenLockOnBackPressedCallback(boolean enabled) {
if( MyDebug.LOG )
Log.d(TAG, "enableScreenLockOnBackPressedCallback: " + enabled);
if( screenLockOnBackPressedCallback != null ) {
screenLockOnBackPressedCallback.setEnabled(enabled);
}
}
// should no longer use onBackPressed() - instead use OnBackPressedCallback, for upcoming changes in Android 14+ (predictive back gestures)
/*@Override
public void onBackPressed() {
if( MyDebug.LOG )
Log.d(TAG, "onBackPressed");
if( screen_is_locked ) {
preview.showToast(screen_locked_toast, R.string.screen_is_locked);
return;
}
if( settingsIsOpen() ) {
settingsClosing();
}
else if( preview != null && preview.isPreviewPaused() ) {
if( MyDebug.LOG )
Log.d(TAG, "preview was paused, so unpause it");
preview.startCameraPreview();
return;
}
else {
if( popupIsOpen() ) {
closePopup();
return;
}
}
super.onBackPressed();
}*/
/** Returns whether we are always running in edge-to-edge mode. (If false, we may still sometimes
* run edge-to-edge.)
*/
public boolean getEdgeToEdgeMode() {
return this.edge_to_edge_mode;
}
/** Whether to allow the application to show under the navigation bar, or not.
* Arguably we could enable this all the time, but in practice we only enable for cases when
* want_no_limits==true and navigation_gap!=0 (if want_no_limits==false, there's no need to
* show under the navigation bar; if navigation_gap==0, there is no navigation bar).
*/
private void showUnderNavigation(boolean enable) {
if( MyDebug.LOG )
Log.d(TAG, "showUnderNavigation: " + enable);
if( edge_to_edge_mode ) {
// we are already always in edge-to-edge mode
return;
}
{
// We used to use window flag FLAG_LAYOUT_NO_LIMITS, but this didn't work properly on
// Android 11 (didn't take effect until orientation changed or application paused/resumed).
// Although system ui visibility flags are deprecated on Android 11, this still works better
// than the FLAG_LAYOUT_NO_LIMITS flag (which was not well documented anyway).
// Update, now using WindowCompat.setDecorFitsSystemWindows. This is non-deprecated, and
// documented at https://developer.android.com/develop/ui/views/layout/edge-to-edge-manually .
/*int flags = getWindow().getDecorView().getSystemUiVisibility();
if( enable ) {
getWindow().getDecorView().setSystemUiVisibility(flags | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
else {
getWindow().getDecorView().setSystemUiVisibility(flags & ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}*/
test_set_show_under_navigation = enable;
// in theory the VANILLA_ICE_CREAM is redundant as we shouldn't be here on Android 15+ anyway (since edge_to_edge_mode==true), but
// wrapping in case this helps Google Play recommendation to avoid deprecated APIs for edge-to-edge
if( Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM ) {
WindowCompat.setDecorFitsSystemWindows(getWindow(), !enable);
}
}
// in theory the VANILLA_ICE_CREAM is redundant as we shouldn't be here on Android 15+ anyway (since edge_to_edge_mode==true), but
// wrapping in case this helps Google Play recommendation to avoid deprecated APIs for edge-to-edge
if( Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM ) {
getWindow().setNavigationBarColor(enable ? Color.TRANSPARENT : Color.BLACK);
}
}
public int getNavigationGap() {
return (want_no_limits || edge_to_edge_mode) ? navigation_gap : 0;
}
public int getNavigationGapLandscape() {
return edge_to_edge_mode ? navigation_gap_landscape : 0;
}
public int getNavigationGapReverseLandscape() {
return edge_to_edge_mode ? navigation_gap_reverse_landscape : 0;
}
/** The system is now such that we have entered or exited immersive mode. If visible is true,
* system UI is now visible such that we should exit immersive mode. If visible is false, the
* system has entered immersive mode.
*/
private void immersiveModeChanged(boolean visible) {
if( MyDebug.LOG )
Log.d(TAG, "immersiveModeChanged: " + visible);
if( !usingKitKatImmersiveMode() )
return;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_off");
boolean hide_ui = immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything");
if( visible ) {
if( MyDebug.LOG )
Log.d(TAG, "system bars now visible");
// change UI due to having exited immersive mode
if( hide_ui )
mainUI.setImmersiveMode(false);
setImmersiveTimer();
}
else {
if( MyDebug.LOG )
Log.d(TAG, "system bars now NOT visible");
// change UI due to having entered immersive mode
if( hide_ui )
mainUI.setImmersiveMode(true);
}
}
/** Set up listener to handle listening for system ui changes (for immersive mode), and setting
* a WindowsInsetsListener to find the navigation_gap.
*/
private void setupSystemUiVisibilityListener() {
View decorView = getWindow().getDecorView();
{
// set a window insets listener to find the navigation_gap
if( MyDebug.LOG )
Log.d(TAG, "set a window insets listener");
this.set_window_insets_listener = true;
decorView.getRootView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
private boolean has_last_system_orientation;
private SystemOrientation last_system_orientation;
@Override
public @NonNull WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets windowInsets) {
if( MyDebug.LOG )
Log.d(TAG, "onApplyWindowInsets");
int inset_left;
int inset_top;
int inset_right;
int inset_bottom;
if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) {
// take opportunity to use non-deprecated versions; also for edge_to_edge_mode==true, we need to use getInsetsIgnoringVisibility for
// immersive mode (since for edge_to_edge_mode==true, we are not using setSystemUiVisibility() / SYSTEM_UI_FLAG_LAYOUT_STABLE in setImmersiveMode())
// also compare with MyApplicationInterface.getDisplaySize() - in particular we don't care about caption/system bar that is returned on e.g.
// OnePlus Pad for insets.top when in landscape orientation (since the system bar isn't shown); however we also need to subtract any from the cutout -
// since this code is for finding what margins we need to set to avoid navigation bars; avoiding the cutout is done below for the entire
// Open Camera view
Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout());
Insets cutout_insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.displayCutout());
if( test_force_window_insets ) {
insets = test_insets;
cutout_insets = test_cutout_insets;
}
inset_left = insets.left - cutout_insets.left;
inset_top = insets.top - cutout_insets.top;
inset_right = insets.right - cutout_insets.right;
inset_bottom = insets.bottom - cutout_insets.bottom;
}
else {
inset_left = windowInsets.getSystemWindowInsetLeft();
inset_top = windowInsets.getSystemWindowInsetTop();
inset_right = windowInsets.getSystemWindowInsetRight();
inset_bottom = windowInsets.getSystemWindowInsetBottom();
}
if( MyDebug.LOG ) {
Log.d(TAG, "inset left: " + inset_left);
Log.d(TAG, "inset top: " + inset_top);
Log.d(TAG, "inset right: " + inset_right);
Log.d(TAG, "inset bottom: " + inset_bottom);
}
if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) {
// easier to ensure the entire activity avoids display cutouts - for the preview, we still support
// it showing under the navigation bar
Insets insets = windowInsets.getInsets(WindowInsets.Type.displayCutout());
if( test_force_window_insets ) {
insets = test_cutout_insets;
}
v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
// also handle change of immersive mode (instead of using deprecated setOnSystemUiVisibilityChangeListener below
immersiveModeChanged( windowInsets.isVisible(WindowInsets.Type.navigationBars()) );
}
resetCachedSystemOrientation(); // don't want to get cached result - this can sometimes happen e.g. on Pixel 6 Pro when switching between landscape and reverse landscape
SystemOrientation system_orientation = getSystemOrientation();
int new_navigation_gap, new_navigation_gap_landscape, new_navigation_gap_reverse_landscape;
switch ( system_orientation ) {
case PORTRAIT:
if( MyDebug.LOG )
Log.d(TAG, "portrait");
new_navigation_gap = inset_bottom;
new_navigation_gap_landscape = inset_left;
new_navigation_gap_reverse_landscape = inset_right;
break;
case LANDSCAPE:
if( MyDebug.LOG )
Log.d(TAG, "landscape");
new_navigation_gap = inset_right;
new_navigation_gap_landscape = inset_bottom;
new_navigation_gap_reverse_landscape = inset_top;
break;
case REVERSE_LANDSCAPE:
if( MyDebug.LOG )
Log.d(TAG, "reverse landscape");
new_navigation_gap = inset_left;
new_navigation_gap_landscape = inset_top;
new_navigation_gap_reverse_landscape = inset_bottom;
break;
default:
Log.e(TAG, "unknown system_orientation?!: " + system_orientation);
new_navigation_gap = 0;
new_navigation_gap_landscape = 0;
new_navigation_gap_reverse_landscape = 0;
break;
}
if( !edge_to_edge_mode ) {
// we only care about avoiding a landscape navigation bar (e.g., large tablets in landscape orientation) for edge_to_edge_mode==true
// in theory this could be useful when edge_to_edge_mode==false, but in practice we will never enter edge-to-edge-mode if the
// navigation bar is along the landscape-edge, so restrict behaviour change to edge_to_edge_mode==true
new_navigation_gap_landscape = 0;
new_navigation_gap_reverse_landscape = 0;
}
// for edge_to_edge_mode==false, we only enter this case if system orientation changes, due to issues where this callback may be called first with 0 navigation gap
// (see notes below)
// for edge_to_edge_mode==true, simpler to always react to updated insets - in particular, in split-window mode, the navigation gaps can
// change when device rotates, even though the application remains in the same orientation
if( (edge_to_edge_mode || (has_last_system_orientation && system_orientation != last_system_orientation)) && (new_navigation_gap != navigation_gap || new_navigation_gap_landscape != navigation_gap_landscape || new_navigation_gap_reverse_landscape != navigation_gap_reverse_landscape ) ) {
if( MyDebug.LOG )
Log.d(TAG, "navigation_gap changed from " + navigation_gap + " to " + new_navigation_gap);
navigation_gap = new_navigation_gap;
navigation_gap_landscape = new_navigation_gap_landscape;
navigation_gap_reverse_landscape = new_navigation_gap_reverse_landscape;
if( MyDebug.LOG )
Log.d(TAG, "want_no_limits: " + want_no_limits);
if( want_no_limits || edge_to_edge_mode ) {
// If we want no_limits mode, then need to take care in case of device orientation
// in cases where that changes the navigation_gap:
// - Need to set showUnderNavigation() (in case navigation_gap when from zero to non-zero or vice versa).
// - Need to call layoutUI() (for different value of navigation_gap)
// Need to call showUnderNavigation() from handler for it to take effect.
// Similarly we have problems if we call layoutUI without post-ing it -
// sometimes when rotating a device, we get a call to OnApplyWindowInsetsListener
// with 0 navigation_gap followed by the call with the correct non-zero values -
// posting the call to layoutUI means it runs after the second call, so we have the
// correct navigation_gap.
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
if( MyDebug.LOG )
Log.d(TAG, "runnable for change in navigation_gap due to orientation change");
if( navigation_gap != 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(true);
}
else {
if( MyDebug.LOG )
Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(false);
}
// needed for OnePlus Pad when rotating, to avoid delay in updating last_take_photo_top_time (affects placement of on-screen text e.g. zoom)
// need to do this from handler for this to take effect (otherwise last_take_photo_top_time won't update to new value)
applicationInterface.getDrawPreview().onNavigationGapChanged();
if( MyDebug.LOG )
Log.d(TAG, "layout UI due to changing navigation_gap");
mainUI.layoutUI();
}
});
}
}
else if( !edge_to_edge_mode && navigation_gap == 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "navigation_gap changed from zero to " + new_navigation_gap);
navigation_gap = new_navigation_gap;
// Sometimes when this callback is called, the navigation_gap may still be 0 even if
// the device doesn't have physical navigation buttons - we need to wait
// until we have found a non-zero value before switching to no limits.
// On devices with physical navigation bar, navigation_gap should remain 0
// (and there's no point setting FLAG_LAYOUT_NO_LIMITS)
if( want_no_limits && navigation_gap != 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(true);
}
}
if( has_last_system_orientation && (
( system_orientation == SystemOrientation.LANDSCAPE && last_system_orientation == SystemOrientation.REVERSE_LANDSCAPE ) ||
( system_orientation == SystemOrientation.REVERSE_LANDSCAPE && last_system_orientation == SystemOrientation.LANDSCAPE )
) ) {
// hack - this should be done via MyDisplayListener.onDisplayChanged(), but that doesn't work on Galaxy S24+ (either MyDisplayListener.onDisplayChanged()
// isn't called, or getDefaultDisplay().getRotation() is still returning the old rotation)
if( MyDebug.LOG )
Log.d(TAG, "onApplyWindowInsets: switched between landscape and reverse orientation");
onSystemOrientationChanged();
}
has_last_system_orientation = true;
last_system_orientation = system_orientation;
// see comments in MainUI.layoutUI() for why we don't use this
/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSystemOrientation() == SystemOrientation.LANDSCAPE ) {
Rect privacy_indicator_rect = windowInsets.getPrivacyIndicatorBounds();
if( privacy_indicator_rect != null ) {
Rect window_bounds = getWindowManager().getCurrentWindowMetrics().getBounds();
if( MyDebug.LOG ) {
Log.d(TAG, "privacy_indicator_rect: " + privacy_indicator_rect);
Log.d(TAG, "window_bounds: " + window_bounds);
}
privacy_indicator_gap = window_bounds.right - privacy_indicator_rect.left;
if( privacy_indicator_gap < 0 )
privacy_indicator_gap = 0; // just in case??
if( MyDebug.LOG )
Log.d(TAG, "privacy_indicator_gap: " + privacy_indicator_gap);
}
}
else {
privacy_indicator_gap = 0;
}*/
return getWindow().getDecorView().getRootView().onApplyWindowInsets(windowInsets);
}
});
}
if( edge_to_edge_mode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ) {
// already handled by the setOnApplyWindowInsetsListener above
}
else {
decorView.setOnSystemUiVisibilityChangeListener
(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Note that system bars will only be "visible" if none of the
// LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.
if( MyDebug.LOG )
Log.d(TAG, "onSystemUiVisibilityChange: " + visibility);
// Note that Android example code says to test against SYSTEM_UI_FLAG_FULLSCREEN,
// but this stopped working on Android 11, as when calling setSystemUiVisibility(0)
// to exit immersive mode, when we arrive here the flag SYSTEM_UI_FLAG_FULLSCREEN
// is still set. Fixed by checking for SYSTEM_UI_FLAG_HIDE_NAVIGATION instead -
// which makes some sense since we run in fullscreen mode all the time anyway.
//if( (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 ) {
if( (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0 ) {
immersiveModeChanged(true);
}
else {
immersiveModeChanged(false);
}
}
});
}
}
public boolean usingKitKatImmersiveMode() {
// whether we are using a Kit Kat style immersive mode (either hiding navigation bar, GUI, or everything)
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_off");
if( immersive_mode.equals("immersive_mode_navigation") || immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything") )
return true;
return false;
}
public boolean usingKitKatImmersiveModeEverything() {
// whether we are using a Kit Kat style immersive mode for everything
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_off");
if( immersive_mode.equals("immersive_mode_everything") )
return true;
return false;
}
private Handler immersive_timer_handler = null;
private Runnable immersive_timer_runnable = null;
private void cancelImmersiveTimer() {
if( immersive_timer_handler != null && immersive_timer_runnable != null ) {
immersive_timer_handler.removeCallbacks(immersive_timer_runnable);
immersive_timer_handler = null;
immersive_timer_runnable = null;
}
}
private void setImmersiveTimer() {
cancelImmersiveTimer();
if( app_is_paused ) {
// don't want to enter immersive mode from background
// problem that even after onPause, we can end up here via various callbacks
return;
}
immersive_timer_handler = new Handler();
immersive_timer_handler.postDelayed(immersive_timer_runnable = new Runnable(){
@Override
public void run(){
if( MyDebug.LOG )
Log.d(TAG, "setImmersiveTimer: run");
// even though timer should have been cancelled when in background, check app_is_paused just in case
if( !app_is_paused && !camera_in_background && !popupIsOpen() && usingKitKatImmersiveMode() )
setImmersiveMode(true);
}
}, 5000);
}
public void initImmersiveMode() {
if( !usingKitKatImmersiveMode() ) {
setImmersiveMode(true);
}
else {
// don't start in immersive mode, only after a timer
setImmersiveTimer();
}
}
void setImmersiveMode(boolean on) {
if( MyDebug.LOG )
Log.d(TAG, "setImmersiveMode: " + on);
// n.b., preview.setImmersiveMode() is called from onSystemUiVisibilityChange()
// don't allow the kitkat-style immersive mode for panorama mode (problem that in "full" immersive mode, the gyro spot can't be seen - we could fix this, but simplest to just disallow)
boolean enable_immersive = on && usingKitKatImmersiveMode() && applicationInterface.getPhotoMode() != MyApplicationInterface.PhotoMode.Panorama;
if( MyDebug.LOG )
Log.d(TAG, "enable_immersive?: " + enable_immersive);
if( edge_to_edge_mode ) {
// take opportunity to avoid deprecated setSystemUiVisibility
WindowInsetsControllerCompat windowInsetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
int type = WindowInsetsCompat.Type.navigationBars(); // only show/hide navigation bars, as we run with system bars always hidden
if( enable_immersive ) {
windowInsetsController.hide(type);
}
else {
windowInsetsController.show(type);
}
}
else {
// save whether we set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - since this flag might be enabled for showUnderNavigation(true), at least indirectly by setDecorFitsSystemWindows() on old versions of Android
int saved_flags = getWindow().getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
if( MyDebug.LOG )
Log.d(TAG, "saved_flags?: " + saved_flags);
if( enable_immersive ) {
getWindow().getDecorView().setSystemUiVisibility(saved_flags | View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
}
else {
getWindow().getDecorView().setSystemUiVisibility(saved_flags);
}
}
}
/** Sets the brightness level for normal operation (when camera preview is visible).
* If force_max is true, this always forces maximum brightness; otherwise this depends on user preference.
*/
public void setBrightnessForCamera(boolean force_max) {
if( MyDebug.LOG )
Log.d(TAG, "setBrightnessForCamera");
// set screen to max brightness - see http://stackoverflow.com/questions/11978042/android-screen-brightness-max-value
// done here rather than onCreate, so that changing it in preferences takes effect without restarting app
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
final WindowManager.LayoutParams layout = getWindow().getAttributes();
if( force_max || sharedPreferences.getBoolean(PreferenceKeys.MaxBrightnessPreferenceKey, false) ) {
layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL;
}
else {
layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
}
// this must be called from the ui thread
// sometimes this method may be called not on UI thread, e.g., Preview.takePhotoWhenFocused->CameraController2.takePicture
// ->CameraController2.runFakePrecapture->Preview/onFrontScreenTurnOn->MyApplicationInterface.turnFrontScreenFlashOn
// -> this.setBrightnessForCamera
this.runOnUiThread(new Runnable() {
public void run() {
getWindow().setAttributes(layout);
}
});
}
/**
* Set the brightness to minimal in case the preference key is set to do it
*/
public void setBrightnessToMinimumIfWanted() {
if( MyDebug.LOG )
Log.d(TAG, "setBrightnessToMinimum");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
final WindowManager.LayoutParams layout = getWindow().getAttributes();
if( sharedPreferences.getBoolean(PreferenceKeys.DimWhenDisconnectedPreferenceKey, false) ) {
layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
}
else {
layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
}
this.runOnUiThread(new Runnable() {
public void run() {
getWindow().setAttributes(layout);
}
});
}
/** Sets the window flags for normal operation (when camera preview is visible).
*/
public void setWindowFlagsForCamera() {
if( MyDebug.LOG )
Log.d(TAG, "setWindowFlagsForCamera");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ) {
// we set this to prevent what's on the preview being used to show under the "recent apps" view - potentially useful
// for privacy reasons
setRecentsScreenshotEnabled(false);
}
if( lock_to_landscape ) {
// force to landscape mode
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
//setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); // testing for devices with unusual sensor orientation (e.g., Nexus 5X)
}
else {
// allow orientation to change for camera, even if user has locked orientation
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
if( preview != null ) {
// also need to call preview.setCameraDisplayOrientation, as this handles if the user switched from portrait to reverse landscape whilst in settings/etc
// as switching from reverse landscape back to landscape isn't detected in onConfigurationChanged
// update: now probably irrelevant now that we close/reopen the camera, but keep it here anyway
preview.setCameraDisplayOrientation();
}
if( preview != null && mainUI != null ) {
// layoutUI() is needed because even though we call layoutUI from MainUI.onOrientationChanged(), certain things
// (ui_rotation) depend on the system orientation too.
// Without this, going to Settings, then changing orientation, then exiting settings, would show the icons with the
// wrong orientation.
// We put this here instead of onConfigurationChanged() as onConfigurationChanged() isn't called when switching from
// reverse landscape to landscape orientation: so it's needed to fix if the user starts in portrait, goes to settings
// or a dialog, then switches to reverse landscape, then exits settings/dialog - the system orientation will switch
// to landscape (which Open Camera is forced to).
mainUI.layoutUI();
}
// keep screen active - see http://stackoverflow.com/questions/2131948/force-screen-on
if( sharedPreferences.getBoolean(PreferenceKeys.KeepDisplayOnPreferenceKey, true) ) {
if( MyDebug.LOG )
Log.d(TAG, "do keep screen on");
this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
else {
if( MyDebug.LOG )
Log.d(TAG, "don't keep screen on");
this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
if( sharedPreferences.getBoolean(PreferenceKeys.ShowWhenLockedPreferenceKey, false) ) {
if( MyDebug.LOG )
Log.d(TAG, "do show when locked");
// keep Open Camera on top of screen-lock (will still need to unlock when going to gallery or settings)
showWhenLocked(true);
}
else {
if( MyDebug.LOG )
Log.d(TAG, "don't show when locked");
showWhenLocked(false);
}
if( want_no_limits && navigation_gap != 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(true);
}
setBrightnessForCamera(false);
initImmersiveMode();
camera_in_background = false;
magneticSensor.clearDialog(); // if the magnetic accuracy was opened, it must have been closed now
if( !app_is_paused ) {
// Needs to be called after camera_in_background is set to false.
// Note that the app_is_paused guard is in some sense unnecessary, as initLocation tests for that too,
// but useful for error tracking - ideally we want to make sure that initLocation is never called when
// app is paused. It can happen here because setWindowFlagsForCamera() is called from
// onCreate()
initLocation();
// Similarly only want to reopen the camera if no longer paused
if( preview != null ) {
preview.onResume();
}
}
}
private void setWindowFlagsForSettings() {
setWindowFlagsForSettings(true);
}
/** Sets the window flags for when the settings window is open.
* @param set_lock_protect If true, then window flags will be set to protect by screen lock, no
* matter what the preference setting
* PreferenceKeys.getShowWhenLockedPreferenceKey() is set to. This
* should be true for the Settings window, and anything else that might
* need protecting. But some callers use this method for opening other
* things (such as info dialogs).
*/
public void setWindowFlagsForSettings(boolean set_lock_protect) {
if( MyDebug.LOG )
Log.d(TAG, "setWindowFlagsForSettings: " + set_lock_protect);
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ) {
// in settings mode, okay to revert to default behaviour for using a screenshot for "recent apps" view
setRecentsScreenshotEnabled(true);
}
// allow screen rotation
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
// revert to standard screen blank behaviour
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if( want_no_limits && navigation_gap != 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(false);
}
if( set_lock_protect ) {
// settings should still be protected by screen lock
showWhenLocked(false);
}
{
WindowManager.LayoutParams layout = getWindow().getAttributes();
layout.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
getWindow().setAttributes(layout);
}
setImmersiveMode(false);
camera_in_background = true;
// we disable location listening when showing settings or a dialog etc - saves battery life, also better for privacy
applicationInterface.getLocationSupplier().freeLocationListeners();
// similarly we close the camera
preview.onPause(false);
push_switched_camera = false; // just in case
}
private void showWhenLocked(boolean show) {
if( MyDebug.LOG )
Log.d(TAG, "showWhenLocked: " + show);
// although FLAG_SHOW_WHEN_LOCKED is deprecated, setShowWhenLocked(false) does not work
// correctly: if we turn screen off and on when camera is open (so we're now running above
// the lock screen), going to settings does not show the lock screen, i.e.,
// setShowWhenLocked(false) does not take effect!
/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
if( MyDebug.LOG )
Log.d(TAG, "use setShowWhenLocked");
setShowWhenLocked(show);
}
else*/ {
if( show ) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
}
}
/** Use this is place of simply alert.show(), if the orientation has just been set to allow
* rotation via setWindowFlagsForSettings(). On some devices (e.g., OnePlus 3T with Android 8),
* the dialog doesn't show properly if the phone is held in portrait. A workaround seems to be
* to use postDelayed. Note that postOnAnimation() doesn't work.
*/
public void showAlert(final AlertDialog alert) {
if( MyDebug.LOG )
Log.d(TAG, "showAlert");
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
alert.show();
}
}, 20);
// note that 1ms usually fixes the problem, but not always; 10ms seems fine, have set 20ms
// just in case
}
public void showPreview(boolean show) {
if( MyDebug.LOG )
Log.d(TAG, "showPreview: " + show);
final ViewGroup container = findViewById(R.id.hide_container);
container.setVisibility(show ? View.GONE : View.VISIBLE);
}
/** Rotates the supplied bitmap according to the orientation tag stored in the exif data. If no
* rotation is required, the input bitmap is returned. If rotation is required, the input
* bitmap is recycled.
* @param uri Uri containing the JPEG with Exif information to use.
*/
public Bitmap rotateForExif(Bitmap bitmap, Uri uri) throws IOException {
ExifInterface exif;
InputStream inputStream = null;
try {
inputStream = this.getContentResolver().openInputStream(uri);
exif = new ExifInterface(inputStream);
}
finally {
if( inputStream != null )
inputStream.close();
}
if( exif != null ) {
int exif_orientation_s = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
boolean needs_tf = false;
int exif_orientation = 0;
// see http://jpegclub.org/exif_orientation.html
// and http://stackoverflow.com/questions/20478765/how-to-get-the-correct-orientation-of-the-image-selected-from-the-default-image
if( exif_orientation_s == ExifInterface.ORIENTATION_UNDEFINED || exif_orientation_s == ExifInterface.ORIENTATION_NORMAL ) {
// leave unchanged
}
else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_180 ) {
needs_tf = true;
exif_orientation = 180;
}
else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_90 ) {
needs_tf = true;
exif_orientation = 90;
}
else if( exif_orientation_s == ExifInterface.ORIENTATION_ROTATE_270 ) {
needs_tf = true;
exif_orientation = 270;
}
else {
// just leave unchanged for now
if( MyDebug.LOG )
Log.e(TAG, " unsupported exif orientation: " + exif_orientation_s);
}
if( MyDebug.LOG )
Log.d(TAG, " exif orientation: " + exif_orientation);
if( needs_tf ) {
if( MyDebug.LOG )
Log.d(TAG, " need to rotate bitmap due to exif orientation tag");
Matrix m = new Matrix();
m.setRotate(exif_orientation, bitmap.getWidth() * 0.5f, bitmap.getHeight() * 0.5f);
Bitmap rotated_bitmap = Bitmap.createBitmap(bitmap, 0, 0,bitmap.getWidth(), bitmap.getHeight(), m, true);
if( rotated_bitmap != bitmap ) {
bitmap.recycle();
bitmap = rotated_bitmap;
}
}
}
return bitmap;
}
/** Loads a thumbnail from the supplied image uri (not videos). Note this loads from the bitmap
* rather than reading from MediaStore. Therefore this works with SAF uris as well as
* MediaStore uris, as well as allowing control over the resolution of the thumbnail.
* If sample_factor is 1, this returns a bitmap scaled to match the display resolution. If
* sample_factor is greater than 1, it will be scaled down to a lower resolution.
* We now use this for photos in preference to APIs like
* MediaStore.Images.Thumbnails.getThumbnail(). Advantages are simplifying the code, reducing
* number of different codepaths, but also seems to help against device specific bugs
* in getThumbnail() e.g. Pixel 6 Pro with x-night in portrait.
*/
private Bitmap loadThumbnailFromUri(Uri uri, int sample_factor) {
Bitmap thumbnail = null;
try {
//thumbnail = MediaStore.Images.Media.getBitmap(getContentResolver(), media.uri);
// only need to load a bitmap as large as the screen size
BitmapFactory.Options options = new BitmapFactory.Options();
InputStream is = getContentResolver().openInputStream(uri);
// get dimensions
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
int bitmap_width = options.outWidth;
int bitmap_height = options.outHeight;
Point display_size = new Point();
applicationInterface.getDisplaySize(display_size, true);
if( MyDebug.LOG ) {
Log.d(TAG, "bitmap_width: " + bitmap_width);
Log.d(TAG, "bitmap_height: " + bitmap_height);
Log.d(TAG, "display width: " + display_size.x);
Log.d(TAG, "display height: " + display_size.y);
}
// align dimensions
if( display_size.x < display_size.y ) {
//noinspection SuspiciousNameCombination
display_size.set(display_size.y, display_size.x);
}
if( bitmap_width < bitmap_height ) {
int dummy = bitmap_width;
//noinspection SuspiciousNameCombination
bitmap_width = bitmap_height;
bitmap_height = dummy;
}
if( MyDebug.LOG ) {
Log.d(TAG, "bitmap_width: " + bitmap_width);
Log.d(TAG, "bitmap_height: " + bitmap_height);
Log.d(TAG, "display width: " + display_size.x);
Log.d(TAG, "display height: " + display_size.y);
}
// only care about height, to save worrying about different aspect ratios
options.inSampleSize = 1;
while( bitmap_height / (2*options.inSampleSize) >= display_size.y ) {
options.inSampleSize *= 2;
}
options.inSampleSize *= sample_factor;
if( MyDebug.LOG ) {
Log.d(TAG, "inSampleSize: " + options.inSampleSize);
}
options.inJustDecodeBounds = false;
// need a new inputstream, see https://stackoverflow.com/questions/2503628/bitmapfactory-decodestream-returning-null-when-options-are-set
is.close();
is = getContentResolver().openInputStream(uri);
thumbnail = BitmapFactory.decodeStream(is, null, options);
if( thumbnail == null ) {
Log.e(TAG, "decodeStream returned null bitmap for ghost image last");
}
is.close();
thumbnail = rotateForExif(thumbnail, uri);
}
catch(IOException e) {
MyDebug.logStackTrace(TAG, "failed to load bitmap for ghost image last", e);
}
return thumbnail;
}
/** Shows the default "blank" gallery icon, when we don't have a thumbnail available.
*/
private void updateGalleryIconToBlank() {
if( MyDebug.LOG )
Log.d(TAG, "updateGalleryIconToBlank");
ImageButton galleryButton = this.findViewById(R.id.gallery);
int bottom = galleryButton.getPaddingBottom();
int top = galleryButton.getPaddingTop();
int right = galleryButton.getPaddingRight();
int left = galleryButton.getPaddingLeft();
/*if( MyDebug.LOG )
Log.d(TAG, "padding: " + bottom);*/
galleryButton.setImageBitmap(null);
galleryButton.setImageResource(R.drawable.baseline_photo_library_white_48);
// workaround for setImageResource also resetting padding, Android bug
galleryButton.setPadding(left, top, right, bottom);
gallery_bitmap = null;
}
/** Shows a thumbnail for the gallery icon.
*/
void updateGalleryIcon(Bitmap thumbnail) {
if( MyDebug.LOG )
Log.d(TAG, "updateGalleryIcon: " + thumbnail);
// If we're currently running the background task to update the gallery (see updateGalleryIcon()), we should cancel that!
// Otherwise if user takes a photo whilst the background task is still running, the thumbnail from the latest photo will
// be overridden when the background task completes. This is more likely when using SAF on Android 10+ with scoped storage,
// due to SAF's poor performance for folders with large number of files.
if( update_gallery_future != null ) {
if( MyDebug.LOG )
Log.d(TAG, "cancel update_gallery_future");
update_gallery_future.cancel(true);
}
ImageButton galleryButton = this.findViewById(R.id.gallery);
galleryButton.setImageBitmap(thumbnail);
gallery_bitmap = thumbnail;
}
/** Updates the gallery icon by searching for the most recent photo.
* Launches the task in a separate thread.
*/
public void updateGalleryIcon() {
long debug_time = 0;
if( MyDebug.LOG ) {
Log.d(TAG, "updateGalleryIcon");
debug_time = System.currentTimeMillis();
}
if( update_gallery_future != null ) {
Log.d(TAG, "previous updateGalleryIcon task already running");
return;
}
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String ghost_image_pref = sharedPreferences.getString(PreferenceKeys.GhostImagePreferenceKey, "preference_ghost_image_off");
final boolean ghost_image_last = ghost_image_pref.equals("preference_ghost_image_last");
final Handler handler = new Handler(Looper.getMainLooper());
//new AsyncTask<Void, Void, Bitmap>() {
Runnable runnable = new Runnable() {
private static final String TAG = "updateGalleryIcon";
private Uri uri;
private boolean is_raw;
private boolean is_video;
@Override
//protected Bitmap doInBackground(Void... params) {
public void run() {
if( MyDebug.LOG )
Log.d(TAG, "doInBackground");
StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia();
Bitmap thumbnail = null;
KeyguardManager keyguard_manager = (KeyguardManager)MainActivity.this.getSystemService(Context.KEYGUARD_SERVICE);
boolean is_locked = keyguard_manager != null && keyguard_manager.inKeyguardRestrictedInputMode();
if( MyDebug.LOG )
Log.d(TAG, "is_locked?: " + is_locked);
if( media != null && getContentResolver() != null && !is_locked ) {
// check for getContentResolver() != null, as have had reported Google Play crashes
uri = media.getMediaStoreUri(MainActivity.this);
is_raw = media.filename != null && StorageUtils.filenameIsRaw(media.filename);
is_video = media.video;
if( ghost_image_last && !media.video ) {
if( MyDebug.LOG )
Log.d(TAG, "load full size bitmap for ghost image last photo");
// use sample factor of 1 so that it's full size for ghost image
thumbnail = loadThumbnailFromUri(media.uri, 1);
}
if( thumbnail == null ) {
try {
if( !media.video ) {
if( MyDebug.LOG )
Log.d(TAG, "load thumbnail for photo");
// use sample factor as this image is only used for thumbnail; and
// unlike code in MyApplicationInterface.saveImage() we don't need to
// worry about the thumbnail animation when taking/saving a photo
thumbnail = loadThumbnailFromUri(media.uri, 8);
}
else if( !media.mediastore ) {
if( MyDebug.LOG )
Log.d(TAG, "load thumbnail for video from SAF uri");
ParcelFileDescriptor pfd_saf = null; // keep a reference to this as long as retriever, to avoid risk of pfd_saf being garbage collected
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
pfd_saf = getContentResolver().openFileDescriptor(media.uri, "r");
retriever.setDataSource(pfd_saf.getFileDescriptor());
thumbnail = retriever.getFrameAtTime(-1);
}
catch(Exception e) {
MyDebug.logStackTrace(TAG, "failed to load video thumbnail", e);
}
finally {
try {
retriever.release();
}
catch(RuntimeException ex) {
// ignore
}
try {
if( pfd_saf != null ) {
pfd_saf.close();
}
}
catch(IOException e) {
MyDebug.logStackTrace(TAG, "failed to close pfd_saf", e);
}
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "load thumbnail for video");
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) {
final Size size = new Size(512, 384); // same as MediaStore.ThumbnailConstants.MINI_SIZE, which is used for MediaStore.Video.Thumbnails.MINI_KIND
thumbnail = getContentResolver().loadThumbnail(media.uri, size, new CancellationSignal());
}
else {
// non-deprecated getContentResolver().loadThumbnail requires Android Q
//noinspection deprecation
thumbnail = MediaStore.Video.Thumbnails.getThumbnail(getContentResolver(), media.id, MediaStore.Video.Thumbnails.MINI_KIND, null);
}
}
}
catch(Throwable e) {
// have had Google Play NoClassDefFoundError crashes from getThumbnail() for Galaxy Ace4 (vivalto3g), Galaxy S Duos3 (vivalto3gvn)
// also NegativeArraySizeException - best to catch everything
if( MyDebug.LOG )
Log.e(TAG, "thumbnail exception");
MyDebug.logStackTrace(TAG, "thumbnail exception", e);
}
}
}
//return thumbnail;
final Bitmap thumbnail_f = thumbnail;
handler.post(new Runnable() {
@Override
public void run() {
onPostExecute(thumbnail_f);
}
});
}
/** Runs on UI thread, after background work is complete.
*/
private void onPostExecute(Bitmap thumbnail) {
if( MyDebug.LOG )
Log.d(TAG, "onPostExecute");
if( update_gallery_future != null && update_gallery_future.isCancelled() ) {
if( MyDebug.LOG )
Log.d(TAG, "was cancelled");
update_gallery_future = null;
return;
}
// since we're now setting the thumbnail to the latest media on disk, we need to make sure clicking the Gallery goes to this
applicationInterface.getStorageUtils().clearLastMediaScanned();
if( uri != null ) {
if( MyDebug.LOG ) {
Log.d(TAG, "found media uri: " + uri);
Log.d(TAG, " is_raw?: " + is_raw);
}
applicationInterface.getStorageUtils().setLastMediaScanned(uri, is_raw, false, null);
}
if( thumbnail != null ) {
if( MyDebug.LOG )
Log.d(TAG, "set gallery button to thumbnail");
updateGalleryIcon(thumbnail);
applicationInterface.getDrawPreview().updateThumbnail(thumbnail, is_video, false); // needed in case last ghost image is enabled
}
else {
if( MyDebug.LOG )
Log.d(TAG, "set gallery button to blank");
updateGalleryIconToBlank();
}
update_gallery_future = null;
}
//}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
};
ExecutorService executor = Executors.newSingleThreadExecutor();
//executor.execute(runnable);
update_gallery_future = executor.submit(runnable);
if( MyDebug.LOG )
Log.d(TAG, "updateGalleryIcon: total time to update gallery icon: " + (System.currentTimeMillis() - debug_time));
}
void savingImage(final boolean started) {
if( MyDebug.LOG )
Log.d(TAG, "savingImage: " + started);
this.runOnUiThread(new Runnable() {
public void run() {
final ImageButton galleryButton = findViewById(R.id.gallery);
if( started ) {
//galleryButton.setColorFilter(0x80ffffff, PorterDuff.Mode.MULTIPLY);
if( gallery_save_anim == null ) {
gallery_save_anim = ValueAnimator.ofInt(Color.argb(200, 255, 255, 255), Color.argb(63, 255, 255, 255));
gallery_save_anim.setEvaluator(new ArgbEvaluator());
gallery_save_anim.setRepeatCount(ValueAnimator.INFINITE);
gallery_save_anim.setRepeatMode(ValueAnimator.REVERSE);
gallery_save_anim.setDuration(500);
}
gallery_save_anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(@NonNull ValueAnimator animation) {
galleryButton.setColorFilter((Integer)animation.getAnimatedValue(), PorterDuff.Mode.MULTIPLY);
}
});
gallery_save_anim.start();
}
else
if( gallery_save_anim != null ) {
gallery_save_anim.cancel();
}
galleryButton.setColorFilter(null);
}
});
}
/** Called when the number of images being saved in ImageSaver changes (or otherwise something
* that changes our calculation of whether we can take a new photo, e.g., changing photo mode).
*/
void imageQueueChanged() {
if( MyDebug.LOG )
Log.d(TAG, "imageQueueChanged");
applicationInterface.getDrawPreview().setImageQueueFull( !applicationInterface.canTakeNewPhoto() );
/*if( applicationInterface.getImageSaver().getNImagesToSave() == 0) {
cancelImageSavingNotification();
}
else if( has_notification ) {
// call again to update the text of remaining images
createImageSavingNotification();
}*/
}
/** Creates a notification to indicate still saving images (or updates an existing one).
* Update: notifications now removed due to needing permissions on Android 13+.
*/
private void createImageSavingNotification() {
if( MyDebug.LOG )
Log.d(TAG, "createImageSavingNotification");
/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
int n_images_to_save = applicationInterface.getImageSaver().getNRealImagesToSave();
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_notify_take_photo)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.image_saving_notification) + " " + n_images_to_save + " " + getString(R.string.remaining))
//.setStyle(new Notification.BigTextStyle()
// .bigText("Much longer text that cannot fit one line..."))
//.setPriority(Notification.PRIORITY_DEFAULT)
;
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.notify(image_saving_notification_id, builder.build());
has_notification = true;
}*/
}
/** Cancels the notification for saving images.
* Update: notifications now removed due to needing permissions on Android 13+.
*/
private void cancelImageSavingNotification() {
if( MyDebug.LOG )
Log.d(TAG, "cancelImageSavingNotification");
/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.cancel(image_saving_notification_id);
has_notification = false;
}*/
}
public void clickedGallery(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedGallery");
openGallery();
}
private void openGallery() {
if( MyDebug.LOG )
Log.d(TAG, "openGallery");
//Intent intent = new Intent(Intent.ACTION_VIEW, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
Uri uri = applicationInterface.getStorageUtils().getLastMediaScanned();
boolean is_raw = uri != null && applicationInterface.getStorageUtils().getLastMediaScannedIsRaw();
if( MyDebug.LOG && uri != null ) {
Log.d(TAG, "found cached most recent uri: " + uri);
Log.d(TAG, " is_raw: " + is_raw);
}
if( uri == null ) {
if( MyDebug.LOG )
Log.d(TAG, "go to latest media");
StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia();
if( media != null ) {
if( MyDebug.LOG ) {
Log.d(TAG, "latest uri:" + media.uri);
Log.d(TAG, "filename: " + media.filename);
}
uri = media.getMediaStoreUri(this);
if( MyDebug.LOG )
Log.d(TAG, "media uri:" + uri);
is_raw = media.filename != null && StorageUtils.filenameIsRaw(media.filename);
if( MyDebug.LOG )
Log.d(TAG, "is_raw:" + is_raw);
}
}
if( uri != null && !MainActivity.useScopedStorage() ) {
// check uri exists
// note, with scoped storage this isn't reliable when using SAF - since we don't actually have permission to access mediastore URIs that
// were created via Storage Access Framework, even though Open Camera was the application that saved them(!)
try {
ContentResolver cr = getContentResolver();
ParcelFileDescriptor pfd = cr.openFileDescriptor(uri, "r");
if( pfd == null ) {
if( MyDebug.LOG )
Log.d(TAG, "uri no longer exists (1): " + uri);
uri = null;
is_raw = false;
}
else {
pfd.close();
}
}
catch(IOException e) {
if( MyDebug.LOG )
Log.d(TAG, "uri no longer exists (2): " + uri);
uri = null;
is_raw = false;
}
}
if( uri == null ) {
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
is_raw = false;
}
if( !is_test ) {
// don't do if testing, as unclear how to exit activity to finish test (for testGallery())
if( MyDebug.LOG )
Log.d(TAG, "launch uri:" + uri);
final String REVIEW_ACTION = "com.android.camera.action.REVIEW";
boolean done = false;
if( !is_raw ) {
// REVIEW_ACTION means we can view video files without autoplaying.
// However, Google Photos at least has problems with going to a RAW photo (in RAW only mode),
// unless we first pause and resume Open Camera.
// Update: on Galaxy S10e with Android 11 at least, no longer seem to have problems, but leave
// the check for is_raw just in case for older devices.
if( MyDebug.LOG )
Log.d(TAG, "try REVIEW_ACTION");
try {
Intent intent = new Intent(REVIEW_ACTION, uri);
this.startActivity(intent);
done = true;
}
catch(ActivityNotFoundException e) {
MyDebug.logStackTrace(TAG, "failed to start REVIEW_ACTION intent", e);
}
}
if( !done ) {
if( MyDebug.LOG )
Log.d(TAG, "try ACTION_VIEW");
try {
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
this.startActivity(intent);
}
catch(ActivityNotFoundException e) {
MyDebug.logStackTrace(TAG, "failed to start ACTION_VIEW intent", e);
preview.showToast(null, R.string.no_gallery_app);
}
catch(SecurityException e) {
// have received this crash from Google Play - don't display a toast, simply do nothing
MyDebug.logStackTrace(TAG, "SecurityException from ACTION_VIEW startActivity", e);
}
}
}
}
/** Opens the Storage Access Framework dialog to select a folder for save location.
* @param from_preferences Whether called from the Preferences
*/
void openFolderChooserDialogSAF(boolean from_preferences) {
if( MyDebug.LOG )
Log.d(TAG, "openFolderChooserDialogSAF: " + from_preferences);
this.saf_dialog_from_preferences = from_preferences;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
//Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
//intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, CHOOSE_SAVE_FOLDER_SAF_CODE);
}
/** Opens the Storage Access Framework dialog to select a file for ghost image.
* @param from_preferences Whether called from the Preferences
*/
void openGhostImageChooserDialogSAF(boolean from_preferences) {
if( MyDebug.LOG )
Log.d(TAG, "openGhostImageChooserDialogSAF: " + from_preferences);
this.saf_dialog_from_preferences = from_preferences;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
try {
startActivityForResult(intent, CHOOSE_GHOST_IMAGE_SAF_CODE);
}
catch(ActivityNotFoundException e) {
// see https://stackoverflow.com/questions/34021039/action-open-document-not-working-on-miui/34045627
preview.showToast(null, R.string.open_files_saf_exception_ghost);
MyDebug.logStackTrace(TAG, "ActivityNotFoundException from startActivityForResult", e);
}
}
/** Opens the Storage Access Framework dialog to select a file for loading settings.
* @param from_preferences Whether called from the Preferences
*/
void openLoadSettingsChooserDialogSAF(boolean from_preferences) {
if( MyDebug.LOG )
Log.d(TAG, "openLoadSettingsChooserDialogSAF: " + from_preferences);
this.saf_dialog_from_preferences = from_preferences;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/xml"); // note that application/xml doesn't work (can't select the xml files)!
try {
startActivityForResult(intent, CHOOSE_LOAD_SETTINGS_SAF_CODE);
}
catch(ActivityNotFoundException e) {
// see https://stackoverflow.com/questions/34021039/action-open-document-not-working-on-miui/34045627
preview.showToast(null, R.string.open_files_saf_exception_generic);
MyDebug.logStackTrace(TAG, "ActivityNotFoundException from startActivityForResult", e);
}
}
/** Call when the SAF save history has been updated.
* This is only public so we can call from testing.
* @param save_folder The new SAF save folder Uri.
*/
public void updateFolderHistorySAF(String save_folder) {
if( MyDebug.LOG )
Log.d(TAG, "updateSaveHistorySAF");
if( save_location_history_saf == null ) {
save_location_history_saf = new SaveLocationHistory(this, "save_location_history_saf", save_folder);
}
save_location_history_saf.updateFolderHistory(save_folder, true);
}
/** Listens for the response from the Storage Access Framework dialog to select a folder
* (as opened with openFolderChooserDialogSAF()).
*/
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if( MyDebug.LOG )
Log.d(TAG, "onActivityResult: " + requestCode);
super.onActivityResult(requestCode, resultCode, resultData);
switch( requestCode ) {
case CHOOSE_SAVE_FOLDER_SAF_CODE:
if( resultCode == RESULT_OK && resultData != null ) {
Uri treeUri = resultData.getData();
if( MyDebug.LOG )
Log.d(TAG, "returned treeUri: " + treeUri);
// see https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions :
final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
try {
/*if( true )
throw new SecurityException(); // test*/
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.SaveLocationSAFPreferenceKey, treeUri.toString());
editor.apply();
if( MyDebug.LOG )
Log.d(TAG, "update folder history for saf");
updateFolderHistorySAF(treeUri.toString());
String file = applicationInterface.getStorageUtils().getImageFolderPath();
if( file != null ) {
preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + file);
}
}
catch(SecurityException e) {
MyDebug.logStackTrace(TAG, "SecurityException failed to take permission", e);
preview.showToast(null, R.string.saf_permission_failed);
// failed - if the user had yet to set a save location, make sure we switch SAF back off
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String uri = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, "");
if( uri.isEmpty() ) {
if( MyDebug.LOG )
Log.d(TAG, "no SAF save location was set");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.UsingSAFPreferenceKey, false);
editor.apply();
}
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "SAF dialog cancelled");
// cancelled - if the user had yet to set a save location, make sure we switch SAF back off
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String uri = sharedPreferences.getString(PreferenceKeys.SaveLocationSAFPreferenceKey, "");
if( uri.isEmpty() ) {
if( MyDebug.LOG )
Log.d(TAG, "no SAF save location was set");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PreferenceKeys.UsingSAFPreferenceKey, false);
editor.apply();
preview.showToast(null, R.string.saf_cancelled);
}
}
if( !saf_dialog_from_preferences ) {
setWindowFlagsForCamera();
showPreview(true);
}
break;
case CHOOSE_GHOST_IMAGE_SAF_CODE:
if( resultCode == RESULT_OK && resultData != null ) {
Uri fileUri = resultData.getData();
if( MyDebug.LOG )
Log.d(TAG, "returned single fileUri: " + fileUri);
// persist permission just in case?
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
/*if( true )
throw new SecurityException(); // test*/
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(fileUri, takeFlags);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.GhostSelectedImageSAFPreferenceKey, fileUri.toString());
editor.apply();
}
catch(SecurityException e) {
MyDebug.logStackTrace(TAG, "SecurityException failed to take permission", e);
preview.showToast(null, R.string.saf_permission_failed_open_image);
// failed - if the user had yet to set a ghost image
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String uri = sharedPreferences.getString(PreferenceKeys.GhostSelectedImageSAFPreferenceKey, "");
if( uri.isEmpty() ) {
if( MyDebug.LOG )
Log.d(TAG, "no SAF ghost image was set");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.GhostImagePreferenceKey, "preference_ghost_image_off");
editor.apply();
}
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "SAF dialog cancelled");
// cancelled - if the user had yet to set a ghost image, make sure we switch the option back off
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String uri = sharedPreferences.getString(PreferenceKeys.GhostSelectedImageSAFPreferenceKey, "");
if( uri.isEmpty() ) {
if( MyDebug.LOG )
Log.d(TAG, "no SAF ghost image was set");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.GhostImagePreferenceKey, "preference_ghost_image_off");
editor.apply();
}
}
if( !saf_dialog_from_preferences ) {
setWindowFlagsForCamera();
showPreview(true);
}
break;
case CHOOSE_LOAD_SETTINGS_SAF_CODE:
if( resultCode == RESULT_OK && resultData != null ) {
Uri fileUri = resultData.getData();
if( MyDebug.LOG )
Log.d(TAG, "returned single fileUri: " + fileUri);
// persist permission just in case?
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
/*if( true )
throw new SecurityException(); // test*/
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(fileUri, takeFlags);
settingsManager.loadSettings(fileUri);
}
catch(SecurityException e) {
MyDebug.logStackTrace(TAG, "SecurityException failed to take permission", e);
preview.showToast(null, R.string.restore_settings_failed);
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "SAF dialog cancelled");
}
if( !saf_dialog_from_preferences ) {
setWindowFlagsForCamera();
showPreview(true);
}
break;
}
}
/** Update the save folder (for non-SAF methods).
*/
void updateSaveFolder(String new_save_location) {
if( MyDebug.LOG )
Log.d(TAG, "updateSaveFolder: " + new_save_location);
if( new_save_location != null ) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String orig_save_location = this.applicationInterface.getStorageUtils().getSaveLocation();
if( !orig_save_location.equals(new_save_location) ) {
if( MyDebug.LOG )
Log.d(TAG, "changed save_folder to: " + this.applicationInterface.getStorageUtils().getSaveLocation());
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(PreferenceKeys.SaveLocationPreferenceKey, new_save_location);
editor.apply();
this.save_location_history.updateFolderHistory(this.getStorageUtils().getSaveLocation(), true);
String save_folder_name = getHumanReadableSaveFolder(this.applicationInterface.getStorageUtils().getSaveLocation());
this.preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name);
}
}
}
public static class MyFolderChooserDialog extends FolderChooserDialog {
@Override
public void onDismiss(DialogInterface dialog) {
if( MyDebug.LOG )
Log.d(TAG, "FolderChooserDialog dismissed");
// n.b., fragments have to be static (as they might be inserted into a new Activity - see http://stackoverflow.com/questions/15571010/fragment-inner-class-should-be-static),
// so we access the MainActivity via the fragment's getActivity().
MainActivity main_activity = (MainActivity)this.getActivity();
// activity may be null, see https://stackoverflow.com/questions/13116104/best-practice-to-reference-the-parent-activity-of-a-fragment
// have had Google Play crashes from this
if( main_activity != null ) {
main_activity.setWindowFlagsForCamera();
main_activity.showPreview(true);
String new_save_location = this.getChosenFolder();
main_activity.updateSaveFolder(new_save_location);
}
else {
if( MyDebug.LOG )
Log.e(TAG, "activity no longer exists!");
}
super.onDismiss(dialog);
}
}
/** Processes a user specified save folder. This should be used with the non-SAF scoped storage
* method, where the user types a folder directly.
*/
public static String processUserSaveLocation(String folder) {
// filter repeated '/', e.g., replace // with /:
String strip = "//";
while( !folder.isEmpty() && folder.contains(strip) ) {
folder = folder.replaceAll(strip, "/");
}
if( !folder.isEmpty() && folder.charAt(0) == '/' ) {
// strip '/' as first character - as absolute paths not allowed with scoped storage
// whilst we do block entering a '/' as first character in the InputFilter, users could
// get around this (e.g., put a '/' as second character, then delete the first character)
folder = folder.substring(1);
}
if( !folder.isEmpty() && folder.charAt(folder.length()-1) == '/' ) {
// strip '/' as last character - MediaStore will ignore it, but seems cleaner to strip it out anyway
// (we still need to allow '/' as last character in the InputFilter, otherwise users won't be able to type it whilst writing a subfolder)
folder = folder.substring(0, folder.length()-1);
}
return folder;
}
/** Creates a dialog builder for specifying a save folder dialog (used when not using SAF,
* and on scoped storage, as an alternative to using FolderChooserDialog).
*/
public AlertDialog.Builder createSaveFolderDialog() {
final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.preference_save_location);
final View dialog_view = LayoutInflater.from(this).inflate(R.layout.alertdialog_edittext, null);
final EditText editText = dialog_view.findViewById(R.id.edit_text);
// set hint instead of content description for EditText, see https://support.google.com/accessibility/android/answer/6378120
editText.setHint(getResources().getString(R.string.preference_save_location));
editText.setInputType(InputType.TYPE_CLASS_TEXT);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
editText.setText(sharedPreferences.getString(PreferenceKeys.SaveLocationPreferenceKey, "OpenCamera"));
InputFilter filter = new InputFilter() {
// whilst Android seems to allow any characters on internal memory, SD cards are typically formatted with FAT32
final String disallowed = "|\\?*<\":>";
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
for(int i=start;i<end;i++) {
if( disallowed.indexOf( source.charAt(i) ) != -1 ) {
return "";
}
}
// also check for '/', not allowed at start
if( dstart == 0 && start < source.length() && source.charAt(start) == '/' ) {
return "";
}
return null;
}
};
editText.setFilters(new InputFilter[]{filter});
alertDialog.setView(dialog_view);
alertDialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if( MyDebug.LOG )
Log.d(TAG, "save location clicked okay");
String folder = editText.getText().toString();
folder = processUserSaveLocation(folder);
updateSaveFolder(folder);
}
});
alertDialog.setNegativeButton(android.R.string.cancel, null);
return alertDialog;
}
/** Opens Open Camera's own (non-Storage Access Framework) dialog to select a folder.
*/
private void openFolderChooserDialog() {
if( MyDebug.LOG )
Log.d(TAG, "openFolderChooserDialog");
showPreview(false);
setWindowFlagsForSettings();
if( MainActivity.useScopedStorage() ) {
AlertDialog.Builder alertDialog = createSaveFolderDialog();
final AlertDialog alert = alertDialog.create();
// AlertDialog.Builder.setOnDismissListener() requires API level 17, so do it this way instead
alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface arg0) {
if( MyDebug.LOG )
Log.d(TAG, "save folder dialog dismissed");
setWindowFlagsForCamera();
showPreview(true);
}
});
alert.show();
}
else {
File start_folder = getStorageUtils().getImageFolder();
FolderChooserDialog fragment = new MyFolderChooserDialog();
fragment.setStartFolder(start_folder);
// use commitAllowingStateLoss() instead of fragment.show(), does to "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState" crash seen on Google Play
// see https://stackoverflow.com/questions/14262312/java-lang-illegalstateexception-can-not-perform-this-action-after-onsaveinstanc
//fragment.show(getFragmentManager(), "FOLDER_FRAGMENT");
getFragmentManager().beginTransaction().add(fragment, "FOLDER_FRAGMENT").commitAllowingStateLoss();
}
}
/** Returns a human readable string for the save_folder (as stored in the preferences).
*/
String getHumanReadableSaveFolder(String save_folder) {
if( applicationInterface.getStorageUtils().isUsingSAF() ) {
// try to get human readable form if possible
String file_name = applicationInterface.getStorageUtils().getFilePathFromDocumentUriSAF(Uri.parse(save_folder), true);
if( file_name != null ) {
save_folder = file_name;
}
}
else {
// The strings can either be a sub-folder of DCIM, or (pre-scoped-storage) a full path, so normally either can be displayed.
// But with scoped storage, an empty string is used to mean DCIM, so seems clearer to say that instead of displaying a blank line!
if( MainActivity.useScopedStorage() && save_folder.isEmpty() ) {
save_folder = "DCIM";
}
}
return save_folder;
}
/** User can long-click on gallery to select a recent save location from the history, of if not available,
* go straight to the file dialog to pick a folder.
*/
private void longClickedGallery() {
if( MyDebug.LOG )
Log.d(TAG, "longClickedGallery");
if( applicationInterface.getStorageUtils().isUsingSAF() ) {
if( save_location_history_saf == null || save_location_history_saf.size() <= 1 ) {
if( MyDebug.LOG )
Log.d(TAG, "go straight to choose folder dialog for SAF");
openFolderChooserDialogSAF(false);
return;
}
}
else {
if( save_location_history.size() <= 1 ) {
if( MyDebug.LOG )
Log.d(TAG, "go straight to choose folder dialog");
openFolderChooserDialog();
return;
}
}
final SaveLocationHistory history = applicationInterface.getStorageUtils().isUsingSAF() ? save_location_history_saf : save_location_history;
showPreview(false);
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle(R.string.choose_save_location);
CharSequence [] items = new CharSequence[history.size()+2];
int index=0;
// history is stored in order most-recent-last
for(int i=0;i<history.size();i++) {
String folder_name = history.get(history.size() - 1 - i);
folder_name = getHumanReadableSaveFolder(folder_name);
items[index++] = folder_name;
}
final int clear_index = index;
items[index++] = getResources().getString(R.string.clear_folder_history);
final int new_index = index;
//noinspection UnusedAssignment
items[index++] = getResources().getString(R.string.choose_another_folder);
//alertDialog.setItems(items, new DialogInterface.OnClickListener() {
alertDialog.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( which == clear_index ) {
if( MyDebug.LOG )
Log.d(TAG, "selected clear save history");
new AlertDialog.Builder(MainActivity.this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.clear_folder_history)
.setMessage(R.string.clear_folder_history_question)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( MyDebug.LOG )
Log.d(TAG, "confirmed clear save history");
if( applicationInterface.getStorageUtils().isUsingSAF() )
clearFolderHistorySAF();
else
clearFolderHistory();
setWindowFlagsForCamera();
showPreview(true);
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( MyDebug.LOG )
Log.d(TAG, "don't clear save history");
setWindowFlagsForCamera();
showPreview(true);
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
if( MyDebug.LOG )
Log.d(TAG, "cancelled clear save history");
setWindowFlagsForCamera();
showPreview(true);
}
})
.show();
}
else if( which == new_index ) {
if( MyDebug.LOG )
Log.d(TAG, "selected choose new folder");
if( applicationInterface.getStorageUtils().isUsingSAF() ) {
openFolderChooserDialogSAF(false);
}
else {
openFolderChooserDialog();
}
}
else {
if( MyDebug.LOG )
Log.d(TAG, "selected: " + which);
if( which >= 0 && which < history.size() ) {
String save_folder = history.get(history.size() - 1 - which);
if( MyDebug.LOG )
Log.d(TAG, "changed save_folder from history to: " + save_folder);
String save_folder_name = getHumanReadableSaveFolder(save_folder);
preview.showToast(null, getResources().getString(R.string.changed_save_location) + "\n" + save_folder_name);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
SharedPreferences.Editor editor = sharedPreferences.edit();
if( applicationInterface.getStorageUtils().isUsingSAF() )
editor.putString(PreferenceKeys.SaveLocationSAFPreferenceKey, save_folder);
else
editor.putString(PreferenceKeys.SaveLocationPreferenceKey, save_folder);
editor.apply();
history.updateFolderHistory(save_folder, true); // to move new selection to most recent
}
setWindowFlagsForCamera();
showPreview(true);
}
dialog.dismiss(); // need to explicitly dismiss for setSingleChoiceItems
}
});
alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
setWindowFlagsForCamera();
showPreview(true);
}
});
//getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
setWindowFlagsForSettings();
showAlert(alertDialog.create());
}
/** Clears the non-SAF folder history.
*/
public void clearFolderHistory() {
if( MyDebug.LOG )
Log.d(TAG, "clearFolderHistory");
save_location_history.clearFolderHistory(getStorageUtils().getSaveLocation());
}
/** Clears the SAF folder history.
*/
public void clearFolderHistorySAF() {
if( MyDebug.LOG )
Log.d(TAG, "clearFolderHistorySAF");
save_location_history_saf.clearFolderHistory(getStorageUtils().getSaveLocationSAF());
}
static private void putBundleExtra(Bundle bundle, String key, List<String> values) {
if( values != null ) {
String [] values_arr = new String[values.size()];
int i=0;
for(String value: values) {
values_arr[i] = value;
i++;
}
bundle.putStringArray(key, values_arr);
}
}
public void clickedShare(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedShare");
applicationInterface.shareLastImage();
}
public void clickedTrash(View view) {
if( MyDebug.LOG )
Log.d(TAG, "clickedTrash");
applicationInterface.trashLastImage();
}
/** User has pressed the take picture button, or done an equivalent action to request this (e.g.,
* volume buttons, audio trigger).
* @param photo_snapshot If true, then the user has requested taking a photo whilst video
* recording. If false, either take a photo or start/stop video depending
* on the current mode.
*/
public void takePicture(boolean photo_snapshot) {
if( MyDebug.LOG )
Log.d(TAG, "takePicture");
if( applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama ) {
if( preview.isTakingPhoto() ) {
if( MyDebug.LOG )
Log.d(TAG, "ignore whilst taking panorama photo");
}
else if( applicationInterface.getGyroSensor().isRecording() ) {
if( MyDebug.LOG )
Log.d(TAG, "panorama complete");
applicationInterface.finishPanorama();
return;
}
else if( !applicationInterface.canTakeNewPhoto() ) {
if( MyDebug.LOG )
Log.d(TAG, "can't start new panoroma, still saving in background");
// we need to test here, otherwise the Preview won't take a new photo - but we'll think we've
// started the panorama!
}
else {
if( MyDebug.LOG )
Log.d(TAG, "start panorama");
applicationInterface.startPanorama();
}
}
this.takePicturePressed(photo_snapshot, false);
}
/** Returns whether the last photo operation was a continuous fast burst.
*/
boolean lastContinuousFastBurst() {
return this.last_continuous_fast_burst;
}
/**
* @param photo_snapshot If true, then the user has requested taking a photo whilst video
* recording. If false, either take a photo or start/stop video depending
* on the current mode.
* @param continuous_fast_burst If true, then start a continuous fast burst.
*/
void takePicturePressed(boolean photo_snapshot, boolean continuous_fast_burst) {
if( MyDebug.LOG )
Log.d(TAG, "takePicturePressed");
closePopup();
this.last_continuous_fast_burst = continuous_fast_burst;
this.preview.takePicturePressed(photo_snapshot, continuous_fast_burst);
}
/** Lock the screen - this is Open Camera's own lock to guard against accidental presses,
* not the standard Android lock.
*/
void lockScreen() {
findViewById(R.id.locker).setOnTouchListener(new View.OnTouchListener() {
@SuppressLint("ClickableViewAccessibility") @Override
public boolean onTouch(View arg0, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
//return true;
}
});
screen_is_locked = true;
this.enableScreenLockOnBackPressedCallback(true); // also disable back button
}
/** Unlock the screen (see lockScreen()).
*/
void unlockScreen() {
findViewById(R.id.locker).setOnTouchListener(null);
screen_is_locked = false;
this.enableScreenLockOnBackPressedCallback(false); // reenable back button
}
/** Whether the screen is locked (see lockScreen()).
*/
public boolean isScreenLocked() {
return screen_is_locked;
}
/** Listen for gestures.
* Doing a swipe will unlock the screen (see lockScreen()).
*/
private class MyGestureDetector extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
try {
if( MyDebug.LOG )
Log.d(TAG, "from " + e1.getX() + " , " + e1.getY() + " to " + e2.getX() + " , " + e2.getY());
final ViewConfiguration vc = ViewConfiguration.get(MainActivity.this);
//final int swipeMinDistance = 4*vc.getScaledPagingTouchSlop();
final float scale = getResources().getDisplayMetrics().density;
final int swipeMinDistance = (int) (160 * scale + 0.5f); // convert dps to pixels
final int swipeThresholdVelocity = vc.getScaledMinimumFlingVelocity();
if( MyDebug.LOG ) {
Log.d(TAG, "from " + e1.getX() + " , " + e1.getY() + " to " + e2.getX() + " , " + e2.getY());
Log.d(TAG, "swipeMinDistance: " + swipeMinDistance);
}
float xdist = e1.getX() - e2.getX();
float ydist = e1.getY() - e2.getY();
float dist2 = xdist*xdist + ydist*ydist;
float vel2 = velocityX*velocityX + velocityY*velocityY;
if( dist2 > swipeMinDistance*swipeMinDistance && vel2 > swipeThresholdVelocity*swipeThresholdVelocity ) {
preview.showToast(screen_locked_toast, R.string.unlocked);
unlockScreen();
}
}
catch(Exception e) {
MyDebug.logStackTrace(TAG, "onFling failed", e);
}
return false;
}
@Override
public boolean onDown(@NonNull MotionEvent e) {
preview.showToast(screen_locked_toast, R.string.screen_is_locked);
return true;
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle state) {
if( MyDebug.LOG )
Log.d(TAG, "onSaveInstanceState");
super.onSaveInstanceState(state);
if( this.preview != null ) {
preview.onSaveInstanceState(state);
}
if( this.applicationInterface != null ) {
applicationInterface.onSaveInstanceState(state);
}
}
public boolean supportsExposureButton() {
if( preview.isVideoHighSpeed() ) {
// manual ISO/exposure not supported for high speed video mode
// it's safer not to allow opening the panel at all (otherwise the user could open it, and switch to manual)
return false;
}
if( applicationInterface.isCameraExtensionPref() ) {
// nothing in this UI (exposure compensation, manual ISO/exposure, manual white balance) is supported for camera extensions
return false;
}
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String iso_value = sharedPreferences.getString(PreferenceKeys.ISOPreferenceKey, CameraController.ISO_DEFAULT);
boolean manual_iso = !iso_value.equals(CameraController.ISO_DEFAULT);
return preview.supportsExposures() || (manual_iso && preview.supportsISORange() );
}
void cameraSetup() {
long debug_time = 0;
if( MyDebug.LOG ) {
Log.d(TAG, "cameraSetup");
debug_time = System.currentTimeMillis();
}
if( preview.getCameraController() == null ) {
if( MyDebug.LOG )
Log.d(TAG, "camera controller is null");
return;
}
boolean old_want_no_limits = want_no_limits;
this.want_no_limits = false;
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode() ) {
if( MyDebug.LOG )
Log.d(TAG, "multi-window mode");
// don't support want_no_limits mode in multi-window mode - extra complexity that the
// preview size could change from simply resizing the window; also problem that the
// navigation_gap, and whether we'd want want_no_limits, can both change depending on
// device orientation (because application can e.g. be in landscape mode even if device
// has switched to portrait)
}
else if( set_window_insets_listener && !edge_to_edge_mode ) {
Point display_size = new Point();
applicationInterface.getDisplaySize(display_size, true);
int display_width = Math.max(display_size.x, display_size.y);
int display_height = Math.min(display_size.x, display_size.y);
double display_aspect_ratio = ((double)display_width)/(double)display_height;
double preview_aspect_ratio = preview.getCurrentPreviewAspectRatio();
if( MyDebug.LOG ) {
Log.d(TAG, "display_aspect_ratio: " + display_aspect_ratio);
Log.d(TAG, "preview_aspect_ratio: " + preview_aspect_ratio);
}
boolean preview_is_wide = preview_aspect_ratio > display_aspect_ratio + 1.0e-5f;
if( test_preview_want_no_limits ) {
preview_is_wide = test_preview_want_no_limits_value;
}
if( preview_is_wide ) {
if( MyDebug.LOG )
Log.d(TAG, "preview is wide, set want_no_limits");
this.want_no_limits = true;
if( !old_want_no_limits ) {
if( MyDebug.LOG )
Log.d(TAG, "need to change to FLAG_LAYOUT_NO_LIMITS");
// Ideally we'd just go straight to FLAG_LAYOUT_NO_LIMITS mode, but then all calls to onApplyWindowInsets()
// end up returning a value of 0 for the navigation_gap! So we need to wait until we know the navigation_gap.
if( navigation_gap != 0 ) {
// already have navigation gap, can go straight into no limits mode
if( MyDebug.LOG )
Log.d(TAG, "set FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(true);
// need to layout the UI again due to now taking the navigation gap into account
if( MyDebug.LOG )
Log.d(TAG, "layout UI due to changing want_no_limits behaviour");
mainUI.layoutUI();
}
else {
if( MyDebug.LOG )
Log.d(TAG, "but navigation_gap is 0");
}
}
}
else if( old_want_no_limits && navigation_gap != 0 ) {
if( MyDebug.LOG )
Log.d(TAG, "clear FLAG_LAYOUT_NO_LIMITS");
showUnderNavigation(false);
// need to layout the UI again due to no longer taking the navigation gap into account
if( MyDebug.LOG )
Log.d(TAG, "layout UI due to changing want_no_limits behaviour");
mainUI.layoutUI();
}
}
if( this.supportsForceVideo4K() && preview.usingCamera2API() ) {
if( MyDebug.LOG )
Log.d(TAG, "using Camera2 API, so can disable the force 4K option");
this.disableForceVideo4K();
}
if( this.supportsForceVideo4K() && preview.getVideoQualityHander().getSupportedVideoSizes() != null ) {
for(CameraController.Size size : preview.getVideoQualityHander().getSupportedVideoSizes()) {
if( size.width >= 3840 && size.height >= 2160 ) {
if( MyDebug.LOG )
Log.d(TAG, "camera natively supports 4K, so can disable the force option");
this.disableForceVideo4K();
}
}
}
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after handling Force 4K option: " + (System.currentTimeMillis() - debug_time));
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
{
if( MyDebug.LOG )
Log.d(TAG, "set up zoom");
if( MyDebug.LOG )
Log.d(TAG, "has_zoom? " + preview.supportsZoom());
SeekBar zoomSeekBar = findViewById(R.id.zoom_seekbar);
if( preview.supportsZoom() ) {
zoomSeekBar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
zoomSeekBar.setMax(preview.getMaxZoom());
zoomSeekBar.setProgress(preview.getMaxZoom()-preview.getCameraController().getZoom());
zoomSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private long last_haptic_time;
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if( MyDebug.LOG )
Log.d(TAG, "zoom onProgressChanged: " + progress);
// note we zoom even if !fromUser, as various other UI controls (multitouch, volume key zoom)
// indirectly set zoom via this method, from setting the zoom slider
// if hasSmoothZoom()==true, then the preview already handled zooming to the current value
if( !preview.hasSmoothZoom() ) {
int new_zoom_factor = preview.getMaxZoom() - progress;
if( fromUser && preview.getCameraController() != null ) {
float old_zoom_ratio = preview.getZoomRatio();
float new_zoom_ratio = preview.getZoomRatio(new_zoom_factor);
if( new_zoom_ratio != old_zoom_ratio ) {
last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
}
}
preview.zoomTo(new_zoom_factor, false, true);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
if( sharedPreferences.getBoolean(PreferenceKeys.ShowZoomSliderControlsPreferenceKey, true) ) {
if( !mainUI.inImmersiveMode() ) {
zoomSeekBar.setVisibility(View.VISIBLE);
}
}
else {
zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this; in future we might want this similarly for exposure panel
}
}
else {
zoomSeekBar.setVisibility(View.INVISIBLE); // should be INVISIBLE not GONE, as the focus_seekbar is aligned to be left to this; in future we might want this similarly for the exposure panel
}
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after setting up zoom: " + (System.currentTimeMillis() - debug_time));
View takePhotoButton = findViewById(R.id.take_photo);
if( sharedPreferences.getBoolean(PreferenceKeys.ShowTakePhotoPreferenceKey, true) ) {
if( !mainUI.inImmersiveMode() ) {
takePhotoButton.setVisibility(View.VISIBLE);
}
}
else {
takePhotoButton.setVisibility(View.INVISIBLE);
}
}
{
if( MyDebug.LOG )
Log.d(TAG, "set up manual focus");
setManualFocusSeekbar(false);
setManualFocusSeekbar(true);
}
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after setting up manual focus: " + (System.currentTimeMillis() - debug_time));
{
if( preview.supportsISORange()) {
if( MyDebug.LOG )
Log.d(TAG, "set up iso");
final SeekBar iso_seek_bar = findViewById(R.id.iso_seekbar);
iso_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
//setProgressSeekbarExponential(iso_seek_bar, preview.getMinimumISO(), preview.getMaximumISO(), preview.getCameraController().getISO());
manualSeekbars.setProgressSeekbarISO(iso_seek_bar, preview.getMinimumISO(), preview.getMaximumISO(), preview.getCameraController().getISO());
iso_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private long last_haptic_time;
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if( MyDebug.LOG )
Log.d(TAG, "iso seekbar onProgressChanged: " + progress);
/*double frac = progress/(double)iso_seek_bar.getMax();
if( MyDebug.LOG )
Log.d(TAG, "exposure_time frac: " + frac);
double scaling = MainActivity.seekbarScaling(frac);
if( MyDebug.LOG )
Log.d(TAG, "exposure_time scaling: " + scaling);
int min_iso = preview.getMinimumISO();
int max_iso = preview.getMaximumISO();
int iso = min_iso + (int)(scaling * (max_iso - min_iso));*/
/*int min_iso = preview.getMinimumISO();
int max_iso = preview.getMaximumISO();
int iso = (int)exponentialScaling(frac, min_iso, max_iso);*/
// n.b., important to update even if fromUser==false (e.g., so this works when user changes ISO via clicking
// the ISO buttons rather than moving the slider directly, see MainUI.setupExposureUI())
preview.setISO( manualSeekbars.getISO(progress) );
mainUI.updateSelectedISOButton();
if( fromUser ) {
last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
if( preview.supportsExposureTime() ) {
if( MyDebug.LOG )
Log.d(TAG, "set up exposure time");
final SeekBar exposure_time_seek_bar = findViewById(R.id.exposure_time_seekbar);
exposure_time_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
//setProgressSeekbarExponential(exposure_time_seek_bar, preview.getMinimumExposureTime(), preview.getMaximumExposureTime(), preview.getCameraController().getExposureTime());
manualSeekbars.setProgressSeekbarShutterSpeed(exposure_time_seek_bar, preview.getMinimumExposureTime(), preview.getMaximumExposureTime(), preview.getCameraController().getExposureTime());
exposure_time_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private long last_haptic_time;
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if( MyDebug.LOG )
Log.d(TAG, "exposure_time seekbar onProgressChanged: " + progress);
/*double frac = progress/(double)exposure_time_seek_bar.getMax();
if( MyDebug.LOG )
Log.d(TAG, "exposure_time frac: " + frac);
long min_exposure_time = preview.getMinimumExposureTime();
long max_exposure_time = preview.getMaximumExposureTime();
long exposure_time = exponentialScaling(frac, min_exposure_time, max_exposure_time);*/
preview.setExposureTime( manualSeekbars.getExposureTime(progress) );
if( fromUser ) {
last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
}
}
setManualWBSeekbar();
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after setting up iso: " + (System.currentTimeMillis() - debug_time));
{
exposure_seekbar_values = null;
if( preview.supportsExposures() ) {
if( MyDebug.LOG )
Log.d(TAG, "set up exposure compensation");
final int min_exposure = preview.getMinimumExposure();
SeekBar exposure_seek_bar = findViewById(R.id.exposure_seekbar);
exposure_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
final int exposure_seekbar_n_repeated_zero = 3; // how many times to repeat 0 for R.id.exposure_seekbar, so that it "sticks" to zero when changing seekbar
//exposure_seek_bar.setMax( preview.getMaximumExposure() - min_exposure + exposure_seekbar_n_repeated_zero-1 );
//exposure_seek_bar.setProgress( preview.getCurrentExposure() - min_exposure );
exposure_seekbar_values = new ArrayList<>();
int current_exposure = preview.getCurrentExposure();
int current_progress = 0;
for(int i=min_exposure;i<=preview.getMaximumExposure();i++) {
exposure_seekbar_values.add(i);
if( i == 0 ) {
exposure_seekbar_values_zero = exposure_seekbar_values.size()-1;
exposure_seekbar_values_zero += (exposure_seekbar_n_repeated_zero-1)/2; // centre within the region of zeroes
for(int j=0;j<exposure_seekbar_n_repeated_zero-1;j++) {
exposure_seekbar_values.add(i);
}
}
if( i == current_exposure ) {
if( i == 0 ) {
current_progress += exposure_seekbar_values_zero;
}
else {
current_progress = exposure_seekbar_values.size()-1;
}
}
}
exposure_seek_bar.setMax( exposure_seekbar_values.size()-1 );
exposure_seek_bar.setProgress( current_progress );
exposure_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private long last_haptic_time;
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if( MyDebug.LOG )
Log.d(TAG, "exposure seekbar onProgressChanged: " + progress);
if( exposure_seekbar_values == null ) {
Log.e(TAG, "exposure_seekbar_values is null");
return;
}
int new_exposure = getExposureSeekbarValue(progress);
if( fromUser ) {
// check if not scrolling past the repeated zeroes
if( preview.getCurrentExposure() != new_exposure ) {
last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
}
}
preview.setExposure(new_exposure);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
}
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after setting up exposure: " + (System.currentTimeMillis() - debug_time));
// On-screen icons such as exposure lock, white balance lock, face detection etc are made visible if necessary in
// MainUI.showGUI()
// However still need to enable visibility of icons where visibility depends on camera setup - e.g., exposure button
// not supported for high speed video frame rates - see testTakeVideoFPSHighSpeedManual().
// (Disabling is done in checkDisableGUIIcons(), called below.)
View exposureButton = findViewById(R.id.exposure);
//exposureButton.setVisibility(supportsExposureButton() && !mainUI.inImmersiveMode() ? View.VISIBLE : View.GONE);
if( supportsExposureButton() && !mainUI.inImmersiveMode() )
exposureButton.setVisibility(View.VISIBLE);
// needed as availability of some icons is per-camera (e.g., flash, RAW)
// for making icons visible, this is done elsewhere in call to MainUI.showGUI()
if( checkDisableGUIIcons() ) {
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: need to layoutUI as we hid some icons");
mainUI.layoutUI();
}
// need to update some icons, e.g., white balance and exposure lock due to them being turned off when pause/resuming
mainUI.updateOnScreenIcons();
mainUI.setPopupIcon(); // needed so that the icon is set right even if no flash mode is set when starting up camera (e.g., switching to front camera with no flash)
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after setting popup icon: " + (System.currentTimeMillis() - debug_time));
mainUI.setTakePhotoIcon();
mainUI.setSwitchCameraContentDescription();
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: time after setting take photo icon: " + (System.currentTimeMillis() - debug_time));
if( !block_startup_toast ) {
this.showPhotoVideoToast(false);
}
block_startup_toast = false;
if( MyDebug.LOG )
Log.d(TAG, "cameraSetup: total time for cameraSetup: " + (System.currentTimeMillis() - debug_time));
this.getApplicationInterface().getDrawPreview().setDimPreview(false);
if( push_switched_camera ) {
push_switched_camera = false;
View switchCameraButton = findViewById(R.id.switch_camera);
switchCameraButton.animate().rotationBy(180).setDuration(250).setInterpolator(new AccelerateDecelerateInterpolator()).start();
}
}
public static long performHapticFeedback(SeekBar seekBar, long last_haptic_time) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(seekBar.getContext());
if( sharedPreferences.getBoolean(PreferenceKeys.AllowHapticFeedbackPreferenceKey, true) ) {
long time_ms = System.currentTimeMillis();
if( time_ms > last_haptic_time + 16 ) {
last_haptic_time = time_ms;
// SEGMENT_TICK or SEGMENT_TICK doesn't work on Galaxy S24+ at least, even though on Android 14!
/*if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ) {
seekBar.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK);
}
else*/ {
seekBar.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
}
}
}
return last_haptic_time;
}
public void setManualFocusSeekbarProgress(final boolean is_target_distance, float focus_distance) {
final SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar);
ManualSeekbars.setProgressSeekbarScaled(focusSeekBar, 0.0, preview.getMinimumFocusDistance(), focus_distance);
}
private void setManualFocusSeekbar(final boolean is_target_distance) {
if( MyDebug.LOG )
Log.d(TAG, "setManualFocusSeekbar");
final SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar);
focusSeekBar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
setManualFocusSeekbarProgress(is_target_distance, is_target_distance ? preview.getCameraController().getFocusBracketingTargetDistance() : preview.getCameraController().getFocusDistance());
focusSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private boolean has_saved_zoom;
private int saved_zoom_factor;
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if( !is_target_distance && applicationInterface.isFocusBracketingSourceAutoPref() ) {
// source is set from continuous focus, not by changing the seekbar
if( fromUser ) {
// but if user has manually changed, then exit auto mode
applicationInterface.setFocusBracketingSourceAutoPref(false);
mainUI.destroyPopup(); // need to recreate popup
}
else {
return;
}
}
double frac = progress/(double)focusSeekBar.getMax();
double scaling = ManualSeekbars.seekbarScaling(frac);
float focus_distance = (float)(scaling * preview.getMinimumFocusDistance());
preview.setFocusDistance(focus_distance, is_target_distance, true);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if( MyDebug.LOG )
Log.d(TAG, "manual focus seekbar: onStartTrackingTouch");
has_saved_zoom = false;
if( preview.supportsZoom() ) {
int focus_assist = applicationInterface.getFocusAssistPref();
if( focus_assist > 0 && preview.getCameraController() != null ) {
has_saved_zoom = true;
saved_zoom_factor = preview.getCameraController().getZoom();
if( MyDebug.LOG )
Log.d(TAG, "zoom by " + focus_assist + " for focus assist, zoom factor was: " + saved_zoom_factor);
int new_zoom_factor = preview.getScaledZoomFactor(focus_assist);
preview.getCameraController().setZoom(new_zoom_factor);
}
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if( MyDebug.LOG )
Log.d(TAG, "manual focus seekbar: onStopTrackingTouch");
if( has_saved_zoom && preview.getCameraController() != null ) {
if( MyDebug.LOG )
Log.d(TAG, "unzoom for focus assist, zoom factor was: " + saved_zoom_factor);
preview.getCameraController().setZoom(saved_zoom_factor);
}
preview.stoppedSettingFocusDistance(is_target_distance);
}
});
setManualFocusSeekBarVisibility(is_target_distance);
}
public boolean showManualFocusSeekbar(final boolean is_target_distance) {
if( (applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.FocusBracketing) && !preview.isVideo() ) {
return true; // both seekbars shown in focus bracketing mode
}
if( is_target_distance ) {
return false; // target seekbar only shown in focus bracketing mode
}
boolean is_visible = preview.getCurrentFocusValue() != null && this.getPreview().getCurrentFocusValue().equals("focus_mode_manual2");
return is_visible;
}
void setManualFocusSeekBarVisibility(final boolean is_target_distance) {
boolean is_visible = showManualFocusSeekbar(is_target_distance);
SeekBar focusSeekBar = findViewById(is_target_distance ? R.id.focus_bracketing_target_seekbar : R.id.focus_seekbar);
final int visibility = is_visible ? View.VISIBLE : View.GONE;
focusSeekBar.setVisibility(visibility);
if( is_visible ) {
applicationInterface.getDrawPreview().updateSettings(); // needed so that we reset focus_seekbars_margin_left, as the focus seekbars can only be updated when visible
}
}
public void setManualWBSeekbar() {
if( MyDebug.LOG )
Log.d(TAG, "setManualWBSeekbar");
if( preview.getSupportedWhiteBalances() != null && preview.supportsWhiteBalanceTemperature() ) {
if( MyDebug.LOG )
Log.d(TAG, "set up manual white balance");
SeekBar white_balance_seek_bar = findViewById(R.id.white_balance_seekbar);
white_balance_seek_bar.setOnSeekBarChangeListener(null); // clear an existing listener - don't want to call the listener when setting up the progress bar to match the existing state
final int minimum_temperature = preview.getMinimumWhiteBalanceTemperature();
final int maximum_temperature = preview.getMaximumWhiteBalanceTemperature();
/*
// white balance should use linear scaling
white_balance_seek_bar.setMax(maximum_temperature - minimum_temperature);
white_balance_seek_bar.setProgress(preview.getCameraController().getWhiteBalanceTemperature() - minimum_temperature);
*/
manualSeekbars.setProgressSeekbarWhiteBalance(white_balance_seek_bar, minimum_temperature, maximum_temperature, preview.getCameraController().getWhiteBalanceTemperature());
white_balance_seek_bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private long last_haptic_time;
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if( MyDebug.LOG )
Log.d(TAG, "white balance seekbar onProgressChanged: " + progress);
//int temperature = minimum_temperature + progress;
//preview.setWhiteBalanceTemperature(temperature);
preview.setWhiteBalanceTemperature( manualSeekbars.getWhiteBalanceTemperature(progress) );
if( fromUser ) {
last_haptic_time = performHapticFeedback(seekBar, last_haptic_time);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
}
public boolean supportsAutoStabilise() {
if( applicationInterface.isRawOnly() )
return false; // if not saving JPEGs, no point having auto-stabilise mode, as it won't affect the RAW images
if( applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama )
return false; // not supported in panorama mode
return this.supports_auto_stabilise;
}
/** Returns whether the device supports auto-level at all. Most callers probably want to use
* supportsAutoStabilise() which also checks whether auto-level is allowed with current options.
*/
public boolean deviceSupportsAutoStabilise() {
return this.supports_auto_stabilise;
}
public boolean supportsDRO() {
if( applicationInterface.isRawOnly(MyApplicationInterface.PhotoMode.DRO) )
return false; // if not saving JPEGs, no point having DRO mode, as it won't affect the RAW images
return true;
}
public boolean supportsHDR() {
// we also require the device have sufficient memory to do the processing
return large_heap_memory >= 128 && preview.supportsExpoBracketing();
}
public boolean supportsExpoBracketing() {
if( applicationInterface.isImageCaptureIntent() )
return false; // don't support expo bracketing mode if called from image capture intent
return preview.supportsExpoBracketing();
}
public boolean supportsFocusBracketing() {
if( applicationInterface.isImageCaptureIntent() )
return false; // don't support focus bracketing mode if called from image capture intent
return preview.supportsFocusBracketing();
}
/** Whether we support the auto mode for setting source focus distance for focus bracketing mode.
* Note the caller should still separately call supportsFocusBracketing() to see if focus
* bracketing is supported in the first place.
*/
public boolean supportsFocusBracketingSourceAuto() {
return preview.supportsFocus() && preview.getSupportedFocusValues().contains("focus_mode_continuous_picture");
}
public boolean supportsPanorama() {
// don't support panorama mode if called from image capture intent
// in theory this works, but problem that currently we'd end up doing the processing on the UI thread, so risk ANR
if( applicationInterface.isImageCaptureIntent() )
return false;
// require 256MB just to be safe, due to the large number of images that may be created
// remember to update the FAQ "Why isn't Panorama supported on my device?" if this changes
return large_heap_memory >= 256 && applicationInterface.getGyroSensor().hasSensors();
//return false; // currently blocked for release
}
public boolean supportsFastBurst() {
if( applicationInterface.isImageCaptureIntent() )
return false; // don't support burst mode if called from image capture intent
// require 512MB just to be safe, due to the large number of images that may be created
return( preview.usingCamera2API() && large_heap_memory >= 512 && preview.supportsBurst() );
}
public boolean supportsNoiseReduction() {
// we require Android 7 to limit to more modern devices (for performance reasons)
return( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && preview.usingCamera2API() && large_heap_memory >= 512 && preview.supportsBurst() && preview.supportsExposureTime() );
//return false; // currently blocked for release
}
/** Whether the Camera vendor extension is supported (see
* https://developer.android.com/reference/android/hardware/camera2/CameraExtensionCharacteristics ).
*/
public boolean supportsCameraExtension(int extension) {
return preview.supportsCameraExtension(extension);
}
/** Whether RAW mode would be supported for various burst modes (expo bracketing etc).
* Note that caller should still separately check preview.supportsRaw() if required.
*/
public boolean supportsBurstRaw() {
return( large_heap_memory >= 512 );
}
public boolean supportsOptimiseFocusLatency() {
// whether to support optimising focus for latency
// in theory this works on any device, as well as old or Camera2 API, but restricting this for now to avoid risk of poor default behaviour
// on older devices
return( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preview.usingCamera2API() );
}
public boolean supportsPreviewBitmaps() {
// In practice we only use TextureView on Android 5+ (with Camera2 API enabled) anyway, but have put an explicit check here -
return preview.getView() instanceof TextureView && large_heap_memory >= 128;
}
public boolean supportsPreShots() {
// Need at least Android 5+ for TextureView
// Need at least Android 8+ for video encoding classes
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && preview.getView() instanceof TextureView && large_heap_memory >= 512;
}
private int maxExpoBracketingNImages() {
return preview.maxExpoBracketingNImages();
}
public boolean supportsForceVideo4K() {
return this.supports_force_video_4k;
}
public boolean supportsCamera2() {
return this.supports_camera2;
}
private void disableForceVideo4K() {
this.supports_force_video_4k = false;
}
public Preview getPreview() {
return this.preview;
}
public boolean isCameraInBackground() {
return this.camera_in_background;
}
public boolean isAppPaused() {
return this.app_is_paused;
}
public BluetoothRemoteControl getBluetoothRemoteControl() {
return bluetoothRemoteControl;
}
public PermissionHandler getPermissionHandler() {
return permissionHandler;
}
public SettingsManager getSettingsManager() {
return settingsManager;
}
public MainUI getMainUI() {
return this.mainUI;
}
public ManualSeekbars getManualSeekbars() {
return this.manualSeekbars;
}
public MyApplicationInterface getApplicationInterface() {
return this.applicationInterface;
}
public TextFormatter getTextFormatter() {
return this.textFormatter;
}
SoundPoolManager getSoundPoolManager() {
return this.soundPoolManager;
}
public LocationSupplier getLocationSupplier() {
return this.applicationInterface.getLocationSupplier();
}
public StorageUtils getStorageUtils() {
return this.applicationInterface.getStorageUtils();
}
public File getImageFolder() {
return this.applicationInterface.getStorageUtils().getImageFolder();
}
public ToastBoxer getChangedAutoStabiliseToastBoxer() {
return changed_auto_stabilise_toast;
}
private String getPhotoModeString(MyApplicationInterface.PhotoMode photo_mode, boolean string_for_std) {
String photo_mode_string = null;
switch( photo_mode ) {
case Standard:
if( string_for_std )
photo_mode_string = getResources().getString(R.string.photo_mode_standard_full);
break;
case DRO:
photo_mode_string = getResources().getString(R.string.photo_mode_dro);
break;
case HDR:
photo_mode_string = getResources().getString(R.string.photo_mode_hdr);
break;
case ExpoBracketing:
photo_mode_string = getResources().getString(R.string.photo_mode_expo_bracketing_full);
break;
case FocusBracketing: {
photo_mode_string = getResources().getString(R.string.photo_mode_focus_bracketing_full);
int n_images = applicationInterface.getFocusBracketingNImagesPref();
photo_mode_string += " (" + n_images + ")";
break;
}
case FastBurst: {
photo_mode_string = getResources().getString(R.string.photo_mode_fast_burst_full);
int n_images = applicationInterface.getBurstNImages();
photo_mode_string += " (" + n_images + ")";
break;
}
case NoiseReduction:
photo_mode_string = getResources().getString(R.string.photo_mode_noise_reduction_full);
break;
case Panorama:
photo_mode_string = getResources().getString(R.string.photo_mode_panorama_full);
break;
case X_Auto:
photo_mode_string = getResources().getString(R.string.photo_mode_x_auto_full);
break;
case X_HDR:
photo_mode_string = getResources().getString(R.string.photo_mode_x_hdr_full);
break;
case X_Night:
photo_mode_string = getResources().getString(R.string.photo_mode_x_night_full);
break;
case X_Bokeh:
photo_mode_string = getResources().getString(R.string.photo_mode_x_bokeh_full);
break;
case X_Beauty:
photo_mode_string = getResources().getString(R.string.photo_mode_x_beauty_full);
break;
}
return photo_mode_string;
}
/** Displays a toast with information about the current preferences.
* If always_show is true, the toast is always displayed; otherwise, we only display
* a toast if it's important to notify the user (i.e., unusual non-default settings are
* set). We want a balance between not pestering the user too much, whilst also reminding
* them if certain settings are on.
*/
private void showPhotoVideoToast(boolean always_show) {
if( MyDebug.LOG ) {
Log.d(TAG, "showPhotoVideoToast");
Log.d(TAG, "always_show? " + always_show);
}
CameraController camera_controller = preview.getCameraController();
if( camera_controller == null || this.camera_in_background ) {
if( MyDebug.LOG )
Log.d(TAG, "camera not open or in background");
return;
}
String toast_string;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean simple = true;
boolean video_high_speed = preview.isVideoHighSpeed();
MyApplicationInterface.PhotoMode photo_mode = applicationInterface.getPhotoMode();
if( preview.isVideo() ) {
VideoProfile profile = preview.getVideoProfile();
String extension_string = profile.fileExtension;
if( !profile.fileExtension.equals("mp4") ) {
simple = false;
}
String bitrate_string;
if( profile.videoBitRate >= 10000000 )
bitrate_string = profile.videoBitRate/1000000 + "Mbps";
else if( profile.videoBitRate >= 10000 )
bitrate_string = profile.videoBitRate/1000 + "Kbps";
else
bitrate_string = profile.videoBitRate + "bps";
String bitrate_value = applicationInterface.getVideoBitratePref();
if( !bitrate_value.equals("default") ) {
simple = false;
}
double capture_rate = profile.videoCaptureRate;
String capture_rate_string = (capture_rate < 9.5f) ? new DecimalFormat("#0.###").format(capture_rate) : String.valueOf((int) (profile.videoCaptureRate + 0.5));
toast_string = getResources().getString(R.string.video) + ": " + profile.videoFrameWidth + "x" + profile.videoFrameHeight + "\n" +
capture_rate_string + getResources().getString(R.string.fps) + (video_high_speed ? " [" + getResources().getString(R.string.high_speed) + "]" : "") + ", " + bitrate_string + " (" + extension_string + ")";
String fps_value = applicationInterface.getVideoFPSPref();
if( !fps_value.equals("default") || video_high_speed ) {
simple = false;
}
float capture_rate_factor = applicationInterface.getVideoCaptureRateFactor();
if( Math.abs(capture_rate_factor - 1.0f) > 1.0e-5 ) {
toast_string += "\n" + getResources().getString(R.string.preference_video_capture_rate) + ": " + capture_rate_factor + "x";
simple = false;
}
{
CameraController.TonemapProfile tonemap_profile = applicationInterface.getVideoTonemapProfile();
if( tonemap_profile != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && preview.supportsTonemapCurve() ) {
if( applicationInterface.getVideoTonemapProfile() != CameraController.TonemapProfile.TONEMAPPROFILE_OFF && preview.supportsTonemapCurve() ) {
int string_id = 0;
switch( tonemap_profile ) {
case TONEMAPPROFILE_REC709:
string_id = R.string.preference_video_rec709;
break;
case TONEMAPPROFILE_SRGB:
string_id = R.string.preference_video_srgb;
break;
case TONEMAPPROFILE_LOG:
string_id = R.string.video_log;
break;
case TONEMAPPROFILE_GAMMA:
string_id = R.string.preference_video_gamma;
break;
case TONEMAPPROFILE_JTVIDEO:
string_id = R.string.preference_video_jtvideo;
break;
case TONEMAPPROFILE_JTLOG:
string_id = R.string.preference_video_jtlog;
break;
case TONEMAPPROFILE_JTLOG2:
string_id = R.string.preference_video_jtlog2;
break;
}
if( string_id != 0 ) {
simple = false;
toast_string += "\n" + getResources().getString(string_id);
if( tonemap_profile == CameraController.TonemapProfile.TONEMAPPROFILE_GAMMA ) {
toast_string += " " + applicationInterface.getVideoProfileGamma();
}
}
else {
Log.e(TAG, "unknown tonemap_profile: " + tonemap_profile);
}
}
}
}
boolean record_audio = applicationInterface.getRecordAudioPref();
if( !record_audio ) {
toast_string += "\n" + getResources().getString(R.string.audio_disabled);
simple = false;
}
String max_duration_value = sharedPreferences.getString(PreferenceKeys.VideoMaxDurationPreferenceKey, "0");
if( !max_duration_value.isEmpty() && !max_duration_value.equals("0") ) {
String [] entries_array = getResources().getStringArray(R.array.preference_video_max_duration_entries);
String [] values_array = getResources().getStringArray(R.array.preference_video_max_duration_values);
int index = Arrays.asList(values_array).indexOf(max_duration_value);
if( index != -1 ) { // just in case!
String entry = entries_array[index];
toast_string += "\n" + getResources().getString(R.string.max_duration) +": " + entry;
simple = false;
}
}
long max_filesize = applicationInterface.getVideoMaxFileSizeUserPref();
if( max_filesize != 0 ) {
toast_string += "\n" + getResources().getString(R.string.max_filesize) +": ";
if( max_filesize >= 1024*1024*1024 ) {
long max_filesize_gb = max_filesize/(1024*1024*1024);
toast_string += max_filesize_gb + getResources().getString(R.string.gb_abbreviation);
}
else {
long max_filesize_mb = max_filesize/(1024*1024);
toast_string += max_filesize_mb + getResources().getString(R.string.mb_abbreviation);
}
simple = false;
}
if( applicationInterface.getVideoFlashPref() && preview.supportsFlash() ) {
toast_string += "\n" + getResources().getString(R.string.preference_video_flash);
simple = false;
}
}
else {
if( photo_mode == MyApplicationInterface.PhotoMode.Panorama ) {
// don't show resolution in panorama mode
toast_string = "";
}
else {
toast_string = getResources().getString(R.string.photo);
CameraController.Size current_size = preview.getCurrentPictureSize();
toast_string += " " + current_size.width + "x" + current_size.height;
}
String photo_mode_string = getPhotoModeString(photo_mode, false);
if( photo_mode_string != null ) {
toast_string += (toast_string.isEmpty() ? "" : "\n") + getResources().getString(R.string.photo_mode) + ": " + photo_mode_string;
if( photo_mode != MyApplicationInterface.PhotoMode.DRO && photo_mode != MyApplicationInterface.PhotoMode.HDR && photo_mode != MyApplicationInterface.PhotoMode.NoiseReduction )
simple = false;
}
if( preview.supportsFocus() && preview.getSupportedFocusValues().size() > 1 && photo_mode != MyApplicationInterface.PhotoMode.FocusBracketing ) {
String focus_value = preview.getCurrentFocusValue();
if( focus_value != null && !focus_value.equals("focus_mode_auto") && !focus_value.equals("focus_mode_continuous_picture") ) {
String focus_entry = preview.findFocusEntryForValue(focus_value);
if( focus_entry != null ) {
toast_string += "\n" + focus_entry;
}
}
}
if( applicationInterface.getAutoStabilisePref() ) {
// important as users are sometimes confused at the behaviour if they don't realise the option is on
toast_string += (toast_string.isEmpty() ? "" : "\n") + getResources().getString(R.string.preference_auto_stabilise);
simple = false;
}
}
if( applicationInterface.getFaceDetectionPref() ) {
// important so that the user realises why touching for focus/metering areas won't work - easy to forget that face detection has been turned on!
toast_string += "\n" + getResources().getString(R.string.preference_face_detection);
simple = false;
}
if( !video_high_speed ) {
//manual ISO only supported for high speed video
String iso_value = applicationInterface.getISOPref();
if( !iso_value.equals(CameraController.ISO_DEFAULT) ) {
toast_string += "\nISO: " + iso_value;
if( preview.supportsExposureTime() ) {
long exposure_time_value = applicationInterface.getExposureTimePref();
toast_string += " " + preview.getExposureTimeString(exposure_time_value);
}
simple = false;
}
int current_exposure = camera_controller.getExposureCompensation();
if( current_exposure != 0 ) {
toast_string += "\n" + preview.getExposureCompensationString(current_exposure);
simple = false;
}
}
try {
String scene_mode = camera_controller.getSceneMode();
String white_balance = camera_controller.getWhiteBalance();
String color_effect = camera_controller.getColorEffect();
if( scene_mode != null && !scene_mode.equals(CameraController.SCENE_MODE_DEFAULT) ) {
toast_string += "\n" + getResources().getString(R.string.scene_mode) + ": " + mainUI.getEntryForSceneMode(scene_mode);
simple = false;
}
if( white_balance != null && !white_balance.equals(CameraController.WHITE_BALANCE_DEFAULT) ) {
toast_string += "\n" + getResources().getString(R.string.white_balance) + ": " + mainUI.getEntryForWhiteBalance(white_balance);
if( white_balance.equals("manual") && preview.supportsWhiteBalanceTemperature() ) {
toast_string += " " + camera_controller.getWhiteBalanceTemperature();
}
simple = false;
}
if( color_effect != null && !color_effect.equals(CameraController.COLOR_EFFECT_DEFAULT) ) {
toast_string += "\n" + getResources().getString(R.string.color_effect) + ": " + mainUI.getEntryForColorEffect(color_effect);
simple = false;
}
}
catch(RuntimeException e) {
// catch runtime error from camera_controller old API from camera.getParameters()
MyDebug.logStackTrace(TAG, "failed to get info from camera controller", e);
}
String lock_orientation = applicationInterface.getLockOrientationPref();
if( !lock_orientation.equals("none") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) {
// panorama locks to portrait, but don't want to display that in the toast
String [] entries_array = getResources().getStringArray(R.array.preference_lock_orientation_entries);
String [] values_array = getResources().getStringArray(R.array.preference_lock_orientation_values);
int index = Arrays.asList(values_array).indexOf(lock_orientation);
if( index != -1 ) { // just in case!
String entry = entries_array[index];
toast_string += "\n" + entry;
simple = false;
}
}
String timer = sharedPreferences.getString(PreferenceKeys.TimerPreferenceKey, "0");
if( !timer.equals("0") && photo_mode != MyApplicationInterface.PhotoMode.Panorama ) {
String [] entries_array = getResources().getStringArray(R.array.preference_timer_entries);
String [] values_array = getResources().getStringArray(R.array.preference_timer_values);
int index = Arrays.asList(values_array).indexOf(timer);
if( index != -1 ) { // just in case!
String entry = entries_array[index];
toast_string += "\n" + getResources().getString(R.string.preference_timer) + ": " + entry;
simple = false;
}
}
String repeat = applicationInterface.getRepeatPref();
if( !repeat.equals("1") ) {
String [] entries_array = getResources().getStringArray(R.array.preference_burst_mode_entries);
String [] values_array = getResources().getStringArray(R.array.preference_burst_mode_values);
int index = Arrays.asList(values_array).indexOf(repeat);
if( index != -1 ) { // just in case!
String entry = entries_array[index];
toast_string += "\n" + getResources().getString(R.string.preference_burst_mode) + ": " + entry;
simple = false;
}
}
/*if( audio_listener != null ) {
toast_string += "\n" + getResources().getString(R.string.preference_audio_noise_control);
}*/
if( MyDebug.LOG ) {
Log.d(TAG, "toast_string: " + toast_string);
Log.d(TAG, "simple?: " + simple);
Log.d(TAG, "push_info_toast_text: " + push_info_toast_text);
}
final boolean use_fake_toast = true;
if( !simple || always_show ) {
if( push_info_toast_text != null ) {
toast_string = push_info_toast_text + "\n" + toast_string;
}
preview.showToast(switch_video_toast, toast_string, use_fake_toast);
}
else if( push_info_toast_text != null ) {
preview.showToast(switch_video_toast, push_info_toast_text, use_fake_toast);
}
push_info_toast_text = null; // reset
}
private void freeAudioListener(boolean wait_until_done) {
if( MyDebug.LOG )
Log.d(TAG, "freeAudioListener");
if( audio_listener != null ) {
audio_listener.release(wait_until_done);
audio_listener = null;
}
mainUI.audioControlStopped();
}
private void startAudioListener() {
if( MyDebug.LOG )
Log.d(TAG, "startAudioListener");
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
// we restrict the checks to Android 6 or later just in case, see note in LocationSupplier.setupLocationListener()
if( MyDebug.LOG )
Log.d(TAG, "check for record audio permission");
if( ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) {
if( MyDebug.LOG )
Log.d(TAG, "record audio permission not available");
applicationInterface.requestRecordAudioPermission();
return;
}
}
MyAudioTriggerListenerCallback callback = new MyAudioTriggerListenerCallback(this);
audio_listener = new AudioListener(callback);
if( audio_listener.status() ) {
preview.showToast(audio_control_toast, R.string.audio_listener_started, true);
audio_listener.start();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String sensitivity_pref = sharedPreferences.getString(PreferenceKeys.AudioNoiseControlSensitivityPreferenceKey, "0");
int audio_noise_sensitivity;
switch(sensitivity_pref) {
case "3":
audio_noise_sensitivity = 50;
break;
case "2":
audio_noise_sensitivity = 75;
break;
case "1":
audio_noise_sensitivity = 125;
break;
case "-1":
audio_noise_sensitivity = 150;
break;
case "-2":
audio_noise_sensitivity = 200;
break;
case "-3":
audio_noise_sensitivity = 400;
break;
default:
// default
audio_noise_sensitivity = 100;
break;
}
callback.setAudioNoiseSensitivity(audio_noise_sensitivity);
mainUI.audioControlStarted();
}
else {
audio_listener.release(true); // shouldn't be needed, but just to be safe
audio_listener = null;
preview.showToast(null, R.string.audio_listener_failed);
}
}
public boolean hasAudioControl() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
String audio_control = sharedPreferences.getString(PreferenceKeys.AudioControlPreferenceKey, "none");
/*if( audio_control.equals("voice") ) {
return speechControl.hasSpeechRecognition();
}
else*/ if( audio_control.equals("noise") ) {
return true;
}
return false;
}
/*void startAudioListeners() {
initAudioListener();
// no need to restart speech recognizer, as we didn't free it in stopAudioListeners(), and it's controlled by a user button
}*/
public void stopAudioListeners() {
freeAudioListener(true);
/*if( speechControl.hasSpeechRecognition() ) {
// no need to free the speech recognizer, just stop it
speechControl.stopListening();
}*/
}
public void initLocation() {
if( MyDebug.LOG )
Log.d(TAG, "initLocation");
if( app_is_paused ) {
if( MyDebug.LOG )
Log.d(TAG, "initLocation: app is paused!");
// we shouldn't need this (as we only call initLocation() when active), but just in case we end up here after onPause...
// in fact this happens when we need to grant permission for location - the call to initLocation() from
// MainActivity.onRequestPermissionsResult()->PermissionsHandler.onRequestPermissionsResult() will be when the application
// is still paused - so we won't do anything here, but instead initLocation() will be called after when resuming.
}
else if( camera_in_background ) {
if( MyDebug.LOG )
Log.d(TAG, "initLocation: camera in background!");
// we will end up here if app is pause/resumed when camera in background (settings, dialog, etc)
}
else if( !applicationInterface.getLocationSupplier().setupLocationListener() ) {
if( MyDebug.LOG )
Log.d(TAG, "location permission not available, so request permission");
permissionHandler.requestLocationPermission();
}
}
private void initGyroSensors() {
if( MyDebug.LOG )
Log.d(TAG, "initGyroSensors");
if( applicationInterface.getPhotoMode() == MyApplicationInterface.PhotoMode.Panorama ) {
applicationInterface.getGyroSensor().enableSensors();
}
else {
applicationInterface.getGyroSensor().disableSensors();
}
}
void speak(String text) {
if( textToSpeech != null && textToSpeechSuccess ) {
textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if( MyDebug.LOG )
Log.d(TAG, "onRequestPermissionsResult: requestCode " + requestCode);
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
permissionHandler.onRequestPermissionsResult(requestCode, grantResults);
}
public void restartOpenCamera() {
if( MyDebug.LOG )
Log.d(TAG, "restartOpenCamera");
this.waitUntilImageQueueEmpty();
// see http://stackoverflow.com/questions/2470870/force-application-to-restart-on-first-activity
Intent intent = this.getBaseContext().getPackageManager().getLaunchIntentForPackage( this.getBaseContext().getPackageName() );
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
this.startActivity(intent);
}
public void takePhotoButtonLongClickCancelled() {
if( MyDebug.LOG )
Log.d(TAG, "takePhotoButtonLongClickCancelled");
if( preview.getCameraController() != null && preview.getCameraController().isContinuousBurstInProgress() ) {
preview.getCameraController().stopContinuousBurst();
}
}
// for testing:
public SaveLocationHistory getSaveLocationHistory() {
return this.save_location_history;
}
public SaveLocationHistory getSaveLocationHistorySAF() {
return this.save_location_history_saf;
}
public void usedFolderPicker() {
if( applicationInterface.getStorageUtils().isUsingSAF() ) {
save_location_history_saf.updateFolderHistory(getStorageUtils().getSaveLocationSAF(), true);
}
else {
save_location_history.updateFolderHistory(getStorageUtils().getSaveLocation(), true);
}
}
public boolean hasThumbnailAnimation() {
return this.applicationInterface.hasThumbnailAnimation();
}
/*public boolean testHasNotification() {
return has_notification;
}*/
}