diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 560dffb..d072e62 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "com.hotdeal.alarm" minSdk = 31 targetSdk = 35 - versionCode = 22 - versionName = "1.11.5" + versionCode = 23 + versionName = "1.11.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 9bc05dd..897f82c 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 @@ -2,6 +2,7 @@ package com.hotdeal.alarm.presentation.main import android.content.BroadcastReceiver import android.os.Bundle +import android.util.Log import android.view.View import android.view.WindowInsets import android.view.WindowManager @@ -21,6 +22,7 @@ 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.PermissionHelper import com.hotdeal.alarm.util.UpdateInfo import com.hotdeal.alarm.util.VersionManager import com.hotdeal.alarm.worker.WorkerScheduler @@ -31,6 +33,10 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { + companion object { + private const val TAG = "MainActivity" + } + @Inject lateinit var workerScheduler: WorkerScheduler @@ -55,6 +61,7 @@ class MainActivity : ComponentActivity() { // 업데이트 체크 var updateInfo by remember { mutableStateOf(null) } var showUpdateDialog by remember { mutableStateOf(false) } + var showPermissionDialog by remember { mutableStateOf(false) } var downloadProgress by remember { mutableStateOf(0) } var isDownloading by remember { mutableStateOf(false) } var downloadId by remember { mutableStateOf(-1L) } @@ -78,7 +85,11 @@ class MainActivity : ComponentActivity() { if (!isDownloading) showUpdateDialog = false }, onUpdate = { - // APK 다이렉트 다운로드 + if (!PermissionHelper.canInstallUnknownApps(this@MainActivity)) { + showPermissionDialog = true + return@UpdateDialog + } + isDownloading = true downloadId = ApkDownloadManager.downloadApk( 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) } } catch (e: Exception) { - // 업데이트 체크 실패 시 무시 + Log.e(TAG, "업데이트 체크 실패", e) + Toast.makeText( + this@MainActivity, + "업데이트 확인 실패: 네트워크 연결을 확인하세요", + Toast.LENGTH_SHORT + ).show() } } } 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 01b543b..67c12a7 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 @@ -267,6 +267,8 @@ private fun MoreTab(viewModel: MainViewModel) { val scope = rememberCoroutineScope() var showDeleteDialog by remember { mutableStateOf(false) } var isCheckingUpdate by remember { mutableStateOf(false) } + var downloadId by remember { mutableStateOf(null) } + var isDownloading by remember { mutableStateOf(false) } 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( modifier = Modifier .fillMaxSize() @@ -415,13 +439,14 @@ private fun MoreTab(viewModel: MainViewModel) { isCheckingUpdate = false if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) { - downloadAndInstallApk(context, remoteInfo) + downloadId = startDownload(context, remoteInfo) + isDownloading = true } else { Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show() } } }, - enabled = !isCheckingUpdate, + enabled = !isCheckingUpdate && !isDownloading, shape = RoundedCornerShape(12.dp) ) { if (isCheckingUpdate) { @@ -429,6 +454,11 @@ private fun MoreTab(viewModel: MainViewModel) { modifier = Modifier.size(18.dp), strokeWidth = 2.dp ) + } else if (isDownloading) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp + ) } else { Icon( imageVector = Icons.Outlined.SystemUpdate, @@ -437,7 +467,13 @@ private fun MoreTab(viewModel: MainViewModel) { ) } 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) { - val downloadId = ApkDownloadManager.downloadApk(context, updateInfo) - - ApkDownloadManager.registerDownloadCompleteReceiver( - context = context, - downloadId = downloadId, - onComplete = { - ApkDownloadManager.installApk(context) - }, - onFailed = { - Toast.makeText(context, "다운로드 실패. 다시 시도해주세요.", Toast.LENGTH_SHORT).show() - } - ) + private fun startDownload(context: Context, updateInfo: com.hotdeal.alarm.util.UpdateInfo): Long { + return ApkDownloadManager.downloadApk(context, updateInfo) } // ==================== 공통 컴포넌트 ==================== diff --git a/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt b/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt index 3c18541..b8436b0 100644 --- a/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt +++ b/app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt @@ -8,6 +8,7 @@ import android.content.IntentFilter import android.net.Uri import android.os.Build import android.os.Environment +import android.provider.Settings import android.util.Log import android.widget.Toast import androidx.core.content.FileProvider @@ -193,13 +194,22 @@ object ApkDownloadManager { /** * 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) if (!apkFile.exists()) { Toast.makeText(context, "APK 파일을 찾을 수 없습니다", Toast.LENGTH_SHORT).show() - return + return false } try { @@ -216,8 +226,10 @@ object ApkDownloadManager { } context.startActivity(intent) + return true } catch (e: Exception) { Toast.makeText(context, "설치를 시작할 수 없습니다: ${e.message}", Toast.LENGTH_SHORT).show() + return false } } diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..c98a9ad --- /dev/null +++ b/release.sh @@ -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 " +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//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 "" diff --git a/version.json b/version.json index 910dbf9..c49cd54 100644 --- a/version.json +++ b/version.json @@ -1,11 +1,11 @@ { - "version": "1.11.5", - "versionCode": 22, - "updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases/download/v1.11.5/app-release.apk", + "version": "1.11.6", + "versionCode": 23, + "updateUrl": "https://git.webpluss.net/sanjeok77/NeFLIX_release/releases/download/v1.11.6/app-release.apk", "changelog": [ - "비저빌리티 새로고침 버그 수정", - "폴링 간격 설정이 저장되지 않던 문제 수정", - "배터리 부족 시에도 폴링 동작하도록 개선", - "부팅 시 저장된 폴링 간격 사용하도록 수정" + "자동 업데이트 설치 권한 체크 추가", + "업데이트 리시버 메모리 누수 수정", + "업데이트 체크 에러 처리 개선", + "설정 화면 업데이트 다운로드 진행률 표시" ] }