Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,10 @@ dependencies {
implementation(libs.androidx.core.splashscreen)

testImplementation(libs.bundles.non.android.test)
testRuntimeOnly(libs.bundles.junit5.runtime)
androidTestImplementation(libs.bundles.android.test)
}

tasks.withType<Test> {
useJUnitPlatform()
}
126 changes: 57 additions & 69 deletions app/src/main/java/com/acon/acon/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.acon.acon.core.ads_api.AdProvider
import com.acon.acon.core.ads_api.LocalSpotListAdProvider
import com.acon.acon.core.analytics.amplitude.AconAmplitude
import com.acon.acon.core.analytics.constants.EventNames
import com.acon.acon.core.common.DeepLinkHandler
import com.acon.acon.core.designsystem.R
import com.acon.acon.core.designsystem.component.bottomsheet.SignInBottomSheet
Expand All @@ -44,26 +48,24 @@ import com.acon.acon.core.designsystem.component.dialog.v2.AconTwoActionDialog
import com.acon.acon.core.designsystem.effect.LocalHazeState
import com.acon.acon.core.designsystem.effect.rememberHazeState
import com.acon.acon.core.designsystem.theme.AconTheme
import com.acon.acon.domain.repository.AconAppRepository
import com.acon.acon.domain.repository.SocialRepository
import com.acon.acon.domain.repository.UserRepository
import com.acon.acon.core.navigation.LocalNavController
import com.acon.acon.core.navigation.route.AreaVerificationRoute
import com.acon.acon.core.navigation.route.SpotRoute
import com.acon.acon.navigation.AconNavigation
import com.acon.acon.core.ads_api.AdProvider
import com.acon.acon.core.ads_api.LocalSpotListAdProvider
import com.acon.acon.core.analytics.amplitude.AconAmplitude
import com.acon.acon.core.analytics.constants.EventNames
import com.acon.acon.provider.ads_impl.SpotListAdProvider
import com.acon.acon.core.common.utils.firstNotNull
import com.acon.acon.core.navigation.LocalNavController
import com.acon.acon.core.ui.android.launchPlayStore
import com.acon.acon.core.ui.compose.LocalDeepLinkHandler
import com.acon.acon.core.ui.compose.LocalLocation
import com.acon.acon.core.ui.compose.LocalRequestLocationPermission
import com.acon.acon.core.ui.compose.LocalRequestSignIn
import com.acon.acon.core.ui.compose.LocalSnackbarHostState
import com.acon.acon.core.ui.compose.LocalUserType
import com.acon.acon.core.ui.android.launchPlayStore
import com.acon.acon.domain.repository.AconAppRepository
import com.acon.acon.domain.repository.SocialRepository
import com.acon.acon.domain.repository.UserRepository
import com.acon.acon.navigation.AconNavigation
import com.acon.acon.provider.ads_impl.SpotListAdProvider
import com.acon.acon.update.AppUpdateHandler
import com.acon.acon.update.AppUpdateHandlerImpl
import com.acon.acon.update.UpdateState
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
Expand All @@ -73,15 +75,12 @@ import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.gms.location.Priority
import com.google.android.gms.location.SettingsClient
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.InstallState
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.ActivityResult
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import dagger.hilt.android.AndroidEntryPoint
import io.branch.referral.Branch
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
Expand Down Expand Up @@ -124,33 +123,6 @@ class MainActivity : ComponentActivity() {
private val appUpdateManager by lazy {
AppUpdateManagerFactory.create(application)
}
private val appUpdateInfo = flow {
emit(appUpdateManager.appUpdateInfo.await())
}.stateIn(
scope = lifecycleScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)

private val appInstallStateListener by lazy {
InstallStateUpdatedListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
lifecycleScope.launch {
val result = viewModel.state.value.snackbarHostState.showSnackbar(
message = getString(R.string.update_complete),
actionLabel = getString(R.string.restart)
)
when (result) {
SnackbarResult.ActionPerformed -> {
appUpdateManager.completeUpdate()
}

SnackbarResult.Dismissed -> Unit
}
}
}
}
}
private val appUpdateActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == RESULT_OK) { // Immediate에서는 받을 일 없음
Expand All @@ -162,6 +134,40 @@ class MainActivity : ComponentActivity() {
}
}

private val appInstallStateListener by lazy {
object: InstallStateUpdatedListener {
override fun onStateUpdate(state: InstallState) {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
lifecycleScope.launch {
val result = viewModel.state.value.snackbarHostState.showSnackbar(
message = getString(R.string.update_complete),
actionLabel = getString(R.string.restart)
)
when (result) {
SnackbarResult.ActionPerformed -> {
appUpdateManager.completeUpdate()
}

SnackbarResult.Dismissed -> Unit
}
}
} else if (state.installStatus() == InstallStatus.INSTALLED) {
appUpdateManager.unregisterListener(this)
}
}
}
}

