-
Notifications
You must be signed in to change notification settings - Fork 19
/
MainActivity.java
5697 lines (5214 loc) · 275 KB
/
MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package net.sourceforge.opencamera;
import android.Manifest;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
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.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.renderscript.RenderScript;
import android.speech.tts.TextToSpeech;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spanned;
import android.util.Log;
import android.view.Display;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
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.widget.EditText;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.ZoomControls;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.exifinterface.media.ExifInterface;
import com.googleresearch.capturesync.SoftwareSyncController;
import com.googleresearch.capturesync.softwaresync.SoftwareSyncLeader;
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.recsync.SyncSettingsContainer;
import net.sourceforge.opencamera.remotecontrol.BluetoothRemoteControl;
import net.sourceforge.opencamera.sensorlogging.RawSensorInfo;
import net.sourceforge.opencamera.sensorremote.RemoteRpcServer;
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.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** The main Activity for Open Camera.
*/
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private static final int WRITE_EXTERNAL_STORAGE = 0;
private static final int REQUEST_PERMISSION = 0;
private static int activity_count = 0;
private boolean app_is_paused = true;
private boolean settings_is_active = false;
private SensorManager mSensorManager;
private Sensor mSensorAccelerometer;
private RawSensorInfo mRawSensorInfo;
// components: always non-null (after onCreate())
private BluetoothRemoteControl bluetoothRemoteControl;
private PermissionHandler permissionHandler;
private SettingsManager settingsManager;
private MainUI mainUI;
private ManualSeekbars manualSeekbars;
private ExtendedAppInterface applicationInterface;
private TextFormatter textFormatter;
private SoundPoolManager soundPoolManager;
private MagneticSensor magneticSensor;
private SpeechControl speechControl;
private Preview preview;
private OrientationEventListener orientationEventListener;
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 TextToSpeech textToSpeech;
private boolean textToSpeechSuccess;
private AudioListener audio_listener; // may be null - created when needed
//private boolean ui_placement_right = true;
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;
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;
// 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 final ToastBoxer rec_sync_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()
// 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;
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;
private RemoteRpcServer mRpcServer;
/**
* Outer onCreate() for OpenCamera Sensors additional
* initial operations
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
int permissionCheckStorage = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permissionCheckStorage != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions( MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERMISSION);
return;
}
this.mRawSensorInfo = new RawSensorInfo(this);
if (MyDebug.LOG) {
Log.d(TAG, "Created RawSensorInfo object");
}
onCreateInner(savedInstanceState);
super.onCreate(savedInstanceState);
}
public RawSensorInfo getRawSensorInfoManager() {
return this.mRawSensorInfo;
}
/**
* Inner onCreate() from Open Camera code, should be called in outer
* @param savedInstanceState
*/
private void onCreateInner(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);
super.onCreate(savedInstanceState);
// don't show orientation animations
WindowManager.LayoutParams layout = getWindow().getAttributes();
layout.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
getWindow().setAttributes(layout);
setContentView(R.layout.activity_main);
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // initialise any unset preferences to their default values
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting default preference values: " + (System.currentTimeMillis() - debug_time));
// Enable all sensors on startup
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor prefEditor = sharedPreferences.edit();
prefEditor.putBoolean(PreferenceKeys.AccelPreferenceKey, true);
prefEditor.putBoolean(PreferenceKeys.GyroPreferenceKey, true);
prefEditor.putBoolean(PreferenceKeys.MagnetometerPrefKey, true);
prefEditor.apply();
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());
}
// 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, "standard max memory = " + activityManager.getMemoryClass() + "MB");
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
// also added the check for having 128MB standard heap, to support modded LG G2, which has 128MB standard, 256MB large - see https://sourceforge.net/p/opencamera/tickets/9/
if( activityManager.getMemoryClass() >= 128 || 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 ExtendedAppInterface(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
initCamera2Support();
// 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));
// 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.size() > 0 )
n_facing++;
if( front_camera_ids.size() > 0 )
n_facing++;
if( other_camera_ids.size() > 0 )
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);
View switchVideoButton = findViewById(R.id.switch_video);
switchVideoButton.setEnabled(true);
// 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 zoomControls = findViewById(R.id.zoom);
zoomControls.setVisibility(View.GONE);
View zoomSeekbar = findViewById(R.id.zoom_seekbar);
zoomSeekbar.setVisibility(View.INVISIBLE);
// initialise state of on-screen icons
mainUI.updateOnScreenIcons();
// listen for orientation event change
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));
// 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));
View decorView = getWindow().getDecorView();
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) {
// 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() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
if( MyDebug.LOG )
Log.d(TAG, "inset: " + insets.getSystemWindowInsetRight());
if( navigation_gap == 0 ) {
navigation_gap = insets.getSystemWindowInsetRight();
if( MyDebug.LOG )
Log.d(TAG, "navigation_gap is " + navigation_gap);
// Sometimes when this callback is called, the navigation_gap may still be 0 even if
// 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");
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
}
return getWindow().getDecorView().getRootView().onApplyWindowInsets(insets);
}
});
}
// set up listener to handle immersive mode options
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( !usingKitKatImmersiveMode() )
return;
if( MyDebug.LOG )
Log.d(TAG, "onSystemUiVisibilityChange: " + visibility);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
String immersive_mode = sharedPreferences.getString(PreferenceKeys.ImmersiveModePreferenceKey, "immersive_mode_low_profile");
boolean hide_ui = immersive_mode.equals("immersive_mode_gui") || immersive_mode.equals("immersive_mode_everything");
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
if( MyDebug.LOG )
Log.d(TAG, "system bars now visible");
// The system bars are visible. Make any desired
// adjustments to your UI, such as showing the action bar or
// other navigational controls.
if( hide_ui )
mainUI.setImmersiveMode(false);
setImmersiveTimer();
}
else {
if( MyDebug.LOG )
Log.d(TAG, "system bars now NOT visible");
// The system bars are NOT visible. Make any desired
// adjustments to your UI, such as hiding the action bar or
// other navigational controls.
if( hide_ui )
mainUI.setImmersiveMode(true);
}
}
});
if( MyDebug.LOG )
Log.d(TAG, "onCreate: time after setting immersive mode listener: " + (System.currentTimeMillis() - debug_time));
// show "about" dialog for first time use; also set some per-device defaults
boolean has_done_first_time = sharedPreferences.contains(PreferenceKeys.FirstTimePreferenceKey);
if( MyDebug.LOG )
Log.d(TAG, "has_done_first_time: " + has_done_first_time);
if( !has_done_first_time ) {
setDeviceDefaults();
}
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) {
if( MyDebug.LOG )
Log.d(TAG, "NameNotFoundException exception trying to get version number");
e.printStackTrace();
}
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 = 80; // 1.48.3
whats_new_version = Math.min(whats_new_version, version_code); // whats_new_version should always be <= version_code, but just in case!
if( MyDebug.LOG ) {
Log.d(TAG, "whats_new_version: " + whats_new_version);
}
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.setNegativeButton(R.string.donate, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if( MyDebug.LOG )
Log.d(TAG, "donate");
// 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(MainActivity.DonateLink));
startActivity(browserIntent);
}
});*/
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();
// create notification channel - only needed on Android 8+
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);
}
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() {
// Disable this for our app until this part is integrated properly
return false;
//return true;
//return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public int getNavigationGap() {
return want_no_limits ? navigation_gap : 0;
}
/** 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 is only one camera with the same front/back/external
* "facing" (e.g., imagine a device with two back cameras, but only one front camera).
*/
public boolean showSwitchMultiCamIcon() {
if( isMultiCamEnabled() ) {
int cameraId = getActualCameraId();
switch( preview.getCameraControllerManager().getFacing(cameraId) ) {
case FACING_BACK:
if( back_camera_ids.size() > 0 )
return true;
break;
case FACING_FRONT:
if( front_camera_ids.size() > 0 )
return true;
break;
default:
if( other_camera_ids.size() > 0 )
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");
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 ) {
// workaround needed for Samsung Galaxy S7 at least (tested on Samsung RTL)
// workaround needed for OnePlus 3 at least (see http://forum.xda-developers.com/oneplus-3/help/camera2-support-t3453103 )
// update for v1.37: significant improvements have been made for standard flash and Camera2 API. But OnePlus 3T still has problem
// that photos come out with a blue tinge if flash is on, and the scene is bright enough not to need it; Samsung devices also seem
// to work okay, testing on S7 on RTL, but still keeping the fake flash mode in place for these devices, until we're sure of good
// behaviour
// update for testing on Galaxy S10e: still needs fake flash
// has also been reported to me that OnePlus 8 and 8 Pro have problems with flash on Camera2 API unless fake flash enabled
if( MyDebug.LOG )
Log.d(TAG, "set fake flash for camera2");
SharedPreferences.Editor editor = sharedPreferences.edit();
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.getCamera2FastBurstPreferenceKey(), false);
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;
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) {
// 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;
}
}
}