Add version management and update check system

- Add version.json for remote version check
- Add VersionManager utility
- Show update dialog on app start
- Add version info in settings screen
- Version format: x.y.z (1.0.0)
- versionCode increments by 1
This commit is contained in:
sanjeok77
2026-03-04 01:45:16 +09:00
parent 037925a040
commit c22cfafe88
4 changed files with 357 additions and 323 deletions

View File

@@ -1,17 +1,37 @@
package com.hotdeal.alarm.presentation.main
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.ui.unit.dp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.lifecycleScope
import com.hotdeal.alarm.ui.theme.HotDealTheme
import com.hotdeal.alarm.util.UpdateInfo
import com.hotdeal.alarm.util.VersionManager
import com.hotdeal.alarm.worker.WorkerScheduler
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@@ -24,7 +44,8 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
workerScheduler.schedulePeriodicPolling(15)
// 2분 간격으로 폴<> 시작
workerScheduler.schedulePeriodicPolling(2)
setContent {
HotDealTheme {
@@ -33,9 +54,109 @@ class MainActivity : ComponentActivity() {
color = MaterialTheme.colorScheme.background
) {
val viewModel: MainViewModel = hiltViewModel()
// 업데이트 체크
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
var showUpdateDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
checkForUpdate { info ->
updateInfo = info
showUpdateDialog = true
}
}
MainScreen(viewModel = viewModel)
// 업데이트 다이얼로그
if (showUpdateDialog && updateInfo != null) {
UpdateDialog(
updateInfo = updateInfo!!,
onDismiss = { showUpdateDialog = false },
onUpdate = {
openUpdateUrl(updateInfo!!.updateUrl)
showUpdateDialog = false
}
)
}
}
}
}
}
/**
* 업데이트 체크
*/
private fun checkForUpdate(onUpdateAvailable: (UpdateInfo) -> Unit) {
lifecycleScope.launch {
try {
val currentVersionCode = VersionManager.getCurrentVersionCode(this@MainActivity)
val remoteInfo = VersionManager.checkForUpdate()
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentVersionCode, remoteInfo.versionCode)) {
// 토스트로 알림
Toast.makeText(
this@MainActivity,
"새로운 버전 ${remoteInfo.version}이(가) 있습니다",
Toast.LENGTH_LONG
).show()
// 다이얼로그 표시
onUpdateAvailable(remoteInfo)
}
} catch (e: Exception) {
// 업데이트 체크 실패 시 무시
}
}
}
/**
* 업데이트 URL 열기
*/
private fun openUpdateUrl(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, "브라우저를 열 수 없습니다", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 업데이트 다이얼로그
*/
@Composable
fun UpdateDialog(
updateInfo: UpdateInfo,
onDismiss: () -> Unit,
onUpdate: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("업데이트 가능") },
text = {
androidx.compose.foundation.layout.Column {
Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.")
androidx.compose.foundation.layout.Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
Text(
"변경사항:",
style = MaterialTheme.typography.labelMedium
)
updateInfo.changelog.take(3).forEach { change ->
Text("$change", style = MaterialTheme.typography.bodySmall)
}
}
},
confirmButton = {
Button(onClick = onUpdate) {
Text("업데이트")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("나중에")
}
}
)
}

View File

