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)