1.增加g40固件升级

2.优化dfu弹窗布局
This commit is contained in:
yezhiqiu
2026-04-16 10:42:38 +08:00
parent d8ab8599da
commit 06bf4687fe
10 changed files with 228 additions and 23 deletions

View File

@@ -30,7 +30,7 @@ android {
targetSdkVersion 35
versionCode 2202
// versionName "2.2.2"
versionName "2.2.2-Beta1"
versionName "2.2.2-Beta2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -319,8 +319,11 @@ dependencies {
//支付宝支付
implementation 'com.alipay.sdk:alipaysdk-android:+@aar'
//dfu升级https://github.com/NordicSemiconductor/Android-DFU-Library
implementation 'no.nordicsemi.android:dfu:2.9.0'
//g30 dfu升级https://github.com/NordicSemiconductor/Android-DFU-Library
implementation 'no.nordicsemi.android:dfu:2.10.1'
//g40 dfu升级https://github.com/nordicsemi/Android-nRF-Connect-Device-Manager?tab=readme-ov-file#migration-from-the-original-repo
// implementation 'no.nordicsemi.android:mcumgr-core:2.7.4'
implementation 'no.nordicsemi.android:mcumgr-ble:2.7.4'
//适配Android 12以下SplashScreen启动动画闪屏图片
implementation 'androidx.core:core-splashscreen:1.0.1'

View File

@@ -25,8 +25,8 @@ class DFUNewDialog(context: Context) :
}
fun startDfuState(version: String, onClick: () -> Unit) {
mVersion = version
val ver = String.format(mContext.getString(R.string.txt_new_firmware_version), mVersion)
mVersion = "\t$version"
val ver = String.format(mContext.getString(R.string.txt_about_version), mVersion)
mViewBinding.apply {
rlDialogDfuNewAfterLayout.visibility = View.GONE
llDialogDfuNewBeforeLayout.visibility = View.VISIBLE
@@ -36,7 +36,7 @@ class DFUNewDialog(context: Context) :
inDFUState()
onClick()
}
0 }
}
}
private fun inDFUState() {

View File

@@ -111,7 +111,7 @@ class SharePetActivityActivity :
//先设置文件fileProvider
EasyPhotos.createCamera(this@SharePetActivityActivity, false)
.setFileProviderAuthority("${BuildConfig.APPLICATION_ID}.fileprovider")
.setFileProviderAuthority(FileUtil.FILE_PROVIDER)
mTakePhotoAndCompressViewModel.registerCropImageActivityResult(this, 3, 4)
}

View File

