diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2623318e..f0e64bf1a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Display the hide amount button by default and remove its settings - Linux: add support for Wayland - Fix the copy buttons in the Pocket order confirmation page +- Android: handle device disconnect while the app is in the background # 4.46.3 - Fix camera access on linux diff --git a/backend/backend.go b/backend/backend.go index 5f02ef0d27..acfe4070cd 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -193,6 +193,8 @@ type Backend struct { devices map[string]device.Interface + usbManager *usb.Manager + accountsAndKeystoreLock locker.Locker accounts AccountsList // keystore is nil if no keystore is connected. @@ -612,13 +614,14 @@ func (backend *Backend) OnDeviceUninit(f func(string)) { // Start starts the background services. It returns a channel of events to handle by the library // client. func (backend *Backend) Start() <-chan interface{} { - usb.NewManager( + backend.usbManager = usb.NewManager( backend.arguments.MainDirectoryPath(), backend.arguments.BitBox02DirectoryPath(), backend.socksProxy, backend.environment.DeviceInfos, backend.Register, - backend.Deregister).Start() + backend.Deregister) + backend.usbManager.Start() httpClient, err := backend.socksProxy.GetHTTPClient() if err != nil { @@ -638,6 +641,13 @@ func (backend *Backend) Start() <-chan interface{} { return backend.events } +// UsbUpdate triggers a scan of the USB devices to detect connects/disconnects. +func (backend *Backend) UsbUpdate() { + if backend.usbManager != nil { + backend.usbManager.Update() + } +} + // DevicesRegistered returns a map of device IDs to device of registered devices. func (backend *Backend) DevicesRegistered() map[string]device.Interface { return backend.devices diff --git a/backend/bridgecommon/bridgecommon.go b/backend/bridgecommon/bridgecommon.go index 7d4e306724..6a878a7512 100644 --- a/backend/bridgecommon/bridgecommon.go +++ b/backend/bridgecommon/bridgecommon.go @@ -359,3 +359,13 @@ func Shutdown() { log.Info("Shutdown called, but backend not running") } } + +// UsbUpdate wraps backend.UsbUpdate. +func UsbUpdate() { + mu.RLock() + defer mu.RUnlock() + if globalBackend == nil { + return + } + globalBackend.UsbUpdate() +} diff --git a/backend/devices/usb/manager.go b/backend/devices/usb/manager.go index a384d26d60..c0e679d1c9 100644 --- a/backend/devices/usb/manager.go +++ b/backend/devices/usb/manager.go @@ -86,6 +86,8 @@ type Manager struct { onRegister func(device.Interface) error onUnregister func(string) + updateCh chan struct{} + socksProxy socksproxy.SocksProxy log *logrus.Entry @@ -111,6 +113,7 @@ func NewManager( deviceInfos: deviceInfos, onRegister: onRegister, onUnregister: onUnregister, + updateCh: make(chan struct{}), socksProxy: socksProxy, log: logging.Get().WithGroup("manager"), @@ -255,8 +258,19 @@ func (manager *Manager) checkIfRemoved(deviceID string) bool { return true } +// Update triggers a scan of the USB devices to detect connects/disconnects. +func (manager *Manager) Update() { + go func() { + manager.updateCh <- struct{}{} + }() +} + func (manager *Manager) listen() { for { + select { + case <-manager.updateCh: + case <-time.After(time.Second): + } for deviceID, device := range manager.devices { // Check if device was removed. if manager.checkIfRemoved(deviceID) { @@ -310,11 +324,11 @@ func (manager *Manager) listen() { manager.log.WithError(err).Error("Failed to execute on-register") } } - time.Sleep(time.Second) } } // Start listens for inserted/removed devices forever. func (manager *Manager) Start() { go manager.listen() + manager.Update() } diff --git a/backend/mobileserver/mobileserver.go b/backend/mobileserver/mobileserver.go index 9128048045..0bc401f1b6 100644 --- a/backend/mobileserver/mobileserver.go +++ b/backend/mobileserver/mobileserver.go @@ -155,6 +155,11 @@ func UsingMobileDataChanged() { bridgecommon.UsingMobileDataChanged() } +// UsbUpdate exposes `bridgecommon.UsbUpdate` to Java/Kotlin. +func UsbUpdate() { + bridgecommon.UsbUpdate() +} + type goLogHook struct { } diff --git a/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoService.java b/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoService.java index bd2768c637..5b1bf931ee 100644 --- a/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoService.java +++ b/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoService.java @@ -5,12 +5,18 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.Binder; import android.os.Build; import android.os.IBinder; +import android.hardware.usb.UsbManager; import androidx.core.app.NotificationCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -31,6 +37,20 @@ public class GoService extends Service { private final int notificationId = 8; + private ViewModelStoreOwner viewModelStoreOwner; + + private final BroadcastReceiver usbStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (viewModelStoreOwner != null && UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + GoViewModel viewModel = new ViewModelProvider(viewModelStoreOwner).get(GoViewModel.class); + viewModel.setDevice(null); + Mobileserver.usbUpdate(); + } + } + }; + @Override public void onCreate() { Util.log("GoService onCreate()"); @@ -68,12 +88,26 @@ public void onCreate() { // focus. This is needed to avoid timeouts when the backend is polling the BitBox for e.g. // an address verification. startForeground(notificationId, notification); + + // Register USB broadcast receiver to detect USB disconnects, even while the app is in the + // background. + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(usbStateReceiver, filter, Context.RECEIVER_EXPORTED); + } else { + registerReceiver(usbStateReceiver, filter); + } + Util.log("GoService onCreate completed"); } @Override public void onDestroy() { Util.log("GoService onDestroy()"); + super.onDestroy(); + unregisterReceiver(usbStateReceiver); // It would be nice to call MobileServer.shutdown() here, but that function // is currently incomplete and can lead to unpredictable results. } @@ -98,6 +132,10 @@ GoService getService() { } } + public void setViewModelStoreOwner(ViewModelStoreOwner owner) { + this.viewModelStoreOwner = owner; + } + @Override public IBinder onBind(Intent intent) { return binder; diff --git a/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/MainActivity.java b/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/MainActivity.java index d1af72e082..879e90e3bd 100644 --- a/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/MainActivity.java +++ b/frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/MainActivity.java @@ -88,6 +88,7 @@ public void onServiceConnected(ComponentName className, IBinder service) { GoService.GoServiceBinder binder = (GoService.GoServiceBinder) service; goService = binder.getService(); + goService.setViewModelStoreOwner(MainActivity.this); Util.log("Bind connection completed!"); startServer(); }