最近做了一个小功能,由于没有需求,只有一个一代的app services功能实现进行提示。由于更换了外包厂商,所以在升级版本上需要自行研发。然而一直从事底层开发的我,一脸懵逼,后来验证了,这根本就是n脸懵逼。
首先下载dex2jar对apk进行反编译,然后用jd-gui打开。经理说可以参考这个进行开发,我当时一看这不是很easy么,源码都有了,再编译一下就成。然而实在是太年轻,当时不明白这些变量的名字为什么这么奇怪,后来才知道,apk经过混淆,变量名都变了的。首先摸清楚业务逻辑就花了大力气,因为混淆过的app反解出来的代码,逻辑不完全一样,之可以知道大概做了些什么事,至于逻辑,需要重新组织。
至于需要使用的第三方dfu library也是不甚了解,这里将上面反解出来的classes-dex2jar.jar作为jar包,放入到android studio工程中的app/libs即可以完成dfu library库的导入。这里就可以使用第三方的接口进行升级操作了。首先新建一个android项目,然后如上导入jar包,然后开始创建reciever。
package com.包名;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import java.lang.reflect.Method;
import java.io.File;
public class ControllerOTARec extends BroadcastReceiver {
private static final String TAG = "ControllerRec";
private static final String ACTION_READY_FOR_OTA = "com.action.readyForOTA";
private static final String DEVICE_NAME = "DEVICE";
public static final String RETRY_OTA_UPGRADE = "retry.ota.upgrade";
private static final String BT_CONNECTED = "android.bluetooth.device.action.ACL_CONNECTED";
private static final String GET_HAND_VERSION ="com.test.gethandversion";
private static final File otaFile = new File(ControllerOTAService.mFile);
public static boolean mHaveDisable = false;
public static boolean mHaveStart = false;
private int mOldVersion;
private int mNewVersion;
private Method getStringMethod = null;
public static BluetoothDevice mDevice = null;
@Override
public void onReceive(final Context context, final Intent intent0)
{
String actionStr = intent0.getAction();
if (BT_CONNECTED.equals(actionStr))
{
mDevice = (BluetoothDevice)intent0.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
} else if(ACTION_READY_FOR_OTA.equals(actionStr)){
mNewVersion = getStringMethod("ro.build.controller.version", null);
int mOldVersion = Integer.parseInt(getStringMethod("sys.hand.appVersion","1"));
//catch if target version lower than present
if ((mNewVersion <= mOldVersion) || (mOldVersion == 0)) {
Log.d(TAG, "controller version is very new,not ota!");
return;
}
//catch if zipfile is not exist
if (!otaFile.exists()) {
Log.d(TAG, "ota file not exists");
return;
}
//begin to do the things
if (mDevice != null) {
final String deviceName = mDevice.getName();
final String deviceAddr = mDevice.getAddress();
int deviceBond = mDevice.getBondState();
Log.d(TAG, "deviceName:" + deviceName + ";and bond state is " + deviceBond + "; and device address is " + deviceAddr);
//通知蓝牙状态机马上进入ota模式,停止
if(!mHaveDisable) {
Log.d(TAG, "tell the bluetooth machine to stop for ota");
Intent intent = new Intent("bluetooth_ota_update");
intent.putExtra("start_bluetooth_state", false);
context.sendBroadcast(intent);
//delay 500ms for bt to stop
try {
Thread.sleep(500);
} catch (Exception e){
Log.d(TAG, "Sleep error");
}
}
if (!mHaveStart) {
removeBondStatus(mDevice);//这是清楚蓝牙配对,这是遇到的最大的一个问题
//储存设备地址和设备名信息
Log.d(TAG, "mHaveStart = true");
ControllerOTARec.mHaveStart = true;
SharedPreferences.Editor editor = context.getSharedPreferences("Controller_OTA", 0).edit();
editor.putString("deviceName", deviceName);
editor.putString("deviceAddress", deviceAddr);
editor.commit();
//告诉cs开始进入ota模式
Log.d(TAG,"will tell the controller service to stop for ota");
Intent intent1 = new Intent("com.startsignal");
context.sendBroadcast(intent1);
//准备工作完成,启动服务
Log.d(TAG, "start ota:" + mDevice.getAddress());
Intent intent2 = new Intent(context, ControllerOTAService.class);
intent2.putExtra("deviceName", deviceName);
intent2.putExtra("deviceAddress", deviceAddr);
context.startService(intent2);
}
}
} else if(RETRY_OTA_UPGRADE.equals(actionStr)){
Log.d(TAG, "wille retry for hand devices ota");
Intent intent = new Intent(context, ControllerOTAService.class);
String add = context.getSharedPreferences("Controller_OTA",0).getString("deviceAddress","default");
intent.putExtra("deviceName",DEVICE_NAME);
intent.putExtra("deviceAddress",add);
context.startService(intent);
}
}
//反射的方式访问系统属性
public String getStringMethod(final String key, final String def) {
try {
if (getStringMethod == null) {
getStringMethod = Class.forName("android.os.SystemProperties").getMethod("get", String.class, String.class);
}
return ((String) getStringMethod.invoke(null, key, def)).toString();
} catch (Exception e) {
return def;
}
}
//反射的方式访问蓝牙设备的隐藏接口removebond
public void removeBondStatus(BluetoothDevice btDevices){
boolean result = false;
try {
final Method removeBondStatus = btDevices.getClass().getMethod("removeBond");
if (removeBondStatus != null) {
result = (Boolean) removeBondStatus.invoke(btDevices);
Log.w(TAG, "removeBondStatus result is " + result);
while (btDevices.getBondState() != BluetoothDevice.BOND_NONE){
Thread.sleep(20);
}
}
} catch (final Exception e) {
Log.w(TAG, "An exception occurred while removing bond information", e);
}
}
}
然后创建服务
package com.包名;
import android.app.ProgressDialog;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
public class ControllerOTAService extends Service {
//定义安装包路径
public static final String ZIP_FILE_PATH = "/sdcard/Controller/";
//获取安装包文件名
public static final String mFile = getmPathFile();
//进度dialog
private ProgressDialog mDialog;
//通过路径找文件名
public static String getmPathFile(){
String otapackagepath = null;
File file = new File(ZIP_FILE_PATH);
File[] array = file.listFiles();
if(array[0].isFile()){
otapackagepath = ZIP_FILE_PATH +array[0].getName();
Log.d("ControllerOTAService", "file path is :" + otapackagepath);
} else{
Log.d("ControllerOTAService", "can not find the zip file");
}
return otapackagepath;
}
public ControllerOTAService() {
}
//注销监听,停止服务
private void stopService()
{
DfuServiceListenerHelper.unregisterProgressListener(this, mDfuProgressListener);//取消监听升级回调
stopSelf();
}
//启动升级流程
private void beginToUpdate(String devname, String addr)
{
Log.d("ControllerOTAService", "begin to update hand device by DFU,create dialog");
createDialog();
if (this.mDialog != null)
this.mDialog.show();
Log.d("ControllerOTAService", "devices name is " + devname +", address is " + addr + ", zip file path is " + mFile);
final DfuServiceInitiator dfuservice = new DfuServiceInitiator(addr)
.setDeviceName(devname)
.setKeepBond(true);//升级完成后保持连接
dfuservice.setZip(null, mFile);
dfuservice.start(this, DfuService.class);
DfuServiceListenerHelper.registerProgressListener(this, mDfuProgressListener);//注册监听
Log.d("ControllerOTAService", "dismiss dialog");
//取消dialog
new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(5000);
ControllerOTAService.this.mDialog.dismiss();
} catch(InterruptedException e){
e.printStackTrace();
}
}
}).start();
}
//创建dialog
private void createDialog()
{
Log.d("ControllerOTAService", "create dialog");
this.mDialog = new ProgressDialog(this);
this.mDialog.setMax(100);
this.mDialog.setProgress(0);
this.mDialog.setProgressStyle(1);
this.mDialog.setTitle("hand ota");
this.mDialog.setMessage("start update hand device,please use head mode");
this.mDialog.setIndeterminate(false);
this.mDialog.setCancelable(true);
this.mDialog.getWindow().setType(2003);
}
//startservice会调到这里
public int onStartCommand(Intent intent, int int1, int int2)
{
String devname = intent.getStringExtra("deviceName");
String addr = intent.getStringExtra("deviceAddress");
Log.d("ControllerOTAService", "deviceName:" + devname + " deviceAddress:" + addr);
beginToUpdate(devname, addr);
return Service.START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
//新建dfu升级流程监听(新建后自动弹出接口函数)
private final DfuProgressListener mDfuProgressListener = new DfuProgressListener() {
//device connecting
@Override
public void onDeviceConnecting(String deviceAddress) {
Log.d("ControllerOTAService", "onDeviceConnecting");
}
//devices begin to connect
@Override
public void onDeviceConnected(String deviceAddress) {
Log.d("ControllerOTAService", "onDeviceConnected");
}
//before ota process start
@Override
public void onDfuProcessStarting(String deviceAddress) {
Log.d("ControllerOTAService", "onDfuProcessStarting");
}
@Override
public void onDfuProcessStarted(String deviceAddress) {
Log.d("ControllerOTAService", "onDfuProcessStarted");
}
@Override
public void onEnablingDfuMode(String deviceAddress) {
Log.d("ControllerOTAService", "onEnablingDfuMode");
}
@Override
public void onProgressChanged(String deviceAddress, int percent, float speed, float avgSpeed, int currentPart, int partsTotal) {
Log.d("ControllerOTAService", "onProgressChanged");
}
@Override
public void onFirmwareValidating(String deviceAddress) {
Log.d("ControllerOTAService", "onFirmwareValidating");
}
@Override
public void onDeviceDisconnecting(String deviceAddress) {
Log.d("ControllerOTAService", "onDeviceDisconnecting");
}
@Override
public void onDeviceDisconnected(String deviceAddress) {
Log.d("ControllerOTAService", "onDeviceDisconnected");
}
//完成升级会调入到这里
@Override
public void onDfuCompleted(String deviceAddress) {
Log.d("ControllerOTAService", "onDfuCompleted");
//发出停止广播
Intent intent = new Intent("com.handota.stop");
ControllerOTAService.this.sendBroadcast(intent);
//恢复状态机
Intent intent1 = new Intent("bluetooth_ota_update");
intent1.putExtra("start_bluetooth_state", true);
ControllerOTAService.this.sendBroadcast(intent1);
//是否启动ota状态改为false
ControllerOTARec.mHaveStart = false;
//tell app the result
Intent intent2 = new Intent("hand_device_ota_process");
intent2.putExtra("handOTAProcess",1);
ControllerOTAService.this.sendBroadcast(intent2);
//DFU completed will delete the zip file
Log.d("ControllerOTAService", "begin to delete the update zipfile");
clearFile(ZIP_FILE_PATH);
Log.d("ControllerOTAService", "<<
ControllerOTAService.this.stopService();
}
@Override
public void onDfuAborted(String deviceAddress) {
Log.d("ControllerOTAService", "onDfuAborted");
Intent intent = new Intent("bluetooth_ota_update");
intent.putExtra("start_bluetooth_state", true);
ControllerOTAService.this.sendBroadcast(intent);
ControllerOTARec.mHaveStart = false;
//tell app the result
Intent intent2 = new Intent("hand_device_ota_process");
intent2.putExtra("handOTAProcess",2);
ControllerOTAService.this.sendBroadcast(intent2);
//DFU completed will delete the zip file
Log.d("ControllerOTAService", "begin to delete the update zipfile");
clearFile(ZIP_FILE_PATH);
ControllerOTAService.this.stopService();
}
@Override
public void onError(String deviceAddress, int error, int errorType, String message) {
Log.d("", "ERROR"+error+"message"+message);
Intent intent = new Intent("bluetooth_ota_update");
intent.putExtra("start_bluetooth_state", true);
ControllerOTAService.this.sendBroadcast(intent);
//tell app the result
Intent intent2 = new Intent("hand_device_ota_process");
intent2.putExtra("handOTAProcess",3);
ControllerOTAService.this.sendBroadcast(intent2);
ControllerOTARec.mHaveStart = false;
//DFU completed will delete the zip file
Log.d("ControllerOTAService", "begin to delete the update zipfile");
clearFile(ZIP_FILE_PATH);
ControllerOTAService.this.stopService();
}
};
//删除文件函数
public static void clearFile(String filePath){
File targetFile = new File(filePath);
File[] fileList = targetFile.listFiles();
Log.d("ControllerOTAService", "clearFile");
if(fileList[0].exists()){
fileList[0].delete();
Log.d("ControllerOTAService", "clearFile success");
}else {
Log.d("ControllerOTAService", "no file to delete");
}
}
}
这里还要创建dfuservices类
package com.包名;
import android.app.Activity;
import no.nordicsemi.android.dfu.DfuBaseService;
public class DfuService extends DfuBaseService
{
protected Class extends Activity> getNotificationTarget()
{
return NotificationActivity.class;
}
protected boolean isDebug()
{
return true;
}
}
创建NotificationActivity通知类
package com.包名;
import android.os.Bundle;
import android.app.Activity;
public class NotificationActivity extends Activity {
@Override
protected void onCreate(Bundle bundle)
{
super.onCreate(bundle);
finish();
}
}
配置xml
package="com包名">
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:name=".ControllerOTAService"
android:enabled="true"
android:exported="true" />
android:name=".DfuService"
android:enabled="true"
android:exported="true" />
配合蓝牙状态机升级流程大致如下
image.png
遇到的问题:
1.不同版本的官方nordic软件,升级方式也不一样,1.61.3版本连接状态下可以直接进行升级;2.6.0版本必须设备通过硬件进入ota模式才能正常升级,同时,高版本的dfu代码在18年7月已经对O代码做了适应,同时到O有些接口发生了变化,得确认是否在自己项目里面支持,不对应的话,需要下载对应的dfu版本的三方库;
2.读取设备版本这里遇到过一些问题,由于代码设计问题,设备当前版本在目前项目里面经常被重置为1,这里采取临时的系统属性来储存正确读取的当前版本;
3.由于一开始使用的是反编译的apk做jar包,有些库函数并不能被反解出来,导致内部逻辑无法获知,后来在github上下载了源码,就可以看到了;
4.遇到一个读取dfu版本失败的问题,追Src read.p_value ptr is NULL发现深陷蓝牙协议栈中的调用无法自拔,找不到根本原因,追Reading DFU version number发现是dfuservices要发起一此读取dfu版本信息的请求,然后gattserver收到这个请教,通过jni,向蓝牙协议栈发送了这个请求,然后返回的结果不对,status为1,正确的情况status为0,一开始蓝牙这一块的东西什么都不懂的时候有点摸不清头脑。为什么这里要去读这个,而且这个读取操作在升级过程中有两到三次。整个升级流程也是一头雾水。后来慢慢的梳理log,发现在gatt server向手柄发送了进入ota模式的命令后,然后再去读取手柄相关属性就会失败。
这里涉及到一个隐藏任务,设备在ota模式和非ota模式,属性值会发生变化,在手柄进入ota模式后,主机设备的蓝牙服务还会进行一次扫描,然后才会开始发送安装包进行升级的操作。然而新产品上为什么协议栈没有进行扫描,这一点需要一定蓝牙知识进行调查可能会更快解决。这里的临时做法,就是先进行设备配对信息清除,清楚完之后,设备就会进行扫描(可能跟产品里面的状态机有关系,在有配对信息的情况下,就不会进行扫描。)。即是这里的removeBondStatus(mDevice);这一块困住了很长时间,在宇神的一次操作下成功绕出去了,就得到了这个workaround。
DfuImpl : Reading DFU version number...
WCNSS_FILTER: ibs_recv_ibs_cmd: Received IBS_WAKE_IND: 0xFD
WCNSS_FILTER: ibs_recv_ibs_cmd: Writing IBS_WAKE_ACK
WCNSS_FILTER: do_write: IBS write: fc
bt_btif : btapp_gattc_req_data :Src read.p_value ptr is NULL for event 0x3
BluetoothGatt: onCharacteristicRead() - Device=D0:F0:63:61:D4:B1 handle=28 Status=1
DfuImpl : Characteristic read error: 1
DfuBaseService: Unable to read version number (error 1)
DfuBaseService: Disconnecting from the device...
BluetoothGatt: cancelOpen() - device: D0:F0:63:61:D4:B1
BtGatt.GattService: clientDisconnect() - address=D0:F0:63:61:D4:B1, connId=8
BtGatt.GattService: onDisconnected() - clientIf=8, connId=8, address=D0:F0:63:61:D4:B1
BluetoothGatt: onClientConnectionState() - status=0 clientIf=8 device=D0:F0:63:61:D4:B1
DfuBaseService: Disconnected from GATT server
DfuBaseService: Cleaning up...
BluetoothGatt: close()
BluetoothGatt: unregisterApp() - mClientIf=8
5.apk实现后,怎样将android studio集成到android source项目当中去也是因为不太熟悉这块有太多的问题,首先是sdk版本过高,将apk放入android项目失败的问题。这里去android studio中手动从O降级到N,第三方库的降级方法就是一个个去解决编译错误,找到对应的地版本对象进行替换。这里的25都是由27转换过来的。
apply plugin: 'com.android.application'
android {
signingConfigs {
config {
keyAlias 'controllerota'
keyPassword '123456'
storeFile file('/home/edward/bin/android-studio/keystore/key.jks')
storePassword '123456'
}
}
compileSdkVersion 25
defaultConfig {
applicationId "com.qiyi.controllerota1"
minSdkVersion 23
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:25.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation files('libs/DFUlibrary.jar')
}
6.关于系统ro属性的修改方法,在前一篇中记录了,之修改buildinfo.sh似乎是不奏效的。