diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b06ddbf..b0373cf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,4 +38,5 @@ + diff --git a/android/app/src/main/java/com/codebridgex/webapp/AppUpdateChecker.java b/android/app/src/main/java/com/codebridgex/webapp/AppUpdateChecker.java new file mode 100644 index 0000000..deaaba3 --- /dev/null +++ b/android/app/src/main/java/com/codebridgex/webapp/AppUpdateChecker.java @@ -0,0 +1,226 @@ +package com.codebridgex.webapp; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.util.Log; + +import androidx.core.content.FileProvider; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class AppUpdateChecker { + + private static final String TAG = "AppUpdateChecker"; + private static final String API_BASE_URL = "https://api.codebridge-x.com"; + private static final String API_KEY = "sam-api-key-2025"; + + private final Activity activity; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + public AppUpdateChecker(Activity activity) { + this.activity = activity; + } + + /** + * 서버에서 최신 버전 확인 + */ + public void checkForUpdate() { + executor.execute(() -> { + try { + int currentVersionCode = getCurrentVersionCode(); + String urlStr = API_BASE_URL + "/api/v1/app/version?platform=android¤t_version_code=" + currentVersionCode; + + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("X-API-KEY", API_KEY); + conn.setRequestProperty("Accept", "application/json"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + Log.w(TAG, "Version check failed: HTTP " + responseCode); + return; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + conn.disconnect(); + + JSONObject json = new JSONObject(sb.toString()); + if (!json.optBoolean("success", false)) return; + + JSONObject data = json.getJSONObject("data"); + if (!data.optBoolean("has_update", false)) { + Log.d(TAG, "No update available"); + return; + } + + JSONObject latestVersion = data.getJSONObject("latest_version"); + String versionName = latestVersion.getString("version_name"); + String releaseNotes = latestVersion.optString("release_notes", ""); + boolean forceUpdate = latestVersion.optBoolean("force_update", false); + String downloadUrl = latestVersion.getString("download_url"); + + activity.runOnUiThread(() -> showUpdateDialog(versionName, releaseNotes, forceUpdate, downloadUrl)); + + } catch (Exception e) { + Log.e(TAG, "Update check error", e); + } + }); + } + + /** + * 업데이트 다이얼로그 표시 + */ + private void showUpdateDialog(String versionName, String releaseNotes, boolean forceUpdate, String downloadUrl) { + if (activity.isFinishing() || activity.isDestroyed()) return; + + StringBuilder message = new StringBuilder(); + message.append("새 버전 v").append(versionName).append("이 있습니다.\n"); + if (releaseNotes != null && !releaseNotes.isEmpty()) { + message.append("\n").append(releaseNotes); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity) + .setTitle("업데이트 알림") + .setMessage(message.toString()) + .setCancelable(!forceUpdate) + .setPositiveButton("업데이트", (dialog, which) -> { + dialog.dismiss(); + startDownload(downloadUrl, versionName); + }); + + if (forceUpdate) { + // 강제 업데이트: "나중에" 버튼 없음, 뒤로가기/외부 터치로 닫을 수 없음 + builder.setOnCancelListener(dialog -> activity.finishAffinity()); + } else { + builder.setNegativeButton("나중에", (dialog, which) -> dialog.dismiss()); + } + + builder.show(); + } + + /** + * DownloadManager로 APK 다운로드 + */ + private void startDownload(String downloadUrl, String versionName) { + try { + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl)); + request.addRequestHeader("X-API-KEY", API_KEY); + request.setTitle("SAM 업데이트 v" + versionName); + request.setDescription("앱 업데이트를 다운로드하고 있습니다..."); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + + String fileName = "sam-v" + versionName + ".apk"; + request.setDestinationInExternalFilesDir(activity, Environment.DIRECTORY_DOWNLOADS, fileName); + + DownloadManager dm = (DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE); + long downloadId = dm.enqueue(request); + + // 다운로드 완료 감지 + registerDownloadReceiver(downloadId, fileName); + + } catch (Exception e) { + Log.e(TAG, "Download start error", e); + } + } + + /** + * 다운로드 완료 시 설치 화면 표시 + */ + private void registerDownloadReceiver(long downloadId, String fileName) { + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + if (id != downloadId) return; + + try { + activity.unregisterReceiver(this); + } catch (Exception ignored) {} + + installApk(fileName); + } + }; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + activity.registerReceiver(receiver, + new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), + Context.RECEIVER_NOT_EXPORTED); + } else { + activity.registerReceiver(receiver, + new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + } + } + + /** + * APK 설치 Intent 실행 + */ + private void installApk(String fileName) { + try { + File apkFile = new File(activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName); + if (!apkFile.exists()) { + Log.e(TAG, "APK file not found: " + apkFile.getAbsolutePath()); + return; + } + + Uri apkUri = FileProvider.getUriForFile( + activity, + activity.getPackageName() + ".fileprovider", + apkFile + ); + + Intent installIntent = new Intent(Intent.ACTION_VIEW); + installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + activity.startActivity(installIntent); + + } catch (Exception e) { + Log.e(TAG, "Install APK error", e); + } + } + + /** + * 현재 앱 버전 코드 + */ + private int getCurrentVersionCode() { + try { + PackageInfo pInfo = activity.getPackageManager() + .getPackageInfo(activity.getPackageName(), 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return (int) pInfo.getLongVersionCode(); + } else { + return pInfo.versionCode; + } + } catch (Exception e) { + Log.e(TAG, "Get version code error", e); + return 0; + } + } +} diff --git a/android/app/src/main/java/com/codebridgex/webapp/MainActivity.java b/android/app/src/main/java/com/codebridgex/webapp/MainActivity.java index 61fbd14..5ce3fab 100644 --- a/android/app/src/main/java/com/codebridgex/webapp/MainActivity.java +++ b/android/app/src/main/java/com/codebridgex/webapp/MainActivity.java @@ -26,6 +26,9 @@ public class MainActivity extends BridgeActivity { super.onCreate(savedInstanceState); createNotificationChannels(); + // 인앱 업데이트 체크 + new AppUpdateChecker(this).checkForUpdate(); + // WebView 줌 설정 (핀치 줌 활성화) getBridge().getWebView().getSettings().setSupportZoom(true); getBridge().getWebView().getSettings().setBuiltInZoomControls(true); diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml index bd0c4d8..b27adb7 100644 --- a/android/app/src/main/res/xml/file_paths.xml +++ b/android/app/src/main/res/xml/file_paths.xml @@ -2,4 +2,5 @@ + \ No newline at end of file