@@ -1,11 +1,10 @@
package com.hotdeal.alarm.presentation.settings
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -15,11 +14,9 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.hotdeal.alarm.domain.model.Keyword
import com.hotdeal.alarm.domain.model.SiteConfig
@@ -28,13 +25,16 @@ import com.hotdeal.alarm.presentation.components.PermissionDialog
import com.hotdeal.alarm.presentation.main.MainUiState
import com.hotdeal.alarm.presentation.main.MainViewModel
import com.hotdeal.alarm.util.PermissionHelper
import com.hotdeal.alarm.util.VersionManager
import com.hotdeal.alarm.worker.WorkerScheduler
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(viewModel: MainViewModel) {
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
var showPermissionDialog by remember { mutableStateOf(false) }
var permissionDialogTitle by remember { mutableStateOf("") }
@@ -46,7 +46,7 @@ fun SettingsScreen(viewModel: MainViewModel) {
) { isGranted ->
if (!isGranted) {
permissionDialogTitle = "알림 권한 필요"
permissionDialogMessage = "핫딜 알림을 받으려면 알림 권한이 필요합니다.\n설정에서 권한을 허용해주세요."
permissionDialogMessage = "핫딜 알림을 받으려면 알림 권한이 필요합니다."
permissionDialogAction = {
PermissionHelper.openNotificationSettings(context)
}
@@ -54,12 +54,9 @@ fun SettingsScreen(viewModel: MainViewModel) {
}
}
// 권한 상태 확인
val permissionStatus = PermissionHelper.checkAllPermissions(context)
val hasEnabledSites = (uiState as? MainUiState.Success)
?.siteConfigs?.any { it.isEnabled } ?: false
val hasKeywords = (uiState as? MainUiState.Success)
?.keywords?.isNotEmpty() ?: false
LazyColumn(
modifier = Modifier
@@ -67,30 +64,21 @@ fun SettingsScreen(viewModel: MainViewModel) {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 권한 상태 섹션
item {
PermissionStatusSection(
permissionStatus = permissionStatus,
hasEnabledSites = hasEnabledSites,
hasKeywords = hasKeywords,
onRequestNotificationPermission = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
},
onRequestExactAlarmPermission = {
PermissionHelper.openExactAlarmSettings(context)
},
onOpenAppSettings = {
PermissionHelper.openAppSettings(context)
}
)
}
// 폴<> 주기 설정
item {
PollingIntervalSection(
currentInterval = 2, // 기본 2분
currentInterval = 2,
onIntervalChange = { minutes ->
viewModel.stopPolling()
viewModel.startPolling(minutes)
@@ -98,12 +86,8 @@ fun SettingsScreen(viewModel: MainViewModel) {
)
}
// 사이트 선택 섹션
item {
Text(
text = "사이트 선택",
style = MaterialTheme.typography.titleMedium
)
Text("사이트 선택", style = MaterialTheme.typography.titleMedium)
}
when (val state = uiState) {
@@ -122,20 +106,11 @@ fun SettingsScreen(viewModel: MainViewModel) {
item {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "키워드 설정",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "특정 키워드가 포함된 핫딜만 알림받으려면 키워드를 추가하세요",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text("키워드 설정", style = MaterialTheme.typography.titleMedium)
}
item {
var keywordText by remember { mutableStateOf("") }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@@ -169,20 +144,28 @@ fun SettingsScreen(viewModel: MainViewModel) {
)
}
if (state.keywords.isEmpty()) {
// 버전 정보 섹션
item {
Text(
text = "등록된 키워드가 없습니다",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
Spacer(modifier = Modifier.height(24.dp))
VersionSection(
currentVersion = VersionManager.getCurrentVersion(context),
onCheckUpdate = {
scope.launch {
val currentCode = VersionManager.getCurrentVersionCode(context)
val remoteInfo = VersionManager.checkForUpdate()
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) {
Toast.makeText(context, "새 버전 ${remoteInfo.version} 사용 가능", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show()
}
}
}
)
}
}
}
else -> {
item {
CircularProgressIndicator()
}
item { CircularProgressIndicator() }
}
}
}
@@ -192,82 +175,58 @@ fun SettingsScreen(viewModel: MainViewModel) {
title = permissionDialogTitle,
message = permissionDialogMessage,
onDismiss = { showPermissionDialog = false },
onOpenSettings = {
showPermissionDialog = false
permissionDialogAction()
}
onOpenSettings = { showPermissionDialog = false; permissionDialogAction() }
)
}
}
@Composable
fun PollingIntervalSection(
currentInterval: Int,
onIntervalChange: (Long) -> Unit
) {
var selectedInterval by remember { mutableStateOf(currentInterval) }
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
fun VersionSection(currentVersion: String, onCheckUpdate: () -> Unit) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Schedule,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "<EFBFBD> 주기 설정",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Column {
Text("앱 버전", style = MaterialTheme.typography.labelMedium)
Text("v$currentVersion", style = MaterialTheme.typography.titleMedium)
}
Button(onClick = onCheckUpdate) {
Text("업데이트 확인")
}
}
}
}
}
Text(
text = "핫딜을 확인하는 간격을 설정합니다.\n짧을수록 배터리와 데이터 소모가 증가합니다.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// 간격 선택
Column {
@Composable
fun PollingIntervalSection(currentInterval: Int, onIntervalChange: (Long) -> Unit) {
var selected by remember { mutableStateOf(currentInterval) }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("<EFBFBD> 주기", style = MaterialTheme.typography.titleMedium)
Text("2분 권장 (배터리/데이터 절약)", style = MaterialTheme.typography.bodySmall)
Spacer(modifier = Modifier.height(8.dp))
listOf(1, 2, 5, 10, 15, 30).forEach { minutes ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = selectedInterval == minutes,
selected = selected == minutes,
onClick = {
selectedInterval = minutes
selected = minutes
onIntervalChange(minutes.toLong())
}
)
Text(
text = when (minutes) {
1 -> "1분 (빠름 - 배터리 소모 큼)"
Text(when(minutes) {
1 -> "1분 (빠름)"
2 -> "2분 (권장)"
5 -> "5분 (보통)"
10 -> "10분 (느림)"
15 -> "15분 (매우 느림)"
30 -> "30분 (절전)"
else -> "${minutes}"
},
style = MaterialTheme.typography.bodyMedium
)
}
})
}
}
}
@@ -278,217 +237,50 @@ fun PollingIntervalSection(
fun PermissionStatusSection(
permissionStatus: PermissionHelper.PermissionStatus,
hasEnabledSites: Boolean,
hasKeywords: Boolean,
onRequestNotificationPermission: () -> Unit,
onRequestExactAlarmPermission: () -> Unit,
onOpenAppSettings: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (permissionStatus.isAllGranted && hasEnabledSites) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
} else {
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
}
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "알림 설정 상태",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
// 알림 권한 상태
PermissionItem(
icon = Icons.Default.Notifications,
title = "알림 권한",
isGranted = permissionStatus.hasNotificationPermission,
requiredVersion = "Android 13+",
onClick = onRequestNotificationPermission
)
// 정확한 알람 권한 상태
PermissionItem(
icon = Icons.Default.Alarm,
title = "정확한 알람 권한",
isGranted = permissionStatus.hasExactAlarmPermission,
requiredVersion = "Android 12+",
onClick = onRequestExactAlarmPermission
)
// 사이트 활성화 상태
PermissionItem(
icon = Icons.Default.Language,
title = "사이트 선택",
isGranted = hasEnabledSites,
description = if (!hasEnabledSites) "최소 1개 사이트 활성화 필요" else null,
onClick = null
)
// 키워드 상태 (선택사항)
if (!hasKeywords) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "키워드를 추가하면 해당 키워드가 포함된 핫딜만 알림받습니다",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// 전체 상태 요약
if (!permissionStatus.isAllGranted || !hasEnabledSites) {
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.background(
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f),
shape = MaterialTheme.shapes.small
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "알림을 받으려면 모든 필수 권한을 허용하고 사이트를 선택하세요",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
} else {
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "모든 설정이 완료되었습니다",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
@Composable
fun PermissionItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String,
isGranted: Boolean,
requiredVersion: String? = null,
description: String? = null,
onClick: (() -> Unit)?
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (isGranted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = title,
style = MaterialTheme.typography.bodyMedium
)
if (requiredVersion != null) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "($requiredVersion)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (description != null) {
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (onClick != null && !isGranted) {
FilledTonalButton(
onClick = onClick,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
) {
Text("설정", style = MaterialTheme.typography.labelMedium)
}
} else {
Icon(
imageVector = if (isGranted) Icons.Default.Check else Icons.Default.Close,
contentDescription = if (isGranted) "허용됨" else "거부됨",
tint = if (isGranted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp)
)
}
}
}
@Composable
fun SiteSection(
siteType: SiteType,
configs: List<SiteConfig>,
onToggle: (String, Boolean) -> Unit
onRequestNotificationPermission: () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = siteType.displayName,
style = MaterialTheme.typography.titleSmall
)
Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.width(12.dp))
Text("알림 권한", modifier = Modifier.weight(1f))
if (!permissionStatus.hasNotificationPermission) {
Button(onClick = onRequestNotificationPermission) { Text("설정") }
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.width(12.dp))
Text("사이트 선택", modifier = Modifier.weight(1f))
}
}
}
}
@Composable
fun SiteSection(siteType: SiteType, configs: List<SiteConfig>, onToggle: (String, Boolean) -> Unit) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(siteType.displayName, style = MaterialTheme.typography.titleSmall)
configs.forEach { config ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = config.displayName.substringAfter(" - "),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f)
)
Text(config.displayName.substringAfter(" - "), modifier = Modifier.weight(1f))
Switch(
checked = config.isEnabled,
onCheckedChange = { onToggle(config.siteBoardKey, it) }
@@ -500,19 +292,12 @@ fun SiteSection(
}
@Composable
fun KeywordItem(
keyword: Keyword,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
fun KeywordItem(keyword: Keyword, onToggle: () -> Unit, onDelete: () -> Unit) {
ListItem(
headlineContent = { Text(keyword.keyword) },
trailingContent = {
Row {
Switch(
checked = keyword.isEnabled,
onCheckedChange = { onToggle() }
)
Switch(checked = keyword.isEnabled, onCheckedChange = { onToggle() })
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "삭제")
}

View File

@@ -0,0 +1,114 @@
package com.hotdeal.alarm.util
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.net.URL
/**
* 버전 관리 유틸리티
*
* 버전 규칙: x.y.z
* - x: 메이저 (API 변경, 호환성 깨짐)
* - y: 마이너 (기능 추가, 하위 호환)
* - z: 패치 (버그 수정)
*
* versionCode: 1씩 증가 (낮은 버전 = 이전 버전)
*/
object VersionManager {
private const val TAG = "VersionManager"
private const val VERSION_URL = "https://git.webpluss.net/sanjeok77/hotdeal_alarm/raw/branch/main/version.json"
/**
* 현재 앱 버전 가져오기
*/
fun getCurrentVersion(context: Context): String {
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
packageInfo.versionName ?: "1.0.0"
} catch (e: Exception) {
Log.e(TAG, "버전 가져오기 실패: ${e.message}")
"1.0.0"
}
}
/**
* 현재 버전 코드 가져오기
*/
fun getCurrentVersionCode(context: Context): Int {
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
packageInfo.longVersionCode.toInt()
} catch (e: Exception) {
Log.e(TAG, "버전 코드 가져오기 실패: ${e.message}")
1
}
}
/**
* 원격 버전 정보 가져오기
*/
suspend fun checkForUpdate(): UpdateInfo? = withContext(Dispatchers.IO) {
try {
val json = URL(VERSION_URL).readText()
val jsonObject = JSONObject(json)
UpdateInfo(
version = jsonObject.getString("version"),
versionCode = jsonObject.getInt("versionCode"),
forceUpdate = jsonObject.optBoolean("forceUpdate", false),
updateUrl = jsonObject.getString("updateUrl"),
changelog = jsonObject.optJSONArray("changelog")?.let { array ->
List(array.length()) { array.getString(it) }
} ?: emptyList()
)
} catch (e: Exception) {
Log.e(TAG, "버전 체크 실패: ${e.message}")
null
}
}
/**
* 업데이트 필요 여부 확인
*/
fun isUpdateAvailable(currentVersionCode: Int, remoteVersionCode: Int): Boolean {
return remoteVersionCode > currentVersionCode
}
/**
* 버전 문자열 비교
* 1.0.0 < 1.0.1 < 1.1.0 < 2.0.0
*/
fun compareVersions(v1: String, v2: String): Int {
val parts1 = v1.split(".").map { it.toIntOrNull() ?: 0 }
val parts2 = v2.split(".").map { it.toIntOrNull() ?: 0 }
val maxLength = maxOf(parts1.size, parts2.size)
for (i in 0 until maxLength) {
val p1 = parts1.getOrElse(i) { 0 }
val p2 = parts2.getOrElse(i) { 0 }
when {
p1 > p2 -> return 1
p1 < p2 -> return -1
}
}
return 0
}
}
/**
* 업데이트 정보
*/
data class UpdateInfo(
val version: String,
val versionCode: Int,
val forceUpdate: Boolean,
val updateUrl: String,
val changelog: List<String>
)

14
version.json Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "1.0.0",
"versionCode": 1,
"minSdk": 31,
"targetSdk": 35,
"forceUpdate": false,
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases",
"changelog": [
"초기 릴리즈",
"뽐뿌, 클리앙, 루리웹, 쿨엔조이 지원",
"키워드 알림 기능",
"사이트 필터 기능"
]
}