diff --git a/app/build.gradle b/app/build.gradle index f7b3141..46117e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/com/abbidot/tracker/dialog/DFUNewDialog.kt b/app/src/main/java/com/abbidot/tracker/dialog/DFUNewDialog.kt index 3d424b2..bb3e39d 100644 --- a/app/src/main/java/com/abbidot/tracker/dialog/DFUNewDialog.kt +++ b/app/src/main/java/com/abbidot/tracker/dialog/DFUNewDialog.kt @@ -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() { diff --git a/app/src/main/java/com/abbidot/tracker/ui/activity/data/SharePetActivityActivity.kt b/app/src/main/java/com/abbidot/tracker/ui/activity/data/SharePetActivityActivity.kt index 66cb29e..506ff81 100644 --- a/app/src/main/java/com/abbidot/tracker/ui/activity/data/SharePetActivityActivity.kt +++ b/app/src/main/java/com/abbidot/tracker/ui/activity/data/SharePetActivityActivity.kt @@ -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) } diff --git a/app/src/main/java/com/abbidot/tracker/ui/fragment/device/HomeTrackFragment.kt b/app/src/main/java/com/abbidot/tracker/ui/fragment/device/HomeTrackFragment.kt index 8963a68..556c74c 100644 --- a/app/src/main/java/com/abbidot/tracker/ui/fragment/device/HomeTrackFragment.kt +++ b/app/src/main/java/com/abbidot/tracker/ui/fragment/device/HomeTrackFragment.kt @@ -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,9 +1080,13 @@ class HomeTrackFragment : getHomeV2Activity()?.apply { //保持屏幕常亮 window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - DfuServiceListenerHelper.registerProgressListener( - mContext, mDeviceDFUViewModel.mDfuProgressListener - ) + getPet(false)?.let { + if (it.deviceType == ConstantInt.Type1) { + DfuServiceListenerHelper.registerProgressListener( + mContext, mDeviceDFUViewModel.mDfuProgressListener + ) + } + } } } @@ -1078,9 +1094,13 @@ class HomeTrackFragment : getHomeV2Activity()?.apply { //清除屏幕常亮 window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - DfuServiceListenerHelper.unregisterProgressListener( - mContext, mDeviceDFUViewModel.mDfuProgressListener - ) + getPet(false)?.let { + if (it.deviceType == ConstantInt.Type1) { + DfuServiceListenerHelper.unregisterProgressListener( + mContext, mDeviceDFUViewModel.mDfuProgressListener + ) + } + } } } diff --git a/app/src/main/java/com/abbidot/tracker/util/FileUtil.kt b/app/src/main/java/com/abbidot/tracker/util/FileUtil.kt index a0ff3af..032aca0 100644 --- a/app/src/main/java/com/abbidot/tracker/util/FileUtil.kt +++ b/app/src/main/java/com/abbidot/tracker/util/FileUtil.kt @@ -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" + /** * 获取缓存目录 *

@@ -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) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/abbidot/tracker/util/ViewUtil.kt b/app/src/main/java/com/abbidot/tracker/util/ViewUtil.kt index 4491637..b5db77a 100644 --- a/app/src/main/java/com/abbidot/tracker/util/ViewUtil.kt +++ b/app/src/main/java/com/abbidot/tracker/util/ViewUtil.kt @@ -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) diff --git a/app/src/main/java/com/abbidot/tracker/vm/DeviceDFUViewModel.kt b/app/src/main/java/com/abbidot/tracker/vm/DeviceDFUViewModel.kt index 5b14e1f..655556e 100644 --- a/app/src/main/java/com/abbidot/tracker/vm/DeviceDFUViewModel.kt +++ b/app/src/main/java/com/abbidot/tracker/vm/DeviceDFUViewModel.kt @@ -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 { + 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>() + + 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 diff --git a/app/src/main/res/layout/dialog_dfu_new.xml b/app/src/main/res/layout/dialog_dfu_new.xml index be736d9..34ea466 100644 --- a/app/src/main/res/layout/dialog_dfu_new.xml +++ b/app/src/main/res/layout/dialog_dfu_new.xml @@ -17,9 +17,18 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e3fa9f..c7b733f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1078,5 +1078,6 @@ Baidu Map Timeline + Keep phone close to device \ No newline at end of file diff --git a/baselibrary/build.gradle b/baselibrary/build.gradle index 604f243..a476f85 100644 --- a/baselibrary/build.gradle +++ b/baselibrary/build.gradle @@ -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'