Compare commits

2 Commits

Author SHA1 Message Date
sanjeok77
ba0cdf3dd6 Fix: Correct updateUrl in version.json 2026-03-17 07:57:56 +09:00
sanjeok77
b2e15c8111 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>
2026-03-17 07:40:47 +09:00
6 changed files with 240 additions and 29 deletions

View File

@@ -24,8 +24,8 @@ android {
applicationId = "com.hotdeal.alarm" applicationId = "com.hotdeal.alarm"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 22 versionCode = 23
versionName = "1.11.5" versionName = "1.11.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -2,6 +2,7 @@ package com.hotdeal.alarm.presentation.main
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowManager import android.view.WindowManager
@@ -21,6 +22,7 @@ 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.ApkDownloadManager
import com.hotdeal.alarm.util.PermissionHelper
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
@@ -31,6 +33,10 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
companion object {
private const val TAG = "MainActivity"
}
@Inject @Inject
lateinit var workerScheduler: WorkerScheduler lateinit var workerScheduler: WorkerScheduler
@@ -55,6 +61,7 @@ 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 showPermissionDialog by remember { mutableStateOf(false) }
var downloadProgress by remember { mutableStateOf(0) } var downloadProgress by remember { mutableStateOf(0) }
var isDownloading by remember { mutableStateOf(false) } var isDownloading by remember { mutableStateOf(false) }
var downloadId by remember { mutableStateOf(-1L) } var downloadId by remember { mutableStateOf(-1L) }
@@ -78,7 +85,11 @@ class MainActivity : ComponentActivity() {
if (!isDownloading) showUpdateDialog = false if (!isDownloading) showUpdateDialog = false
}, },
onUpdate = { onUpdate = {
// APK 다이렉트 다운로드 if (!PermissionHelper.canInstallUnknownApps(this@MainActivity)) {
showPermissionDialog = true
return@UpdateDialog
}
isDownloading = true isDownloading = true
downloadId = ApkDownloadManager.downloadApk( downloadId = ApkDownloadManager.downloadApk(
this@MainActivity, 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) onUpdateAvailable(remoteInfo)
} }
} catch (e: Exception) { } catch (e: Exception) {
// 업데이트 체크 실패 시 무시 Log.e(TAG, "업데이트 체크 실패", e)
Toast.makeText(
this@MainActivity,
"업데이트 확인 실패: 네트워크 연결을 확인하세요",
Toast.LENGTH_SHORT
).show()
} }
} }
} }

View File

@@ -267,6 +267,8 @@ private fun MoreTab(viewModel: MainViewModel) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var isCheckingUpdate 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) 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( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -415,13 +439,14 @@ private fun MoreTab(viewModel: MainViewModel) {
isCheckingUpdate = false isCheckingUpdate = false
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) { if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) {
downloadAndInstallApk(context, remoteInfo) downloadId = startDownload(context, remoteInfo)
isDownloading = true
} else { } else {
Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show() Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show()
} }
} }
}, },
enabled = !isCheckingUpdate, enabled = !isCheckingUpdate && !isDownloading,
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
if (isCheckingUpdate) { if (isCheckingUpdate) {
@@ -429,6 +454,11 @@ private fun MoreTab(viewModel: MainViewModel) {
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
strokeWidth = 2.dp strokeWidth = 2.dp
) )
} else if (isDownloading) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp
)
} else { } else {
Icon( Icon(
imageVector = Icons.Outlined.SystemUpdate, imageVector = Icons.Outlined.SystemUpdate,
@@ -437,7 +467,13 @@ private fun MoreTab(viewModel: MainViewModel) {
) )
} }
Spacer(modifier = Modifier.width(6.dp)) 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) { private fun startDownload(context: Context, updateInfo: com.hotdeal.alarm.util.UpdateInfo): Long {
val downloadId = ApkDownloadManager.downloadApk(context, updateInfo) return ApkDownloadManager.downloadApk(context, updateInfo)
ApkDownloadManager.registerDownloadCompleteReceiver(
context = context,
downloadId = downloadId,
onComplete = {
ApkDownloadManager.installApk(context)
},
onFailed = {
Toast.makeText(context, "다운로드 실패. 다시 시도해주세요.", Toast.LENGTH_SHORT).show()
}
)
} }
// ==================== 공통 컴포넌트 ==================== // ==================== 공통 컴포넌트 ====================

