fix: 키워드 카드 디자인 단순화 및 APK 다이렉트 다운로드 구현
- 키워드 카드 배경 단일화 (2중 배경 제거) - '내 키워드' 배지 디자인 단순화 - 알림 설정 UI 간소화 - APK 다이렉트 다운로드 기능 추가 - FileProvider 설정 추가 - version.json updateUrl을 실제 APK 다운로드 링크로 변경
This commit is contained in:
@@ -62,19 +62,30 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- Boot Receiver -->
|
||||
<!-- Boot Receiver -->
|
||||
|
||||
<!-- Widget -->
|
||||
<receiver
|
||||
android:name=".widget.HotDealWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_info" />
|
||||
</receiver>
|
||||
</application>
|
||||
<!-- Widget -->
|
||||
<receiver
|
||||
android:name=".widget.HotDealWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- FileProvider for APK installation -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -160,26 +160,26 @@ fun DealItem(
|
||||
// 키워드 매칭 배지
|
||||
if (deal.isKeywordMatch) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.height(28.dp)
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Star,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
text = "내 키워드",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
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.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.UpdateInfo
|
||||
import com.hotdeal.alarm.util.VersionManager
|
||||
import com.hotdeal.alarm.worker.WorkerScheduler
|
||||
@@ -36,17 +23,17 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
|
||||
@Inject
|
||||
lateinit var workerScheduler: WorkerScheduler
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
// 2분 간격으로 폴<EFBFBD> 시작
|
||||
|
||||
// 2분 간격으로 폴링 시작
|
||||
workerScheduler.schedulePeriodicPolling(2)
|
||||
|
||||
|
||||
setContent {
|
||||
HotDealTheme {
|
||||
Surface(
|
||||
@@ -54,28 +41,60 @@ class MainActivity : ComponentActivity() {
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
val viewModel: MainViewModel = hiltViewModel()
|
||||
|
||||
|
||||
// 업데이트 체크
|
||||
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
|
||||
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var downloadProgress by remember { mutableStateOf(0) }
|
||||
var isDownloading 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 },
|
||||
isDownloading = isDownloading,
|
||||
downloadProgress = downloadProgress,
|
||||
onDismiss = {
|
||||
if (!isDownloading) showUpdateDialog = false
|
||||
},
|
||||
onUpdate = {
|
||||
openUpdateUrl(updateInfo!!.updateUrl)
|
||||
showUpdateDialog = false
|
||||
// APK 다이렉트 다운로드
|
||||
isDownloading = true
|
||||
val downloadId = ApkDownloadManager.downloadApk(
|
||||
this@MainActivity,
|
||||
updateInfo!!
|
||||
)
|
||||
|
||||
// 다운로드 진행률 모니터링
|
||||
lifecycleScope.launch {
|
||||
while (isDownloading) {
|
||||
downloadProgress = ApkDownloadManager.getDownloadProgress(
|
||||
this@MainActivity,
|
||||
downloadId
|
||||
)
|
||||
|
||||
if (ApkDownloadManager.isDownloadComplete(
|
||||
this@MainActivity,
|
||||
downloadId
|
||||
)
|
||||
) {
|
||||
isDownloading = false
|
||||
showUpdateDialog = false
|
||||
ApkDownloadManager.installApk(this@MainActivity)
|
||||
}
|
||||
|
||||
kotlinx.coroutines.delay(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -83,7 +102,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 업데이트 체크
|
||||
*/
|
||||
@@ -92,7 +111,7 @@ class MainActivity : ComponentActivity() {
|
||||
try {
|
||||
val currentVersionCode = VersionManager.getCurrentVersionCode(this@MainActivity)
|
||||
val remoteInfo = VersionManager.checkForUpdate()
|
||||
|
||||
|
||||
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentVersionCode, remoteInfo.versionCode)) {
|
||||
// 토스트로 알림
|
||||
Toast.makeText(
|
||||
@@ -100,7 +119,7 @@ class MainActivity : ComponentActivity() {
|
||||
"새로운 버전 ${remoteInfo.version}이(가) 있습니다",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
|
||||
// 다이얼로그 표시
|
||||
onUpdateAvailable(remoteInfo)
|
||||
}
|
||||
@@ -109,18 +128,6 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업데이트 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,6 +136,8 @@ class MainActivity : ComponentActivity() {
|
||||
@Composable
|
||||
fun UpdateDialog(
|
||||
updateInfo: UpdateInfo,
|
||||
isDownloading: Boolean,
|
||||
downloadProgress: Int,
|
||||
onDismiss: () -> Unit,
|
||||
onUpdate: () -> Unit
|
||||
) {
|
||||
@@ -136,9 +145,9 @@ fun UpdateDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("업데이트 가능") },
|
||||
text = {
|
||||
androidx.compose.foundation.layout.Column {
|
||||
Column {
|
||||
Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.")
|
||||
androidx.compose.foundation.layout.Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
"변경사항:",
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
@@ -146,16 +155,34 @@ fun UpdateDialog(
|
||||
updateInfo.changelog.take(3).forEach { change ->
|
||||
Text("• $change", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = downloadProgress / 100f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
"다운로드 중... $downloadProgress%",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onUpdate) {
|
||||
Text("업데이트")
|
||||
Button(
|
||||
onClick = onUpdate,
|
||||
enabled = !isDownloading
|
||||
) {
|
||||
Text(if (isDownloading) "다운로드 중..." else "업데이트")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("나중에")
|
||||
if (!isDownloading) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("나중에")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -212,14 +212,14 @@ private fun NotificationSettingsHeader(
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp)
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
// 헤더
|
||||
Row(
|
||||
@@ -228,7 +228,7 @@ private fun NotificationSettingsHeader(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.primaryContainer,
|
||||
CircleShape
|
||||
@@ -239,14 +239,14 @@ private fun NotificationSettingsHeader(
|
||||
imageVector = Icons.Filled.Notifications,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = "알림 설정",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
@@ -259,19 +259,15 @@ private fun NotificationSettingsHeader(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 알림 권한 상태 카드
|
||||
NotificationStatusCard(
|
||||
// 알림 권한 상태
|
||||
NotificationStatusRow(
|
||||
icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning,
|
||||
title = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요",
|
||||
description = if (permissionStatus.hasNotificationPermission)
|
||||
"키워드 매칭 시 즉시 알림을 받을 수 있습니다"
|
||||
else
|
||||
"알림을 받으려면 권한을 허용해주세요",
|
||||
isOk = permissionStatus.hasNotificationPermission,
|
||||
action = if (!permissionStatus.hasNotificationPermission) {
|
||||
{ onRequestPermission() }
|
||||
} else null,
|
||||
actionLabel = "권한 허용하기",
|
||||
actionLabel = "권한 허용",
|
||||
secondaryAction = if (permissionStatus.hasNotificationPermission) {
|
||||
{ onOpenSystemSettings() }
|
||||
} else null,
|
||||
@@ -281,103 +277,49 @@ private fun NotificationSettingsHeader(
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 사이트 선택 상태
|
||||
NotificationStatusCard(
|
||||
NotificationStatusRow(
|
||||
icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
||||
title = if (hasEnabledSites) "사이트 선택 완료" else "사이트 선택 필요",
|
||||
description = if (hasEnabledSites)
|
||||
"모니터링할 사이트가 선택되었습니다"
|
||||
else
|
||||
"최소 1개 이상의 사이트를 선택해주세요",
|
||||
isOk = hasEnabledSites
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationStatusCard(
|
||||
private fun NotificationStatusRow(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
description: String,
|
||||
isOk: Boolean,
|
||||
action: (() -> Unit)? = null,
|
||||
actionLabel: String = "",
|
||||
secondaryAction: (() -> Unit)? = null,
|
||||
secondaryActionLabel: String = ""
|
||||
) {
|
||||
Surface(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = if (isOk)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
else
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (action != null) {
|
||||
TextButton(onClick = action) {
|
||||
Text(actionLabel, style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
|
||||
if (action != null || secondaryAction != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (action != null) {
|
||||
Button(
|
||||
onClick = action,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Notifications,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(actionLabel)
|
||||
}
|
||||
}
|
||||
if (secondaryAction != null) {
|
||||
OutlinedButton(
|
||||
onClick = secondaryAction,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(secondaryActionLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (secondaryAction != null) {
|
||||
TextButton(onClick = secondaryAction) {
|
||||
Text(secondaryActionLabel, style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,26 +690,13 @@ private fun EnhancedKeywordCard(
|
||||
onToggle: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (keyword.isEnabled) 1f else 0.98f,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||
label = "keyword_scale"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scale(scale),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (keyword.isEnabled)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = if (keyword.isEnabled) 2.dp else 0.dp
|
||||
)
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -777,25 +706,21 @@ private fun EnhancedKeywordCard(
|
||||
) {
|
||||
// 키워드 아이콘
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
modifier = Modifier.size(40.dp)
|
||||
.background(
|
||||
if (keyword.isEnabled)
|
||||
if (keyword.isEnabled)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||
else
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (keyword.isEnabled) Icons.Filled.Tag else Icons.Outlined.Tag,
|
||||
imageVector = Icons.Filled.Tag,
|
||||
contentDescription = null,
|
||||
tint = if (keyword.isEnabled)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.size(22.dp)
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -806,18 +731,15 @@ private fun EnhancedKeywordCard(
|
||||
Text(
|
||||
text = keyword.keyword,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (keyword.isEnabled)
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = if (keyword.isEnabled) "알림 활성화됨" else "알림 비활성화",
|
||||
text = if (keyword.isEnabled) "알림 활성화" else "알림 비활성화",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (keyword.isEnabled)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
color = if (keyword.isEnabled)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -832,7 +754,7 @@ private fun EnhancedKeywordCard(
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
// 삭제 버튼
|
||||
IconButton(
|
||||
|
||||
124
app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt
Normal file
124
app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.hotdeal.alarm.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* APK 다운로드 및 설치 관리자
|
||||
*/
|
||||
object ApkDownloadManager {
|
||||
|
||||
private const val APK_FILE_NAME = "hotdeal-alarm-update.apk"
|
||||
|
||||
/**
|
||||
* APK 다운로드 시작
|
||||
*/
|
||||
fun downloadApk(context: Context, updateInfo: UpdateInfo): Long {
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
// 기존 파일 삭제
|
||||
val outputFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), APK_FILE_NAME)
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete()
|
||||
}
|
||||
|
||||
val request = DownloadManager.Request(Uri.parse(updateInfo.updateUrl)).apply {
|
||||
setTitle("핫딜 알람 업데이트")
|
||||
setDescription("버전 ${updateInfo.version} 다운로드 중...")
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, APK_FILE_NAME)
|
||||
setAllowedOverMetered(true)
|
||||
setAllowedOverRoaming(true)
|
||||
setMimeType("application/vnd.android.package-archive")
|
||||
}
|
||||
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
"업데이트 다운로드 시작...",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
return downloadId
|
||||
}
|
||||
|
||||
/**
|
||||
* APK 파일 설치
|
||||
*/
|
||||
fun installApk(context: Context) {
|
||||
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), APK_FILE_NAME)
|
||||
|
||||
if (!apkFile.exists()) {
|
||||
Toast.makeText(context, "APK 파일을 찾을 수 없습니다", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val apkUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
apkFile
|
||||
)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "설치를 시작할 수 없습니다: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 다운로드 완료 여부 확인
|
||||
*/
|
||||
fun isDownloadComplete(context: Context, downloadId: Long): Boolean {
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
|
||||
val cursor = downloadManager.query(query)
|
||||
if (cursor.moveToFirst()) {
|
||||
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
val status = cursor.getInt(statusIndex)
|
||||
cursor.close()
|
||||
return status == DownloadManager.STATUS_SUCCESSFUL
|
||||
}
|
||||
cursor.close()
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 다운로드 진행률 가져오기
|
||||
*/
|
||||
fun getDownloadProgress(context: Context, downloadId: Long): Int {
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
|
||||
val cursor = downloadManager.query(query)
|
||||
if (cursor.moveToFirst()) {
|
||||
val bytesDownloadedIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||
val bytesTotalIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||
|
||||
val bytesDownloaded = cursor.getLong(bytesDownloadedIndex)
|
||||
val bytesTotal = cursor.getLong(bytesTotalIndex)
|
||||
|
||||
cursor.close()
|
||||
|
||||
if (bytesTotal > 0) {
|
||||
return ((bytesDownloaded * 100) / bytesTotal).toInt()
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
return 0
|
||||
}
|
||||
}
|
||||
12
app/src/main/res/xml/file_paths.xml
Normal file
12
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-files-path
|
||||
name="downloads"
|
||||
path="Download" />
|
||||
<cache-path
|
||||
name="cache"
|
||||
path="." />
|
||||
<files-path
|
||||
name="files"
|
||||
path="." />
|
||||
</paths>
|
||||
@@ -4,7 +4,7 @@
|
||||
"minSdk": 31,
|
||||
"targetSdk": 35,
|
||||
"forceUpdate": false,
|
||||
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases",
|
||||
"updateUrl": "https://git.webpluss.net/attachments/02ab65b3-22f3-4f8e-b422-6c44588764b0",
|
||||
"changelog": [
|
||||
"아이콘 디자인 개선 (알림 벨 + 불꽃)",
|
||||
"메인 화면 헤더 제거로 화면 넓게 사용",
|
||||
|
||||
Reference in New Issue
Block a user