diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 08f4a12..4130517 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -62,19 +62,30 @@
android:exported="false"
android:foregroundServiceType="dataSync" />
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt
index f418f06..6b22fd9 100644
--- a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt
+++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt
@@ -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
)
}
}
diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt
index 6d91405..7df48a9 100644
--- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt
+++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt
@@ -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분 간격으로 폴� 시작
+
+ // 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(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("나중에")
+ }
}
}
)
diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt
index 564d571..00c7e22 100644
--- a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt
@@ -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(
diff --git a/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt b/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt
new file mode 100644
index 0000000..18bc043
--- /dev/null
+++ b/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt
@@ -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
+ }
+}
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..c842ff4
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/version.json b/version.json
index b155494..e17492c 100644
--- a/version.json
+++ b/version.json
@@ -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": [
"아이콘 디자인 개선 (알림 벨 + 불꽃)",
"메인 화면 헤더 제거로 화면 넓게 사용",