diff --git a/README.md b/README.md index 0d8f639..1017956 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,13 @@ Android APK: - 已生成 Android debug APK:`android/app/build/outputs/apk/debug/app-debug.apk` - 已生成 Android signed release APK:`android/app/build/outputs/apk/release/app-release.apk` - `npm run apk:release` 还会额外产出带版本号的文件:`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk` -- 当前最新 release 构建版本:`2.1.0`(`versionCode=7`) +- 当前最新 release 构建版本:`2.1.1`(`versionCode=8`) - 当前 APK 已切到原生 Android 客户端:`MainActivity + BossApiClient + 原生 XML 布局` - 当前原生活动页已经覆盖:会话首页、项目详情、项目目标、版本记录、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、技能、运维中心、关于 - 原生客户端当前直接调用 `https://boss.hyzq.net` 的 Boss API,不再打开 WebView - `2.0.1` 已修复华为真机上因 `Theme.SplashScreen` 与 `AppCompatActivity` 不兼容导致的启动闪退 - `2.1.0` 已在本机连接的华为真机上完成签名包覆盖安装与启动复核,原生三栏入口和子活动页声明已全部接通 +- `2.1.1` 已补上原生 OTA 下载链路:关于页会直接请求受保护的 `/api/v1/user/ota/package`,下载完成后可拉起系统安装器 ## 本地启动 @@ -203,12 +204,21 @@ cd /Users/kris/code/boss npm run apk:release ``` +```bash +cd /Users/kris/code/boss +npm run aab:release +``` + 说明: - `npm run apk:debug` 现在会在 Gradle 构建完成后自动执行 `scripts/publish-apk-to-public.sh` - `npm run apk:release` 会先准备本机 release keystore,再构建 signed release APK,并发布到 `public/downloads` - 最新 APK 会同步到 `public/downloads/boss-android-latest.apk` - 同时也会额外保留一份带版本号的 APK:`public/downloads/boss-android-v{versionName}-{flavor}.apk` +- `npm run aab:release` 会先准备本机 release keystore,再构建 signed release AAB,并发布到 `public/downloads` +- 最新 AAB 会同步到 `public/downloads/boss-android-latest.aab` +- 同时也会额外保留一份带版本号的 AAB:`public/downloads/boss-android-v{versionName}-{flavor}.aab` +- AAB 归档元数据会写入 `public/downloads/boss-android-latest-aab.json` - OTA 下载入口固定走受保护的 `GET /api/v1/user/ota/package` - release 签名文件当前放在本机: - `android/keystores/boss-release.keystore` diff --git a/android/app/build.gradle b/android/app/build.gradle index f053c42..f70d3cc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,8 +33,8 @@ android { applicationId "com.hyzq.boss" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 7 - versionName "2.1.0" + versionCode 8 + versionName "2.1.1" buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d52a9b4..31d2b9b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + - - - diff --git a/android/app/src/main/java/com/hyzq/boss/AboutActivity.java b/android/app/src/main/java/com/hyzq/boss/AboutActivity.java index b32ee97..0eec69a 100644 --- a/android/app/src/main/java/com/hyzq/boss/AboutActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/AboutActivity.java @@ -1,8 +1,15 @@ package com.hyzq.boss; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Environment; +import android.provider.Settings; import android.widget.Button; import android.widget.LinearLayout; @@ -12,13 +19,47 @@ import org.json.JSONArray; import org.json.JSONObject; public class AboutActivity extends BossScreenActivity { + private long activeDownloadId = -1L; + private @Nullable JSONObject otaPayload; + + private final BroadcastReceiver otaDownloadReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { + return; + } + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L); + if (downloadId <= 0 || downloadId != activeDownloadId) { + return; + } + activeDownloadId = -1L; + handleCompletedDownload(downloadId); + } + }; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); configureScreen("关于 / OTA", "原生版本中心"); + IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(otaDownloadReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(otaDownloadReceiver, filter); + } reload(); } + @Override + protected void onDestroy() { + try { + unregisterReceiver(otaDownloadReceiver); + } catch (IllegalArgumentException ignored) { + // already unregistered + } + super.onDestroy(); + } + @Override protected void reload() { setRefreshing(true); @@ -46,6 +87,7 @@ public class AboutActivity extends BossScreenActivity { private void renderAbout(@Nullable JSONObject user, JSONObject ota, @Nullable JSONObject session) { replaceContent(); + otaPayload = ota; if (user != null) { appendContent(BossUi.buildCard( this, @@ -77,11 +119,8 @@ public class AboutActivity extends BossScreenActivity { Button apply = BossUi.buildSecondaryButton(this, "登记应用 OTA"); apply.setOnClickListener(v -> performOtaAction("apply")); actionCard.addView(apply); - Button download = BossUi.buildSecondaryButton(this, "下载最新 APK"); - download.setOnClickListener(v -> { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apiClient.getProtectedOtaPackageUrl())); - startActivity(intent); - }); + Button download = BossUi.buildSecondaryButton(this, "应用内下载 APK"); + download.setOnClickListener(v -> downloadLatestApk()); actionCard.addView(download); appendContent(actionCard); @@ -119,4 +158,87 @@ public class AboutActivity extends BossScreenActivity { } }); } + + private void downloadLatestApk() { + executor.execute(() -> { + try { + BossApiClient.ApiResponse session = apiClient.getSession(); + if (!session.ok()) { + session = apiClient.restoreSession(); + } + if (!session.ok()) { + throw new IllegalStateException("SESSION_UNAVAILABLE"); + } + runOnUiThread(this::enqueueOtaDownload); + } catch (Exception error) { + runOnUiThread(() -> showMessage("准备下载失败:" + error.getMessage())); + } + }); + } + + private void enqueueOtaDownload() { + DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + if (manager == null) { + showMessage("当前设备不支持 DownloadManager"); + return; + } + + JSONObject availableRelease = otaPayload == null ? null : otaPayload.optJSONObject("availableRelease"); + String fileName = availableRelease == null + ? "boss-android-latest.apk" + : availableRelease.optString("packageFileName", "boss-android-latest.apk"); + + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apiClient.getProtectedOtaPackageUrl())); + request.setTitle(fileName); + request.setDescription("Boss 原生客户端更新包"); + request.setMimeType("application/vnd.android.package-archive"); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + request.setVisibleInDownloadsUi(true); + request.addRequestHeader("Cookie", apiClient.getSessionCookie()); + request.addRequestHeader("x-boss-native-app", "1"); + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); + + activeDownloadId = manager.enqueue(request); + showMessage("已开始下载,完成后会自动拉起安装。"); + } + + private void handleCompletedDownload(long downloadId) { + DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + if (manager == null) { + showMessage("无法读取下载状态"); + return; + } + + DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId); + try (android.database.Cursor cursor = manager.query(query)) { + if (cursor == null || !cursor.moveToFirst()) { + showMessage("下载完成,但无法读取文件信息"); + return; + } + int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); + if (status != DownloadManager.STATUS_SUCCESSFUL) { + showMessage("下载未成功完成"); + return; + } + } + + Uri apkUri = manager.getUriForDownloadedFile(downloadId); + if (apkUri == null) { + showMessage("下载完成,但找不到安装包"); + return; + } + + if (!getPackageManager().canRequestPackageInstalls()) { + showMessage("请先允许 Boss 安装未知来源应用,然后重新打开安装包。"); + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return; + } + + Intent installIntent = new Intent(Intent.ACTION_VIEW); + installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(installIntent); + } } diff --git a/android/app/src/main/java/com/hyzq/boss/MainActivity.java b/android/app/src/main/java/com/hyzq/boss/MainActivity.java index 30be0a8..ac7b4c5 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -44,6 +44,9 @@ public class MainActivity extends AppCompatActivity { private LinearLayout screenContent; private String activeTab = "conversations"; + private String preferredEntryTab = "conversations"; + private boolean explicitTabRequest = false; + private boolean userSelectedTab = false; private @Nullable JSONObject sessionData; private @Nullable JSONObject otaData; private @Nullable JSONArray conversationsData; @@ -66,10 +69,24 @@ public class MainActivity extends AppCompatActivity { setIntent(intent); applyInitialTab(intent); if (contentPanel.getVisibility() == View.VISIBLE) { + maybeApplyPreferredEntry(); renderCurrentTab(); } } + @Override + public void onBackPressed() { + if (contentPanel.getVisibility() == View.VISIBLE && !"conversations".equals(activeTab)) { + setActiveTab("conversations", false); + return; + } + if (contentPanel.getVisibility() == View.VISIBLE) { + moveTaskToBack(true); + return; + } + super.onBackPressed(); + } + @Override protected void onDestroy() { executor.shutdownNow(); @@ -97,16 +114,18 @@ public class MainActivity extends AppCompatActivity { loginButton.setOnClickListener(v -> performAutoLogin()); backButton.setVisibility(View.GONE); refreshButton.setOnClickListener(v -> refreshCurrentTab()); - tabConversations.setOnClickListener(v -> switchTab("conversations")); - tabDevices.setOnClickListener(v -> switchTab("devices")); - tabMe.setOnClickListener(v -> switchTab("me")); + tabConversations.setOnClickListener(v -> setActiveTab("conversations", true)); + tabDevices.setOnClickListener(v -> setActiveTab("devices", true)); + tabMe.setOnClickListener(v -> setActiveTab("me", true)); screenRefresh.setOnRefreshListener(this::refreshCurrentTab); } private void applyInitialTab(@Nullable Intent intent) { + explicitTabRequest = false; String requested = intent == null ? null : intent.getStringExtra(EXTRA_INITIAL_TAB); if ("devices".equals(requested) || "me".equals(requested) || "conversations".equals(requested)) { activeTab = requested; + explicitTabRequest = true; } } @@ -182,7 +201,8 @@ public class MainActivity extends AppCompatActivity { BossApiClient.ApiResponse conversations = apiClient.getConversations(); BossApiClient.ApiResponse devices = apiClient.getDevices(); BossApiClient.ApiResponse ota = apiClient.getOtaStatus(); - if (!conversations.ok() || !devices.ok() || !ota.ok()) { + BossApiClient.ApiResponse settings = apiClient.getSettings(); + if (!conversations.ok() || !devices.ok() || !ota.ok() || !settings.ok()) { throw new IOException("API_REFRESH_FAILED"); } @@ -192,6 +212,11 @@ public class MainActivity extends AppCompatActivity { conversationsData = conversations.json.optJSONArray("conversations"); devicesData = devices.json.optJSONArray("devices"); otaData = ota.json; + JSONObject settingsPayload = settings.json.optJSONObject("settings"); + if (settingsPayload != null) { + preferredEntryTab = settingsPayload.optString("preferredEntryPoint", "conversations"); + } + maybeApplyPreferredEntry(); renderCurrentTab(); startRefreshing(false); }); @@ -217,7 +242,7 @@ public class MainActivity extends AppCompatActivity { private void showContent() { loginPanel.setVisibility(View.GONE); contentPanel.setVisibility(View.VISIBLE); - switchTab(activeTab); + setActiveTab(activeTab, false); } private void setLoginLoading(boolean loading, String hint) { @@ -227,12 +252,24 @@ public class MainActivity extends AppCompatActivity { loginHint.setText(hint); } - private void switchTab(String tab) { + private void setActiveTab(String tab, boolean fromUser) { activeTab = tab; + if (fromUser) { + userSelectedTab = true; + } updateTabStyles(); renderCurrentTab(); } + private void maybeApplyPreferredEntry() { + if (explicitTabRequest || userSelectedTab) { + return; + } + if ("devices".equals(preferredEntryTab) || "me".equals(preferredEntryTab) || "conversations".equals(preferredEntryTab)) { + activeTab = preferredEntryTab; + } + } + private void renderCurrentTab() { if (contentPanel.getVisibility() != View.VISIBLE) { return; diff --git a/docs/architecture/ai_handoff_index_cn.md b/docs/architecture/ai_handoff_index_cn.md index 189c83c..ecb5d4b 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -130,7 +130,7 @@ - 邮件:`Postfix + Dovecot` - Android:`AppCompatActivity + 原生 XML 布局 + HttpURLConnection` - 原生登录恢复:`SharedPreferences + restore token` -- 当前最新原生 APK:`2.1.0`(`versionCode=7`) +- 当前最新原生 APK:`2.1.1`(`versionCode=8`) 当前不要误判成已经用了: diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 97362d6..9ce55b3 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -138,6 +138,7 @@ - `code` - 当前行为: - 当前已临时切到免验证模式,点击登录会直接创建 `17600003315` 的最高管理员会话 + - 原生 Android 端登录后会持久化 `boss_session + restore token`,用于 30 天登录保持和 OTA / 覆盖安装后的会话恢复 - 当前阶段不会因为账号、密码或验证码为空而拒绝登录 - 校验通过后会写入 `boss_session` Cookie - 当请求头带 `x-boss-native-app: 1` 时,还会额外返回 `restoreToken` diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index 525890f..d2317d5 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -41,6 +41,11 @@ cd /Users/kris/code/boss npm run apk:release ``` +```bash +cd /Users/kris/code/boss +npm run aab:release +``` + ```bash cd /Users/kris/code/boss ./scripts/start-local-agent.sh ./local-agent/config.example.json @@ -94,17 +99,21 @@ cd /Users/kris/code/boss - `npm run apk:debug` 当前会自动把最新 APK 发布到 `public/downloads/boss-android-latest.apk`,并写入 `public/downloads/boss-android-latest.json` - `npm run apk:release` 当前会先准备本机 release keystore,再构建 signed release APK 并发布到 `public/downloads/boss-android-latest.apk` - APK 发布脚本当前还会额外保留带版本号的安装包:`public/downloads/boss-android-v{versionName}-{flavor}.apk` +- `npm run aab:release` 当前会先准备本机 release keystore,再构建 signed release AAB 并发布到 `public/downloads/boss-android-latest.aab` +- AAB 发布脚本当前还会额外保留带版本号的归档包:`public/downloads/boss-android-v{versionName}-{flavor}.aab` +- AAB 归档元数据会写入 `public/downloads/boss-android-latest-aab.json` - 当前默认管理员账号:`17600003315` - 当前默认测试密码:`boss123456` - 登录页当前是临时免验证入口;Web 登录页和原生 Android 登录页都会直接创建会话 - 当前已生成 Android debug APK:`android/app/build/outputs/apk/debug/app-debug.apk` - 当前已生成 Android signed release APK:`android/app/build/outputs/apk/release/app-release.apk` - 当前 release 构建还会额外生成带版本号的 APK:`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk` -- 当前最新 release 构建版本:`2.1.0`(`versionCode=7`) +- 当前最新 release 构建版本:`2.1.1`(`versionCode=8`) - 当前 release keystore 位于本机 `android/keystores/boss-release.keystore`,签名参数位于 `android/signing/release-signing.properties` - `2.0.1` 已在本机连接的华为真机上复核通过,修复了 `Theme.SplashScreen` 导致的 `AppCompatActivity` 启动闪退 - `2.1.0` 已把 Web 一级页和主要二级页全部补成原生活动页:`MainActivity / ProjectDetailActivity / ProjectGoalsActivity / ProjectVersionsActivity / ProjectForwardActivity / ThreadDetailActivity / DeviceDetailActivity / DeviceEnrollmentActivity / SkillInventoryActivity / SecurityActivity / SettingsActivity / AiAccountsActivity / OpsCenterActivity / AboutActivity` - `2.1.0` 已完成签名包覆盖安装到本机连接的华为真机,并确认 `com.hyzq.boss` 可以成功拉起进程 +- `2.1.1` 已补上原生 OTA 下载安装引导、`REQUEST_INSTALL_PACKAGES` 权限声明,以及根页默认入口/返回逻辑收口 ## 2. 服务器状态 diff --git a/package.json b/package.json index ddc0ec6..612d702 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "BOSS_RUNTIME_ROOT=\"$PWD\" BOSS_STATE_FILE=\"$PWD/data/boss-state.json\" node .next/standalone/server.js", "lint": "eslint", "apk:debug": "cd android && ./gradlew assembleDebug && cd .. && zsh ./scripts/publish-apk-to-public.sh", - "apk:release": "zsh ./scripts/build-release-apk.sh" + "apk:release": "zsh ./scripts/build-release-apk.sh", + "aab:release": "zsh ./scripts/build-release-aab.sh" }, "dependencies": { "@capacitor/android": "^8.2.0", diff --git a/public/downloads/boss-android-latest-aab.json b/public/downloads/boss-android-latest-aab.json new file mode 100644 index 0000000..a97c014 --- /dev/null +++ b/public/downloads/boss-android-latest-aab.json @@ -0,0 +1,11 @@ +{ + "artifactType": "aab", + "fileName": "boss-android-v2.1.1-release.aab", + "urlPath": "/downloads/boss-android-v2.1.1-release.aab", + "sizeBytes": 2853490, + "updatedAt": "2026-03-26T15:51:15Z", + "sha256": "c67341ca50d219e3d75baa6c88520c11d475611bed33237710d4518f292779c9", + "versionName": "2.1.1", + "versionCode": 8, + "buildFlavor": "release" +} diff --git a/public/downloads/boss-android-latest.aab b/public/downloads/boss-android-latest.aab new file mode 100644 index 0000000..18b7c05 Binary files /dev/null and b/public/downloads/boss-android-latest.aab differ diff --git a/public/downloads/boss-android-latest.apk b/public/downloads/boss-android-latest.apk index ec19f21..d29a908 100644 Binary files a/public/downloads/boss-android-latest.apk and b/public/downloads/boss-android-latest.apk differ diff --git a/public/downloads/boss-android-latest.json b/public/downloads/boss-android-latest.json index 421e494..a5135fc 100644 --- a/public/downloads/boss-android-latest.json +++ b/public/downloads/boss-android-latest.json @@ -1,10 +1,10 @@ { - "fileName": "boss-android-v2.1.0-release.apk", + "fileName": "boss-android-v2.1.1-release.apk", "urlPath": "/api/v1/user/ota/package", - "sizeBytes": 3029746, - "updatedAt": "2026-03-26T15:05:40Z", - "sha256": "17d048d256969ca08d36f7e95da39e466ef8a68dcf3c15fc04421310b97db16f", - "versionName": "2.1.0", - "versionCode": 7, + "sizeBytes": 3032808, + "updatedAt": "2026-03-26T15:51:10Z", + "sha256": "453412d605ad2cd3b1cabf806752d1288a8b23f1c61900b007468a264dda3459", + "versionName": "2.1.1", + "versionCode": 8, "buildFlavor": "release" } diff --git a/public/downloads/boss-android-v2.1.0-release.aab b/public/downloads/boss-android-v2.1.0-release.aab new file mode 100644 index 0000000..d703b1b Binary files /dev/null and b/public/downloads/boss-android-v2.1.0-release.aab differ diff --git a/public/downloads/boss-android-v2.1.1-release.aab b/public/downloads/boss-android-v2.1.1-release.aab new file mode 100644 index 0000000..18b7c05 Binary files /dev/null and b/public/downloads/boss-android-v2.1.1-release.aab differ diff --git a/public/downloads/boss-android-v2.1.1-release.apk b/public/downloads/boss-android-v2.1.1-release.apk new file mode 100644 index 0000000..d29a908 Binary files /dev/null and b/public/downloads/boss-android-v2.1.1-release.apk differ diff --git a/scripts/build-release-aab.sh b/scripts/build-release-aab.sh new file mode 100755 index 0000000..6c06730 --- /dev/null +++ b/scripts/build-release-aab.sh @@ -0,0 +1,29 @@ +#!/bin/zsh +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +ANDROID_DIR="$ROOT_DIR/android" +RELEASE_AAB="$ANDROID_DIR/app/build/outputs/bundle/release/app-release.aab" +BUILD_GRADLE="$ROOT_DIR/android/app/build.gradle" + +VERSION_NAME="$(sed -n 's/.*versionName \"\([^\"]*\)\"/\1/p' "$BUILD_GRADLE" | head -n 1)" +VERSIONED_RELEASE_AAB="$ANDROID_DIR/app/build/outputs/bundle/release/boss-android-v${VERSION_NAME}-release.aab" + +zsh "$ROOT_DIR/scripts/prepare-android-signing.sh" + +cd "$ANDROID_DIR" +./gradlew bundleRelease + +cd "$ROOT_DIR" +cp "$RELEASE_AAB" "$VERSIONED_RELEASE_AAB" +zsh "$ROOT_DIR/scripts/publish-aab-to-public.sh" "$RELEASE_AAB" + +JARSIGNER="$(command -v jarsigner || true)" +if [[ -n "${JARSIGNER:-}" ]]; then + "$JARSIGNER" -verify -verbose -certs "$RELEASE_AAB" +else + echo "jarsigner not found, skipped signature verification output" >&2 +fi + +echo "Signed release AAB ready: $RELEASE_AAB" +echo "Versioned release AAB ready: $VERSIONED_RELEASE_AAB" diff --git a/scripts/publish-aab-to-public.sh b/scripts/publish-aab-to-public.sh new file mode 100755 index 0000000..8ce80b1 --- /dev/null +++ b/scripts/publish-aab-to-public.sh @@ -0,0 +1,59 @@ +#!/bin/zsh +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SOURCE_AAB="${1:-$ROOT_DIR/android/app/build/outputs/bundle/release/app-release.aab}" +TARGET_DIR="$ROOT_DIR/public/downloads" +TARGET_AAB="$TARGET_DIR/boss-android-latest.aab" +TARGET_META="$TARGET_DIR/boss-android-latest-aab.json" +STANDALONE_DIR="$ROOT_DIR/.next/standalone/public/downloads" +BUILD_GRADLE="$ROOT_DIR/android/app/build.gradle" +SOURCE_NAME="$(basename "$SOURCE_AAB")" +BUILD_FLAVOR="release" + +if [[ "$SOURCE_NAME" == *debug* ]]; then + BUILD_FLAVOR="debug" +fi + +if [[ ! -f "$SOURCE_AAB" ]]; then + echo "AAB not found: $SOURCE_AAB" >&2 + exit 1 +fi + +mkdir -p "$TARGET_DIR" +cp "$SOURCE_AAB" "$TARGET_AAB" + +SIZE_BYTES="$(stat -f '%z' "$TARGET_AAB" 2>/dev/null || stat -c '%s' "$TARGET_AAB")" +UPDATED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +SHA256="$(shasum -a 256 "$TARGET_AAB" | awk '{print $1}')" +VERSION_NAME="$(sed -n 's/.*versionName \"\([^\"]*\)\"/\1/p' "$BUILD_GRADLE" | head -n 1)" +VERSION_CODE="$(sed -n 's/.*versionCode \([0-9][0-9]*\).*/\1/p' "$BUILD_GRADLE" | head -n 1)" +VERSIONED_AAB_NAME="boss-android-v${VERSION_NAME}-${BUILD_FLAVOR}.aab" +VERSIONED_TARGET_AAB="$TARGET_DIR/$VERSIONED_AAB_NAME" + +cp "$TARGET_AAB" "$VERSIONED_TARGET_AAB" + +cat > "$TARGET_META" <