453 lines
No EOL
16 KiB
Kotlin
453 lines
No EOL
16 KiB
Kotlin
package com.tangem.tap
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Intent
|
|
import android.content.pm.ActivityInfo
|
|
import android.content.res.Configuration
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.view.KeyEvent
|
|
import android.view.MotionEvent
|
|
import android.view.WindowManager
|
|
import androidx.activity.SystemBarStyle
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.activity.viewModels
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.appcompat.app.AppCompatDelegate
|
|
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.runtime.CompositionLocalProvider
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.semantics.semantics
|
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.toArgb
|
|
import androidx.core.net.toUri
|
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
import androidx.lifecycle.flowWithLifecycle
|
|
import androidx.lifecycle.lifecycleScope
|
|
import arrow.core.getOrElse
|
|
import com.tangem.common.routing.AppRouter
|
|
import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY
|
|
import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter
|
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|
import com.tangem.core.decompose.context.AppComponentContext
|
|
import com.tangem.core.decompose.di.RootAppComponentContext
|
|
import com.tangem.core.navigation.url.UrlOpener
|
|
import com.tangem.core.ui.extensions.LocalUserInteractionTracker
|
|
import com.tangem.core.ui.extensions.UserInteractionTracker
|
|
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
|
|
import com.tangem.data.card.sdk.CardSdkOwner
|
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
|
import com.tangem.domain.card.repository.CardRepository
|
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
|
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
|
|
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
|
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
|
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
|
|
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
|
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
|
import com.tangem.features.tester.api.TesterMenuLauncher
|
|
import com.tangem.google.GoogleServicesHelper
|
|
import com.tangem.operations.backup.BackupService
|
|
import com.tangem.sdk.api.BackupServiceHolder
|
|
import com.tangem.sdk.api.TangemSdkManager
|
|
import com.tangem.tap.common.ActivityResultCallbackHolder
|
|
import com.tangem.tap.common.OnActivityResultCallback
|
|
import com.tangem.tap.common.analytics.events.Push
|
|
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
|
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
|
import com.tangem.tap.features.main.MainViewModel
|
|
import com.tangem.tap.routing.component.RoutingComponent
|
|
import com.tangem.tap.routing.configurator.AppRouterConfig
|
|
import com.tangem.tap.routing.utils.DeepLinkFactory
|
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
|
import com.tangem.utils.extensions.uriValidate
|
|
import com.tangem.utils.logging.TangemLogger
|
|
import com.tangem.wallet.BuildConfig
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
import kotlinx.coroutines.*
|
|
import kotlinx.coroutines.flow.*
|
|
import javax.inject.Inject
|
|
import kotlin.coroutines.CoroutineContext
|
|
import kotlin.time.Duration.Companion.seconds
|
|
|
|
lateinit var tangemSdkManager: TangemSdkManager
|
|
lateinit var backupService: BackupService
|
|
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
|
|
private set
|
|
|
|
private val coroutineContext: CoroutineContext
|
|
get() = SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope")
|
|
val scope = CoroutineScope(coroutineContext)
|
|
|
|
private val mainCoroutineContext: CoroutineContext
|
|
get() = SupervisorJob() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
|
|
val mainScope = CoroutineScope(mainCoroutineContext)
|
|
|
|
@Suppress("LargeClass")
|
|
@AndroidEntryPoint
|
|
class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|
|
|
/** Router for opening tester menu */
|
|
@Inject
|
|
lateinit var cardSdkOwner: CardSdkOwner
|
|
|
|
@Inject
|
|
lateinit var cardSdkConfigRepository: CardSdkConfigRepository
|
|
|
|
@Inject
|
|
lateinit var injectedTangemSdkManager: TangemSdkManager
|
|
|
|
@Inject
|
|
lateinit var settingsRepository: SettingsRepository
|
|
|
|
@Inject
|
|
lateinit var sendUnsubmittedHashesUseCase: SendUnsubmittedHashesUseCase
|
|
|
|
@Inject
|
|
lateinit var analyticsEventsHandler: AnalyticsEventHandler
|
|
|
|
@Inject
|
|
@RootAppComponentContext
|
|
internal lateinit var rootComponentContext: AppComponentContext
|
|
|
|
@Inject
|
|
internal lateinit var appRouterConfig: AppRouterConfig
|
|
|
|
@Inject
|
|
internal lateinit var appRouter: AppRouter
|
|
|
|
@Inject
|
|
internal lateinit var routingComponentFactory: RoutingComponent.Factory
|
|
|
|
@Inject
|
|
lateinit var cardRepository: CardRepository
|
|
|
|
@Inject
|
|
lateinit var backupServiceHolder: BackupServiceHolder
|
|
|
|
@Inject
|
|
lateinit var setGoogleServicesAvailabilityUseCase: SetGoogleServicesAvailabilityUseCase
|
|
|
|
@Inject
|
|
lateinit var setGooglePayAvailabilityUseCase: SetGooglePayAvailabilityUseCase
|
|
|
|
@Inject
|
|
lateinit var dispatchers: CoroutineDispatcherProvider
|
|
|
|
@Inject
|
|
internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector
|
|
|
|
@Inject
|
|
internal lateinit var deeplinkFactory: DeepLinkFactory
|
|
|
|
@Inject
|
|
internal lateinit var urlOpener: UrlOpener
|
|
|
|
@Inject
|
|
internal lateinit var testerMenuLauncher: TesterMenuLauncher
|
|
|
|
@Inject
|
|
internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler
|
|
|
|
@Inject
|
|
internal lateinit var userWalletsListRepository: UserWalletsListRepository
|
|
|
|
@Inject
|
|
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
|
|
|
|
@Inject
|
|
internal lateinit var passwordRequester: HotWalletPasswordRequester
|
|
|
|
private val viewModel: MainViewModel by viewModels()
|
|
|
|
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
|
|
|
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
TangemLogger.i("onCreate")
|
|
// We need to call it before onCreate to prevent unnecessary activity recreation
|
|
installAppTheme()
|
|
|
|
val splashScreen = installSplashScreen()
|
|
TangemLogger.i("Splash screen installed")
|
|
|
|
enableEdgeToEdge(
|
|
navigationBarStyle = SystemBarStyle.auto(
|
|
Color.Transparent.toArgb(),
|
|
Color.Transparent.toArgb(),
|
|
),
|
|
)
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
// We have to allow adjust_resize (can only be specified in the manifest) for Android <=10,
|
|
// in order for imePadding to work correctly
|
|
// for Android 11+ we set SOFT_INPUT_ADJUST_NOTHING to prevent resizing the layout
|
|
// so that we don't have any distortions in the layout when displaying the keyboard
|
|
// https://issuetracker.google.com/issues/266331465
|
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
window.setHideOverlayWindows(true)
|
|
}
|
|
|
|
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
|
|
|
|
installActivityDependencies()
|
|
observeAppThemeModeUpdates()
|
|
|
|
setRootContent()
|
|
|
|
initContent()
|
|
|
|
sendStakingUnsubmittedHashes()
|
|
checkGoogleServicesAvailability()
|
|
|
|
lifecycle.addObserver(WindowObscurationObserver)
|
|
lifecycle.addObserver(defaultDeviceFlipDetector)
|
|
|
|
if (BuildConfig.TESTER_MENU_ENABLED) {
|
|
lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
|
|
}
|
|
|
|
if (intent != null) {
|
|
handleDeepLink(intent = intent, isFromOnNewIntent = false)
|
|
}
|
|
}
|
|
|
|
private fun setRootContent() {
|
|
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
|
|
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
|
|
// we try to init component with previous stack
|
|
val routingComponent = routingComponentFactory.create(
|
|
context = rootComponentContext,
|
|
initialStack = appRouterConfig.stack,
|
|
launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intent),
|
|
)
|
|
|
|
val userInteractionTracker = object : UserInteractionTracker {
|
|
override fun onUserInteraction() {
|
|
lockUserWalletsTimer?.restart()
|
|
}
|
|
}
|
|
|
|
setContent {
|
|
CompositionLocalProvider(LocalUserInteractionTracker provides userInteractionTracker) {
|
|
routingComponent.Content(
|
|
Modifier
|
|
.fillMaxSize()
|
|
.semantics { testTagsAsResourceId = true },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun installActivityDependencies() {
|
|
cardSdkOwner.register(activity = this)
|
|
tangemSdkManager = injectedTangemSdkManager
|
|
|
|
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
|
|
backupService = requireNotNull(backupServiceHolder.backupService.get()) // will be deleted eventually
|
|
|
|
lockUserWalletsTimer = LockUserWalletsTimer(
|
|
context = this,
|
|
settingsRepository = settingsRepository,
|
|
coroutineScope = mainScope,
|
|
userWalletsListRepository = userWalletsListRepository,
|
|
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
|
|
passwordRequester = passwordRequester,
|
|
appRouter = appRouter,
|
|
)
|
|
}
|
|
|
|
private fun installAppTheme() {
|
|
appThemeModeFlow = createAppThemeModeFlow()
|
|
val mode = runBlocking {
|
|
withTimeoutOrNull(APP_THEME_LOAD_TIMEOUT.seconds) {
|
|
appThemeModeFlow.first()
|
|
} ?: AppThemeMode.DEFAULT
|
|
}
|
|
|
|
updateAppTheme(mode)
|
|
}
|
|
|
|
private fun observeAppThemeModeUpdates() {
|
|
appThemeModeFlow
|
|
.flowWithLifecycle(lifecycle)
|
|
.onEach(::updateAppTheme)
|
|
.launchIn(lifecycleScope)
|
|
}
|
|
|
|
@SuppressLint("SourceLockedOrientationActivity")
|
|
private fun initContent() {
|
|
supportFragmentManager.registerFragmentLifecycleCallbacks(
|
|
NavBarInsetsFragmentLifecycleCallback(),
|
|
true,
|
|
)
|
|
|
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
}
|
|
|
|
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> {
|
|
val tangemApplication = requireNotNull(application as? TangemApplication) {
|
|
"Application is null"
|
|
}
|
|
|
|
return tangemApplication.getAppThemeModeUseCase()
|
|
.filterNotNull()
|
|
.distinctUntilChanged()
|
|
.map { maybeMode ->
|
|
maybeMode.getOrElse { AppThemeMode.DEFAULT }
|
|
}
|
|
.shareIn(
|
|
scope = lifecycleScope,
|
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
|
|
)
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
TangemLogger.i("onStart")
|
|
}
|
|
|
|
override fun onStop() {
|
|
super.onStop()
|
|
TangemLogger.i("onStop")
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
TangemLogger.i("onDestroy")
|
|
// workaround: kill process when activity destroy to avoid state when lock() wallets
|
|
// and navigation to unlock screen was skipped because system kills activity but not process
|
|
if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) {
|
|
android.os.Process.killProcess(android.os.Process.myPid())
|
|
}
|
|
super.onDestroy()
|
|
}
|
|
|
|
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
|
val mode = when (appThemeMode) {
|
|
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
|
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
|
AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
}
|
|
|
|
setDefaultNightMode(mode)
|
|
|
|
MutableAppThemeModeHolder.value = appThemeMode
|
|
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
|
}
|
|
|
|
private fun isDarkTheme(): Boolean {
|
|
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
|
Configuration.UI_MODE_NIGHT_YES -> true
|
|
Configuration.UI_MODE_NIGHT_NO -> false
|
|
Configuration.UI_MODE_NIGHT_UNDEFINED -> false
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
super.onNewIntent(intent)
|
|
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
|
|
|
|
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
|
|
if (isFromPush) {
|
|
analyticsEventsHandler.send(Push.PushNotificationOpened())
|
|
}
|
|
|
|
handleDeepLink(intent = intent, isFromOnNewIntent = true)
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
super.onActivityResult(requestCode, resultCode, data)
|
|
|
|
onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) }
|
|
}
|
|
|
|
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
|
|
onActivityResultCallbacks.remove(callback)
|
|
onActivityResultCallbacks.add(callback)
|
|
}
|
|
|
|
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
|
|
onActivityResultCallbacks.remove(callback)
|
|
}
|
|
|
|
override fun onUserInteraction() {
|
|
super.onUserInteraction()
|
|
|
|
lockUserWalletsTimer?.restart()
|
|
}
|
|
|
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
val isHandled = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler)
|
|
return if (isHandled) super.dispatchTouchEvent(event) else false
|
|
}
|
|
|
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
|
testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
|
|
} else {
|
|
super.dispatchKeyEvent(event)
|
|
}
|
|
}
|
|
|
|
private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) {
|
|
val deepLinkExtras = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri()
|
|
val webLink = intent.getStringExtra(WEBLINK_KEY)
|
|
|
|
val receivedDeepLink = intent.data ?: deepLinkExtras
|
|
|
|
when {
|
|
receivedDeepLink != null -> {
|
|
deeplinkFactory.handleDeeplink(
|
|
deeplinkUri = receivedDeepLink,
|
|
coroutineScope = lifecycleScope,
|
|
isFromOnNewIntent = isFromOnNewIntent,
|
|
)
|
|
}
|
|
webLink?.uriValidate() == true -> {
|
|
urlOpener.openUrl(webLink)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun sendStakingUnsubmittedHashes() {
|
|
lifecycleScope.launch {
|
|
sendUnsubmittedHashesUseCase.invoke()
|
|
.onLeft { TangemLogger.e(it.toString()) }
|
|
.onRight { TangemLogger.d("Submitting hashes succeeded") }
|
|
}
|
|
}
|
|
|
|
private fun checkGoogleServicesAvailability() {
|
|
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this)
|
|
|
|
lifecycleScope.launch {
|
|
setGoogleServicesAvailabilityUseCase(isGoogleServicesAvailable)
|
|
|
|
if (isGoogleServicesAvailable) {
|
|
val paymentsClient = GoogleServicesHelper.createPaymentsClient(this@MainActivity)
|
|
val isGooglePayAvailable = GoogleServicesHelper.checkGooglePayAvailability(paymentsClient)
|
|
setGooglePayAvailabilityUseCase(isGooglePayAvailable)
|
|
} else {
|
|
setGooglePayAvailabilityUseCase(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val APP_THEME_LOAD_TIMEOUT = 2
|
|
private const val MOCKED_BUILD_TYPE = "mocked"
|
|
private const val OPENED_FROM_GCM_PUSH = "google.sent_time" // every bundle from FCM contains this key
|
|
}
|
|
} |