@@ -306,7 +306,15 @@ class HomeTrackFragment :
LogUtil.e("固件下载完成")
mBleTrackDeviceBean?.let { b ->
b.bleDevice?.let { ble ->
mDeviceDFUViewModel.startDFU(mContext!!, ble, filePath)
getHomeV2Activity()?.let { m ->
m.getPet(false)?.let { p ->
if (p.deviceType == ConstantInt.Type2) {
mDeviceDFUViewModel.startG40DFU(mContext!!, ble, filePath)
} else {
mDeviceDFUViewModel.startG30DFU(mContext!!, ble, filePath)
}
}
}
}
}
}
@@ -943,7 +951,11 @@ class HomeTrackFragment :
)
)
}
mDeviceDFUViewModel.getFirmware()
getHomeV2Activity()?.let { m ->
m.getPet(false)?.let { p ->
mDeviceDFUViewModel.getFirmware(p.deviceType)
}
}
mBleTrackDeviceBean?.apply {
SRBleUtil.instance.writeData(
bleDevice, SRBleCmdUtil.instance.ledState(SRBleCmdUtil.CMD_READ, 0)
@@ -1068,21 +1080,29 @@ class HomeTrackFragment :
getHomeV2Activity()?.apply {
//保持屏幕常亮
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
getPet(false)?.let {
if (it.deviceType == ConstantInt.Type1) {
DfuServiceListenerHelper.registerProgressListener(
mContext, mDeviceDFUViewModel.mDfuProgressListener
)
}
}
}
}
private fun dfuEnd() {
getHomeV2Activity()?.apply {
//清除屏幕常亮
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
getPet(false)?.let {
if (it.deviceType == ConstantInt.Type1) {
DfuServiceListenerHelper.unregisterProgressListener(
mContext, mDeviceDFUViewModel.mDfuProgressListener
)
}
}
}
}
/**
* 手动连接/断开设备

View File

@@ -2,8 +2,12 @@ package com.abbidot.tracker.util
import android.app.Activity
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.content.FileProvider
import com.abbidot.baselibrary.util.LogUtil
import com.abbidot.tracker.BuildConfig
import java.io.File
import java.math.BigDecimal
@@ -46,6 +50,11 @@ class FileUtil {
*/
const val LOG_DIRECTORY_NAME = "log"
/**
* fileProvider字段
*/
const val FILE_PROVIDER = "${BuildConfig.APPLICATION_ID}.fileprovider"
/**
* 获取缓存目录
* <p>
@@ -144,10 +153,20 @@ class FileUtil {
/**
* 删除文件或文件夹
*/
fun File.clearFile() {
private fun File.clearFile() {
if (isFile) delete()
if (isDirectory) listFiles()?.forEach { it.clearFile() }
}
/**
* 获取文件File的Uri
*/
fun getUri(cxt: Context, file: File): Uri {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(cxt, FILE_PROVIDER, file)
} else {
Uri.fromFile(file)
}
}
}
}

View File

