diff --git a/sdk/src/androidTest/java/network/xyo/client/XyoAccountPrefsRepositoryTest.kt b/sdk/src/androidTest/java/network/xyo/client/XyoAccountPrefsRepositoryTest.kt new file mode 100644 index 0000000..b980365 --- /dev/null +++ b/sdk/src/androidTest/java/network/xyo/client/XyoAccountPrefsRepositoryTest.kt @@ -0,0 +1,98 @@ +package network.xyo.client + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import network.xyo.client.datastore.XyoAccountPrefsRepository +import network.xyo.client.settings.AccountPreferences +import network.xyo.client.witness.system.info.XyoSystemInfoWitness +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals + +class XyoAccountPrefsRepositoryTest { + + private lateinit var appContext: Context + + private val apiDomainBeta = "${TestConstants.nodeUrlBeta}/Archivist" + + @Before + fun useAppContext() { + // Context of the app under test. + this.appContext = InstrumentationRegistry.getInstrumentation().targetContext + } + + @Test + fun testAccountPersistence() { + runBlocking { + val prefsRepository = + XyoAccountPrefsRepository.getInstance(appContext) + prefsRepository.clearSavedAccountKey() + + val panel = XyoPanel( + appContext, arrayListOf(Pair(apiDomainBeta, null)), listOf( + XyoSystemInfoWitness() + ) + ) + panel.resolveNodes() + val generatedAddress = panel.defaultAccount?.address?.hex + assertNotEquals(generatedAddress, null) + + val panel2 = XyoPanel( + appContext, arrayListOf(Pair(apiDomainBeta, null)), listOf( + XyoSystemInfoWitness() + ) + ) + panel2.resolveNodes() + val secondGeneratedAddress = panel2.defaultAccount?.address?.hex + assertEquals(generatedAddress, secondGeneratedAddress) + } + } + + @Test + fun testClearingExistingAccount() { + runBlocking { + val instance = XyoAccountPrefsRepository.getInstance(appContext) + val originalAddress = instance.getAccount().private.hex + + instance.clearSavedAccountKey() + + val refreshedAddress = instance.getAccount().private.hex + + assert(originalAddress !== refreshedAddress) + } + } + + @Test + fun testUpdatingAccountPreferences() { + runBlocking { + val instance = XyoAccountPrefsRepository.getInstance(appContext) + val originalAddress = instance.getAccount().private.hex + + open class UpdatedAccountPreferences : AccountPreferences { + override val fileName = "network-xyo-sdk-prefs-1" + override val storagePath = "__xyo-client-sdk-1__" + } + + val updatedAccountPrefs = UpdatedAccountPreferences() + + val refreshedInstance = + XyoAccountPrefsRepository.refresh(appContext, updatedAccountPrefs) + + // Test that accountPreferences are updated + assertEquals( + refreshedInstance.accountPreferences.fileName, + updatedAccountPrefs.fileName + ) + assertEquals( + refreshedInstance.accountPreferences.storagePath, + updatedAccountPrefs.storagePath + ) + + val refreshedAddress = refreshedInstance.getAccount().private.hex + + assert(originalAddress !== refreshedAddress) + } + } +} \ No newline at end of file diff --git a/sdk/src/androidTest/java/network/xyo/client/XyoPanelTest.kt b/sdk/src/androidTest/java/network/xyo/client/XyoPanelTest.kt index 91e64be..2329d1d 100644 --- a/sdk/src/androidTest/java/network/xyo/client/XyoPanelTest.kt +++ b/sdk/src/androidTest/java/network/xyo/client/XyoPanelTest.kt @@ -9,8 +9,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import network.xyo.client.address.XyoAccount import network.xyo.client.boundwitness.XyoBoundWitnessJson -import network.xyo.client.datastore.PrefsRepository +import network.xyo.client.datastore.XyoAccountPrefsRepository +import network.xyo.client.datastore.defaults import network.xyo.client.payload.XyoPayload +import network.xyo.client.settings.AccountPreferences import network.xyo.client.witness.location.info.LocationActivity import network.xyo.client.witness.location.info.XyoLocationWitness import network.xyo.client.witness.system.info.XyoSystemInfoPayload @@ -94,7 +96,7 @@ class XyoPanelTest { @Test fun testSimplePanelReport() { runBlocking { - val panel = XyoPanel(appContext, fun(_context:Context, _: String?): XyoEventPayload { + val panel = XyoPanel(appContext, fun(_:Context, _: String?): XyoEventPayload { return XyoEventPayload("test_event") }) val result = panel.reportAsyncQuery() @@ -124,22 +126,4 @@ class XyoPanelTest { assertInstanceOf(results.payloads?.first()) } } - - @Test - fun testAccountPersistence() { - runBlocking { - val prefsRepository = PrefsRepository(appContext) - prefsRepository.clearSavedAccountKey() - - val panel = XyoPanel(appContext, arrayListOf(Pair(apiDomainBeta, null)), listOf(XyoSystemInfoWitness())) - panel.resolveNodes() - val generatedAddress = panel.defaultAccount?.address?.hex - assertNotEquals(generatedAddress, null) - - val panel2 = XyoPanel(appContext, arrayListOf(Pair(apiDomainBeta, null)), listOf(XyoSystemInfoWitness())) - panel2.resolveNodes() - val secondGeneratedAddress = panel2.defaultAccount?.address?.hex - assertEquals(generatedAddress, secondGeneratedAddress) - } - } } \ No newline at end of file diff --git a/sdk/src/androidTest/java/network/xyo/client/XyoSdkTest.kt b/sdk/src/androidTest/java/network/xyo/client/XyoSdkTest.kt new file mode 100644 index 0000000..5dae11a --- /dev/null +++ b/sdk/src/androidTest/java/network/xyo/client/XyoSdkTest.kt @@ -0,0 +1,53 @@ +package network.xyo.client + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import network.xyo.client.settings.AccountPreferences +import network.xyo.client.settings.DefaultXyoSdkSettings +import network.xyo.client.settings.XyoSdk +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals + +class XyoSdkTest { + + private lateinit var appContext: Context + + @Before + fun resetSingleton() { + XyoSdk.resetInstance() + } + + @Before + fun useAppContext() { + // Context of the app under test. + this.appContext = InstrumentationRegistry.getInstrumentation().targetContext + } + + @Test + fun testDefaultSettings() { + runBlocking { + val instance = XyoSdk.getInstance(appContext) + assertEquals(instance.settings.accountPreferences.fileName, DefaultXyoSdkSettings().accountPreferences.fileName) + assertEquals(instance.settings.accountPreferences.storagePath, DefaultXyoSdkSettings().accountPreferences.storagePath) + } + } + + @Test + fun testCustomSettings() { + runBlocking { + class UpdatedAccountPreferences : AccountPreferences { + override val fileName = "network-xyo-sdk-prefs-1" + override val storagePath = "__xyo-client-sdk-1__" + } + class UpdatedSettings: DefaultXyoSdkSettings() { + override val accountPreferences = UpdatedAccountPreferences() + } + val updatedSettings = UpdatedSettings() + val instance = XyoSdk.getInstance(appContext, updatedSettings) + assertEquals(instance.settings.accountPreferences.fileName, updatedSettings.accountPreferences.fileName) + assertEquals(instance.settings.accountPreferences.storagePath, updatedSettings.accountPreferences.storagePath) + } + } +} \ No newline at end of file diff --git a/sdk/src/main/java/network/xyo/client/XyoPanel.kt b/sdk/src/main/java/network/xyo/client/XyoPanel.kt index d090533..6cc057b 100644 --- a/sdk/src/main/java/network/xyo/client/XyoPanel.kt +++ b/sdk/src/main/java/network/xyo/client/XyoPanel.kt @@ -12,7 +12,7 @@ import network.xyo.client.archivist.api.XyoArchivistApiConfig import network.xyo.client.archivist.wrapper.ArchivistWrapper import network.xyo.client.boundwitness.XyoBoundWitnessBuilder import network.xyo.client.boundwitness.XyoBoundWitnessJson -import network.xyo.client.datastore.PrefsRepository +import network.xyo.client.datastore.XyoAccountPrefsRepository import network.xyo.client.node.client.NodeClient import network.xyo.client.node.client.PostQueryResult import network.xyo.client.payload.XyoPayload @@ -77,7 +77,7 @@ class XyoPanel(val context: Context, private val archivists: List().let { this@XyoPanel.nodeUrlsAndAccounts?.forEach { pair -> diff --git a/sdk/src/main/java/network/xyo/client/datastore/PrefsRepository.kt b/sdk/src/main/java/network/xyo/client/datastore/PrefsRepository.kt deleted file mode 100644 index 95122b9..0000000 --- a/sdk/src/main/java/network/xyo/client/datastore/PrefsRepository.kt +++ /dev/null @@ -1,72 +0,0 @@ -package network.xyo.client.datastore - -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.dataStore -import network.xyo.data.PrefsDataStoreProtos.PrefsDataStore -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import network.xyo.client.address.XyoAccount -import network.xyo.client.xyoScope - -const val DATA_STORE_FILE_NAME = "network-xyo-sdk-prefs.pb" - -private val Context.prefsDataStore: DataStore by dataStore( - fileName = DATA_STORE_FILE_NAME, - serializer = PrefsDataStoreSerializer, - corruptionHandler = ReplaceFileCorruptionHandler( - produceNewData = { PrefsDataStore.getDefaultInstance() } - ) -) - -class PrefsRepository(context: Context) { - private val prefsDataStore: DataStore - - init { - this.prefsDataStore = context.prefsDataStore - } - - @RequiresApi(Build.VERSION_CODES.M) - suspend fun getAccount(): XyoAccount { - val savedKeyBytes = getAccountKey().encodeToByteArray() - return XyoAccount(savedKeyBytes) - } - @RequiresApi(Build.VERSION_CODES.M) - private suspend fun getAccountKey(): String { - val savedKey = prefsDataStore.data.first().accountKey - return if (savedKey.isEmpty()) { - val newAccount = XyoAccount() - setAccountKey(newAccount.private.hex) - newAccount.private.hex - } else { - return savedKey - } - } - - private suspend fun setAccountKey(accountKey: String): DataStore { - val job = xyoScope.launch { - this@PrefsRepository.prefsDataStore.updateData { currentPrefs -> - currentPrefs.toBuilder() - .setAccountKey(accountKey) - .build() - } - } - job.join() - return prefsDataStore - } - - suspend fun clearSavedAccountKey(): DataStore { - val job = xyoScope.launch { - this@PrefsRepository.prefsDataStore.updateData { currentPrefs -> - currentPrefs.toBuilder() - .setAccountKey("") - .build() - } - } - job.join() - return prefsDataStore - } -} \ No newline at end of file diff --git a/sdk/src/main/java/network/xyo/client/datastore/XyoAccountDataStore.kt b/sdk/src/main/java/network/xyo/client/datastore/XyoAccountDataStore.kt new file mode 100644 index 0000000..c9620ce --- /dev/null +++ b/sdk/src/main/java/network/xyo/client/datastore/XyoAccountDataStore.kt @@ -0,0 +1,29 @@ +package network.xyo.client.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.MultiProcessDataStoreFactory +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import network.xyo.client.settings.DefaultXyoSdkSettings +import network.xyo.data.PrefsDataStoreProtos.PrefsDataStore +import java.io.File + +val defaults = DefaultXyoSdkSettings() + +fun Context.xyoAccountDataStore(name: String?, path: String?): DataStore { + val resolvedName = name ?: defaults.accountPreferences.fileName + val resolvedPath = path ?: defaults.accountPreferences.storagePath + + val dataStoreFile = File(filesDir, "$resolvedPath/$resolvedName") + + return MultiProcessDataStoreFactory.create( + serializer = PrefsDataStoreSerializer, + produceFile = { dataStoreFile }, + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { PrefsDataStore.getDefaultInstance() } + ), + scope = CoroutineScope(Dispatchers.IO) + ) +} \ No newline at end of file diff --git a/sdk/src/main/java/network/xyo/client/datastore/XyoAccountPrefsRepository.kt b/sdk/src/main/java/network/xyo/client/datastore/XyoAccountPrefsRepository.kt new file mode 100644 index 0000000..6fb86f9 --- /dev/null +++ b/sdk/src/main/java/network/xyo/client/datastore/XyoAccountPrefsRepository.kt @@ -0,0 +1,92 @@ +package network.xyo.client.datastore + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.datastore.core.DataStore +import network.xyo.data.PrefsDataStoreProtos.PrefsDataStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import network.xyo.client.address.XyoAccount +import network.xyo.client.settings.AccountPreferences +import network.xyo.client.xyoScope + + +class XyoAccountPrefsRepository(context: Context, private val _accountPreferences: AccountPreferences = defaults.accountPreferences) { + // This should set the proper paths for the prefs datastore each time the the class is instantiated + @Volatile + private var prefsDataStore: DataStore = context.xyoAccountDataStore( + accountPreferences.fileName, accountPreferences.storagePath + ) + + // Exposing as a getter so path/filename preferences can be fetched from a separate location if needed. + val accountPreferences: AccountPreferences + get() = _accountPreferences + + @RequiresApi(Build.VERSION_CODES.M) + suspend fun getAccount(): XyoAccount { + val savedKeyBytes = getAccountKey().encodeToByteArray() + return XyoAccount(savedKeyBytes) + } + + @RequiresApi(Build.VERSION_CODES.M) + private suspend fun getAccountKey(): String { + val savedKey = prefsDataStore.data.first().accountKey + return if (savedKey.isEmpty()) { + val newAccount = XyoAccount() + setAccountKey(newAccount.private.hex) + newAccount.private.hex + } else { + return savedKey + } + } + + private suspend fun setAccountKey(accountKey: String): DataStore { + val job = xyoScope.launch { + this@XyoAccountPrefsRepository.prefsDataStore.updateData { currentPrefs -> + currentPrefs.toBuilder() + .setAccountKey(accountKey) + .build() + } + } + job.join() + return prefsDataStore + } + + suspend fun clearSavedAccountKey(): DataStore { + val job = xyoScope.launch { + this@XyoAccountPrefsRepository.prefsDataStore.updateData { currentPrefs -> + currentPrefs.toBuilder() + .setAccountKey("") + .build() + } + } + job.join() + return prefsDataStore + } + + // Define the singleton instance within a companion object + companion object { + @Volatile + private var INSTANCE: XyoAccountPrefsRepository? = null + + // Method to retrieve the singleton instance + fun getInstance(context: Context, accountPreferences: AccountPreferences = defaults.accountPreferences): XyoAccountPrefsRepository { + val newInstance = INSTANCE ?: synchronized(this) { + INSTANCE ?: XyoAccountPrefsRepository(context, accountPreferences).also { INSTANCE = it } + } + return newInstance + } + + fun refresh(context: Context, accountPreferences: AccountPreferences): XyoAccountPrefsRepository { + synchronized(this) { + INSTANCE = XyoAccountPrefsRepository(context, accountPreferences) + } + return INSTANCE!! // Return the updated instance + } + + fun resetInstance() { + INSTANCE = null + } + } +} \ No newline at end of file diff --git a/sdk/src/main/java/network/xyo/client/settings/Defaults.kt b/sdk/src/main/java/network/xyo/client/settings/Defaults.kt new file mode 100644 index 0000000..f1e8bdf --- /dev/null +++ b/sdk/src/main/java/network/xyo/client/settings/Defaults.kt @@ -0,0 +1,22 @@ +package network.xyo.client.settings + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import network.xyo.client.address.XyoAccount +import network.xyo.client.datastore.XyoAccountPrefsRepository + +open class DefaultXyoSdkSettings: SettingsInterface { + override val accountPreferences: AccountPreferences = DefaultAccountPreferences() + + @RequiresApi(Build.VERSION_CODES.M) + override suspend fun getAccount(context: Context): XyoAccount? { + val repository = XyoAccountPrefsRepository.getInstance(context) + return repository.getAccount() + } +} + +open class DefaultAccountPreferences: AccountPreferences { + override val fileName = "network-xyo-sdk-prefs" + override val storagePath = "__xyo-client-sdk__" +} \ No newline at end of file diff --git a/sdk/src/main/java/network/xyo/client/settings/SettingsInterface.kt b/sdk/src/main/java/network/xyo/client/settings/SettingsInterface.kt new file mode 100644 index 0000000..d9b80da --- /dev/null +++ b/sdk/src/main/java/network/xyo/client/settings/SettingsInterface.kt @@ -0,0 +1,14 @@ +package network.xyo.client.settings + +import android.content.Context +import network.xyo.client.address.XyoAccount + +interface SettingsInterface { + val accountPreferences: AccountPreferences + suspend fun getAccount(context: Context): XyoAccount? +} + +interface AccountPreferences { + val fileName: String? + val storagePath: String? +} \ No newline at end of file diff --git a/sdk/src/main/java/network/xyo/client/settings/XyoSdk.kt b/sdk/src/main/java/network/xyo/client/settings/XyoSdk.kt new file mode 100644 index 0000000..e855b20 --- /dev/null +++ b/sdk/src/main/java/network/xyo/client/settings/XyoSdk.kt @@ -0,0 +1,34 @@ +package network.xyo.client.settings + +import android.content.Context +import network.xyo.client.datastore.XyoAccountPrefsRepository + +class XyoSdk(val settings: SettingsInterface) { + companion object { + @Volatile + private var INSTANCE: XyoSdk? = null + + fun getInstance(context: Context, settings: SettingsInterface = DefaultXyoSdkSettings()): XyoSdk { + // Initialize the singleton with the users accountPreferences + XyoAccountPrefsRepository.getInstance(context, settings.accountPreferences) + + val newInstance = INSTANCE ?: synchronized(this) { + INSTANCE ?: XyoSdk(settings).also { INSTANCE = it } + } + return newInstance + } + + fun refresh(context: Context, settings: SettingsInterface = DefaultXyoSdkSettings()): XyoSdk { + synchronized(this) { + // Initialize the singleton with the users accountPreferences + XyoAccountPrefsRepository.getInstance(context, settings.accountPreferences) + INSTANCE = XyoSdk(settings) + } + return INSTANCE!! + } + + fun resetInstance() { + INSTANCE = null + } + } +} \ No newline at end of file