feat: add native ota delivery and aab release
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -49,7 +52,4 @@
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user