Fix auto-update issues: receiver lifecycle, permission checks, error handling
- SettingsScreen: Add DisposableEffect for BroadcastReceiver lifecycle management - MainActivity: Add error logging and permission checks before download - ApkDownloadManager: Add install permission validation with Settings.canRequestPackageInstalls() - Add user feedback with Toast messages for all error cases - Bump version to 1.11.6 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -24,8 +24,8 @@ android {
|
||||
applicationId = "com.hotdeal.alarm"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 22
|
||||
versionName = "1.11.5"
|
||||
versionCode = 23
|
||||
versionName = "1.11.6"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.hotdeal.alarm.presentation.main
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
@@ -21,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.hotdeal.alarm.ui.theme.HotDealTheme
|
||||
import com.hotdeal.alarm.util.ApkDownloadManager
|
||||
import com.hotdeal.alarm.util.PermissionHelper
|
||||
import com.hotdeal.alarm.util.UpdateInfo
|
||||
import com.hotdeal.alarm.util.VersionManager
|
||||
import com.hotdeal.alarm.worker.WorkerScheduler
|
||||
@@ -31,6 +33,10 @@ import javax.inject.Inject
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var workerScheduler: WorkerScheduler
|
||||
|
||||
@@ -55,6 +61,7 @@ class MainActivity : ComponentActivity() {
|
||||
// 업데이트 체크
|
||||
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
|
||||
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||
var showPermissionDialog by remember { mutableStateOf(false) }
|
||||
var downloadProgress by remember { mutableStateOf(0) }
|
||||
var isDownloading by remember { mutableStateOf(false) }
|
||||
var downloadId by remember { mutableStateOf(-1L) }
|
||||
@@ -78,7 +85,11 @@ class MainActivity : ComponentActivity() {
|
||||
if (!isDownloading) showUpdateDialog = false
|
||||
},
|
||||
onUpdate = {
|
||||
// APK 다이렉트 다운로드
|
||||
if (!PermissionHelper.canInstallUnknownApps(this@MainActivity)) {
|
||||
showPermissionDialog = true
|
||||
return@UpdateDialog
|
||||
}
|
||||
|
||||
isDownloading = true
|
||||
downloadId = ApkDownloadManager.downloadApk(
|
||||
this@MainActivity,
|
||||
@@ -123,6 +134,31 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPermissionDialog = false },
|
||||
title = { Text("설치 권한 필요") },
|
||||
text = {
|
||||
Text("알 수 없는 소스의 앱 설치를 허용해야 업데이트를 설치할 수 있습니다. 설정에서 권한을 허용해주세요.")
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
PermissionHelper.openUnknownAppsSettings(this@MainActivity)
|
||||
showPermissionDialog = false
|
||||
}
|
||||
) {
|
||||
Text("설정 열기")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPermissionDialog = false }) {
|
||||
Text("취소")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +247,12 @@ class MainActivity : ComponentActivity() {
|
||||
onUpdateAvailable(remoteInfo)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 업데이트 체크 실패 시 무시
|
||||
Log.e(TAG, "업데이트 체크 실패", e)
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
"업데이트 확인 실패: 네트워크 연결을 확인하세요",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +267,8 @@ private fun MoreTab(viewModel: MainViewModel) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var isCheckingUpdate by remember { mutableStateOf(false) }
|
||||
var downloadId by remember { mutableStateOf<Long?>(null) }
|
||||
var isDownloading by remember { mutableStateOf(false) }
|
||||
|
||||
val toastEvent by viewModel.toastEvent.collectAsStateWithLifecycle(initialValue = null)
|
||||
|
||||
@@ -276,6 +278,28 @@ private fun MoreTab(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(downloadId) {
|
||||
if (downloadId != null) {
|
||||
val receiver = ApkDownloadManager.registerDownloadCompleteReceiver(
|
||||
context = context,
|
||||
downloadId = downloadId!!,
|
||||
onComplete = {
|
||||
isDownloading = false
|
||||
ApkDownloadManager.installApk(context)
|
||||
},
|
||||
onFailed = {
|
||||
isDownloading = false
|
||||
Toast.makeText(context, "다운로드 실패. 다시 시도해주세요.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
onDispose {
|
||||
ApkDownloadManager.unregisterDownloadCompleteReceiver(context)
|
||||
}
|
||||
} else {
|
||||
onDispose { }
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -415,13 +439,14 @@ private fun MoreTab(viewModel: MainViewModel) {
|
||||
isCheckingUpdate = false
|
||||
|
||||
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) {
|
||||
downloadAndInstallApk(context, remoteInfo)
|
||||
downloadId = startDownload(context, remoteInfo)
|
||||
isDownloading = true
|
||||
} else {
|
||||
Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isCheckingUpdate,
|
||||
enabled = !isCheckingUpdate && !isDownloading,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
if (isCheckingUpdate) {
|
||||
@@ -429,6 +454,11 @@ private fun MoreTab(viewModel: MainViewModel) {
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else if (isDownloading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.SystemUpdate,
|
||||
@@ -437,7 +467,13 @@ private fun MoreTab(viewModel: MainViewModel) {
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(if (isCheckingUpdate) "확인 중..." else "업데이트 확인")
|
||||
Text(
|
||||
when {
|
||||
isCheckingUpdate -> "확인 중..."
|
||||
isDownloading -> "다운로드 중..."
|
||||
else -> "업데이트 확인"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,19 +513,8 @@ private fun MoreTab(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadAndInstallApk(context: Context, updateInfo: com.hotdeal.alarm.util.UpdateInfo) {
|
||||
val downloadId = ApkDownloadManager.downloadApk(context, updateInfo)
|
||||
|
||||
ApkDownloadManager.registerDownloadCompleteReceiver(
|
||||
context = context,
|
||||
downloadId = downloadId,
|
||||
onComplete = {
|
||||
ApkDownloadManager.installApk(context)
|
||||
},
|
||||
onFailed = {
|
||||
Toast.makeText(context, "다운로드 실패. 다시 시도해주세요.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
private fun startDownload(context: Context, updateInfo: com.hotdeal.alarm.util.UpdateInfo): Long {
|
||||
return ApkDownloadManager.downloadApk(context, updateInfo)
|
||||
}
|
||||
|
||||
// ==================== 공통 컴포넌트 ====================
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -193,13 +194,22 @@ object ApkDownloadManager {
|
||||
|
||||
/**
|
||||
* APK 파일 설치
|
||||
* @return Boolean true if installation started, false if permission denied
|
||||
*/
|
||||
fun installApk(context: Context) {
|
||||
fun installApk(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||
Log.w(TAG, "앱 설치 권한 없음")
|
||||
Toast.makeText(context, "설치 권한이 필요합니다. 설정에서 허용해주세요.", Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), APK_FILE_NAME)
|
||||
|
||||
if (!apkFile.exists()) {
|
||||
Toast.makeText(context, "APK 파일을 찾을 수 없습니다", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -216,8 +226,10 @@ object ApkDownloadManager {
|
||||
}
|
||||
|
||||
context.startActivity(intent)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "설치를 시작할 수 없습니다: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user