Skip to content

Commit eaa2958

Browse files
committed
Merge branch 'android-activity'
2 parents ad709f7 + 7be8bc3 commit eaa2958

File tree

5 files changed

+328
-239
lines changed

5 files changed

+328
-239
lines changed
Lines changed: 34 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,41 @@
11
package 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;
83
import android.content.ComponentName;
94
import android.content.Context;
105
import android.content.Intent;
11-
import android.content.IntentFilter;
126
import android.content.ServiceConnection;
13-
import android.content.pm.PackageManager;
147
import android.content.res.Configuration;
15-
import android.hardware.usb.UsbDevice;
16-
import android.hardware.usb.UsbManager;
178
import android.net.Uri;
189
import android.os.Bundle;
19-
import android.os.Handler;
2010
import android.os.IBinder;
21-
import android.os.Looper;
2211
import android.view.View;
2312
import android.view.WindowManager;
24-
import android.webkit.CookieManager;
2513
import android.webkit.WebView;
2614

2715
import androidx.activity.OnBackPressedCallback;
28-
import androidx.activity.result.ActivityResultLauncher;
29-
import androidx.activity.result.contract.ActivityResultContracts;
3016
import androidx.annotation.NonNull;
3117
import androidx.appcompat.app.ActionBar;
3218
import androidx.appcompat.app.AppCompatActivity;
33-
import androidx.core.app.ActivityCompat;
3419
import androidx.core.content.ContextCompat;
3520
import androidx.lifecycle.ViewModelProviders;
3621

37-
import java.util.HashMap;
38-
3922
import mobileserver.Mobileserver;
4023

4124
public 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

Comments
 (0)