From 314e4012d301db66dba07b611dc558fa1192ec5d Mon Sep 17 00:00:00 2001 From: Joseph Sanjaya Date: Wed, 13 Aug 2025 07:20:42 +0700 Subject: [PATCH] feat(#195): adopt koin annotation for dependency injection --- app/build.gradle.kts | 4 + .../java/com/brainwallet/BrainwalletApp.kt | 18 ++- .../FirebaseRemoteConfigRepository.kt | 63 +++++++++ .../data/repository/LtcRepository.kt | 83 ----------- .../data/repository/LtcRepositoryImpl.kt | 93 ++++++++++++ .../repository/SelectedPeersRepository.kt | 84 ----------- .../repository/SelectedPeersRepositoryImpl.kt | 89 ++++++++++++ .../data/repository/SettingRepository.kt | 80 ----------- .../data/repository/SettingRepositoryImpl.kt | 91 ++++++++++++ .../data/source/LocalCacheSource.kt | 2 +- .../data/source/RemoteConfigSource.kt | 59 -------- .../main/java/com/brainwallet/di/AppModule.kt | 95 +++++++++++++ .../main/java/com/brainwallet/di/Module.kt | 132 ------------------ .../navigation/LegacyNavigation.kt | 3 +- .../BrainwalletMessagingService.kt | 8 +- .../notification/NotificationHandler.kt | 125 ++++++++++------- .../brainwallet/ui/BrainwalletViewModel.kt | 2 +- .../buylitecoin/BuyLitecoinViewModel.kt | 3 +- .../ui/screens/home/SettingsViewModel.kt | 3 +- .../home/receive/ReceiveDialogViewModel.kt | 2 + .../screens/inputwords/InputWordsViewModel.kt | 2 + .../ui/screens/ready/ReadyViewModel.kt | 4 +- .../setpasscode/SetPasscodeViewModel.kt | 2 + .../ui/screens/unlock/UnLockViewModel.kt | 3 +- .../ui/screens/welcome/WelcomeViewModel.kt | 2 + .../YourSeedProveItViewModel.kt | 2 + .../yourseedwords/YourSeedWordsViewModel.kt | 2 + .../util/cryptography/KeyStoreKeyGenerator.kt | 61 -------- .../util/cryptography/KeyStoreManager.kt | 4 +- .../impl/KeyStoreKeyGeneratorImpl.kt | 65 +++++++++ .../worker/CurrencyUpdateWorker.kt | 2 + .../brainwallet/BrainwalletScreengrabApp.kt | 4 +- .../FirebaseRemoteConfigRepositoryTest.kt} | 9 +- .../notification/NotificationHandlerTest.kt | 49 +++++-- build.gradle.kts | 1 + gradle/libs.versions.toml | 9 +- 36 files changed, 670 insertions(+), 590 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepository.kt create mode 100644 app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt create mode 100644 app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepositoryImpl.kt create mode 100644 app/src/main/java/com/brainwallet/data/repository/SettingRepositoryImpl.kt create mode 100644 app/src/main/java/com/brainwallet/di/AppModule.kt delete mode 100644 app/src/main/java/com/brainwallet/di/Module.kt create mode 100644 app/src/main/java/com/brainwallet/util/cryptography/impl/KeyStoreKeyGeneratorImpl.kt rename app/src/test/java/com/brainwallet/data/{source/RemoteConfigSourceFirebaseImplTest.kt => repository/FirebaseRemoteConfigRepositoryTest.kt} (91%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66c1b24e..b42bd6bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.jetbrains.kotlin.serialization) alias(libs.plugins.google.services) alias(libs.plugins.firebase.crashlytics) + alias(libs.plugins.ksp) } val localProperties = gradleLocalProperties(rootDir, providers) @@ -215,6 +216,9 @@ dependencies { implementation (libs.airbnb.lottie.compose) implementation(platform(libs.koin.bom)) implementation(libs.bundles.koin) + implementation(platform(libs.koin.annotation.bom)) + implementation(libs.koin.annotation) + ksp(libs.koin.annotation.compiler) implementation(platform(libs.squareup.okhttp.bom)) implementation(libs.bundles.squareup.okhttp) diff --git a/app/src/main/java/com/brainwallet/BrainwalletApp.kt b/app/src/main/java/com/brainwallet/BrainwalletApp.kt index 8a55523b..9143a90f 100644 --- a/app/src/main/java/com/brainwallet/BrainwalletApp.kt +++ b/app/src/main/java/com/brainwallet/BrainwalletApp.kt @@ -6,28 +6,33 @@ import android.app.Application import android.content.Context import android.content.res.Resources import com.appsflyer.AppsFlyerLib -import com.brainwallet.di.appModule -import com.brainwallet.di.dataModule -import com.brainwallet.di.viewModelModule -import com.brainwallet.notification.setupNotificationChannels +import com.brainwallet.data.source.RemoteConfigSource +import com.brainwallet.di.AppModule +import com.brainwallet.notification.NotificationHandler import com.brainwallet.presenter.activities.util.BRActivity import com.brainwallet.presenter.entities.ServiceItems import com.brainwallet.tools.listeners.SyncReceiver import com.brainwallet.tools.manager.AnalyticsManager import com.brainwallet.tools.util.BRConstants import com.brainwallet.tools.util.Utils +import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.GlobalContext.startKoin import org.koin.core.logger.Level +import org.koin.ksp.generated.module import timber.log.Timber import timber.log.Timber.DebugTree import java.util.Timer import java.util.TimerTask import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread open class BrainwalletApp : Application() { + private val remoteConfigSource: RemoteConfigSource by inject() + private val notificationHandler: NotificationHandler by inject() + override fun onCreate() { super.onCreate() @@ -35,7 +40,7 @@ open class BrainwalletApp : Application() { /** DEV: Top placement requirement. **/ val enableCrashlytics = !Utils.isEmulatorOrDebug(this) - setupNotificationChannels(this) + notificationHandler.setupNotificationChannels(this) AnalyticsManager.init(this) AnalyticsManager.logCustomEvent(BRConstants._20191105_AL) @@ -72,8 +77,9 @@ open class BrainwalletApp : Application() { startKoin { androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.ERROR) androidContext(this@BrainwalletApp) - modules(dataModule, viewModelModule, appModule) + modules(AppModule.dataModule, AppModule.module) } + thread { remoteConfigSource.initialize() } } // override fun attachBaseContext(base: Context) { diff --git a/app/src/main/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepository.kt b/app/src/main/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepository.kt new file mode 100644 index 00000000..15d7323f --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepository.kt @@ -0,0 +1,63 @@ +package com.brainwallet.data.repository + +import com.brainwallet.BuildConfig +import com.brainwallet.R +import com.brainwallet.data.source.RemoteConfigSource +import com.google.firebase.remoteconfig.ConfigUpdate +import com.google.firebase.remoteconfig.ConfigUpdateListener +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigException +import com.google.firebase.remoteconfig.remoteConfigSettings +import org.koin.core.annotation.Single +import timber.log.Timber + +@Single(binds = [RemoteConfigSource::class]) +class FirebaseRemoteConfigRepository( + private val remoteConfig: FirebaseRemoteConfig +) : RemoteConfigSource { + + init { + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) { + 0 // fetch every time in debug mode + } else { + 60 * 180 // fetch every 3 hours in production mode + } + } + remoteConfig.setConfigSettingsAsync(configSettings) + remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) + } + + override fun initialize() { + remoteConfig.fetchAndActivate() + .addOnSuccessListener { Timber.d("timber: RemoteConfig Success fetchAndActivate") } + .addOnFailureListener { + Timber.d( + it, + "timber: RemoteConfig Failure fetchAndActivate" + ) + } + remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { + override fun onUpdate(configUpdate: ConfigUpdate) { + Timber.d("timber: [RemoteConfig] onUpdate ${configUpdate.updatedKeys}") + } + + override fun onError(error: FirebaseRemoteConfigException) { + Timber.d("timber: [RemoteConfig] onError ${error.code} | ${error.message}") + } + + }) + } + + override fun getString(key: String): String { + return remoteConfig.getString(key) + } + + override fun getNumber(key: String): Double { + return remoteConfig.getDouble(key) + } + + override fun getBoolean(key: String): Boolean { + return remoteConfig.getBoolean(key) + } +} diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 39542354..b6914b6f 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -1,19 +1,9 @@ package com.brainwallet.data.repository -import android.content.Context -import android.content.SharedPreferences -import androidx.core.net.toUri -import com.brainwallet.BuildConfig import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Fee import com.brainwallet.data.model.MoonpayCurrencyLimit -import com.brainwallet.data.source.RemoteApiSource -import com.brainwallet.data.source.fetchWithCache import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse -import com.brainwallet.tools.manager.BRSharedPrefs -import com.brainwallet.tools.manager.FeeManager -import com.brainwallet.tools.sqlite.CurrencyDataSource -import com.brainwallet.tools.util.Utils interface LtcRepository { suspend fun fetchRates(): List @@ -26,79 +16,6 @@ interface LtcRepository { suspend fun fetchMoonpaySignedUrl(params: Map): String - class Impl( - private val context: Context, - private val remoteApiSource: RemoteApiSource, - private val currencyDataSource: CurrencyDataSource, - private val sharedPreferences: SharedPreferences, - ) : LtcRepository { - - //todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies - override suspend fun fetchRates(): List { - return runCatching { - val rates = remoteApiSource.getRates() - - //legacy logic - FeeManager.updateFeePerKb(context) - val selectedISO = BRSharedPrefs.getIsoSymbol(context) - rates.forEachIndexed { index, currencyEntity -> - if (currencyEntity.code.equals(selectedISO, ignoreCase = true)) { - BRSharedPrefs.putIso(context, currencyEntity.code) - BRSharedPrefs.putCurrencyListPosition(context, index - 1) - } - } - - //save to local - currencyDataSource.putCurrencies(rates) - return rates - }.getOrElse { currencyDataSource.getAllCurrencies(true) } - - } - - /** - * for now we just using [Fee.Default] - * will move to [RemoteApiSource.getFeePerKb] after fix the calculation when we do send - * - * maybe need updaete core if we need to use dynamic fee? - */ - override suspend fun fetchFeePerKb(): Fee = Fee.Default //using static fee - - override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit { - return sharedPreferences.fetchWithCache( - key = "${PREF_KEY_BUY_LIMITS_PREFIX}${baseCurrencyCode.lowercase()}", - cachedAtKey = "${PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT}${baseCurrencyCode.lowercase()}", - cacheTimeMs = 5 * 60 * 1000, //5 minutes - fetchData = { - remoteApiSource.getMoonpayCurrencyLimit(baseCurrencyCode) - } - ) - } - - override suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse = - remoteApiSource.getBuyQuote(params) - - override suspend fun fetchMoonpaySignedUrl(params: Map): String { - val externalTransactionID = Utils.getEncryptedAgentString(context) - val finalParams = params + mapOf( - "defaultCurrencyCode" to "ltc", - "externalTransactionId" to externalTransactionID, - "currencyCode" to "ltc", - "themeId" to "main-v1.0.0", - ) - return remoteApiSource.getMoonpaySignedUrl(finalParams) - .signedUrl.toUri() - .buildUpon() - .apply { - if (BuildConfig.DEBUG) { - authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com - } - } - .build() - .toString() - } - - } - companion object { const val PREF_KEY_NETWORK_FEE_PER_KB = "network_fee_per_kb" const val PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT = "${PREF_KEY_NETWORK_FEE_PER_KB}_cached_at" diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt new file mode 100644 index 00000000..72730ccc --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt @@ -0,0 +1,93 @@ +package com.brainwallet.data.repository + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.net.toUri +import com.brainwallet.BuildConfig +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.MoonpayCurrencyLimit +import com.brainwallet.data.repository.LtcRepository.Companion.PREF_KEY_BUY_LIMITS_PREFIX +import com.brainwallet.data.repository.LtcRepository.Companion.PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT +import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.data.source.fetchWithCache +import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.tools.manager.FeeManager +import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.tools.util.Utils +import org.koin.core.annotation.Single + +@Single(binds = [LtcRepository::class]) +class LtcRepositoryImpl( + private val context: Context, + private val remoteApiSource: RemoteApiSource, + private val currencyDataSource: CurrencyDataSource, + private val sharedPreferences: SharedPreferences, +) : LtcRepository { + + //todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies + override suspend fun fetchRates(): List { + return runCatching { + val rates = remoteApiSource.getRates() + + //legacy logic + FeeManager.updateFeePerKb(context) + val selectedISO = BRSharedPrefs.getIsoSymbol(context) + rates.forEachIndexed { index, currencyEntity -> + if (currencyEntity.code.equals(selectedISO, ignoreCase = true)) { + BRSharedPrefs.putIso(context, currencyEntity.code) + BRSharedPrefs.putCurrencyListPosition(context, index - 1) + } + } + + //save to local + currencyDataSource.putCurrencies(rates) + return rates + }.getOrElse { currencyDataSource.getAllCurrencies(true) } + + } + + /** + * for now we just using [Fee.Default] + * will move to [RemoteApiSource.getFeePerKb] after fix the calculation when we do send + * + * maybe need updaete core if we need to use dynamic fee? + */ + override suspend fun fetchFeePerKb(): Fee = Fee.Default //using static fee + + override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit { + return sharedPreferences.fetchWithCache( + key = "${PREF_KEY_BUY_LIMITS_PREFIX}${baseCurrencyCode.lowercase()}", + cachedAtKey = "${PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT}${baseCurrencyCode.lowercase()}", + cacheTimeMs = 5 * 60 * 1000, //5 minutes + fetchData = { + remoteApiSource.getMoonpayCurrencyLimit(baseCurrencyCode) + } + ) + } + + override suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse = + remoteApiSource.getBuyQuote(params) + + override suspend fun fetchMoonpaySignedUrl(params: Map): String { + val externalTransactionID = Utils.getEncryptedAgentString(context) + val finalParams = params + mapOf( + "defaultCurrencyCode" to "ltc", + "externalTransactionId" to externalTransactionID, + "currencyCode" to "ltc", + "themeId" to "main-v1.0.0", + ) + return remoteApiSource.getMoonpaySignedUrl(finalParams) + .signedUrl.toUri() + .buildUpon() + .apply { + if (BuildConfig.DEBUG) { + authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com + } + } + .build() + .toString() + } + +} diff --git a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt index 0275d542..2d8513ff 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt @@ -1,93 +1,9 @@ package com.brainwallet.data.repository -import android.content.SharedPreferences -import androidx.core.content.edit -import com.brainwallet.di.json -import kotlinx.serialization.json.jsonObject -import okhttp3.Call -import okhttp3.Callback -import okhttp3.OkHttpClient -import okhttp3.Response -import okio.IOException -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - interface SelectedPeersRepository { suspend fun fetchSelectedPeers(): Set - class Impl( - private val okHttpClient: OkHttpClient, - private val sharedPreferences: SharedPreferences, - ) : SelectedPeersRepository { - - private companion object { - const val PREF_KEY_SELECTED_PEERS = "selected_peers" - const val PREF_KEY_SELECTED_PEERS_CACHED_AT = "${PREF_KEY_SELECTED_PEERS}_cached_at" - } - - override suspend fun fetchSelectedPeers(): Set { - val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, 0) - val currentTime = System.currentTimeMillis() - val cachedPeers = sharedPreferences.getStringSet(PREF_KEY_SELECTED_PEERS, null) - - // Check if cache exists and is less than 6 hours old - if (!cachedPeers.isNullOrEmpty() && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) { - return cachedPeers - } - - val request = okhttp3.Request.Builder() - .url(LITECOIN_NODES_URL) - .build() - - return suspendCoroutine { continuation -> - okHttpClient.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - continuation.resume(emptySet()) //just return empty if failed or need hardcoded? - } - - override fun onResponse(call: Call, response: Response) { - val jsonString = response.body?.string() - - if (response.isSuccessful.not()) { - continuation.resume(cachedPeers ?: emptySet()) - return - } - - val parsedResult = jsonString?.let { - val jsonElement = json.parseToJsonElement(it) - val dataObject = jsonElement.jsonObject["data"]?.jsonObject - val nodesObject = dataObject?.get("nodes")?.jsonObject - - //filter criteria - val requiredServices = 0x01 or 0x04 // NODE_NETWORK | NODE_BLOOM - - nodesObject?.entries - ?.filter { entry -> - val flags = - entry.value.jsonObject["flags"]?.toString()?.toIntOrNull() - flags != null && (flags and requiredServices) == requiredServices - } - ?.map { it.key.replace(":9333", "") } - ?.toSet().also { Timber.d("Total Selected Peers ${it?.size}") } - ?: emptySet() - - } ?: emptySet() - - sharedPreferences.edit { - putStringSet(PREF_KEY_SELECTED_PEERS, parsedResult) - putLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, currentTime) - } - - continuation.resume(parsedResult) - } - }) - } - } - - } - companion object { const val LITECOIN_NODES_URL = "https://api.blockchair.com/litecoin/nodes" } diff --git a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepositoryImpl.kt b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepositoryImpl.kt new file mode 100644 index 00000000..454be2d2 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepositoryImpl.kt @@ -0,0 +1,89 @@ +package com.brainwallet.data.repository + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.brainwallet.data.repository.SelectedPeersRepository.Companion.LITECOIN_NODES_URL +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Response +import okio.IOException +import org.koin.core.annotation.Single +import timber.log.Timber +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@Single(binds = [SelectedPeersRepository::class]) +class SelectedPeersRepositoryImpl( + private val okHttpClient: OkHttpClient, + private val sharedPreferences: SharedPreferences, + private val json: Json +) : SelectedPeersRepository { + + private companion object { + const val PREF_KEY_SELECTED_PEERS = "selected_peers" + const val PREF_KEY_SELECTED_PEERS_CACHED_AT = "${PREF_KEY_SELECTED_PEERS}_cached_at" + } + + override suspend fun fetchSelectedPeers(): Set { + val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, 0) + val currentTime = System.currentTimeMillis() + val cachedPeers = sharedPreferences.getStringSet(PREF_KEY_SELECTED_PEERS, null) + + // Check if cache exists and is less than 6 hours old + if (!cachedPeers.isNullOrEmpty() && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) { + return cachedPeers + } + + val request = okhttp3.Request.Builder() + .url(LITECOIN_NODES_URL) + .build() + + return suspendCoroutine { continuation -> + okHttpClient.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resume(emptySet()) //just return empty if failed or need hardcoded? + } + + override fun onResponse(call: Call, response: Response) { + val jsonString = response.body?.string() + + if (response.isSuccessful.not()) { + continuation.resume(cachedPeers ?: emptySet()) + return + } + + val parsedResult = jsonString?.let { + val jsonElement = json.parseToJsonElement(it) + val dataObject = jsonElement.jsonObject["data"]?.jsonObject + val nodesObject = dataObject?.get("nodes")?.jsonObject + + //filter criteria + val requiredServices = 0x01 or 0x04 // NODE_NETWORK | NODE_BLOOM + + nodesObject?.entries + ?.filter { entry -> + val flags = + entry.value.jsonObject["flags"]?.toString()?.toIntOrNull() + flags != null && (flags and requiredServices) == requiredServices + } + ?.map { it.key.replace(":9333", "") } + ?.toSet().also { Timber.d("Total Selected Peers ${it?.size}") } + ?: emptySet() + + } ?: emptySet() + + sharedPreferences.edit { + putStringSet(PREF_KEY_SELECTED_PEERS, parsedResult) + putLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, currentTime) + } + + continuation.resume(parsedResult) + } + }) + } + } + +} diff --git a/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt index 3f217918..484b5249 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt @@ -1,17 +1,8 @@ package com.brainwallet.data.repository -import android.content.SharedPreferences -import androidx.core.content.edit import com.brainwallet.data.model.AppSetting -import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Language -import com.brainwallet.tools.manager.FeeManager -import com.brainwallet.tools.sqlite.CurrencyDataSource import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update interface SettingRepository { @@ -41,77 +32,6 @@ interface SettingRepository { fun getSelectedFeeType(): String - class Impl( - private val sharedPreferences: SharedPreferences, - private val currencyDataSource: CurrencyDataSource - ) : SettingRepository { - - private val _state = MutableStateFlow(load()) - val state: StateFlow = _state.asStateFlow() - - override val settings: Flow - get() = state - - override suspend fun save(setting: AppSetting) { - sharedPreferences.edit { - putBoolean(KEY_IS_DARK_MODE, setting.isDarkMode) - putString(KEY_LANGUAGE_CODE, setting.languageCode) - putString(KEY_FIAT_CURRENCY_CODE, setting.currency.code) - } - _state.update { setting } - } - - override fun getCurrentLanguage(): Language { - return sharedPreferences.getString(KEY_LANGUAGE_CODE, Language.ENGLISH.code) - .let { languageCode -> Language.find(languageCode) } - } - - override fun updateCurrentLanguage(languageCode: String) { - sharedPreferences.edit { putString(KEY_LANGUAGE_CODE, languageCode) } - _state.update { it.copy(languageCode = languageCode) } - } - - override fun isDarkMode(): Boolean { - return sharedPreferences.getBoolean(KEY_IS_DARK_MODE, true) - } - - override fun toggleDarkMode(isDarkMode: Boolean) { - sharedPreferences.edit { - putBoolean(KEY_IS_DARK_MODE, isDarkMode) - } - _state.update { it.copy(isDarkMode = isDarkMode) } - } - - override fun putSelectedFeeType(feeType: String) { - sharedPreferences.edit { - putString(KEY_SELECTED_FEE_TYPE, feeType) - } - } - - override fun getSelectedFeeType(): String = - sharedPreferences.getString(KEY_SELECTED_FEE_TYPE, FeeManager.REGULAR) - ?: FeeManager.REGULAR - - - private fun load(): AppSetting { - return AppSetting( - isDarkMode = sharedPreferences.getBoolean(KEY_IS_DARK_MODE, true), - languageCode = sharedPreferences.getString(KEY_LANGUAGE_CODE, Language.ENGLISH.code) - ?: Language.ENGLISH.code, - currency = sharedPreferences.getString(KEY_FIAT_CURRENCY_CODE, "USD").let { - currencyDataSource.getCurrencyByIso(it) - ?: return@let CurrencyEntity( - "USD", - "US Dollar", - -1f, - "$" - ) - } - - ) - } - } - companion object { const val KEY_IS_DARK_MODE = "is_dark_mode" const val KEY_LANGUAGE_CODE = "language_code" diff --git a/app/src/main/java/com/brainwallet/data/repository/SettingRepositoryImpl.kt b/app/src/main/java/com/brainwallet/data/repository/SettingRepositoryImpl.kt new file mode 100644 index 00000000..fc862d72 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/SettingRepositoryImpl.kt @@ -0,0 +1,91 @@ +package com.brainwallet.data.repository + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.brainwallet.data.model.AppSetting +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.Language +import com.brainwallet.data.repository.SettingRepository.Companion.KEY_FIAT_CURRENCY_CODE +import com.brainwallet.data.repository.SettingRepository.Companion.KEY_IS_DARK_MODE +import com.brainwallet.data.repository.SettingRepository.Companion.KEY_LANGUAGE_CODE +import com.brainwallet.data.repository.SettingRepository.Companion.KEY_SELECTED_FEE_TYPE +import com.brainwallet.tools.manager.FeeManager +import com.brainwallet.tools.sqlite.CurrencyDataSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.annotation.Single + +@Single(binds = [SettingRepository::class]) +class SettingRepositoryImpl( + private val sharedPreferences: SharedPreferences, + private val currencyDataSource: CurrencyDataSource +) : SettingRepository { + + private val _state = MutableStateFlow(load()) + val state: StateFlow = _state.asStateFlow() + + override val settings: Flow + get() = state + + override suspend fun save(setting: AppSetting) { + sharedPreferences.edit { + putBoolean(KEY_IS_DARK_MODE, setting.isDarkMode) + putString(KEY_LANGUAGE_CODE, setting.languageCode) + putString(KEY_FIAT_CURRENCY_CODE, setting.currency.code) + } + _state.update { setting } + } + + override fun getCurrentLanguage(): Language { + return sharedPreferences.getString(KEY_LANGUAGE_CODE, Language.ENGLISH.code) + .let { languageCode -> Language.find(languageCode) } + } + + override fun updateCurrentLanguage(languageCode: String) { + sharedPreferences.edit { putString(KEY_LANGUAGE_CODE, languageCode) } + _state.update { it.copy(languageCode = languageCode) } + } + + override fun isDarkMode(): Boolean { + return sharedPreferences.getBoolean(KEY_IS_DARK_MODE, true) + } + + override fun toggleDarkMode(isDarkMode: Boolean) { + sharedPreferences.edit { + putBoolean(KEY_IS_DARK_MODE, isDarkMode) + } + _state.update { it.copy(isDarkMode = isDarkMode) } + } + + override fun putSelectedFeeType(feeType: String) { + sharedPreferences.edit { + putString(KEY_SELECTED_FEE_TYPE, feeType) + } + } + + override fun getSelectedFeeType(): String = + sharedPreferences.getString(KEY_SELECTED_FEE_TYPE, FeeManager.REGULAR) + ?: FeeManager.REGULAR + + + private fun load(): AppSetting { + return AppSetting( + isDarkMode = sharedPreferences.getBoolean(KEY_IS_DARK_MODE, true), + languageCode = sharedPreferences.getString(KEY_LANGUAGE_CODE, Language.ENGLISH.code) + ?: Language.ENGLISH.code, + currency = sharedPreferences.getString(KEY_FIAT_CURRENCY_CODE, "USD").let { + currencyDataSource.getCurrencyByIso(it) + ?: return@let CurrencyEntity( + "USD", + "US Dollar", + -1f, + "$" + ) + } + + ) + } +} diff --git a/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt b/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt index d86d312d..e00a051e 100644 --- a/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt @@ -2,7 +2,7 @@ package com.brainwallet.data.source import android.content.SharedPreferences import androidx.core.content.edit -import com.brainwallet.di.json +import com.brainwallet.di.AppModule.json import kotlinx.serialization.encodeToString /** diff --git a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt index dc5c7ff4..d39b04fb 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt @@ -1,14 +1,5 @@ package com.brainwallet.data.source -import com.brainwallet.BuildConfig -import com.brainwallet.R -import com.google.firebase.remoteconfig.ConfigUpdate -import com.google.firebase.remoteconfig.ConfigUpdateListener -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.FirebaseRemoteConfigException -import com.google.firebase.remoteconfig.remoteConfigSettings -import timber.log.Timber - interface RemoteConfigSource { companion object { @@ -20,54 +11,4 @@ interface RemoteConfigSource { fun getString(key: String): String fun getNumber(key: String): Double fun getBoolean(key: String): Boolean - - class FirebaseImpl( - private val remoteConfig: FirebaseRemoteConfig - ) : RemoteConfigSource { - - init { - val configSettings = remoteConfigSettings { - minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) { - 0 // fetch every time in debug mode - } else { - 60 * 180 // fetch every 3 hours in production mode - } - } - remoteConfig.setConfigSettingsAsync(configSettings) - remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) - } - - override fun initialize() { - remoteConfig.fetchAndActivate() - .addOnSuccessListener { Timber.d("timber: RemoteConfig Success fetchAndActivate") } - .addOnFailureListener { - Timber.d( - it, - "timber: RemoteConfig Failure fetchAndActivate" - ) - } - remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { - override fun onUpdate(configUpdate: ConfigUpdate) { - Timber.d("timber: [RemoteConfig] onUpdate ${configUpdate.updatedKeys}") - } - - override fun onError(error: FirebaseRemoteConfigException) { - Timber.d("timber: [RemoteConfig] onError ${error.code} | ${error.message}") - } - - }) - } - - override fun getString(key: String): String { - return remoteConfig.getString(key) - } - - override fun getNumber(key: String): Double { - return remoteConfig.getDouble(key) - } - - override fun getBoolean(key: String): Boolean { - return remoteConfig.getBoolean(key) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/di/AppModule.kt b/app/src/main/java/com/brainwallet/di/AppModule.kt new file mode 100644 index 00000000..3c9fd77c --- /dev/null +++ b/app/src/main/java/com/brainwallet/di/AppModule.kt @@ -0,0 +1,95 @@ +package com.brainwallet.di + +import android.content.Context +import android.content.SharedPreferences +import com.brainwallet.BuildConfig +import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.tools.util.BRConstants +import com.google.firebase.Firebase +import com.google.firebase.remoteconfig.remoteConfig +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.android.ext.koin.androidApplication +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlin.getValue + +@Module +@ComponentScan("com.brainwallet") +object AppModule { + + val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + prettyPrint = true + } + + val dataModule = module { + single { Firebase.remoteConfig } + single { json } + factory { provideOkHttpClient() } + single { provideRetrofit(get(), get(), BRConstants.BW_API_PROD_HOST) } + single { provideApi(get()) } + single { CurrencyDataSource.getInstance(get()) } + single { provideSharedPreferences(context = androidApplication()) } + } + + + private fun provideSharedPreferences( + context: Context, + name: String = "${BuildConfig.APPLICATION_ID}.prefs" + ): SharedPreferences { + return context.getSharedPreferences(name, Context.MODE_PRIVATE) + } + + private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() + .addInterceptor { chain -> + val requestBuilder = chain.request() + .newBuilder() + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .addHeader("X-Litecoin-Testnet", "false") + .addHeader("Accept-Language", "en") + chain.proceed(requestBuilder.build()) + } + .addInterceptor(HttpLoggingInterceptor().apply { + setLevel( + when { + BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY + else -> HttpLoggingInterceptor.Level.NONE + } + ) + }) + .build() + + internal fun provideRetrofit( + json: Json, + okHttpClient: OkHttpClient, + baseUrl: String = BRConstants.BW_API_PROD_HOST, + ): Retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory( + json.asConverterFactory( + "application/json; charset=UTF8".toMediaType() + ) + ) + .build() + + internal inline fun provideApi(retrofit: Retrofit): T = + retrofit.create(T::class.java) + + inline fun getKoinInstance(): T { + return object : KoinComponent { + val value: T by inject() + }.value + } +} diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt deleted file mode 100644 index d8f47bd3..00000000 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.brainwallet.di - -import android.content.Context -import android.content.SharedPreferences -import com.brainwallet.BuildConfig -import com.brainwallet.data.repository.LtcRepository -import com.brainwallet.data.repository.SelectedPeersRepository -import com.brainwallet.data.repository.SettingRepository -import com.brainwallet.data.source.RemoteApiSource -import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.sqlite.CurrencyDataSource -import com.brainwallet.tools.util.BRConstants -import com.brainwallet.ui.screens.buylitecoin.BuyLitecoinViewModel -import com.brainwallet.ui.screens.home.SettingsViewModel -import com.brainwallet.ui.screens.home.receive.ReceiveDialogViewModel -import com.brainwallet.ui.screens.inputwords.InputWordsViewModel -import com.brainwallet.ui.screens.ready.ReadyViewModel -import com.brainwallet.ui.screens.setpasscode.SetPasscodeViewModel -import com.brainwallet.ui.screens.unlock.UnLockViewModel -import com.brainwallet.ui.screens.welcome.WelcomeViewModel -import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel -import com.brainwallet.ui.screens.yourseedwords.YourSeedWordsViewModel -import com.brainwallet.util.cryptography.KeyStoreKeyGenerator -import com.brainwallet.util.cryptography.KeyStoreManager -import com.brainwallet.worker.CurrencyUpdateWorker -import com.google.firebase.Firebase -import com.google.firebase.remoteconfig.remoteConfig -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.koin.android.ext.koin.androidApplication -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.koin.core.module.dsl.viewModel -import org.koin.core.module.dsl.viewModelOf -import org.koin.dsl.module -import retrofit2.Retrofit -import retrofit2.converter.kotlinx.serialization.asConverterFactory - -//todo module using koin as di framework here - -val json = Json { - ignoreUnknownKeys = true - explicitNulls = false - prettyPrint = true -} - -val dataModule = module { - factory { provideOkHttpClient() } - single { provideRetrofit(get(), BRConstants.BW_API_PROD_HOST) } - - single { provideApi(get()) } - - single { - RemoteConfigSource.FirebaseImpl(Firebase.remoteConfig).also { - it.initialize() - } - } - single { SelectedPeersRepository.Impl(get(), get()) } - single { CurrencyDataSource.getInstance(get()) } - single { provideSharedPreferences(context = androidApplication()) } - single { SettingRepository.Impl(get(), get()) } - single { LtcRepository.Impl(get(), get(), get(), get()) } -} - -val viewModelModule = module { - viewModelOf(::WelcomeViewModel) - viewModelOf(::SettingsViewModel) - viewModel { ReadyViewModel() } - viewModel { InputWordsViewModel() } - viewModel { SetPasscodeViewModel() } - viewModel { UnLockViewModel() } - viewModel { YourSeedProveItViewModel() } - viewModel { YourSeedWordsViewModel() } - viewModel { ReceiveDialogViewModel(get(), get()) } - viewModel { BuyLitecoinViewModel(get(), get()) } -} - -val appModule = module { - single { KeyStoreManager(get(), KeyStoreKeyGenerator.Impl()) } - single { CurrencyUpdateWorker(get()) } -} - -private fun provideSharedPreferences( - context: Context, - name: String = "${BuildConfig.APPLICATION_ID}.prefs" -): SharedPreferences { - return context.getSharedPreferences(name, Context.MODE_PRIVATE) -} - -private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() - .addInterceptor { chain -> - val requestBuilder = chain.request() - .newBuilder() - .addHeader("Accept", "application/json") - .addHeader("Content-Type", "application/json") - .addHeader("X-Litecoin-Testnet", "false") - .addHeader("Accept-Language", "en") - chain.proceed(requestBuilder.build()) - } - .addInterceptor(HttpLoggingInterceptor().apply { - setLevel( - when { - BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY - else -> HttpLoggingInterceptor.Level.NONE - } - ) - }) - .build() - -internal fun provideRetrofit( - okHttpClient: OkHttpClient, - baseUrl: String = BRConstants.BW_API_PROD_HOST -): Retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .client(okHttpClient) - .addConverterFactory( - json.asConverterFactory( - "application/json; charset=UTF8".toMediaType() - ) - ) - .build() - -internal inline fun provideApi(retrofit: Retrofit): T = - retrofit.create(T::class.java) - -inline fun getKoinInstance(): T { - return object : KoinComponent { - val value: T by inject() - }.value -} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt index 26e1b5dc..10503556 100644 --- a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt +++ b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt @@ -10,8 +10,7 @@ import androidx.core.net.toUri import com.brainwallet.BuildConfig import com.brainwallet.R import com.brainwallet.data.repository.LtcRepository -import com.brainwallet.data.source.RemoteApiSource -import com.brainwallet.di.getKoinInstance +import com.brainwallet.di.AppModule.getKoinInstance import com.brainwallet.presenter.activities.BreadActivity import com.brainwallet.ui.BrainwalletActivity import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/com/brainwallet/notification/BrainwalletMessagingService.kt b/app/src/main/java/com/brainwallet/notification/BrainwalletMessagingService.kt index d44ba2af..06cd68c7 100644 --- a/app/src/main/java/com/brainwallet/notification/BrainwalletMessagingService.kt +++ b/app/src/main/java/com/brainwallet/notification/BrainwalletMessagingService.kt @@ -1,11 +1,13 @@ package com.brainwallet.notification +import com.brainwallet.di.AppModule import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import timber.log.Timber - -class BrainwalletMessagingService : FirebaseMessagingService() { +class BrainwalletMessagingService( + private val notificationHandler: NotificationHandler = AppModule.getKoinInstance() +) : FirebaseMessagingService() { override fun onCreate() { super.onCreate() @@ -19,7 +21,7 @@ class BrainwalletMessagingService : FirebaseMessagingService() { Timber.d("timber: onMessageReceived data=${message.data}") Timber.d("timber: onMessageReceived notification=${message.notification?.title}, ${message.notification?.body}") - if (NotificationHandler.handleMessageReceived(this, message)) { + if (notificationHandler.handleMessageReceived(this, message)) { return } } diff --git a/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt b/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt index e0284842..893d37a6 100644 --- a/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt +++ b/app/src/main/java/com/brainwallet/notification/NotificationHandler.kt @@ -11,15 +11,34 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.brainwallet.R +import com.brainwallet.notification.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE +import com.brainwallet.notification.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ID_GENERAL +import com.brainwallet.notification.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS import com.brainwallet.presenter.activities.BreadActivity import com.google.firebase.messaging.RemoteMessage -import com.brainwallet.notification.NotificationHandler.NOTIFICATION_CHANNEL_ID_GENERAL -import com.brainwallet.notification.NotificationHandler.NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS -import com.brainwallet.notification.NotificationHandler.NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE +import org.koin.core.annotation.Single -object NotificationHandler { +fun interface NotificationHandlerBuilderProvider { + fun createNotificationBuilder(context: Context, channelId: String): NotificationCompat.Builder +} + +fun interface NotificationNotifier { + fun notify(context: Context, notificationBuilder: NotificationCompat.Builder) +} + +@Single +@SuppressLint("MissingPermission") +class NotificationHandler( + private val notificationBuilderProvider: NotificationHandlerBuilderProvider = + NotificationHandlerBuilderProvider { context, channelId -> + NotificationCompat.Builder(context, channelId) + }, + private val notificationNotifier: NotificationNotifier = NotificationNotifier { context, notificationBuilder -> + NotificationManagerCompat.from(context) + .notify(System.currentTimeMillis().toInt(), notificationBuilder.build()) + } +) { - @SuppressLint("MissingPermission") fun handleMessageReceived(context: Context, remoteMessage: RemoteMessage): Boolean { if (remoteMessage.data.containsKey(KEY_DATA_BRAINWALLET).not()) { return false @@ -36,66 +55,68 @@ object NotificationHandler { val intent = Intent(context, BreadActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } - val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val pendingIntent = + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val notification = NotificationCompat.Builder(context, channelId) + val notification = notificationBuilderProvider.createNotificationBuilder(context, channelId) .setContentTitle(title) .setContentText(body) .setSmallIcon(R.drawable.brainwallet_logotype_white) .setContentIntent(pendingIntent) .setAutoCancel(true) - NotificationManagerCompat.from(context) - .notify(System.currentTimeMillis().toInt(), notification.build()) + notificationNotifier.notify(context, notification) return true } - const val KEY_DATA_BRAINWALLET = "brainwallet" + fun setupNotificationChannels(context: Context) { + createNotificationChannel( + context, + NOTIFICATION_CHANNEL_ID_GENERAL, + context.getString(R.string.notification_channel_name_general) + ) + createNotificationChannel( + context, + NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS, + context.getString(R.string.notification_channel_name_litecoin_news) + ) + createNotificationChannel( + context, + NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE, + context.getString(R.string.notification_channel_name_brainwallet_update) + ) + } - const val NOTIFICATION_CHANNEL_ID_GENERAL = "general" - const val NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS = "litecoin-news" - const val NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE = "brainwallet-update" + private fun createNotificationChannel( + context: Context, + channelId: String, + name: String, + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel( + channelId, + name, + NotificationManager.IMPORTANCE_DEFAULT + ).also { notificationChannel -> + val notificationManager = + context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(notificationChannel) + } + } + } - val defaultNotificationChannels = setOf( - NOTIFICATION_CHANNEL_ID_GENERAL, - NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS, - NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE - ) -} + companion object { + const val KEY_DATA_BRAINWALLET = "brainwallet" -fun setupNotificationChannels(context: Context) { - createNotificationChannel( - context, - NOTIFICATION_CHANNEL_ID_GENERAL, - context.getString(R.string.notification_channel_name_general) - ) - createNotificationChannel( - context, - NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS, - context.getString(R.string.notification_channel_name_litecoin_news) - ) - createNotificationChannel( - context, - NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE, - context.getString(R.string.notification_channel_name_brainwallet_update) - ) -} + const val NOTIFICATION_CHANNEL_ID_GENERAL = "general" + const val NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS = "litecoin-news" + const val NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE = "brainwallet-update" -private fun createNotificationChannel( - context: Context, - channelId: String, - name: String, -) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel( - channelId, - name, - NotificationManager.IMPORTANCE_DEFAULT - ).also { notificationChannel -> - val notificationManager = - context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(notificationChannel) - } + val defaultNotificationChannels = setOf( + NOTIFICATION_CHANNEL_ID_GENERAL, + NOTIFICATION_CHANNEL_ID_LITECOIN_NEWS, + NOTIFICATION_CHANNEL_ID_BRAINWALLET_UPDATE + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/brainwallet/ui/BrainwalletViewModel.kt b/app/src/main/java/com/brainwallet/ui/BrainwalletViewModel.kt index 381d9c77..9d526ec7 100644 --- a/app/src/main/java/com/brainwallet/ui/BrainwalletViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/BrainwalletViewModel.kt @@ -2,7 +2,7 @@ package com.brainwallet.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.brainwallet.di.json +import com.brainwallet.di.AppModule.json import com.brainwallet.navigation.UiEffect import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt index 5e6e6316..30975ab3 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt @@ -1,6 +1,5 @@ package com.brainwallet.ui.screens.buylitecoin -import android.util.Log import androidx.lifecycle.viewModelScope import com.brainwallet.R import com.brainwallet.data.model.AppSetting @@ -21,7 +20,9 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel +@KoinViewModel class BuyLitecoinViewModel( private val settingRepository: SettingRepository, private val ltcRepository: LtcRepository diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt index 738af4be..46f5c426 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt @@ -22,8 +22,9 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel - +@KoinViewModel class SettingsViewModel( private val settingRepository: SettingRepository, private val ltcRepository: LtcRepository diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt index f6b2ba8a..e381351c 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt @@ -21,8 +21,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel //todo: wip +@KoinViewModel class ReceiveDialogViewModel( private val settingRepository: SettingRepository, private val ltcRepository: LtcRepository, diff --git a/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt index 7da2b003..bfdd4aad 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt @@ -16,7 +16,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel +@KoinViewModel class InputWordsViewModel : BrainwalletViewModel() { private val _state = MutableStateFlow(InputWordsState()) diff --git a/app/src/main/java/com/brainwallet/ui/screens/ready/ReadyViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/ready/ReadyViewModel.kt index b66a6889..25fac5c8 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/ready/ReadyViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/ready/ReadyViewModel.kt @@ -1,7 +1,9 @@ package com.brainwallet.ui.screens.ready import com.brainwallet.ui.BrainwalletViewModel +import org.koin.android.annotation.KoinViewModel +@KoinViewModel class ReadyViewModel : BrainwalletViewModel() { override fun onEvent(event: ReadyEvent) { @@ -9,4 +11,4 @@ class ReadyViewModel : BrainwalletViewModel() { is ReadyEvent.OnLoad -> Unit } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt index cd867bc7..982dd71d 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/setpasscode/SetPasscodeViewModel.kt @@ -11,7 +11,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel +@KoinViewModel class SetPasscodeViewModel : BrainwalletViewModel() { private val _state = MutableStateFlow(SetPasscodeState()) diff --git a/app/src/main/java/com/brainwallet/ui/screens/unlock/UnLockViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/unlock/UnLockViewModel.kt index 686b562c..30e3274a 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/unlock/UnLockViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/unlock/UnLockViewModel.kt @@ -15,10 +15,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel import timber.log.Timber import java.math.BigDecimal - +@KoinViewModel class UnLockViewModel : BrainwalletViewModel() { private val _state = MutableStateFlow(UnLockState()) diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeViewModel.kt index 5045014e..47a282e0 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeViewModel.kt @@ -20,8 +20,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel //TODO: revisit this later +@KoinViewModel class WelcomeViewModel( private val settingRepository: SettingRepository ) : BrainwalletViewModel() { diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt index c6406a2d..e54f745f 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt @@ -8,7 +8,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel +@KoinViewModel class YourSeedProveItViewModel : BrainwalletViewModel() { private val _state = MutableStateFlow(YourSeedProveItState()) diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt index 562b8b97..b85e71c7 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt @@ -5,7 +5,9 @@ import com.brainwallet.navigation.Route import com.brainwallet.navigation.UiEffect import com.brainwallet.ui.BrainwalletViewModel import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel +@KoinViewModel class YourSeedWordsViewModel : BrainwalletViewModel() { override fun onEvent(event: YourSeedWordsEvent) { diff --git a/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreKeyGenerator.kt b/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreKeyGenerator.kt index ff4c51ac..c8aa3d92 100644 --- a/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreKeyGenerator.kt +++ b/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreKeyGenerator.kt @@ -1,68 +1,7 @@ package com.brainwallet.util.cryptography - -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties.AUTH_BIOMETRIC_STRONG -import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES -import android.security.keystore.KeyProperties.PURPOSE_DECRYPT -import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT -import com.brainwallet.tools.security.BRKeyStore -import com.brainwallet.tools.security.BRKeyStore.NEW_BLOCK_MODE -import com.brainwallet.tools.security.BRKeyStore.NEW_PADDING -import javax.crypto.KeyGenerator import javax.crypto.SecretKey interface KeyStoreKeyGenerator { fun generateKey(alias: String, isAuthRequired: Boolean, authTimeout: Int?): SecretKey - - class Impl : KeyStoreKeyGenerator { - - private val keyGenerator by lazy { - KeyGenerator.getInstance(KEY_ALGORITHM_AES, BRKeyStore.ANDROID_KEY_STORE) - } - - override fun generateKey( - alias: String, - isAuthRequired: Boolean, - authTimeout: Int? - ): SecretKey { - val keySpecBuilder = getKeyGenSpecBuilder(alias) - if (isAuthRequired) { - setUserAuth(keySpecBuilder, authTimeout) - } - keyGenerator.init(keySpecBuilder.build()) - return keyGenerator.generateKey() - } - - private fun getKeyGenSpecBuilder(alias: String): KeyGenParameterSpec.Builder { - val purposes = PURPOSE_DECRYPT or PURPOSE_ENCRYPT - - return KeyGenParameterSpec.Builder(alias, purposes) - .setBlockModes(NEW_BLOCK_MODE) - .setRandomizedEncryptionRequired(false) - .setEncryptionPaddings(NEW_PADDING) - } - - private fun setUserAuth( - builder: KeyGenParameterSpec.Builder, authTimeout: Int? - ) { - builder.setUserAuthenticationRequired(true) - if (authTimeout != null) { - setAuthTimeout(builder, authTimeout) - } - } - - private fun setAuthTimeout(builder: KeyGenParameterSpec.Builder, authTimeout: Int) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (authTimeout != 0) { - builder.setUserAuthenticationParameters(authTimeout, AUTH_BIOMETRIC_STRONG) - } - } else { - val timeout = if (authTimeout == 0) -1 else authTimeout - builder.setUserAuthenticationValidityDurationSeconds(timeout) - } - } - - } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreManager.kt b/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreManager.kt index 6f72268b..1223f2fb 100644 --- a/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreManager.kt +++ b/app/src/main/java/com/brainwallet/util/cryptography/KeyStoreManager.kt @@ -4,14 +4,14 @@ import android.content.Context import android.security.keystore.UserNotAuthenticatedException import com.brainwallet.tools.security.BRKeyStore import com.brainwallet.tools.security.BRKeyStore.AliasObject -import com.brainwallet.tools.security.BRKeyStore.CANARY_ALIAS -import com.brainwallet.tools.security.BRKeyStore.PHRASE_ALIAS import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koin.core.annotation.Single import timber.log.Timber import java.security.KeyStore +@Single class KeyStoreManager( private val context: Context, private val keyGenerator: KeyStoreKeyGenerator, diff --git a/app/src/main/java/com/brainwallet/util/cryptography/impl/KeyStoreKeyGeneratorImpl.kt b/app/src/main/java/com/brainwallet/util/cryptography/impl/KeyStoreKeyGeneratorImpl.kt new file mode 100644 index 00000000..8a14c8db --- /dev/null +++ b/app/src/main/java/com/brainwallet/util/cryptography/impl/KeyStoreKeyGeneratorImpl.kt @@ -0,0 +1,65 @@ +package com.brainwallet.util.cryptography.impl + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties.AUTH_BIOMETRIC_STRONG +import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES +import android.security.keystore.KeyProperties.PURPOSE_DECRYPT +import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT +import com.brainwallet.tools.security.BRKeyStore +import com.brainwallet.tools.security.BRKeyStore.NEW_BLOCK_MODE +import com.brainwallet.tools.security.BRKeyStore.NEW_PADDING +import com.brainwallet.util.cryptography.KeyStoreKeyGenerator +import org.koin.core.annotation.Single +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +@Single(binds = [KeyStoreKeyGenerator::class]) +class KeyStoreKeyGeneratorImpl : KeyStoreKeyGenerator { + + private val keyGenerator by lazy { + KeyGenerator.getInstance(KEY_ALGORITHM_AES, BRKeyStore.ANDROID_KEY_STORE) + } + + override fun generateKey( + alias: String, + isAuthRequired: Boolean, + authTimeout: Int? + ): SecretKey { + val keySpecBuilder = getKeyGenSpecBuilder(alias) + if (isAuthRequired) { + setUserAuth(keySpecBuilder, authTimeout) + } + keyGenerator.init(keySpecBuilder.build()) + return keyGenerator.generateKey() + } + + private fun getKeyGenSpecBuilder(alias: String): KeyGenParameterSpec.Builder { + val purposes = PURPOSE_DECRYPT or PURPOSE_ENCRYPT + + return KeyGenParameterSpec.Builder(alias, purposes) + .setBlockModes(NEW_BLOCK_MODE) + .setRandomizedEncryptionRequired(false) + .setEncryptionPaddings(NEW_PADDING) + } + + private fun setUserAuth( + builder: KeyGenParameterSpec.Builder, authTimeout: Int? + ) { + builder.setUserAuthenticationRequired(true) + if (authTimeout != null) { + setAuthTimeout(builder, authTimeout) + } + } + + private fun setAuthTimeout(builder: KeyGenParameterSpec.Builder, authTimeout: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (authTimeout != 0) { + builder.setUserAuthenticationParameters(authTimeout, AUTH_BIOMETRIC_STRONG) + } + } else { + val timeout = if (authTimeout == 0) -1 else authTimeout + builder.setUserAuthenticationValidityDurationSeconds(timeout) + } + } +} diff --git a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt b/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt index 739da912..d49b325b 100644 --- a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt +++ b/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt @@ -8,7 +8,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.koin.core.annotation.Single +@Single class CurrencyUpdateWorker( private val ltcRepository: LtcRepository ) { diff --git a/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt b/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt index 4dbbde76..6e14bcf3 100644 --- a/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt +++ b/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt @@ -8,6 +8,8 @@ import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.GlobalContext.startKoin import org.koin.core.logger.Level +import com.brainwallet.di.AppModule +import org.koin.ksp.generated.module import timber.log.Timber class BrainwalletScreengrabApp : BrainwalletApp() { @@ -23,7 +25,7 @@ class BrainwalletScreengrabApp : BrainwalletApp() { startKoin { androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.ERROR) androidContext(this@BrainwalletScreengrabApp) - modules(dataModule, viewModelModule, appModule) + modules(AppModule.dataModule, AppModule.module) } } } diff --git a/app/src/test/java/com/brainwallet/data/source/RemoteConfigSourceFirebaseImplTest.kt b/app/src/test/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepositoryTest.kt similarity index 91% rename from app/src/test/java/com/brainwallet/data/source/RemoteConfigSourceFirebaseImplTest.kt rename to app/src/test/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepositoryTest.kt index 7af41f07..6c98db43 100644 --- a/app/src/test/java/com/brainwallet/data/source/RemoteConfigSourceFirebaseImplTest.kt +++ b/app/src/test/java/com/brainwallet/data/repository/FirebaseRemoteConfigRepositoryTest.kt @@ -1,5 +1,6 @@ -package com.brainwallet.data.source +package com.brainwallet.data.repository +import com.brainwallet.data.source.RemoteConfigSource import com.google.android.gms.tasks.Tasks import com.google.firebase.remoteconfig.FirebaseRemoteConfig import io.mockk.every @@ -10,14 +11,14 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -class RemoteConfigSourceFirebaseImplTest { +class FirebaseRemoteConfigRepositoryTest { private val firebaseRemoteConfig: FirebaseRemoteConfig = mockk(relaxed = true) private lateinit var remoteConfigSource: RemoteConfigSource @Before fun setUp() { - remoteConfigSource = RemoteConfigSource.FirebaseImpl(firebaseRemoteConfig) + remoteConfigSource = FirebaseRemoteConfigRepository(firebaseRemoteConfig) } @Test @@ -76,4 +77,4 @@ class RemoteConfigSourceFirebaseImplTest { assertEquals(100.0, actual, .1) } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/brainwallet/notification/NotificationHandlerTest.kt b/app/src/test/java/com/brainwallet/notification/NotificationHandlerTest.kt index 5a860d26..c8a5813e 100644 --- a/app/src/test/java/com/brainwallet/notification/NotificationHandlerTest.kt +++ b/app/src/test/java/com/brainwallet/notification/NotificationHandlerTest.kt @@ -3,15 +3,17 @@ package com.brainwallet.notification import android.content.Context import android.os.Bundle import androidx.collection.arrayMapOf -import androidx.core.app.NotificationManagerCompat +import androidx.core.app.NotificationCompat import com.google.firebase.messaging.Constants.MessageNotificationKeys import com.google.firebase.messaging.Constants.MessagePayloadKeys import com.google.firebase.messaging.RemoteMessage -import io.mockk.Runs +import io.mockk.MockKAnnotations import io.mockk.every -import io.mockk.just -import io.mockk.mockk +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -20,11 +22,36 @@ import org.junit.Test class NotificationHandlerTest { - private val context: Context = mockk(relaxed = true) + @MockK + private lateinit var context: Context + + @MockK + private lateinit var notificationHandlerBuilderProvider: NotificationHandlerBuilderProvider + + @RelaxedMockK + private lateinit var notificationNotifier: NotificationNotifier + + @RelaxedMockK + private lateinit var notificationBuilder: NotificationCompat.Builder + + private lateinit var sut: NotificationHandler @Before fun setUp() { mockkStatic(MessagePayloadKeys::class) + MockKAnnotations.init(this, relaxUnitFun = true) + every { + notificationHandlerBuilderProvider.createNotificationBuilder( + any(), + any() + ) + } returns notificationBuilder + sut = NotificationHandler(notificationHandlerBuilderProvider, notificationNotifier) + } + + @After + fun tearDown() { + unmockkAll() } @Test @@ -34,7 +61,7 @@ class NotificationHandlerTest { every { MessagePayloadKeys.extractDeveloperDefinedPayload(any()) } returns arrayMapOf() - val actual = NotificationHandler.handleMessageReceived(context, remoteMessage) + val actual = sut.handleMessageReceived(context, remoteMessage) assertEquals(false, actual) } @@ -76,13 +103,9 @@ class NotificationHandlerTest { if (testOSVersion == 6 || testOSVersion == 15) { assertTrue(successMessage, true) - } - else { - mockkStatic(NotificationManagerCompat::class) - every { NotificationManagerCompat.from(context).notify(any()) } just Runs - - val actual = NotificationHandler.handleMessageReceived(context, remoteMessage) - assertEquals(failMessage,true, actual) + } else { + val actual = sut.handleMessageReceived(context, remoteMessage) + assertEquals(failMessage, true, actual) } } diff --git a/build.gradle.kts b/build.gradle.kts index 26e5212f..8cbe07fb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.jetbrains.kotlin.serialization) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.firebase.crashlytics) apply false + alias(libs.plugins.ksp) apply false } tasks.register("clean", Delete::class) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd3b7691..fc956caf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ eclipse-jetty = "9.2.19.v20160908" junit = "4.13.2" mockk = "1.13.13" koin-bom = "4.0.2" +koin-annotation-bom = "2.1.0" +ksp = "2.0.0-1.0.24" [libraries] androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } @@ -112,7 +114,9 @@ koin-android = { module = "io.insert-koin:koin-android" } koin-android-compat = { module = "io.insert-koin:koin-android-compat" } koin-compose = { module = "io.insert-koin:koin-compose" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } - +koin-annotation-bom = { module = "io.insert-koin:koin-annotations-bom", version.ref = "koin-annotation-bom" } +koin-annotation = { module = "io.insert-koin:koin-annotations" } +koin-annotation-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotation-bom" } [bundles] androidx-lifecycle = ["androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-viewmodel-compose"] @@ -136,4 +140,5 @@ jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serializati jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version = "2.1.0" } jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } google-services = { id = "com.google.gms.google-services", version = "4.4.2" } -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" } \ No newline at end of file +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file