Updated on 2026-08-14
This commit is contained in:
parent
bc70ed5327
commit
ccd7a7dd18
11 changed files with 98 additions and 39 deletions
|
|
@ -208,10 +208,12 @@ dependencies {
|
||||||
implementation(deps.androidx.paging.runtime)
|
implementation(deps.androidx.paging.runtime)
|
||||||
implementation(deps.androidx.swipeRefreshLayout)
|
implementation(deps.androidx.swipeRefreshLayout)
|
||||||
implementation(deps.androidx.fragment.compose)
|
implementation(deps.androidx.fragment.compose)
|
||||||
|
implementation(deps.androidx.workmanager)
|
||||||
implementation(deps.lifecycle.runtime.ktx)
|
implementation(deps.lifecycle.runtime.ktx)
|
||||||
implementation(deps.lifecycle.common.java8)
|
implementation(deps.lifecycle.common.java8)
|
||||||
implementation(deps.lifecycle.viewModel.ktx)
|
implementation(deps.lifecycle.viewModel.ktx)
|
||||||
implementation(deps.lifecycle.compose)
|
implementation(deps.lifecycle.compose)
|
||||||
|
implementation(deps.hilt.work)
|
||||||
|
|
||||||
/** Compose libraries */
|
/** Compose libraries */
|
||||||
implementation(deps.compose.constraintLayout)
|
implementation(deps.compose.constraintLayout)
|
||||||
|
|
@ -250,6 +252,7 @@ dependencies {
|
||||||
implementation(deps.hilt.android)
|
implementation(deps.hilt.android)
|
||||||
|
|
||||||
kapt(deps.hilt.kapt)
|
kapt(deps.hilt.kapt)
|
||||||
|
kapt(deps.hilt.compilerx)
|
||||||
|
|
||||||
/** Other libraries */
|
/** Other libraries */
|
||||||
implementation(deps.kotlin.immutable.collections)
|
implementation(deps.kotlin.immutable.collections)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name=
|
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||||
"android.support.customtabs.action.CustomTabsService" />
|
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
|
|
@ -145,6 +144,13 @@
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Disable android.startup completely. Used for Worker according doc -->
|
||||||
|
<!--https://developer.android.com/jetpack/androidx/releases/work#version_260_3-->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.tap
|
package com.tangem.tap
|
||||||
|
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import com.tangem.TangemSdkLogger
|
import com.tangem.TangemSdkLogger
|
||||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||||
import com.tangem.data.card.TransactionSignerFactory
|
import com.tangem.data.card.TransactionSignerFactory
|
||||||
|
|
@ -139,4 +140,6 @@ interface ApplicationEntryPoint {
|
||||||
|
|
||||||
@GlobalUiMessageSender
|
@GlobalUiMessageSender
|
||||||
fun getUiMessageSender(): UiMessageSender
|
fun getUiMessageSender(): UiMessageSender
|
||||||
|
|
||||||
|
fun getWorkerFactory(): HiltWorkerFactory
|
||||||
}
|
}
|
||||||
34
app/src/main/java/com/tangem/tap/LockTimerWorker.kt
Normal file
34
app/src/main/java/com/tangem/tap/LockTimerWorker.kt
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.tangem.tap
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.hilt.work.HiltWorker
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
|
import com.tangem.domain.wallets.legacy.asLockable
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@HiltWorker
|
||||||
|
class LockTimerWorker @AssistedInject constructor(
|
||||||
|
@Assisted context: Context,
|
||||||
|
@Assisted params: WorkerParameters,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val userWalletsListManager: UserWalletsListManager,
|
||||||
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
Timber.i("onStart job")
|
||||||
|
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
|
||||||
|
userWalletsListManagerLockable.lock()
|
||||||
|
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
|
||||||
|
Timber.i("onStart job complete")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LOCK_TIMER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
package com.tangem.tap
|
package com.tangem.tap
|
||||||
|
|
||||||
|
import android.app.job.JobScheduler
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
import com.tangem.domain.wallets.legacy.asLockable
|
import com.tangem.domain.wallets.legacy.asLockable
|
||||||
|
import com.tangem.tap.LockTimerWorker.Companion.TAG
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
internal class LockUserWalletsTimer(
|
internal class LockUserWalletsTimer(
|
||||||
owner: LifecycleOwner,
|
private val context: Context,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val duration: Duration = with(Duration) { 10.minutes },
|
private val duration: Duration = with(Duration) { 5.minutes },
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
private val userWalletsListManager: UserWalletsListManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
) : LifecycleOwner by owner,
|
) : LifecycleOwner by context as LifecycleOwner,
|
||||||
DefaultLifecycleObserver {
|
DefaultLifecycleObserver {
|
||||||
|
|
||||||
private var delayJob: Job? = null
|
private var delayJob: Job? = null
|
||||||
|
|
@ -31,20 +40,16 @@ internal class LockUserWalletsTimer(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
|
|
||||||
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
|
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
|
||||||
|
|
||||||
Timber.i(
|
Timber.i(
|
||||||
"""
|
"""
|
||||||
Owner resumed
|
Owner resumed
|
||||||
|- Was stopped: $wasApplicationStopped
|
|
||||||
|- Need to open welcome screen: $shouldOpenWelcomeScreenOnResume
|
|- Need to open welcome screen: $shouldOpenWelcomeScreenOnResume
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
|
||||||
settingsRepository.setWasApplicationStopped(value = false)
|
|
||||||
|
|
||||||
if (shouldOpenWelcomeScreenOnResume) {
|
if (shouldOpenWelcomeScreenOnResume) {
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
||||||
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
|
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
|
||||||
|
|
@ -58,10 +63,9 @@ internal class LockUserWalletsTimer(
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
Timber.i("Owner stopped")
|
Timber.i("Owner stopped")
|
||||||
|
delayJob = null
|
||||||
|
|
||||||
coroutineScope.launch {
|
startTimerWorker()
|
||||||
settingsRepository.setWasApplicationStopped(value = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restart() {
|
fun restart() {
|
||||||
|
|
@ -75,6 +79,15 @@ internal class LockUserWalletsTimer(
|
||||||
start(log = false)
|
start(log = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startTimerWorker() {
|
||||||
|
val lockWorkRequest = OneTimeWorkRequest.Builder(LockTimerWorker::class.java)
|
||||||
|
.addTag(TAG)
|
||||||
|
.setInitialDelay(duration.inWholeSeconds, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueue(lockWorkRequest)
|
||||||
|
}
|
||||||
|
|
||||||
private fun start(log: Boolean = true) {
|
private fun start(log: Boolean = true) {
|
||||||
if (log) {
|
if (log) {
|
||||||
Timber.i(
|
Timber.i(
|
||||||
|
|
@ -87,6 +100,9 @@ internal class LockUserWalletsTimer(
|
||||||
delayJob = createDelayJob()
|
delayJob = createDelayJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This job used only when app is foreground when background use [JobScheduler]
|
||||||
|
*/
|
||||||
private fun createDelayJob(): Job = coroutineScope.launch {
|
private fun createDelayJob(): Job = coroutineScope.launch {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
|
@ -96,22 +112,16 @@ internal class LockUserWalletsTimer(
|
||||||
|
|
||||||
if (userWalletsListManager.hasUserWallets) {
|
if (userWalletsListManager.hasUserWallets) {
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
|
|
||||||
|
|
||||||
Timber.i(
|
Timber.i(
|
||||||
"""
|
"""
|
||||||
Finished
|
Finished
|
||||||
|- App is stopped: $wasApplicationStopped
|
|
||||||
|- Millis passed: ${currentTime - startTime}
|
|- Millis passed: ${currentTime - startTime}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
|
||||||
userWalletsListManager.lock()
|
userWalletsListManager.lock()
|
||||||
if (wasApplicationStopped) {
|
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
||||||
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
|
|
||||||
} else {
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +355,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
}
|
}
|
||||||
|
|
||||||
lockUserWalletsTimer = LockUserWalletsTimer(
|
lockUserWalletsTimer = LockUserWalletsTimer(
|
||||||
owner = this,
|
context = this,
|
||||||
settingsRepository = settingsRepository,
|
settingsRepository = settingsRepository,
|
||||||
userWalletsListManager = userWalletsListManager,
|
userWalletsListManager = userWalletsListManager,
|
||||||
coroutineScope = mainScope,
|
coroutineScope = mainScope,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.tangem.tap
|
package com.tangem.tap
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
|
import androidx.work.Configuration
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||||
|
|
@ -78,7 +80,7 @@ lateinit var store: Store<AppState>
|
||||||
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||||
internal lateinit var derivationsFinder: DerivationsFinder
|
internal lateinit var derivationsFinder: DerivationsFinder
|
||||||
|
|
||||||
abstract class TangemApplication : Application(), ImageLoaderFactory {
|
abstract class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
|
||||||
|
|
||||||
// region DI
|
// region DI
|
||||||
private val entryPoint: ApplicationEntryPoint
|
private val entryPoint: ApplicationEntryPoint
|
||||||
|
|
@ -210,6 +212,14 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
||||||
private val uiMessageSender: UiMessageSender
|
private val uiMessageSender: UiMessageSender
|
||||||
get() = entryPoint.getUiMessageSender()
|
get() = entryPoint.getUiMessageSender()
|
||||||
|
|
||||||
|
private val workerFactory: HiltWorkerFactory
|
||||||
|
get() = entryPoint.getWorkerFactory()
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,10 @@ internal class DefaultCacheRegistry(
|
||||||
private val mutexes = ConcurrentHashMap<String, Mutex>()
|
private val mutexes = ConcurrentHashMap<String, Mutex>()
|
||||||
|
|
||||||
override suspend fun isExpired(key: String): Boolean {
|
override suspend fun isExpired(key: String): Boolean {
|
||||||
val cacheKey = cacheKeysStore.getSyncOrNull(key) ?: return true
|
cacheKeysStore.getSyncOrNull(key) ?: return true
|
||||||
|
|
||||||
return cacheKey.updatedAt
|
// disabled cache expiration by time, update always using refresh flag
|
||||||
.plus(cacheKey.expiresIn)
|
return false
|
||||||
.isBefore(LocalDateTime.now())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun invalidate(key: String) {
|
override suspend fun invalidate(key: String) {
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,6 @@ internal class DefaultSettingsRepository(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun wasApplicationStopped(): Boolean {
|
|
||||||
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_APPLICATION_STOPPED_KEY, default = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setWasApplicationStopped(value: Boolean) {
|
|
||||||
appPreferencesStore.store(key = PreferencesKeys.WAS_APPLICATION_STOPPED_KEY, value = value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun shouldOpenWelcomeScreenOnResume(): Boolean {
|
override suspend fun shouldOpenWelcomeScreenOnResume(): Boolean {
|
||||||
return appPreferencesStore.getSyncOrDefault(
|
return appPreferencesStore.getSyncOrDefault(
|
||||||
key = PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY,
|
key = PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY,
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,6 @@ interface SettingsRepository {
|
||||||
|
|
||||||
suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean)
|
suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean)
|
||||||
|
|
||||||
suspend fun wasApplicationStopped(): Boolean
|
|
||||||
|
|
||||||
suspend fun setWasApplicationStopped(value: Boolean)
|
|
||||||
|
|
||||||
suspend fun shouldOpenWelcomeScreenOnResume(): Boolean
|
suspend fun shouldOpenWelcomeScreenOnResume(): Boolean
|
||||||
|
|
||||||
suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean)
|
suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ androidx-paging = "3.1.1"
|
||||||
androidx-palette = "1.0.0"
|
androidx-palette = "1.0.0"
|
||||||
androidx-datastore = "1.0.0"
|
androidx-datastore = "1.0.0"
|
||||||
androidxWindowManager = "1.3.0"
|
androidxWindowManager = "1.3.0"
|
||||||
|
androidxWorkManager = "2.9.0"
|
||||||
# endregion AndroidX
|
# endregion AndroidX
|
||||||
|
|
||||||
# region Compose
|
# region Compose
|
||||||
|
|
@ -53,6 +54,8 @@ googlePlayReviewKtx = "2.0.1"
|
||||||
googlePlayServicesWallet = "19.1.0"
|
googlePlayServicesWallet = "19.1.0"
|
||||||
hilt = "2.55"
|
hilt = "2.55"
|
||||||
hilt-navigation = "1.0.0"
|
hilt-navigation = "1.0.0"
|
||||||
|
hilt-work = "1.0.0"
|
||||||
|
hilt-compilerx = "1.0.0"
|
||||||
jodatime = "2.12.1"
|
jodatime = "2.12.1"
|
||||||
kotlin-immutable-collections = "0.3.5"
|
kotlin-immutable-collections = "0.3.5"
|
||||||
kotsonGsonExt = "2.5.0"
|
kotsonGsonExt = "2.5.0"
|
||||||
|
|
@ -149,6 +152,8 @@ lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", v
|
||||||
lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
||||||
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" }
|
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" }
|
||||||
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||||
|
androidx-workmanager = { module = "androidx.work:work-runtime", version.ref = "androidxWorkManager" }
|
||||||
|
hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" }
|
||||||
# region AndroidX
|
# region AndroidX
|
||||||
|
|
||||||
# region Compose
|
# region Compose
|
||||||
|
|
@ -218,6 +223,7 @@ googlePlay-services-wallet = { module = "com.google.android.gms:play-services-wa
|
||||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||||
hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
|
hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
|
||||||
hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
||||||
|
hilt-compilerx = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-compilerx" }
|
||||||
jodatime = { module = "joda-time:joda-time", version.ref = "jodatime" }
|
jodatime = { module = "joda-time:joda-time", version.ref = "jodatime" }
|
||||||
kotsonGson = { module = "com.github.salomonbrys.kotson:kotson", version.ref = "kotsonGsonExt" }
|
kotsonGson = { module = "com.github.salomonbrys.kotson:kotson", version.ref = "kotsonGsonExt" }
|
||||||
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
|
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue