feat(ota): OTA APK 下载更新模块
REQ-20260430-0041 新增 UpdateManager: - POST newAppVersion/queryWatch 检查版本(5分钟最小间隔) - OkHttp 下载 APK(进度回调,独立超时配置) - FileProvider + Intent ACTION_VIEW 触发系统安装 - 非 .apk 链接自动跳过 新增 UpdateDialogView: - 全屏覆盖弹窗,三种状态(发现新版本/下载中/下载失败) - 进度条支持百分比和未知大小两种模式 配置变更: - AndroidManifest: REQUEST_INSTALL_PACKAGES + FileProvider - CommonApi.checkVersion: GET→POST,传 version 参数 集成点: - HomeFragment.onResume → MainActivity.checkForUpdate() - MainActivity 管理弹窗交互和下载流程 - 更新时停止蓝牙扫描 + 保持屏幕常亮 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,9 @@
|
||||
<!-- 相机(二维码扫描备用) -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- APK 安装(OTA 更新,API 26+) -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".app.WatchApplication"
|
||||
android:allowBackup="true"
|
||||
@@ -59,6 +62,17 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- FileProvider(OTA APK 安装时共享文件) -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- 开机自启广播接收器 -->
|
||||
<receiver
|
||||
android:name=".app.BootReceiver"
|
||||
|
||||
@@ -9,9 +9,13 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.xiaoqu.watch.databinding.ActivityMainBinding
|
||||
import com.xiaoqu.watch.event.AppEvent
|
||||
import com.xiaoqu.watch.event.EventBus
|
||||
import com.xiaoqu.watch.device.screen.ScreenController
|
||||
import com.xiaoqu.watch.device.sensor.AccelerometerWakeController
|
||||
import com.xiaoqu.watch.service.manager.BluetoothScanManager
|
||||
import com.xiaoqu.watch.service.manager.NotificationManager
|
||||
import com.xiaoqu.watch.service.manager.SystemStateMonitor
|
||||
import com.xiaoqu.watch.service.manager.UpdateManager
|
||||
import com.xiaoqu.watch.ui.widget.UpdateDialogView
|
||||
import com.xiaoqu.watch.ui.widget.NotificationBannerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -37,6 +41,12 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
/** 加速度计抬手亮屏控制器 */
|
||||
@Inject lateinit var accelerometerWake: AccelerometerWakeController
|
||||
/** OTA 更新管理器 */
|
||||
@Inject lateinit var updateManager: UpdateManager
|
||||
@Inject lateinit var screenController: ScreenController
|
||||
@Inject lateinit var bluetoothScanManager: BluetoothScanManager
|
||||
/** OTA 更新弹窗 */
|
||||
lateinit var updateDialog: UpdateDialogView
|
||||
lateinit var notificationBanner: NotificationBannerView
|
||||
private val activityScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
@@ -70,6 +80,10 @@ class MainActivity : AppCompatActivity() {
|
||||
// 初始化通知横幅
|
||||
notificationBanner = binding.notificationBanner
|
||||
|
||||
// 初始化 OTA 更新弹窗
|
||||
updateDialog = binding.updateDialog
|
||||
setupUpdateDialog()
|
||||
|
||||
// 监听 MQTT type=1 → 处理通知 + 显示横幅
|
||||
observeMqttMessages()
|
||||
|
||||
@@ -166,4 +180,64 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== OTA 更新 =====
|
||||
|
||||
/** 设置更新弹窗按钮回调 */
|
||||
private fun setupUpdateDialog() {
|
||||
updateDialog.onActionClick = {
|
||||
startDownloadAndInstall()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查版本更新(由 HomeFragment.onResume 调用)
|
||||
* 有更新时显示弹窗,停止蓝牙扫描
|
||||
*/
|
||||
fun checkForUpdate() {
|
||||
if (updateManager.isUpdating) return
|
||||
activityScope.launch {
|
||||
val info = updateManager.checkUpdate() ?: return@launch
|
||||
// 停止蓝牙扫描
|
||||
bluetoothScanManager.stop()
|
||||
// 显示更新弹窗
|
||||
updateDialog.showDiscover()
|
||||
// 保存下载地址
|
||||
pendingUpdateUrl = info.url
|
||||
}
|
||||
}
|
||||
|
||||
/** 待下载的更新 URL */
|
||||
private var pendingUpdateUrl: String? = null
|
||||
|
||||
/** 开始下载并安装 APK */
|
||||
private fun startDownloadAndInstall() {
|
||||
val url = pendingUpdateUrl ?: return
|
||||
updateManager.isUpdating = true
|
||||
// 保持屏幕常亮
|
||||
screenController.turnOn()
|
||||
// 切换到下载状态
|
||||
updateDialog.showDownloading()
|
||||
|
||||
activityScope.launch {
|
||||
val file = updateManager.downloadApk(url) { progress, bytes ->
|
||||
// 进度回调在 IO 线程,切回主线程更新 UI
|
||||
launch(kotlinx.coroutines.Dispatchers.Main) {
|
||||
updateDialog.updateProgress(progress, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// 回到主线程处理结果
|
||||
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
|
||||
if (file != null) {
|
||||
// 下载成功 → 触发安装
|
||||
updateManager.installApk(file)
|
||||
} else {
|
||||
// 下载失败 → 显示错误
|
||||
updateManager.isUpdating = false
|
||||
updateDialog.showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ interface CommonApi {
|
||||
@GET("watch/getWatchByImei")
|
||||
suspend fun getWatchByImei(@Query("imei") imei: String): ApiResponse<WatchBindInfo>
|
||||
|
||||
/** 检查版本更新 */
|
||||
@GET("newAppVersion/queryWatch")
|
||||
suspend fun checkVersion(@Query("imei") imei: String): ApiResponse<Any>
|
||||
/** 检查版本更新(POST,传当前版本号,返回是否有更新+下载地址) */
|
||||
@POST("newAppVersion/queryWatch")
|
||||
suspend fun checkVersion(@Body params: HashMap<String, Any>): ApiResponse<Map<String, Any>>
|
||||
|
||||
/** 上报设备状态(电量、蓝牙、NFC等) */
|
||||
@POST("watch/setWatchStatusByImeiFormWatch")
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.xiaoqu.watch.service.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.FileProvider
|
||||
import com.xiaoqu.watch.network.ApiResult
|
||||
import com.xiaoqu.watch.network.api.CommonApi
|
||||
import com.xiaoqu.watch.network.safeApiCall
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* OTA 更新管理器
|
||||
* 负责版本检查、APK 下载、触发系统安装
|
||||
*
|
||||
* 流程:
|
||||
* 1. checkUpdate() → POST newAppVersion/queryWatch 查询是否有新版本
|
||||
* 2. downloadApk() → OkHttp 下载 APK 到本地,回调进度
|
||||
* 3. installApk() → FileProvider + Intent ACTION_VIEW 触发系统安装界面
|
||||
*/
|
||||
@Singleton
|
||||
class UpdateManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val commonApi: CommonApi
|
||||
) {
|
||||
companion object {
|
||||
/** 最小检查间隔(毫秒),5 分钟内不重复检查 */
|
||||
private const val MIN_CHECK_INTERVAL_MS = 5 * 60 * 1000L
|
||||
/** 下载文件名 */
|
||||
private const val APK_FILE_NAME = "update.apk"
|
||||
/** FileProvider authority */
|
||||
private const val FILE_PROVIDER_AUTHORITY_SUFFIX = ".fileprovider"
|
||||
}
|
||||
|
||||
/** 上次检查时间 */
|
||||
private var lastCheckTime = 0L
|
||||
|
||||
/** 是否正在更新中(下载或安装) */
|
||||
@Volatile
|
||||
var isUpdating = false
|
||||
|
||||
/** 独立的下载用 OkHttpClient(不复用业务 API 的 client,避免签名拦截器干扰) */
|
||||
private val downloadClient = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* 检查版本更新(5 分钟最小间隔)
|
||||
* @return 有更新返回 UpdateInfo,无更新或检查失败返回 null
|
||||
*/
|
||||
suspend fun checkUpdate(): UpdateInfo? {
|
||||
// 最小间隔检查
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastCheckTime < MIN_CHECK_INTERVAL_MS) {
|
||||
Timber.d("OTA: 距上次检查不足5分钟,跳过")
|
||||
return null
|
||||
}
|
||||
lastCheckTime = now
|
||||
|
||||
// 获取当前版本号
|
||||
val versionName = try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "1.0.0"
|
||||
} catch (e: Exception) {
|
||||
"1.0.0"
|
||||
}
|
||||
|
||||
Timber.d("OTA: 检查更新,当前版本 %s", versionName)
|
||||
|
||||
// 调用 API
|
||||
val params = hashMapOf<String, Any>("version" to versionName)
|
||||
val result = safeApiCall { commonApi.checkVersion(params) }
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Success -> {
|
||||
val data = result.data
|
||||
if (data != null) {
|
||||
val yesONo = data["yesONo"]?.toString() ?: ""
|
||||
val url = data["url"]?.toString() ?: ""
|
||||
|
||||
if (yesONo == "YES" && url.isNotBlank()) {
|
||||
// 只处理 APK 格式的更新
|
||||
if (!url.lowercase().endsWith(".apk")) {
|
||||
Timber.w("OTA: 更新 URL 不是 APK 格式,跳过: %s", url)
|
||||
return null
|
||||
}
|
||||
Timber.d("OTA: 发现新版本,下载地址: %s", url)
|
||||
UpdateInfo(url = url)
|
||||
} else {
|
||||
Timber.d("OTA: 当前已是最新版本")
|
||||
null
|
||||
}
|
||||
} else null
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Timber.w("OTA: 版本检查失败 - %s", result.message)
|
||||
null
|
||||
}
|
||||
is ApiResult.NetworkError -> {
|
||||
Timber.w(result.exception, "OTA: 版本检查网络异常")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载 APK
|
||||
* @param url 下载地址
|
||||
* @param onProgress 进度回调:正数=百分比(0~100),-1=总大小未知(只报告已下载字节数)
|
||||
* @return 成功返回文件,失败返回 null
|
||||
*/
|
||||
suspend fun downloadApk(
|
||||
url: String,
|
||||
onProgress: (progress: Int, downloadedBytes: Long) -> Unit
|
||||
): File? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// 清理旧文件
|
||||
val file = File(context.getExternalFilesDir(null), APK_FILE_NAME)
|
||||
if (file.exists()) file.delete()
|
||||
|
||||
Timber.d("OTA: 开始下载 APK: %s", url)
|
||||
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = downloadClient.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
Timber.w("OTA: 下载失败,HTTP %d", response.code)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val body = response.body ?: return@withContext null
|
||||
val total = body.contentLength()
|
||||
|
||||
file.outputStream().use { output ->
|
||||
body.byteStream().use { input ->
|
||||
val buffer = ByteArray(8192)
|
||||
var downloaded = 0L
|
||||
var read: Int
|
||||
while (input.read(buffer).also { read = it } != -1) {
|
||||
output.write(buffer, 0, read)
|
||||
downloaded += read
|
||||
// 进度回调(切回主线程由调用方处理)
|
||||
if (total > 0) {
|
||||
onProgress((downloaded * 100 / total).toInt(), downloaded)
|
||||
} else {
|
||||
onProgress(-1, downloaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("OTA: 下载完成,文件大小 %d bytes", file.length())
|
||||
file
|
||||
} catch (e: IOException) {
|
||||
Timber.w(e, "OTA: APK 下载异常")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "OTA: APK 下载未知异常")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装 APK(触发系统安装界面)
|
||||
* 使用 FileProvider 共享文件,通过 Intent ACTION_VIEW 启动安装器
|
||||
*/
|
||||
fun installApk(file: File) {
|
||||
// API 26+ 检查是否允许安装未知来源
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||
Timber.w("OTA: 未允许安装未知来源,引导到设置")
|
||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}${FILE_PROVIDER_AUTHORITY_SUFFIX}",
|
||||
file
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
Timber.d("OTA: 已触发系统安装界面")
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "OTA: 触发安装失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新信息 */
|
||||
data class UpdateInfo(
|
||||
val url: String
|
||||
)
|
||||
@@ -146,6 +146,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// 检查版本更新(5分钟最小间隔)
|
||||
(activity as? com.xiaoqu.watch.app.MainActivity)?.checkForUpdate()
|
||||
|
||||
if (notificationManager.pendingCount <= 0) {
|
||||
activeDotCards.clear()
|
||||
renderDots()
|
||||
|
||||
110
app/src/main/java/com/xiaoqu/watch/ui/widget/UpdateDialogView.kt
Normal file
110
app/src/main/java/com/xiaoqu/watch/ui/widget/UpdateDialogView.kt
Normal file
@@ -0,0 +1,110 @@
|
||||
package com.xiaoqu.watch.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import com.xiaoqu.watch.R
|
||||
|
||||
/**
|
||||
* OTA 更新弹窗 View(全屏覆盖)
|
||||
*
|
||||
* 三种状态:
|
||||
* - DISCOVER: "发现新版本" + "立即更新" 按钮
|
||||
* - DOWNLOADING: "请勿关机!正在升级系统..." + 进度条
|
||||
* - ERROR: "下载失败" + "重新下载" 按钮
|
||||
*
|
||||
* 添加到 activity_main.xml 最顶层,默认 GONE。
|
||||
* 通过 show(state) 显示,不提供关闭方式(强制更新)。
|
||||
*/
|
||||
class UpdateDialogView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val tvIcon: TextView
|
||||
private val tvTitle: TextView
|
||||
private val tvDesc: TextView
|
||||
private val progressContainer: View
|
||||
private val progressBar: View
|
||||
private val tvProgress: TextView
|
||||
private val btnAction: TextView
|
||||
|
||||
/** 按钮点击回调("立即更新"或"重新下载") */
|
||||
var onActionClick: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_update_dialog, this, true)
|
||||
tvIcon = findViewById(R.id.tvIcon)
|
||||
tvTitle = findViewById(R.id.tvTitle)
|
||||
tvDesc = findViewById(R.id.tvDesc)
|
||||
progressContainer = findViewById(R.id.progressContainer)
|
||||
progressBar = findViewById(R.id.progressBar)
|
||||
tvProgress = findViewById(R.id.tvProgress)
|
||||
btnAction = findViewById(R.id.btnAction)
|
||||
|
||||
btnAction.setOnClickListener { onActionClick?.invoke() }
|
||||
}
|
||||
|
||||
/** 显示"发现新版本"状态 */
|
||||
fun showDiscover() {
|
||||
visibility = View.VISIBLE
|
||||
tvIcon.text = "\ue60e"
|
||||
tvTitle.text = "发现新版本"
|
||||
tvDesc.visibility = View.GONE
|
||||
progressContainer.visibility = View.GONE
|
||||
tvProgress.visibility = View.GONE
|
||||
btnAction.visibility = View.VISIBLE
|
||||
btnAction.text = "立即更新"
|
||||
}
|
||||
|
||||
/** 显示"下载中"状态 */
|
||||
fun showDownloading() {
|
||||
tvIcon.text = "\ueb2c"
|
||||
tvTitle.text = "请勿关机!"
|
||||
tvDesc.visibility = View.VISIBLE
|
||||
tvDesc.text = "正在升级系统..."
|
||||
progressContainer.visibility = View.VISIBLE
|
||||
tvProgress.visibility = View.VISIBLE
|
||||
btnAction.visibility = View.GONE
|
||||
updateProgress(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新下载进度
|
||||
* @param percent 百分比 0~100,-1 表示总大小未知
|
||||
* @param downloadedBytes 已下载字节数
|
||||
*/
|
||||
fun updateProgress(percent: Int, downloadedBytes: Long = 0) {
|
||||
if (percent >= 0) {
|
||||
// 已知进度:显示百分比
|
||||
val params = progressBar.layoutParams
|
||||
params.width = (progressContainer.width * percent / 100).coerceAtLeast(0)
|
||||
progressBar.layoutParams = params
|
||||
tvProgress.text = "${percent}%"
|
||||
} else {
|
||||
// 未知进度:显示已下载大小
|
||||
val mb = downloadedBytes / 1024f / 1024f
|
||||
tvProgress.text = String.format("已下载 %.1f MB", mb)
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示"下载失败"状态 */
|
||||
fun showError(message: String = "下载失败") {
|
||||
tvIcon.text = "\ue60e"
|
||||
tvTitle.text = message
|
||||
tvDesc.visibility = View.GONE
|
||||
progressContainer.visibility = View.GONE
|
||||
tvProgress.visibility = View.GONE
|
||||
btnAction.visibility = View.VISIBLE
|
||||
btnAction.text = "重新下载"
|
||||
}
|
||||
|
||||
/** 隐藏弹窗 */
|
||||
fun hide() {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,11 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Layer 4: OTA 更新弹窗(最顶层,全屏覆盖,默认隐藏) -->
|
||||
<com.xiaoqu.watch.ui.widget.UpdateDialogView
|
||||
android:id="@+id/updateDialog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
89
app/src/main/res/layout/view_update_dialog.xml
Normal file
89
app/src/main/res/layout/view_update_dialog.xml
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- OTA 更新弹窗(全屏覆盖,阻断用户操作) -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#E6000000"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<!-- 图标 -->
|
||||
<TextView
|
||||
android:id="@+id/tvIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/iconfont"
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="40sp" />
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="发现新版本"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- 描述(下载中显示"请勿关机!正在升级系统...") -->
|
||||
<TextView
|
||||
android:id="@+id/tvDesc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="#AAFFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 进度条(下载中显示) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/progressContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="#33FFFFFF"
|
||||
android:visibility="gone">
|
||||
|
||||
<View
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#4CAF50" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- 进度文字 -->
|
||||
<TextView
|
||||
android:id="@+id/tvProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="#AAFFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 按钮(立即更新 / 重新下载) -->
|
||||
<TextView
|
||||
android:id="@+id/btnAction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_btn_primary"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:text="立即更新"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
5
app/src/main/res/xml/file_paths.xml
Normal file
5
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- FileProvider 路径配置(用于 OTA APK 安装) -->
|
||||
<paths>
|
||||
<external-files-path name="apk" path="." />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user