feat: add native ota delivery and aab release
This commit is contained in:
12
README.md
12
README.md
@@ -90,12 +90,13 @@ Android APK:
|
|||||||
- 已生成 Android debug APK:`android/app/build/outputs/apk/debug/app-debug.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`
|
- 已生成 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`
|
- `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 布局`
|
- 当前 APK 已切到原生 Android 客户端:`MainActivity + BossApiClient + 原生 XML 布局`
|
||||||
- 当前原生活动页已经覆盖:会话首页、项目详情、项目目标、版本记录、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、技能、运维中心、关于
|
- 当前原生活动页已经覆盖:会话首页、项目详情、项目目标、版本记录、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、技能、运维中心、关于
|
||||||
- 原生客户端当前直接调用 `https://boss.hyzq.net` 的 Boss API,不再打开 WebView
|
- 原生客户端当前直接调用 `https://boss.hyzq.net` 的 Boss API,不再打开 WebView
|
||||||
- `2.0.1` 已修复华为真机上因 `Theme.SplashScreen` 与 `AppCompatActivity` 不兼容导致的启动闪退
|
- `2.0.1` 已修复华为真机上因 `Theme.SplashScreen` 与 `AppCompatActivity` 不兼容导致的启动闪退
|
||||||
- `2.1.0` 已在本机连接的华为真机上完成签名包覆盖安装与启动复核,原生三栏入口和子活动页声明已全部接通
|
- `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
|
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:debug` 现在会在 Gradle 构建完成后自动执行 `scripts/publish-apk-to-public.sh`
|
||||||
- `npm run apk:release` 会先准备本机 release keystore,再构建 signed release APK,并发布到 `public/downloads`
|
- `npm run apk:release` 会先准备本机 release keystore,再构建 signed release APK,并发布到 `public/downloads`
|
||||||
- 最新 APK 会同步到 `public/downloads/boss-android-latest.apk`
|
- 最新 APK 会同步到 `public/downloads/boss-android-latest.apk`
|
||||||
- 同时也会额外保留一份带版本号的 APK:`public/downloads/boss-android-v{versionName}-{flavor}.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`
|
- OTA 下载入口固定走受保护的 `GET /api/v1/user/ota/package`
|
||||||
- release 签名文件当前放在本机:
|
- release 签名文件当前放在本机:
|
||||||
- `android/keystores/boss-release.keystore`
|
- `android/keystores/boss-release.keystore`
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ android {
|
|||||||
applicationId "com.hyzq.boss"
|
applicationId "com.hyzq.boss"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 7
|
versionCode 8
|
||||||
versionName "2.1.0"
|
versionName "2.1.1"
|
||||||
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
|
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -49,7 +52,4 @@
|
|||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package com.hyzq.boss;
|
package com.hyzq.boss;
|
||||||
|
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
@@ -12,13 +19,47 @@ import org.json.JSONArray;
|
|||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class AboutActivity extends BossScreenActivity {
|
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
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
configureScreen("关于 / OTA", "原生版本中心");
|
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();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
try {
|
||||||
|
unregisterReceiver(otaDownloadReceiver);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
// already unregistered
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void reload() {
|
protected void reload() {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
@@ -46,6 +87,7 @@ public class AboutActivity extends BossScreenActivity {
|
|||||||
|
|
||||||
private void renderAbout(@Nullable JSONObject user, JSONObject ota, @Nullable JSONObject session) {
|
private void renderAbout(@Nullable JSONObject user, JSONObject ota, @Nullable JSONObject session) {
|
||||||
replaceContent();
|
replaceContent();
|
||||||
|
otaPayload = ota;
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
appendContent(BossUi.buildCard(
|
appendContent(BossUi.buildCard(
|
||||||
this,
|
this,
|
||||||
@@ -77,11 +119,8 @@ public class AboutActivity extends BossScreenActivity {
|
|||||||
Button apply = BossUi.buildSecondaryButton(this, "登记应用 OTA");
|
Button apply = BossUi.buildSecondaryButton(this, "登记应用 OTA");
|
||||||
apply.setOnClickListener(v -> performOtaAction("apply"));
|
apply.setOnClickListener(v -> performOtaAction("apply"));
|
||||||
actionCard.addView(apply);
|
actionCard.addView(apply);
|
||||||
Button download = BossUi.buildSecondaryButton(this, "下载最新 APK");
|
Button download = BossUi.buildSecondaryButton(this, "应用内下载 APK");
|
||||||
download.setOnClickListener(v -> {
|
download.setOnClickListener(v -> downloadLatestApk());
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apiClient.getProtectedOtaPackageUrl()));
|
|
||||||
startActivity(intent);
|
|
||||||
});
|
|
||||||
actionCard.addView(download);
|
actionCard.addView(download);
|
||||||
appendContent(actionCard);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private LinearLayout screenContent;
|
private LinearLayout screenContent;
|
||||||
|
|
||||||
private String activeTab = "conversations";
|
private String activeTab = "conversations";
|
||||||
|
private String preferredEntryTab = "conversations";
|
||||||
|
private boolean explicitTabRequest = false;
|
||||||
|
private boolean userSelectedTab = false;
|
||||||
private @Nullable JSONObject sessionData;
|
private @Nullable JSONObject sessionData;
|
||||||
private @Nullable JSONObject otaData;
|
private @Nullable JSONObject otaData;
|
||||||
private @Nullable JSONArray conversationsData;
|
private @Nullable JSONArray conversationsData;
|
||||||
@@ -66,10 +69,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setIntent(intent);
|
setIntent(intent);
|
||||||
applyInitialTab(intent);
|
applyInitialTab(intent);
|
||||||
if (contentPanel.getVisibility() == View.VISIBLE) {
|
if (contentPanel.getVisibility() == View.VISIBLE) {
|
||||||
|
maybeApplyPreferredEntry();
|
||||||
renderCurrentTab();
|
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
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
@@ -97,16 +114,18 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
loginButton.setOnClickListener(v -> performAutoLogin());
|
loginButton.setOnClickListener(v -> performAutoLogin());
|
||||||
backButton.setVisibility(View.GONE);
|
backButton.setVisibility(View.GONE);
|
||||||
refreshButton.setOnClickListener(v -> refreshCurrentTab());
|
refreshButton.setOnClickListener(v -> refreshCurrentTab());
|
||||||
tabConversations.setOnClickListener(v -> switchTab("conversations"));
|
tabConversations.setOnClickListener(v -> setActiveTab("conversations", true));
|
||||||
tabDevices.setOnClickListener(v -> switchTab("devices"));
|
tabDevices.setOnClickListener(v -> setActiveTab("devices", true));
|
||||||
tabMe.setOnClickListener(v -> switchTab("me"));
|
tabMe.setOnClickListener(v -> setActiveTab("me", true));
|
||||||
screenRefresh.setOnRefreshListener(this::refreshCurrentTab);
|
screenRefresh.setOnRefreshListener(this::refreshCurrentTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyInitialTab(@Nullable Intent intent) {
|
private void applyInitialTab(@Nullable Intent intent) {
|
||||||
|
explicitTabRequest = false;
|
||||||
String requested = intent == null ? null : intent.getStringExtra(EXTRA_INITIAL_TAB);
|
String requested = intent == null ? null : intent.getStringExtra(EXTRA_INITIAL_TAB);
|
||||||
if ("devices".equals(requested) || "me".equals(requested) || "conversations".equals(requested)) {
|
if ("devices".equals(requested) || "me".equals(requested) || "conversations".equals(requested)) {
|
||||||
activeTab = requested;
|
activeTab = requested;
|
||||||
|
explicitTabRequest = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +201,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
BossApiClient.ApiResponse conversations = apiClient.getConversations();
|
BossApiClient.ApiResponse conversations = apiClient.getConversations();
|
||||||
BossApiClient.ApiResponse devices = apiClient.getDevices();
|
BossApiClient.ApiResponse devices = apiClient.getDevices();
|
||||||
BossApiClient.ApiResponse ota = apiClient.getOtaStatus();
|
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");
|
throw new IOException("API_REFRESH_FAILED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +212,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
conversationsData = conversations.json.optJSONArray("conversations");
|
conversationsData = conversations.json.optJSONArray("conversations");
|
||||||
devicesData = devices.json.optJSONArray("devices");
|
devicesData = devices.json.optJSONArray("devices");
|
||||||
otaData = ota.json;
|
otaData = ota.json;
|
||||||
|
JSONObject settingsPayload = settings.json.optJSONObject("settings");
|
||||||
|
if (settingsPayload != null) {
|
||||||
|
preferredEntryTab = settingsPayload.optString("preferredEntryPoint", "conversations");
|
||||||
|
}
|
||||||
|
maybeApplyPreferredEntry();
|
||||||
renderCurrentTab();
|
renderCurrentTab();
|
||||||
startRefreshing(false);
|
startRefreshing(false);
|
||||||
});
|
});
|
||||||
@@ -217,7 +242,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private void showContent() {
|
private void showContent() {
|
||||||
loginPanel.setVisibility(View.GONE);
|
loginPanel.setVisibility(View.GONE);
|
||||||
contentPanel.setVisibility(View.VISIBLE);
|
contentPanel.setVisibility(View.VISIBLE);
|
||||||
switchTab(activeTab);
|
setActiveTab(activeTab, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLoginLoading(boolean loading, String hint) {
|
private void setLoginLoading(boolean loading, String hint) {
|
||||||
@@ -227,12 +252,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
loginHint.setText(hint);
|
loginHint.setText(hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchTab(String tab) {
|
private void setActiveTab(String tab, boolean fromUser) {
|
||||||
activeTab = tab;
|
activeTab = tab;
|
||||||
|
if (fromUser) {
|
||||||
|
userSelectedTab = true;
|
||||||
|
}
|
||||||
updateTabStyles();
|
updateTabStyles();
|
||||||
renderCurrentTab();
|
renderCurrentTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void maybeApplyPreferredEntry() {
|
||||||
|
if (explicitTabRequest || userSelectedTab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("devices".equals(preferredEntryTab) || "me".equals(preferredEntryTab) || "conversations".equals(preferredEntryTab)) {
|
||||||
|
activeTab = preferredEntryTab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void renderCurrentTab() {
|
private void renderCurrentTab() {
|
||||||
if (contentPanel.getVisibility() != View.VISIBLE) {
|
if (contentPanel.getVisibility() != View.VISIBLE) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
- 邮件:`Postfix + Dovecot`
|
- 邮件:`Postfix + Dovecot`
|
||||||
- Android:`AppCompatActivity + 原生 XML 布局 + HttpURLConnection`
|
- Android:`AppCompatActivity + 原生 XML 布局 + HttpURLConnection`
|
||||||
- 原生登录恢复:`SharedPreferences + restore token`
|
- 原生登录恢复:`SharedPreferences + restore token`
|
||||||
- 当前最新原生 APK:`2.1.0`(`versionCode=7`)
|
- 当前最新原生 APK:`2.1.1`(`versionCode=8`)
|
||||||
|
|
||||||
当前不要误判成已经用了:
|
当前不要误判成已经用了:
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@
|
|||||||
- `code`
|
- `code`
|
||||||
- 当前行为:
|
- 当前行为:
|
||||||
- 当前已临时切到免验证模式,点击登录会直接创建 `17600003315` 的最高管理员会话
|
- 当前已临时切到免验证模式,点击登录会直接创建 `17600003315` 的最高管理员会话
|
||||||
|
- 原生 Android 端登录后会持久化 `boss_session + restore token`,用于 30 天登录保持和 OTA / 覆盖安装后的会话恢复
|
||||||
- 当前阶段不会因为账号、密码或验证码为空而拒绝登录
|
- 当前阶段不会因为账号、密码或验证码为空而拒绝登录
|
||||||
- 校验通过后会写入 `boss_session` Cookie
|
- 校验通过后会写入 `boss_session` Cookie
|
||||||
- 当请求头带 `x-boss-native-app: 1` 时,还会额外返回 `restoreToken`
|
- 当请求头带 `x-boss-native-app: 1` 时,还会额外返回 `restoreToken`
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ cd /Users/kris/code/boss
|
|||||||
npm run apk:release
|
npm run apk:release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/kris/code/boss
|
||||||
|
npm run aab:release
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /Users/kris/code/boss
|
cd /Users/kris/code/boss
|
||||||
./scripts/start-local-agent.sh ./local-agent/config.example.json
|
./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: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`
|
- `npm run apk:release` 当前会先准备本机 release keystore,再构建 signed release APK 并发布到 `public/downloads/boss-android-latest.apk`
|
||||||
- APK 发布脚本当前还会额外保留带版本号的安装包:`public/downloads/boss-android-v{versionName}-{flavor}.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`
|
- 当前默认管理员账号:`17600003315`
|
||||||
- 当前默认测试密码:`boss123456`
|
- 当前默认测试密码:`boss123456`
|
||||||
- 登录页当前是临时免验证入口;Web 登录页和原生 Android 登录页都会直接创建会话
|
- 登录页当前是临时免验证入口;Web 登录页和原生 Android 登录页都会直接创建会话
|
||||||
- 当前已生成 Android debug APK:`android/app/build/outputs/apk/debug/app-debug.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`
|
- 当前已生成 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 构建还会额外生成带版本号的 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`
|
- 当前 release keystore 位于本机 `android/keystores/boss-release.keystore`,签名参数位于 `android/signing/release-signing.properties`
|
||||||
- `2.0.1` 已在本机连接的华为真机上复核通过,修复了 `Theme.SplashScreen` 导致的 `AppCompatActivity` 启动闪退
|
- `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` 已把 Web 一级页和主要二级页全部补成原生活动页:`MainActivity / ProjectDetailActivity / ProjectGoalsActivity / ProjectVersionsActivity / ProjectForwardActivity / ThreadDetailActivity / DeviceDetailActivity / DeviceEnrollmentActivity / SkillInventoryActivity / SecurityActivity / SettingsActivity / AiAccountsActivity / OpsCenterActivity / AboutActivity`
|
||||||
- `2.1.0` 已完成签名包覆盖安装到本机连接的华为真机,并确认 `com.hyzq.boss` 可以成功拉起进程
|
- `2.1.0` 已完成签名包覆盖安装到本机连接的华为真机,并确认 `com.hyzq.boss` 可以成功拉起进程
|
||||||
|
- `2.1.1` 已补上原生 OTA 下载安装引导、`REQUEST_INSTALL_PACKAGES` 权限声明,以及根页默认入口/返回逻辑收口
|
||||||
|
|
||||||
## 2. 服务器状态
|
## 2. 服务器状态
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"start": "BOSS_RUNTIME_ROOT=\"$PWD\" BOSS_STATE_FILE=\"$PWD/data/boss-state.json\" node .next/standalone/server.js",
|
"start": "BOSS_RUNTIME_ROOT=\"$PWD\" BOSS_STATE_FILE=\"$PWD/data/boss-state.json\" node .next/standalone/server.js",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"apk:debug": "cd android && ./gradlew assembleDebug && cd .. && zsh ./scripts/publish-apk-to-public.sh",
|
"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": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^8.2.0",
|
"@capacitor/android": "^8.2.0",
|
||||||
|
|||||||
11
public/downloads/boss-android-latest-aab.json
Normal file
11
public/downloads/boss-android-latest-aab.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
BIN
public/downloads/boss-android-latest.aab
Normal file
BIN
public/downloads/boss-android-latest.aab
Normal file
Binary file not shown.
Binary file not shown.
@@ -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",
|
"urlPath": "/api/v1/user/ota/package",
|
||||||
"sizeBytes": 3029746,
|
"sizeBytes": 3032808,
|
||||||
"updatedAt": "2026-03-26T15:05:40Z",
|
"updatedAt": "2026-03-26T15:51:10Z",
|
||||||
"sha256": "17d048d256969ca08d36f7e95da39e466ef8a68dcf3c15fc04421310b97db16f",
|
"sha256": "453412d605ad2cd3b1cabf806752d1288a8b23f1c61900b007468a264dda3459",
|
||||||
"versionName": "2.1.0",
|
"versionName": "2.1.1",
|
||||||
"versionCode": 7,
|
"versionCode": 8,
|
||||||
"buildFlavor": "release"
|
"buildFlavor": "release"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/downloads/boss-android-v2.1.0-release.aab
Normal file
BIN
public/downloads/boss-android-v2.1.0-release.aab
Normal file
Binary file not shown.
BIN
public/downloads/boss-android-v2.1.1-release.aab
Normal file
BIN
public/downloads/boss-android-v2.1.1-release.aab
Normal file
Binary file not shown.
BIN
public/downloads/boss-android-v2.1.1-release.apk
Normal file
BIN
public/downloads/boss-android-v2.1.1-release.apk
Normal file
Binary file not shown.
29
scripts/build-release-aab.sh
Executable file
29
scripts/build-release-aab.sh
Executable file
@@ -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"
|
||||||
59
scripts/publish-aab-to-public.sh
Executable file
59
scripts/publish-aab-to-public.sh
Executable file
@@ -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" <<EOF
|
||||||
|
{
|
||||||
|
"artifactType": "aab",
|
||||||
|
"fileName": "$VERSIONED_AAB_NAME",
|
||||||
|
"urlPath": "/downloads/$VERSIONED_AAB_NAME",
|
||||||
|
"sizeBytes": $SIZE_BYTES,
|
||||||
|
"updatedAt": "$UPDATED_AT",
|
||||||
|
"sha256": "$SHA256",
|
||||||
|
"versionName": "$VERSION_NAME",
|
||||||
|
"versionCode": $VERSION_CODE,
|
||||||
|
"buildFlavor": "$BUILD_FLAVOR"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ -d "$ROOT_DIR/.next/standalone/public" ]]; then
|
||||||
|
mkdir -p "$STANDALONE_DIR"
|
||||||
|
cp "$TARGET_AAB" "$STANDALONE_DIR/boss-android-latest.aab"
|
||||||
|
cp "$VERSIONED_TARGET_AAB" "$STANDALONE_DIR/$VERSIONED_AAB_NAME"
|
||||||
|
cp "$TARGET_META" "$STANDALONE_DIR/boss-android-latest-aab.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Published AAB to $TARGET_AAB"
|
||||||
|
echo "Published versioned AAB to $VERSIONED_TARGET_AAB"
|
||||||
|
echo "Metadata written to $TARGET_META"
|
||||||
Reference in New Issue
Block a user