11package ch .shiftcrypto .bitboxapp ;
22
3- import android .Manifest ;
4- import android .annotation .SuppressLint ;
5- import android .app .AlertDialog ;
6- import android .app .PendingIntent ;
7- import android .content .BroadcastReceiver ;
83import android .content .ComponentName ;
94import android .content .Context ;
105import android .content .Intent ;
11- import android .content .IntentFilter ;
126import android .content .ServiceConnection ;
13- import android .content .pm .PackageManager ;
147import android .content .res .Configuration ;
15- import android .hardware .usb .UsbDevice ;
16- import android .hardware .usb .UsbManager ;
178import android .net .Uri ;
189import android .os .Bundle ;
19- import android .os .Handler ;
2010import android .os .IBinder ;
21- import android .os .Looper ;
2211import android .view .View ;
2312import android .view .WindowManager ;
24- import android .webkit .CookieManager ;
2513import android .webkit .WebView ;
2614
2715import androidx .activity .OnBackPressedCallback ;
28- import androidx .activity .result .ActivityResultLauncher ;
29- import androidx .activity .result .contract .ActivityResultContracts ;
3016import androidx .annotation .NonNull ;
3117import androidx .appcompat .app .ActionBar ;
3218import androidx .appcompat .app .AppCompatActivity ;
33- import androidx .core .app .ActivityCompat ;
3419import androidx .core .content .ContextCompat ;
3520import androidx .lifecycle .ViewModelProviders ;
3621
37- import java .util .HashMap ;
38-
3922import mobileserver .Mobileserver ;
4023
4124public class MainActivity extends AppCompatActivity {
4225 static {
4326 System .loadLibrary ("signal_handler" );
4427 }
4528 public native void initsignalhandler ();
46- private final int PERMISSIONS_REQUEST_CAMERA_QRCODE = 0 ;
47- private static final String ACTION_USB_PERMISSION = "ch.shiftcrypto.bitboxapp.USB_PERMISSION" ;
48- // The WebView is configured with this as the base URL. The purpose is so that requests made
49- // from the app include shiftcrypto.ch in the Origin header to allow Moonpay to load in the
50- // iframe. Moonpay compares the origin against a list of origins configured in the Moonpay admin.
51- // This is a security feature relevant for websites running in browsers, but in the case of the
52- // BitBoxApp, it is useless, as any app can do this.
53- //
54- // Unfortunately there seems to be no simple way to include this header only in requests to Moonpay.
55- private static final String BASE_URL = "https://shiftcrypto.ch/" ;
5629
5730 GoService goService ;
58-
59- private BitBoxWebChromeClient webChrome ;
31+ private GoViewModel goViewModel ;
32+ private WebViewManager webViewManager ;
33+ private UsbDeviceManager usbDeviceManager ;
6034
6135 // Connection to bind with GoService
6236 private final ServiceConnection connection = new ServiceConnection () {
63-
6437 @ Override
65- public void onServiceConnected (ComponentName className ,
66- IBinder service ) {
38+ public void onServiceConnected (ComponentName className , IBinder service ) {
6739 GoService .GoServiceBinder binder = (GoService .GoServiceBinder ) service ;
6840 goService = binder .getService ();
6941 goService .setViewModelStoreOwner (MainActivity .this );
@@ -78,12 +50,6 @@ public void onServiceDisconnected(ComponentName arg0) {
7850 }
7951 };
8052
81- private final BroadcastReceiver usbStateReceiver = new BroadcastReceiver () {
82- public void onReceive (Context context , Intent intent ) {
83- handleIntent (intent );
84- }
85- };
86-
8753 @ Override
8854 public void onConfigurationChanged (Configuration newConfig ) {
8955 int currentNightMode = newConfig .uiMode & Configuration .UI_MODE_NIGHT_MASK ;
@@ -118,89 +84,39 @@ public void setDarkTheme(boolean isDark) {
11884 }
11985 }
12086
121- @ SuppressLint ("SetJavaScriptEnabled" )
12287 @ Override
12388 protected void onCreate (Bundle savedInstanceState ) {
12489 super .onCreate (savedInstanceState );
12590 Util .log ("lifecycle: onCreate" );
126-
12791 initsignalhandler ();
128-
12992 ActionBar actionBar = getSupportActionBar ();
13093 if (actionBar != null ) {
13194 actionBar .hide (); // hide title bar with app name.
13295 }
13396 onConfigurationChanged (getResources ().getConfiguration ());
13497 setContentView (R .layout .activity_main );
13598 final WebView vw = findViewById (R .id .vw );
136- // For onramp iframe'd widgets like MoonPay.
137- CookieManager .getInstance ().setAcceptThirdPartyCookies (vw , true );
13899
139100 // GoModel manages the Go backend. It is in a ViewModel so it only runs once, not every time
140101 // onCreate is called (on a configuration change like orientation change).
141- final GoViewModel goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
102+ goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
103+ webViewManager = new WebViewManager (this , goViewModel );
104+ webViewManager .initialize (vw );
105+ usbDeviceManager = new UsbDeviceManager (this , goViewModel );
142106
143107 // The backend is run inside GoService, to avoid (as much as possible) latency errors due to
144108 // the scheduling when the app is out of focus.
145109 Intent intent = new Intent (this , GoService .class );
146110 bindService (intent , connection , Context .BIND_AUTO_CREATE );
147111
148- goViewModel .setMessageHandlers (
149- new Handler (Looper .getMainLooper (), msg -> {
150- final GoAPI .Response response = (GoAPI .Response ) msg .obj ;
151- vw .evaluateJavascript (
152- "if (window.onMobileCallResponse) {" +
153- "window.onMobileCallResponse(" + response .queryID + ", " + response .response + ")};" ,
154- null );
155- return true ;
156- }),
157- new Handler (Looper .getMainLooper (), msg -> {
158- vw .evaluateJavascript (
159- "if (window.onMobilePushNotification) {" +
160- "window.onMobilePushNotification(" + msg .obj + ")};" ,
161- null );
162- return true ;
163- }));
164- vw .clearCache (true );
165- vw .clearHistory ();
166- vw .getSettings ().setJavaScriptEnabled (true );
167- vw .getSettings ().setAllowUniversalAccessFromFileURLs (true );
168- vw .getSettings ().setAllowFileAccess (true );
169- // Allow widgets to open external links via window.open (handled in BitBoxWebChromeClient).
170- vw .getSettings ().setSupportMultipleWindows (true );
171-
172- // For Moonpay widget: DOM storage and WebRTC camera access required.
173- vw .getSettings ().setDomStorageEnabled (true );
174- vw .getSettings ().setMediaPlaybackRequiresUserGesture (false );
175-
176- // vw.setWebContentsDebuggingEnabled(true); // enable remote debugging in chrome://inspect/#devices
177-
178- vw .setWebViewClient (new BitBoxWebViewClient (BASE_URL , getAssets (), getApplication ()));
179-
180- ActivityResultLauncher <String > fileChooser = registerForActivityResult (new ActivityResultContracts .GetContent (), uri -> webChrome .onFilePickerResult (uri ));
181- BitBoxWebChromeClient .CameraPermissionDelegate cameraPermissionDelegate = () -> ActivityCompat .requestPermissions (
182- this ,
183- new String []{Manifest .permission .CAMERA },
184- PERMISSIONS_REQUEST_CAMERA_QRCODE
185- );
186-
187- webChrome = new BitBoxWebChromeClient (this ,
188- cameraPermissionDelegate ,
189- fileChooser
190- );
191- vw .setWebChromeClient (webChrome );
192-
193- vw .addJavascriptInterface (new JavascriptBridge (this ), "android" );
194- vw .loadUrl (BASE_URL + "index.html" );
195-
196- // We call updateDevice() here in case the app was started while the device was already connected.
197- // In that case, handleIntent() is not called with ACTION_USB_DEVICE_ATTACHED.
198- this .updateDevice ();
112+ // We call refreshConnectedDevice() here in case the app was started while the device was already connected.
113+ // In that case, no intent is fired with ACTION_USB_DEVICE_ATTACHED.
114+ usbDeviceManager .refreshConnectedDevice ();
199115
200116 getOnBackPressedDispatcher ().addCallback (this , new OnBackPressedCallback (true ) {
201117 @ Override
202118 public void handleOnBackPressed () {
203- backPressedHandler ();
119+ webViewManager . handleBackPressed ();
204120 }
205121 });
206122
@@ -261,17 +177,20 @@ public void noAuthConfigured() {
261177 @ Override
262178 public void onRequestPermissionsResult (int requestCode , @ NonNull String [] permissions , @ NonNull int [] grantResults ) {
263179 super .onRequestPermissionsResult (requestCode , permissions , grantResults );
264- if (requestCode == PERMISSIONS_REQUEST_CAMERA_QRCODE ) {
265- webChrome . onCameraPermissionResult ( grantResults . length > 0 && grantResults [ 0 ] == PackageManager . PERMISSION_GRANTED );
180+ if (webViewManager != null ) {
181+ webViewManager . handleRequestPermissionsResult ( requestCode , grantResults );
266182 }
267183 }
268184
269185 private void startServer () {
270- final GoViewModel gVM = ViewModelProviders .of (this ).get (GoViewModel .class );
271- goService .startServer (getApplicationContext ().getFilesDir ().getAbsolutePath (), gVM .getGoEnvironment (), gVM .getGoAPI ());
186+ goService .startServer (
187+ getApplicationContext ().getFilesDir ().getAbsolutePath (),
188+ goViewModel .getGoEnvironment (),
189+ goViewModel .getGoAPI ()
190+ );
272191
273192 // Trigger connectivity and mobile connection check (as the network may already be unavailable when the app starts).
274- gVM .getNetworkHelper ().checkConnectivity ();
193+ goViewModel .getNetworkHelper ().checkConnectivity ();
275194 }
276195
277196 @ Override
@@ -287,7 +206,6 @@ protected void onNewIntent(Intent intent) {
287206 protected void onStart () {
288207 super .onStart ();
289208 Util .log ("lifecycle: onStart" );
290- final GoViewModel goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
291209 goViewModel .getIsDarkTheme ().observe (this , this ::setDarkTheme );
292210 goViewModel .getNetworkHelper ().registerNetworkCallback ();
293211 }
@@ -298,104 +216,26 @@ protected void onResume() {
298216 Util .log ("lifecycle: onResume" );
299217 Mobileserver .triggerAuth ();
300218
301- // This is only called reliably when USB is attached with android:launchMode="singleTop"
302-
303- // Usb device list is updated on ATTACHED / DETACHED intents.
304- // ATTACHED intent is an activity intent in AndroidManifest.xml so the app is launched when
305- // a device is attached. On launch or when it is already running, onIntent() is called
306- // followed by onResume(), where the intent is handled.
307- // DETACHED intent is a broadcast intent which we register here.
308- IntentFilter filter = new IntentFilter ();
309- filter .addAction (UsbManager .ACTION_USB_DEVICE_DETACHED );
310- filter .addAction (ACTION_USB_PERMISSION );
311- ContextCompat .registerReceiver (
312- this ,
313- usbStateReceiver ,
314- filter ,
315- ContextCompat .RECEIVER_NOT_EXPORTED
316- );
219+ usbDeviceManager .startMonitoring ();
317220
318221 // Trigger connectivity check (as the network may already be unavailable when the app starts).
319- final GoViewModel goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
320222 goViewModel .getNetworkHelper ().checkConnectivity ();
321223
322224 Intent intent = getIntent ();
323- handleIntent (intent );
225+ usbDeviceManager .handleUsbIntent (intent );
226+ handleAOPPIntent (intent );
324227 }
325228
326229 @ Override
327230 protected void onPause () {
328231 super .onPause ();
329232 Util .log ("lifecycle: onPause" );
330- unregisterReceiver (this .usbStateReceiver );
331- }
332-
333- private void handleIntent (Intent intent ) {
334- if (ACTION_USB_PERMISSION .equals (intent .getAction ())) {
335- // See https://developer.android.com/guide/topics/connectivity/usb/host#permission-d
336- synchronized (this ) {
337- UsbDevice device = intent .getParcelableExtra (UsbManager .EXTRA_DEVICE );
338- if (intent .getBooleanExtra (UsbManager .EXTRA_PERMISSION_GRANTED , false )) {
339- if (device != null ) {
340- Util .log ("usb: permission granted" );
341- final GoViewModel goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
342- goViewModel .setDevice (device );
343- }
344- } else {
345- Util .log ("usb: permission denied" );
346- }
347- }
348- }
349- if (UsbManager .ACTION_USB_DEVICE_ATTACHED .equals (intent .getAction ())) {
350- Util .log ("usb: attached" );
351- this .updateDevice ();
352- }
353- if (UsbManager .ACTION_USB_DEVICE_DETACHED .equals (intent .getAction ())) {
354- Util .log ("usb: detached" );
355- this .updateDevice ();
356- }
357- // Handle 'aopp:' URIs. This is called when the app is launched and also if it is already
358- // running and brought to the foreground.
359- if (Intent .ACTION_VIEW .equals (intent .getAction ())) {
360- Uri uri = intent .getData ();
361- if (uri != null ) {
362- if ("aopp" .equals (uri .getScheme ())) {
363- Mobileserver .handleURI (uri .toString ());
364- }
365- }
366- }
367- }
368-
369- private void updateDevice () {
370- // Triggered by usb device attached intent and usb device detached broadcast events.
371- final GoViewModel goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
372- goViewModel .setDevice (null );
373- UsbManager manager = (UsbManager ) getApplication ().getSystemService (Context .USB_SERVICE );
374- HashMap <String , UsbDevice > deviceList = manager .getDeviceList ();
375- for (UsbDevice device : deviceList .values ()) {
376- // One other instance where we filter vendor/product IDs is in
377- // @xml/device_filter resource, which is used for USB_DEVICE_ATTACHED
378- // intent to launch the app when a device is plugged and the app is still
379- // closed. This filter, on the other hand, makes sure we feed only valid
380- // devices to the Go backend once the app is launched or opened.
381- //
382- // BitBox02 Vendor ID: 0x03eb, Product ID: 0x2403.
383- if (device .getVendorId () == 1003 && device .getProductId () == 9219 ) {
384- if (manager .hasPermission (device )) {
385- goViewModel .setDevice (device );
386- } else {
387- PendingIntent permissionIntent = PendingIntent .getBroadcast (this , 0 , new Intent (ACTION_USB_PERMISSION ), PendingIntent .FLAG_IMMUTABLE );
388- manager .requestPermission (device , permissionIntent );
389- }
390- break ; // only one device supported for now
391- }
392- }
233+ usbDeviceManager .stopMonitoring ();
393234 }
394235
395236 @ Override
396237 protected void onStop () {
397238 super .onStop ();
398- final GoViewModel goViewModel = ViewModelProviders .of (this ).get (GoViewModel .class );
399239 goViewModel .getNetworkHelper ().unregisterNetworkCallback ();
400240 Util .log ("lifecycle: onStop" );
401241 }
@@ -421,54 +261,15 @@ protected void onDestroy() {
421261 Util .quit (MainActivity .this );
422262 }
423263
424-
425-
426- // Handle Android back button behavior:
427- //
428- // By default, if the webview can go back in browser history, we do that.
429- // If there is no more history, we prompt the user to quit the app. If
430- // confirmed, the app will be force quit.
431- //
432- // The default behavior can be modified by the frontend via the
433- // window.onBackButtonPressed() function. See the `useBackButton` React
434- // hook. It will be called first, and if it returns false, the default
435- // behavior is prevented, otherwise we proceed with the above default
436- // behavior.
437- //
438- // Without forced app process exit, some goroutines may remain active even after
439- // the app resumption at which point new copies of goroutines are spun up.
440- // Note that this is different from users tapping on "home" button or switching
441- // to another app and then back, in which case no extra goroutines are created.
442- //
443- // A proper fix is to make the backend process run in a separate system thread.
444- // Until such solution is implemented, forced app exit seems most appropriate.
445- //
446- // See the following for details about task and activity stacks:
447- // https://developer.android.com/guide/components/activities/tasks-and-back-stack
448- private void backPressedHandler () {
449- runOnUiThread (new Runnable () {
450- final WebView vw = findViewById (R .id .vw );
451- @ Override
452- public void run () {
453- vw .evaluateJavascript ("window.onBackButtonPressed();" , value -> {
454- boolean doDefault = Boolean .parseBoolean (value );
455- if (doDefault ) {
456- // Default behavior: go back in history if we can, otherwise prompt user
457- // if they want to quit the app.
458- if (vw .canGoBack ()) {
459- vw .goBack ();
460- return ;
461- }
462- new AlertDialog .Builder (MainActivity .this )
463- .setTitle ("Close BitBoxApp" )
464- .setMessage ("Do you really want to exit?" )
465- .setPositiveButton (android .R .string .yes , (dialog , which ) -> Util .quit (MainActivity .this ))
466- .setNegativeButton (android .R .string .no , (dialog , which ) -> dialog .dismiss ())
467- .setIcon (android .R .drawable .ic_dialog_alert )
468- .show ();
469- }
470- });
471- }
472- });
264+ // Handle 'aopp:' URIs. This is called when the app is launched and also if it is already
265+ // running and brought to the foreground.
266+ private void handleAOPPIntent (Intent intent ) {
267+ if (intent == null || !Intent .ACTION_VIEW .equals (intent .getAction ())) {
268+ return ;
269+ }
270+ Uri uri = intent .getData ();
271+ if (uri != null && "aopp" .equals (uri .getScheme ())) {
272+ Mobileserver .handleURI (uri .toString ());
273+ }
473274 }
474275}
0 commit comments