@@ -56,7 +56,6 @@ import com.abbidot.baselibrary.util.AppUtils
import com.abbidot.baselibrary.util.LogUtil
import com.abbidot.baselibrary.util.MMKVUtil
import com.abbidot.baselibrary.util.Utils
import com.abbidot.tracker.BuildConfig
import com.abbidot.tracker.R
import com.abbidot.tracker.adapter.GridItemDecoration
import com.abbidot.tracker.adapter.SelectMapTypeCardShadeAdapter
@@ -582,7 +581,7 @@ class ViewUtil private constructor() {
if (allGranted) {
LogUtil.e("获取READ_MEDIA_IMAGES权限成功")
EasyPhotos.createAlbum(activity, true, false, CoilEngine.instance)
.setFileProviderAuthority("${BuildConfig.APPLICATION_ID}.fileprovider")
.setFileProviderAuthority(FileUtil.FILE_PROVIDER)
.setCount(count).setPuzzleMenu(false).setCleanMenu(false)
.start(requestCode)
//activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)

View File

@@ -2,6 +2,7 @@ package com.abbidot.tracker.vm
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.lifecycle.AndroidViewModel
@@ -11,18 +12,30 @@ import com.abbidot.baselibrary.util.LogUtil
import com.abbidot.tracker.R
import com.abbidot.tracker.bean.DFUStateBean
import com.abbidot.tracker.bean.FirmwareBean
import com.abbidot.tracker.constant.ConstantInt
import com.abbidot.tracker.retrofit2.NetworkApi
import com.abbidot.tracker.service.DfuService
import com.abbidot.tracker.util.FileUtil
import com.abbidot.tracker.util.bluetooth.SRBleUtil
import com.clj.fastble.BleManager
import com.clj.fastble.callback.BleScanCallback
import com.clj.fastble.data.BleDevice
import com.clj.fastble.data.BleScanState
import com.clj.fastble.scan.BleScanRuleConfig
import io.runtime.mcumgr.ble.McuMgrBleTransport
import io.runtime.mcumgr.dfu.FirmwareUpgradeCallback
import io.runtime.mcumgr.dfu.FirmwareUpgradeController
import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager
import io.runtime.mcumgr.exception.McuMgrException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import no.nordicsemi.android.dfu.DfuProgressListener
import no.nordicsemi.android.dfu.DfuServiceInitiator
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
/**
*Created by .yzq on 2023/3/16/016.
@@ -47,10 +60,12 @@ class DeviceDFUViewModel(application: Application) : AndroidViewModel(applicatio
/**
* 获取固件信息
*/
fun getFirmware() {
fun getFirmware(deviceType: Int) {
// activity.showLoading(isShowLoading)
viewModelScope.launch(Dispatchers.IO) {
val result = NetworkApi.getFirmware("GPS_Tracker_01")
val deviceName = if (deviceType == ConstantInt.Type2) "g40"
else "GPS_Tracker_01"
val result = NetworkApi.getFirmware(deviceName)
mFirmwareInfoLiveData.postValue(result)
}
}
@@ -66,7 +81,146 @@ class DeviceDFUViewModel(application: Application) : AndroidViewModel(applicatio
return serviceVersionInt > localVersionInt
}
fun startDFU(context: Context, device: BleDevice, path: String) {
fun startG40DFU(context: Context, bleDevice: BleDevice, path: String) {
val transport = McuMgrBleTransport(context, bleDevice.device)
val dfuManager = FirmwareUpgradeManager(
transport, object : FirmwareUpgradeCallback<FirmwareUpgradeManager.State?> {
override fun onUpgradeStarted(controller: FirmwareUpgradeController?) {
val stateBean = DFUStateBean(context.getString(R.string.txt_upgrade_start))
mDfuStateLiveData.value = stateBean
LogUtil.e("DfuProgressListener -->> onDfuProcessStarting")
}
override fun onUpgradeCompleted() {
val stateBean = DFUStateBean(context.getString(R.string.txt_upgrade_success))
mDfuStateLiveData.value = stateBean
LogUtil.e("DfuProgressListener -->> onUpgradeCompleted")
val state = DFUStateBean(
context.getString(R.string.txt_reconnect_device), mDFUSuccessCode
)
mDfuStateLiveData.value = state
}
override fun onUploadProgressChanged(
bytesSent: Int, imageSize: Int, timestamp: Long
) {
val currentTimeMillis = System.currentTimeMillis()
//500ms更新发送数据不用频繁更新
if (currentTimeMillis - mUpdatePostTimeMillis > 500 || bytesSent == imageSize) {
val progress = (bytesSent / imageSize.toFloat())
mUpdatePostTimeMillis = currentTimeMillis
val dFUStateBean = DFUStateBean(
context.getString(R.string.txt_upgrading) + " ",
(progress * 100).toInt()
)
mDfuStateLiveData.value = dFUStateBean
}
}
override fun onUpgradeCanceled(state: FirmwareUpgradeManager.State?) {
LogUtil.e("DfuProgressListener -->> onUpgradeCanceled")
}
override fun onUpgradeFailed(
state: FirmwareUpgradeManager.State?, error: McuMgrException?
) {
LogUtil.e("DfuProgressListener -->> onError")
val stateBean =
DFUStateBean(context.getString(R.string.txt_upgrade_fail), mDFUFailCode)
mDfuStateLiveData.value = stateBean
}
override fun onStateChanged(
prevState: FirmwareUpgradeManager.State?,
newState: FirmwareUpgradeManager.State?
) {
LogUtil.e("DfuProgressListener -->> onStateChanged")
}
})
/*
* 常用设置:
* estimatedSwapTime: 设备重启后 MCUboot 交换镜像的大致时间
* windowCapacity: 窗口上传并发数;设备端支持时可以提速
* eraseAppSettings: 是否擦除应用 settings
*
* 这里先给一个稳妥配置:
* - estimatedSwapTime = 20s
* - windowCapacity = 1 (最稳,兼容性最好)
*/
val settings = FirmwareUpgradeManager.Settings.Builder().setEstimatedSwapTime(20000)
.setWindowCapacity(1).setMemoryAlignment(4).setEraseAppSettings(false).build()
dfuManager.setMode(FirmwareUpgradeManager.Mode.CONFIRM_ONLY)
try {
val file = File(path)
// 读取 zip并提取单镜像 .bin
val imageData = extractSingleBinFromZip(context, FileUtil.getUri(context, file))
dfuManager.start(imageData!!, settings)
} catch (e: Exception) {
val stateBean = DFUStateBean(context.getString(R.string.txt_upgrade_fail), mDFUFailCode)
mDfuStateLiveData.value = stateBean
LogUtil.e("G40DFU 失败:${e.message}")
}
}
/**
* 从 zip 中提取“单镜像”的 .bin 文件。
*
* 适用于:
* - g40_app.zip 里只有一个主要 app bin
*
* 如果 zip 中有多个 .bin当前实现会优先返回
* 1. 文件名包含 app 的
* 2. 否则返回第一个 .bin
*/
private fun extractSingleBinFromZip(context: Context, zipUri: Uri): ByteArray? {
context.contentResolver.openInputStream(zipUri).use { input ->
requireNotNull(input) { "无法打开 zip 文件: $zipUri" }
val bins = mutableListOf<Pair<String, ByteArray>>()
ZipInputStream(input.buffered()).use { zis ->
var entry: ZipEntry? = zis.nextEntry
while (entry != null) {
if (!entry.isDirectory) {
val name = entry.name
if (name.endsWith(".bin", ignoreCase = true)) {
bins.add(name to zis.readAllBytesCompat())
}
}
zis.closeEntry()
entry = zis.nextEntry
}
}
if (bins.isEmpty()) return null
if (bins.size == 1) return bins.first().second
// 多个 bin 时,优先选名字里带 app 的
bins.firstOrNull { it.first.contains("app", ignoreCase = true) }?.let {
return it.second
}
// 否则退化成第一个
return bins.first().second
}
}
private fun InputStream.readAllBytesCompat(): ByteArray {
val buffer = ByteArray(4096)
val bos = ByteArrayOutputStream()
while (true) {
val len = this.read(buffer)
if (len <= 0) break
bos.write(buffer, 0, len)
}
return bos.toByteArray()
}
fun startG30DFU(context: Context, device: BleDevice, path: String) {
viewModelScope.launch {
val dFUStateBean = DFUStateBean(context.getString(R.string.txt_dfu_model))
mDfuStateLiveData.value = dFUStateBean

View File

@@ -17,9 +17,18 @@
<com.abbidot.tracker.widget.TypefaceTextView
android:id="@+id/tv_dialog_dfu_new_title"
style="@style/my_TextView_style"
android:layout_marginTop="@dimen/dp_20"
android:text="@string/txt_about_version"
android:textColor="@color/block_color2"
android:textSize="@dimen/textSize12"
app:lineHeight="@dimen/textSize20"
app:typeface="@string/roboto_regular_font" />
<com.abbidot.tracker.widget.TypefaceTextView
style="@style/my_TextView_style"
android:layout_marginTop="@dimen/dp_18"
android:text="@string/txt_new_firmware_version"
android:text="@string/txt_phone_close_device"
android:textSize="@dimen/textSize14"
android:textStyle="bold"
app:lineHeight="@dimen/textSize20" />

View File

@@ -1078,5 +1078,6 @@
<string name="map_navigate_map_baidu">Baidu Map</string>
<string name="txt_time_line">Timeline</string>
<string name="txt_phone_close_device">Keep phone close to device</string>
</resources>

View File

@@ -48,7 +48,7 @@ dependencies {
api 'com.google.android.material:material:1.10.0'
//SharedPreferences的替代https://github.com/Tencent/MMKV
implementation 'com.tencent:mmkv-static:2.2.2'
implementation 'com.tencent:mmkv-static:2.4.0'
// Retrofit2
api 'com.squareup.retrofit2:retrofit:3.0.0'