private val appUpdateHandler: AppUpdateHandler = AppUpdateHandlerImpl(
appUpdateManager = appUpdateManager.apply {
registerListener(appInstallStateListener)
},
aconAppRepository = aconAppRepository,
appUpdateActivityResultLauncher = appUpdateActivityResultLauncher,
application = this.application,
scope = lifecycleScope
)

private val _isLocationPermissionGranted = MutableStateFlow(false)
private val isLocationPermissionGranted = flow {
emit(
Expand Down Expand Up @@ -285,29 +291,15 @@ class MainActivity : ComponentActivity() {


lifecycleScope.launch {
val shouldUpdateAppDeferred = lifecycleScope.async {
val currentAppVersion = try {
val packageInfo =
application.packageManager.getPackageInfo(application.packageName, 0)
packageInfo.versionName
} catch (e: Exception) {
null
}
currentAppVersion?.let { v ->
aconAppRepository.shouldUpdateApp(v).getOrElse { false }
val updateState = appUpdateHandler.getUpdateState()
when(updateState) {
UpdateState.FORCE -> {
viewModel.shouldUpdate()
}
}

appUpdateInfo.firstNotNull().let { updateInfo ->
if (updateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
if (shouldUpdateAppDeferred.await() == true) { // 2. 강제 업데이트 (스토어 이동)
viewModel.shouldUpdate()
} else if (updateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { // 3. 선택적 업데이트 (인앱)
viewModel.canOptionalUpdate()
} else if (updateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { // 1. 강제 업데이트 (인앱)
// Not used
}
UpdateState.OPTIONAL -> {
viewModel.canOptionalUpdate()
}
UpdateState.NONE -> Unit
}
}

Expand Down Expand Up @@ -468,11 +460,7 @@ class MainActivity : ComponentActivity() {
viewModel.dismissOptionalUpdateModal()
}, onAction2 = {
viewModel.dismissOptionalUpdateModal()
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo.value ?: return@AconTwoActionDialog,
appUpdateActivityResultLauncher,
AppUpdateOptions.defaultOptions(AppUpdateType.FLEXIBLE)
)
appUpdateHandler.startFlexibleUpdate()
}, onDismissRequest = {
viewModel.dismissOptionalUpdateModal()
}
Expand Down
93 changes: 93 additions & 0 deletions app/src/main/java/com/acon/acon/update/AppUpdateHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.acon.acon.update

import android.app.Application
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import com.acon.acon.core.common.utils.firstNotNull
import com.acon.acon.domain.repository.AconAppRepository
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.tasks.await

/**
* 앱 업데이트 로직 핸들러
* 1. 강제 업데이트 : 스토어 이동
* 2. 선택 업데이트 : 인앱 업데이트 수행
*/
interface AppUpdateHandler {

/**
* 업데이트 상태 반환
*/
suspend fun getUpdateState() : UpdateState

/**
* 인앱 업데이트 수행
*/
fun startFlexibleUpdate()
}

class AppUpdateHandlerImpl(
private val appUpdateManager: AppUpdateManager,
private val aconAppRepository: AconAppRepository,
private val appUpdateActivityResultLauncher: ActivityResultLauncher<IntentSenderRequest>,
private val application: Application,
private val scope: CoroutineScope
) : AppUpdateHandler {

private val appUpdateInfo = flow {
emit(appUpdateManager.appUpdateInfo.await())
}.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)

override suspend fun getUpdateState() : UpdateState {
val shouldUpdateAppDeferred = scope.async {
val currentAppVersion = try {
val packageInfo =
application.packageManager.getPackageInfo(application.packageName, 0)
packageInfo.versionName
} catch (e: Exception) {
null
}
if (currentAppVersion == null)
return@async false

aconAppRepository.shouldUpdateApp(currentAppVersion).getOrElse { false }
}

appUpdateInfo.firstNotNull().let { updateInfo ->
if (updateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
if (shouldUpdateAppDeferred.await()) { // 강제 업데이트 (스토어 이동)
return UpdateState.FORCE
} else if (updateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { // 선택적 업데이트 (인앱)
return UpdateState.OPTIONAL
} else if (updateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { // 강제 업데이트 (인앱)
// Not used
}
}
}
return UpdateState.NONE
}

override fun startFlexibleUpdate() {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo.value ?: return,
appUpdateActivityResultLauncher,
AppUpdateOptions.defaultOptions(AppUpdateType.FLEXIBLE)
)
}
}

enum class UpdateState {
FORCE, OPTIONAL, NONE
}
Loading