fix: 키워드 카드 디자인 단순화 및 APK 다이렉트 다운로드 구현

- 키워드 카드 배경 단일화 (2중 배경 제거)
- '내 키워드' 배지 디자인 단순화
- 알림 설정 UI 간소화
- APK 다이렉트 다운로드 기능 추가
- FileProvider 설정 추가
- version.json updateUrl을 실제 APK 다운로드 링크로 변경
This commit is contained in:
sanjeok77
2026-03-04 09:04:33 +09:00
parent 324f68256c
commit f78647200c
7 changed files with 297 additions and 201 deletions

View File

@@ -75,6 +75,17 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/widget_info" /> android:resource="@xml/widget_info" />
</receiver> </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> </application>
</manifest> </manifest>

View File

@@ -160,26 +160,26 @@ fun DealItem(
// 키워드 매칭 배지 // 키워드 매칭 배지
if (deal.isKeywordMatch) { if (deal.isKeywordMatch) {
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.height(28.dp) modifier = Modifier.height(24.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 10.dp) modifier = Modifier.padding(horizontal = 8.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Star, imageVector = Icons.Filled.Star,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary, tint = Color.White,
modifier = Modifier.size(14.dp) modifier = Modifier.size(12.dp)
) )
Text( Text(
text = "내 키워드", text = "내 키워드",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onPrimary color = Color.White
) )
} }
} }

View File