View File

@@ -8,6 +8,7 @@ import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@@ -193,13 +194,22 @@ object ApkDownloadManager {
/** /**
* APK 파일 설치 * 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) val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), APK_FILE_NAME)
if (!apkFile.exists()) { if (!apkFile.exists()) {
Toast.makeText(context, "APK 파일을 찾을 수 없습니다", Toast.LENGTH_SHORT).show() Toast.makeText(context, "APK 파일을 찾을 수 없습니다", Toast.LENGTH_SHORT).show()
return return false
} }
try { try {
@@ -216,8 +226,10 @@ object ApkDownloadManager {
} }
context.startActivity(intent) context.startActivity(intent)
return true
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(context, "설치를 시작할 수 없습니다: ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(context, "설치를 시작할 수 없습니다: ${e.message}", Toast.LENGTH_SHORT).show()
return false
} }
} }

133
release.sh Normal file
View File

@@ -0,0 +1,133 @@
#!/bin/bash
#
# Android TV App 자동 릴리즈 스크립트
# 사용법: ./release.sh [버전타입] (major|minor|patch)
# 예: ./release.sh patch
set -e
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 현재 버전 정보 가져오기
CURRENT_VERSION=$(grep -o 'versionName = "[^"]*"' app/build.gradle.kts | cut -d'"' -f2)
CURRENT_CODE=$(grep -o 'versionCode = [0-9]*' app/build.gradle.kts | grep -o '[0-9]*')
# 버전 타입 (기본값: patch)
VERSION_TYPE=${1:-patch}
echo -e "${YELLOW}현재 버전: v${CURRENT_VERSION} (build ${CURRENT_CODE})${NC}"
echo -e "${YELLOW}업데이트 타입: ${VERSION_TYPE}${NC}"
# 새 버전 계산
IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION"
MAJOR=${VERSION_PARTS[0]}
MINOR=${VERSION_PARTS[1]}
PATCH=${VERSION_PARTS[2]}
if [ "$VERSION_TYPE" = "major" ]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [ "$VERSION_TYPE" = "minor" ]; then
MINOR=$((MINOR + 1))
PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
NEW_CODE=$((CURRENT_CODE + 1))
echo -e "${GREEN}새 버전: v${NEW_VERSION} (build ${NEW_CODE})${NC}"
# 확인
read -p "계속하시겠습니까? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${RED}취소되었습니다.${NC}"
exit 1
fi
# 1. build.gradle.kts 업데이트
echo -e "${YELLOW}1. build.gradle.kts 업데이트 중...${NC}"
sed -i "s/versionCode = ${CURRENT_CODE}/versionCode = ${NEW_CODE}/" app/build.gradle.kts
sed -i "s/versionName = \"${CURRENT_VERSION}\"/versionName = \"${NEW_VERSION}\"/" app/build.gradle.kts
echo -e "${GREEN} 완료${NC}"
# 2. version.json 업데이트
echo -e "${YELLOW}2. version.json 업데이트 중...${NC}"
cat > version.json << EOF
{
"versionCode": ${NEW_CODE},
"versionName": "${NEW_VERSION}",
"apkUrl": "https://git.webpluss.net/sanjeok77/NeFLIX/releases/download/v${NEW_VERSION}/app-release.apk",
"updateMessage": "v${NEW_VERSION} 업데이트\n- "
}
EOF
echo -e "${GREEN} 완료${NC}"
echo -e "${YELLOW} ⚠️ version.json의 updateMessage를 수동으로 수정하세요!${NC}"
# 3. 변경사항 스테이징
echo -e "${YELLOW}3. Git 스테이징 중...${NC}"
git add app/build.gradle.kts version.json
echo -e "${GREEN} 완료${NC}"
# 4. 커밋
echo -e "${YELLOW}4. Git 커밋 중...${NC}"
echo "커밋 메시지를 입력하세요 (기본값: 'feat: 버전 업데이트 v${NEW_VERSION}'):"
read -r COMMIT_MSG
if [ -z "$COMMIT_MSG" ]; then
COMMIT_MSG="feat: 버전 업데이트 v${NEW_VERSION}"
fi
git commit -m "$COMMIT_MSG" -m "" -m "- versionCode: ${NEW_CODE}" -m "- versionName: ${NEW_VERSION}" -m "" -m "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"
echo -e "${GREEN} 완료${NC}"
# 5. 태그 생성
echo -e "${YELLOW}5. Git 태그 생성 중...${NC}"
echo "태그 메시지를 입력하세요 (기본값: 'v${NEW_VERSION}'):"
read -r TAG_MSG
if [ -z "$TAG_MSG" ]; then
TAG_MSG="v${NEW_VERSION}"
fi
git tag -a "v${NEW_VERSION}" -m "$TAG_MSG"
echo -e "${GREEN} 완료${NC}"
# 6. 푸시
echo -e "${YELLOW}6. Git 푸시 중...${NC}"
git push origin main
git push origin "v${NEW_VERSION}"
echo -e "${GREEN} 완료${NC}"
# 7. 릴리즈 빌드
echo -e "${YELLOW}7. 릴리즈 빌드 중...${NC}"
./gradlew assembleRelease
echo -e "${GREEN} 완료${NC}"
# 8. 릴리즈 정보 출력
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}릴리즈 준비 완료!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "다음 단계:"
echo "1. Gitea에서 릴리즈를 생성하세요:"
echo " URL: https://git.webpluss.net/sanjeok77/NeFLIX/releases/new"
echo ""
echo "2. 또는 curl 명령어로 생성:"
echo " curl -X POST \"https://git.webpluss.net/api/v1/repos/sanjeok77/NeFLIX/releases\" \\"
echo " -H \"Authorization: token \$TOKEN\" \\"
echo " -H \"Content-Type: application/json; charset=utf-8\" \\"
echo " -d '{\"tag_name\": \"v${NEW_VERSION}\", \"name\": \"v${NEW_VERSION}\", \"body\": \"v${NEW_VERSION} 업데이트\\n- \", \"draft\": false, \"prerelease\": false}'"
echo ""
echo "3. APK 업로드:"
echo " curl -X POST \"https://git.webpluss.net/api/v1/repos/sanjeok77/NeFLIX/releases/<release_id>/assets?name=app-release.apk\" \\"
echo " -H \"Authorization: token \$TOKEN\" \\"
echo " -H \"Content-Type: application/octet-stream\" \\"
echo " --data-binary @\"app/build/outputs/apk/release/app-release.apk\""
echo ""
echo -e "${YELLOW}⚠️ version.json의 updateMessage를 수정한 후 다시 커밋하세요!${NC}"
echo ""

View File

@@ -1,11 +1,11 @@
{ {
"version": "1.11.5", "version": "1.11.6",
"versionCode": 22, "versionCode": 23,
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases/download/v1.11.5/app-release.apk", "updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases/download/v1.11.6/app-release.apk",
"changelog": [ "changelog": [
"비저빌리티 새로고침 버그 수정", "자동 업데이트 설치 권한 체크 추가",
"폴링 간격 설정이 저장되지 않던 문제 수정", "업데이트 리시버 메모리 누수 수정",
"배터리 부족 시에도 폴링 동작하도록 개선", "업데이트 체크 에러 처리 개선",
"부팅 시 저장된 폴링 간격 사용하도록 수정" "설정 화면 업데이트 다운로드 진행률 표시"
] ]
} }