diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt index 84f2e75..9ac0db1 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt @@ -15,7 +15,7 @@ import java.io.OutputStream class MoneroSdkClient(private val context: Context) { private val providerDeferred = CoroutineScope(Dispatchers.IO).async { - WalletProvider.connect(context) + SandboxedWalletProvider.connect(context) } suspend fun createWallet(network: MoneroNetwork, filename: String): MoneroWallet { diff --git a/lib/android/src/main/AndroidManifest.xml b/lib/android/src/main/AndroidManifest.xml index 68e2d3f..b9ee357 100644 --- a/lib/android/src/main/AndroidManifest.xml +++ b/lib/android/src/main/AndroidManifest.xml @@ -5,10 +5,13 @@ + + + diff --git a/lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl b/lib/android/src/main/aidl/im/molly/monero/internal/IWalletService.aidl similarity index 83% rename from lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl rename to lib/android/src/main/aidl/im/molly/monero/internal/IWalletService.aidl index 7ceb57e..bcd6d73 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/internal/IWalletService.aidl @@ -1,11 +1,11 @@ -package im.molly.monero; +package im.molly.monero.internal; import im.molly.monero.IStorageAdapter; -import im.molly.monero.IWalletServiceCallbacks; -import im.molly.monero.IWalletServiceListener; import im.molly.monero.SecretKey; import im.molly.monero.WalletConfig; import im.molly.monero.internal.IHttpRpcClient; +import im.molly.monero.internal.IWalletServiceCallbacks; +import im.molly.monero.internal.IWalletServiceListener; interface IWalletService { oneway void createWallet(in WalletConfig config, in IStorageAdapter storage, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback); diff --git a/lib/android/src/main/aidl/im/molly/monero/IWalletServiceCallbacks.aidl b/lib/android/src/main/aidl/im/molly/monero/internal/IWalletServiceCallbacks.aidl similarity index 78% rename from lib/android/src/main/aidl/im/molly/monero/IWalletServiceCallbacks.aidl rename to lib/android/src/main/aidl/im/molly/monero/internal/IWalletServiceCallbacks.aidl index ea1368d..a177050 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWalletServiceCallbacks.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/internal/IWalletServiceCallbacks.aidl @@ -1,4 +1,4 @@ -package im.molly.monero; +package im.molly.monero.internal; import im.molly.monero.IWallet; diff --git a/lib/android/src/main/aidl/im/molly/monero/IWalletServiceListener.aidl b/lib/android/src/main/aidl/im/molly/monero/internal/IWalletServiceListener.aidl similarity index 77% rename from lib/android/src/main/aidl/im/molly/monero/IWalletServiceListener.aidl rename to lib/android/src/main/aidl/im/molly/monero/internal/IWalletServiceListener.aidl index 2bef2ee..9d0281b 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWalletServiceListener.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/internal/IWalletServiceListener.aidl @@ -1,4 +1,4 @@ -package im.molly.monero; +package im.molly.monero.internal; oneway interface IWalletServiceListener { void onLogMessage(int priority, String tag, String msg, String cause); diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt b/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt index 5737f4c..6fbbf5a 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt @@ -1,68 +1,17 @@ package im.molly.monero -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import kotlinx.coroutines.* - -// TODO: Rename to SandboxedWalletProvider and extract interface, add InProcessWalletProvider -class WalletProvider private constructor( - private val context: Context, - private val service: IWalletService, - private val serviceConnection: ServiceConnection, -// TODO: Remove DataStore dependencies if unused -// private val dataStore: DataStore, -) { - companion object { - suspend fun connect(context: Context): WalletProvider { - val (serviceConnection, service) = bindService(context) - return WalletProvider(context, service, serviceConnection) - } - - private suspend fun bindService(context: Context): Pair { - val deferredService = CompletableDeferred() - val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - val service = IWalletService.Stub.asInterface(binder) - service.setListener(WalletServiceListener) - deferredService.complete(service) - } - - override fun onServiceDisconnected(name: ComponentName?) { - deferredService.completeExceptionally(ServiceNotBoundException()) - } - } - Intent(context, WalletService::class.java).also { intent -> - if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { - throw ServiceNotBoundException() - } - } - return serviceConnection to deferredService.await() - } - } - - /** Exception thrown by [WalletProvider] if the remote service can't be bound. */ - class ServiceNotBoundException : Exception() - - private val logger = loggerFor() +import im.molly.monero.internal.WalletServiceClient +import im.molly.monero.service.InProcessWalletService +import im.molly.monero.service.SandboxedWalletService +import java.io.Closeable +interface WalletProvider : Closeable { suspend fun createNewWallet( network: MoneroNetwork, dataStore: WalletDataStore? = null, client: MoneroNodeClient? = null, - ): MoneroWallet { - require(client == null || client.network == network) - val storageAdapter = StorageAdapter(dataStore) - val wallet = suspendCancellableCoroutine { continuation -> - service.createWallet( - buildConfig(network), storageAdapter, client?.httpRpcClient, - WalletResultCallback(continuation), - ) - } - return MoneroWallet(wallet, storageAdapter, client) - } + ): MoneroWallet suspend fun restoreWallet( network: MoneroNetwork, @@ -70,70 +19,44 @@ class WalletProvider private constructor( client: MoneroNodeClient? = null, secretSpendKey: SecretKey, restorePoint: RestorePoint, - ): MoneroWallet { - require(client == null || client.network == network) - if (restorePoint is BlockchainTime) { - require(restorePoint.network == network) - } - val storageAdapter = StorageAdapter(dataStore) - val wallet = suspendCancellableCoroutine { continuation -> - service.restoreWallet( - buildConfig(network), storageAdapter, client?.httpRpcClient, - WalletResultCallback(continuation), - secretSpendKey, - restorePoint.toLong(), - ) - } - return MoneroWallet(wallet, storageAdapter, client) - } + ): MoneroWallet suspend fun openWallet( network: MoneroNetwork, dataStore: WalletDataStore, client: MoneroNodeClient? = null, - ): MoneroWallet { - require(client == null || client.network == network) - val storageAdapter = StorageAdapter(dataStore) - val wallet = suspendCancellableCoroutine { continuation -> - service.openWallet( - buildConfig(network), storageAdapter, client?.httpRpcClient, - WalletResultCallback(continuation), - ) - } - return MoneroWallet(wallet, storageAdapter, client) + ): MoneroWallet + + fun disconnect() + + override fun close() { + disconnect() } - private fun buildConfig(network: MoneroNetwork): WalletConfig { - return WalletConfig(network.id) - } + /** Exception thrown if the wallet service cannot be bound. */ + class ServiceNotBoundException : Exception() +} - fun disconnect() { - context.unbindService(serviceConnection) - } - - @OptIn(ExperimentalCoroutinesApi::class) - private class WalletResultCallback( - private val continuation: CancellableContinuation - ) : IWalletServiceCallbacks.Stub() { - override fun onWalletResult(wallet: IWallet?) { - when { - wallet != null -> { - continuation.resume(wallet) { - wallet.close() - } - } - - else -> TODO() - } - } +/** + * Provides wallet services using a sandboxed process. + */ +object SandboxedWalletProvider { + /** + * Connects to the sandboxed wallet service and returns a connected [WalletProvider]. + */ + suspend fun connect(context: Context): WalletProvider { + return WalletServiceClient.connect(context, SandboxedWalletService::class.java) } } -object WalletServiceListener : IWalletServiceListener.Stub() { - override fun onLogMessage(priority: Int, tag: String, msg: String, cause: String?) { - if (Logger.adapter.isLoggable(priority, tag)) { - val tr = if (cause != null) Throwable(cause) else null - Logger.adapter.print(priority, tag, msg, tr) - } +/** + * Provides wallet services using an in-process bound service. + */ +object InProcessWalletProvider { + /** + * Connects to the in-process wallet service and returns a connected [WalletProvider]. + */ + suspend fun connect(context: Context): WalletProvider { + return WalletServiceClient.connect(context, InProcessWalletService::class.java) } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletService.kt b/lib/android/src/main/kotlin/im/molly/monero/internal/NativeWalletService.kt similarity index 76% rename from lib/android/src/main/kotlin/im/molly/monero/WalletService.kt rename to lib/android/src/main/kotlin/im/molly/monero/internal/NativeWalletService.kt index c1a7989..dcf0d44 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletService.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/internal/NativeWalletService.kt @@ -1,46 +1,42 @@ -package im.molly.monero +package im.molly.monero.internal -import android.content.Intent -import android.os.IBinder -import androidx.lifecycle.LifecycleService -import androidx.lifecycle.lifecycleScope -import im.molly.monero.internal.IHttpRpcClient -import kotlinx.coroutines.* +import im.molly.monero.IStorageAdapter +import im.molly.monero.IWallet +import im.molly.monero.LogAdapter +import im.molly.monero.NativeLoader +import im.molly.monero.SecretKey +import im.molly.monero.WalletConfig +import im.molly.monero.WalletNative +import im.molly.monero.loggerFor +import im.molly.monero.randomSecretKey +import im.molly.monero.setLoggingAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch -class WalletService : LifecycleService() { - private lateinit var service: IWalletService - - override fun onCreate() { - super.onCreate() - service = WalletServiceImpl(application.isIsolatedProcess(), lifecycleScope) - } - - override fun onBind(intent: Intent): IBinder { - super.onBind(intent) - return service.asBinder() - } -} - -internal class WalletServiceImpl( - isIsolated: Boolean, +internal class NativeWalletService( private val serviceScope: CoroutineScope, ) : IWalletService.Stub(), LogAdapter { - private val logger = loggerFor() + private val logger = loggerFor() init { - if (isIsolated) { - setLoggingAdapter(this) - } NativeLoader.loadWalletLibrary(logger = logger) } + fun configureLoggingAdapter() { + setLoggingAdapter(this) + } + private var listener: IWalletServiceListener? = null override fun setListener(l: IWalletServiceListener) { listener = l } + override fun print(priority: Int, tag: String, msg: String?, tr: Throwable?) { + listener?.onLogMessage(priority, tag, msg, tr?.toString()) + } + override fun createWallet( config: WalletConfig, storage: IStorageAdapter, @@ -105,8 +101,4 @@ internal class WalletServiceImpl( coroutineContext = serviceScope.coroutineContext, ) } - - override fun print(priority: Int, tag: String, msg: String?, tr: Throwable?) { - listener?.onLogMessage(priority, tag, msg, tr?.toString()) - } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/internal/WalletServiceClient.kt b/lib/android/src/main/kotlin/im/molly/monero/internal/WalletServiceClient.kt new file mode 100644 index 0000000..320be19 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/internal/WalletServiceClient.kt @@ -0,0 +1,160 @@ +package im.molly.monero.internal + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import im.molly.monero.BlockchainTime +import im.molly.monero.IWallet +import im.molly.monero.MoneroNetwork +import im.molly.monero.MoneroNodeClient +import im.molly.monero.MoneroWallet +import im.molly.monero.RestorePoint +import im.molly.monero.SecretKey +import im.molly.monero.StorageAdapter +import im.molly.monero.WalletConfig +import im.molly.monero.WalletDataStore +import im.molly.monero.WalletProvider +import im.molly.monero.loggerFor +import im.molly.monero.service.BaseWalletService +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine + +internal class WalletServiceClient( + private val context: Context, + private val service: IWalletService, + private val serviceConnection: ServiceConnection, +) : WalletProvider { + + companion object { + suspend fun connect( + context: Context, + serviceClass: Class + ): WalletServiceClient { + val (serviceConnection, service) = bindService(context, serviceClass) + return WalletServiceClient(context, service, serviceConnection) + } + + private suspend fun bindService( + context: Context, + serviceClass: Class, + ): Pair { + val deferredService = CompletableDeferred() + val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + val service = IWalletService.Stub.asInterface(binder).apply { + setListener(WalletServiceLogListener) + } + deferredService.complete(service) + } + + override fun onServiceDisconnected(name: ComponentName?) { + deferredService.completeExceptionally(WalletProvider.ServiceNotBoundException()) + } + } + + val intent = Intent(context, serviceClass) + if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { + throw WalletProvider.ServiceNotBoundException() + } + + return serviceConnection to deferredService.await() + } + } + + private val logger = loggerFor() + + override suspend fun createNewWallet( + network: MoneroNetwork, + dataStore: WalletDataStore?, + client: MoneroNodeClient?, + ): MoneroWallet { + validateClientNetwork(client, network) + + val storageAdapter = StorageAdapter(dataStore) + val wallet = suspendCancellableCoroutine { continuation -> + service.createWallet( + buildConfig(network), storageAdapter, client?.httpRpcClient, + WalletResultCallback(continuation), + ) + } + return MoneroWallet(wallet, storageAdapter, client) + } + + override suspend fun restoreWallet( + network: MoneroNetwork, + dataStore: WalletDataStore?, + client: MoneroNodeClient?, + secretSpendKey: SecretKey, + restorePoint: RestorePoint, + ): MoneroWallet { + validateClientNetwork(client, network) + validateRestorePoint(restorePoint, network) + + val storageAdapter = StorageAdapter(dataStore) + val wallet = suspendCancellableCoroutine { continuation -> + service.restoreWallet( + buildConfig(network), storageAdapter, client?.httpRpcClient, + WalletResultCallback(continuation), + secretSpendKey, + restorePoint.toLong(), + ) + } + return MoneroWallet(wallet, storageAdapter, client) + } + + override suspend fun openWallet( + network: MoneroNetwork, + dataStore: WalletDataStore, + client: MoneroNodeClient?, + ): MoneroWallet { + validateClientNetwork(client, network) + + val storageAdapter = StorageAdapter(dataStore) + val wallet = suspendCancellableCoroutine { continuation -> + service.openWallet( + buildConfig(network), storageAdapter, client?.httpRpcClient, + WalletResultCallback(continuation), + ) + } + return MoneroWallet(wallet, storageAdapter, client) + } + + private fun buildConfig(network: MoneroNetwork): WalletConfig { + return WalletConfig(network.id) + } + + private fun validateClientNetwork(client: MoneroNodeClient?, network: MoneroNetwork) { + require(client == null || client.network == network) { + "Client network mismatch: expected $network, got ${client?.network}" + } + } + + private fun validateRestorePoint(restorePoint: RestorePoint, network: MoneroNetwork) { + if (restorePoint is BlockchainTime) { + require(restorePoint.network == network) { + "Restore point network mismatch: expected $network, got ${restorePoint.network}" + } + } + } + + override fun disconnect() { + context.unbindService(serviceConnection) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private class WalletResultCallback( + private val continuation: CancellableContinuation + ) : IWalletServiceCallbacks.Stub() { + override fun onWalletResult(wallet: IWallet?) { + if (wallet != null) { + continuation.resume(wallet) { wallet.close() } + } else { + TODO() + } + } + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/internal/WalletServiceLogListener.kt b/lib/android/src/main/kotlin/im/molly/monero/internal/WalletServiceLogListener.kt new file mode 100644 index 0000000..c3f5924 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/internal/WalletServiceLogListener.kt @@ -0,0 +1,12 @@ +package im.molly.monero.internal + +import im.molly.monero.Logger + +internal object WalletServiceLogListener : IWalletServiceListener.Stub() { + override fun onLogMessage(priority: Int, tag: String, msg: String, cause: String?) { + if (Logger.adapter.isLoggable(priority, tag)) { + val tr = if (cause != null) Throwable(cause) else null + Logger.adapter.print(priority, tag, msg, tr) + } + } +} \ No newline at end of file diff --git a/lib/android/src/main/kotlin/im/molly/monero/service/BaseWalletService.kt b/lib/android/src/main/kotlin/im/molly/monero/service/BaseWalletService.kt new file mode 100644 index 0000000..7ad0640 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/service/BaseWalletService.kt @@ -0,0 +1,23 @@ +package im.molly.monero.service + +import android.content.Intent +import android.os.IBinder +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import im.molly.monero.internal.IWalletService +import im.molly.monero.internal.NativeWalletService + +abstract class BaseWalletService(private val isolated: Boolean) : LifecycleService() { + private val service: IWalletService by lazy { + NativeWalletService(lifecycleScope).apply { + if (isolated) { + configureLoggingAdapter() + } + } + } + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return service.asBinder() + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/service/InProcessWalletService.kt b/lib/android/src/main/kotlin/im/molly/monero/service/InProcessWalletService.kt new file mode 100644 index 0000000..8da3b6e --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/service/InProcessWalletService.kt @@ -0,0 +1,3 @@ +package im.molly.monero.service + +class InProcessWalletService : BaseWalletService(isolated = false) \ No newline at end of file diff --git a/lib/android/src/main/kotlin/im/molly/monero/service/SandboxedWalletService.kt b/lib/android/src/main/kotlin/im/molly/monero/service/SandboxedWalletService.kt new file mode 100644 index 0000000..74fcbf0 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/service/SandboxedWalletService.kt @@ -0,0 +1,3 @@ +package im.molly.monero.service + +class SandboxedWalletService : BaseWalletService(isolated = true)