@@ -1,32 +1,19 @@
package com.hotdeal.alarm.presentation.main package com.hotdeal.alarm.presentation.main
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Spacer import androidx.compose.material3.*
import androidx.compose.foundation.layout.height import androidx.compose.runtime.*
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.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.hotdeal.alarm.ui.theme.HotDealTheme import com.hotdeal.alarm.ui.theme.HotDealTheme
import com.hotdeal.alarm.util.ApkDownloadManager
import com.hotdeal.alarm.util.UpdateInfo import com.hotdeal.alarm.util.UpdateInfo
import com.hotdeal.alarm.util.VersionManager import com.hotdeal.alarm.util.VersionManager
import com.hotdeal.alarm.worker.WorkerScheduler import com.hotdeal.alarm.worker.WorkerScheduler
@@ -44,7 +31,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
// 2분 간격으로 폴<EFBFBD> 시작 // 2분 간격으로 폴 시작
workerScheduler.schedulePeriodicPolling(2) workerScheduler.schedulePeriodicPolling(2)
setContent { setContent {
@@ -58,6 +45,8 @@ class MainActivity : ComponentActivity() {
// 업데이트 체크 // 업데이트 체크
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) } var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
var showUpdateDialog by remember { mutableStateOf(false) } var showUpdateDialog by remember { mutableStateOf(false) }
var downloadProgress by remember { mutableStateOf(0) }
var isDownloading by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
checkForUpdate { info -> checkForUpdate { info ->
@@ -72,10 +61,40 @@ class MainActivity : ComponentActivity() {
if (showUpdateDialog && updateInfo != null) { if (showUpdateDialog && updateInfo != null) {
UpdateDialog( UpdateDialog(
updateInfo = updateInfo!!, updateInfo = updateInfo!!,
onDismiss = { showUpdateDialog = false }, isDownloading = isDownloading,
downloadProgress = downloadProgress,
onDismiss = {
if (!isDownloading) showUpdateDialog = false
},
onUpdate = { onUpdate = {
openUpdateUrl(updateInfo!!.updateUrl) // 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 showUpdateDialog = false
ApkDownloadManager.installApk(this@MainActivity)
}
kotlinx.coroutines.delay(500)
}
}
} }
) )
} }
@@ -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 @Composable
fun UpdateDialog( fun UpdateDialog(
updateInfo: UpdateInfo, updateInfo: UpdateInfo,
isDownloading: Boolean,
downloadProgress: Int,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onUpdate: () -> Unit onUpdate: () -> Unit
) { ) {
@@ -136,9 +145,9 @@ fun UpdateDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("업데이트 가능") }, title = { Text("업데이트 가능") },
text = { text = {
androidx.compose.foundation.layout.Column { Column {
Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.") Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.")
androidx.compose.foundation.layout.Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
"변경사항:", "변경사항:",
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium
@@ -146,17 +155,35 @@ fun UpdateDialog(
updateInfo.changelog.take(3).forEach { change -> updateInfo.changelog.take(3).forEach { change ->
Text("$change", style = MaterialTheme.typography.bodySmall) 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 = { confirmButton = {
Button(onClick = onUpdate) { Button(
Text("업데이트") onClick = onUpdate,
enabled = !isDownloading
) {
Text(if (isDownloading) "다운로드 중..." else "업데이트")
} }
}, },
dismissButton = { dismissButton = {
if (!isDownloading) {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("나중에") Text("나중에")
} }
} }
}
) )
} }

View File

@@ -212,14 +212,14 @@ private fun NotificationSettingsHeader(
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp) modifier = Modifier.padding(16.dp)
) { ) {
// 헤더 // 헤더
Row( Row(
@@ -228,7 +228,7 @@ private fun NotificationSettingsHeader(
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(44.dp) .size(40.dp)
.background( .background(
MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.primaryContainer,
CircleShape CircleShape
@@ -239,14 +239,14 @@ private fun NotificationSettingsHeader(
imageVector = Icons.Filled.Notifications, imageVector = Icons.Filled.Notifications,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer, tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp) modifier = Modifier.size(22.dp)
) )
} }
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column { Column {
Text( Text(
text = "알림 설정", text = "알림 설정",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
@@ -259,19 +259,15 @@ private fun NotificationSettingsHeader(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 알림 권한 상태 카드 // 알림 권한 상태
NotificationStatusCard( NotificationStatusRow(
icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning, icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning,
title = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요", title = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요",
description = if (permissionStatus.hasNotificationPermission)
"키워드 매칭 시 즉시 알림을 받을 수 있습니다"
else
"알림을 받으려면 권한을 허용해주세요",
isOk = permissionStatus.hasNotificationPermission, isOk = permissionStatus.hasNotificationPermission,
action = if (!permissionStatus.hasNotificationPermission) { action = if (!permissionStatus.hasNotificationPermission) {
{ onRequestPermission() } { onRequestPermission() }
} else null, } else null,
actionLabel = "권한 허용하기", actionLabel = "권한 허용",
secondaryAction = if (permissionStatus.hasNotificationPermission) { secondaryAction = if (permissionStatus.hasNotificationPermission) {
{ onOpenSystemSettings() } { onOpenSystemSettings() }
} else null, } else null,
@@ -281,103 +277,49 @@ private fun NotificationSettingsHeader(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// 사이트 선택 상태 // 사이트 선택 상태
NotificationStatusCard( NotificationStatusRow(
icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error, icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error,
title = if (hasEnabledSites) "사이트 선택 완료" else "사이트 선택 필요", title = if (hasEnabledSites) "사이트 선택 완료" else "사이트 선택 필요",
description = if (hasEnabledSites)
"모니터링할 사이트가 선택되었습니다"
else
"최소 1개 이상의 사이트를 선택해주세요",
isOk = hasEnabledSites isOk = hasEnabledSites
) )
} }
} }
} }
@Composable @Composable
private fun NotificationStatusCard( private fun NotificationStatusRow(
icon: ImageVector, icon: ImageVector,
title: String, title: String,
description: String,
isOk: Boolean, isOk: Boolean,
action: (() -> Unit)? = null, action: (() -> Unit)? = null,
actionLabel: String = "", actionLabel: String = "",
secondaryAction: (() -> Unit)? = null, secondaryAction: (() -> Unit)? = null,
secondaryActionLabel: String = "" secondaryActionLabel: String = ""
) {
Surface(
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)
) {
Column(
modifier = Modifier.padding(16.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(28.dp) modifier = Modifier.size(24.dp)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.height(2.dp))
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (action != null || secondaryAction != null) {
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (action != null) { if (action != null) {
Button( TextButton(onClick = action) {
onClick = action, Text(actionLabel, style = MaterialTheme.typography.labelMedium)
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) { if (secondaryAction != null) {
OutlinedButton( TextButton(onClick = secondaryAction) {
onClick = secondaryAction, Text(secondaryActionLabel, style = MaterialTheme.typography.labelMedium)
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)
}
}
}
} }
} }
} }
@@ -748,26 +690,13 @@ private fun EnhancedKeywordCard(
onToggle: () -> Unit, onToggle: () -> Unit,
onDelete: () -> Unit onDelete: () -> Unit
) { ) {
val scale by animateFloatAsState(
targetValue = if (keyword.isEnabled) 1f else 0.98f,
animationSpec = spring(stiffness = Spring.StiffnessLow),
label = "keyword_scale"
)
Card( Card(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() shape = RoundedCornerShape(12.dp),
.scale(scale),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (keyword.isEnabled) containerColor = MaterialTheme.colorScheme.surface
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
defaultElevation = if (keyword.isEnabled) 2.dp else 0.dp
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -777,25 +706,21 @@ private fun EnhancedKeywordCard(
) { ) {
// 키워드 아이콘 // 키워드 아이콘
Box( Box(
modifier = Modifier modifier = Modifier.size(40.dp)
.size(44.dp)
.background( .background(
if (keyword.isEnabled) if (keyword.isEnabled)
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
else else
MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
CircleShape CircleShape
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = if (keyword.isEnabled) Icons.Filled.Tag else Icons.Outlined.Tag, imageVector = Icons.Filled.Tag,
contentDescription = null, contentDescription = null,
tint = if (keyword.isEnabled) tint = Color.White,
MaterialTheme.colorScheme.onPrimary modifier = Modifier.size(20.dp)
else
MaterialTheme.colorScheme.outline,
modifier = Modifier.size(22.dp)
) )
} }
@@ -806,14 +731,11 @@ private fun EnhancedKeywordCard(
Text( Text(
text = keyword.keyword, text = keyword.keyword,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Medium,
color = if (keyword.isEnabled) color = MaterialTheme.colorScheme.onSurface
MaterialTheme.colorScheme.onSurface
else
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
) )
Text( Text(
text = if (keyword.isEnabled) "알림 활성화" else "알림 비활성화", text = if (keyword.isEnabled) "알림 활성화" else "알림 비활성화",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = if (keyword.isEnabled) color = if (keyword.isEnabled)
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
@@ -832,7 +754,7 @@ private fun EnhancedKeywordCard(
) )
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(4.dp))
// 삭제 버튼 // 삭제 버튼
IconButton( IconButton(

View 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
}
}

View 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>

View File

@@ -4,7 +4,7 @@
"minSdk": 31, "minSdk": 31,
"targetSdk": 35, "targetSdk": 35,
"forceUpdate": false, "forceUpdate": false,
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases", "updateUrl": "https://git.webpluss.net/attachments/02ab65b3-22f3-4f8e-b422-6c44588764b0",
"changelog": [ "changelog": [
"아이콘 디자인 개선 (알림 벨 + 불꽃)", "아이콘 디자인 개선 (알림 벨 + 불꽃)",
"메인 화면 헤더 제거로 화면 넓게 사용", "메인 화면 헤더 제거로 화면 넓게 사용",