对之前做的OTA系统升级项目做一个总结,包括4个部分:OTA系统的介绍,OTA包的制作,代码结构以及待改善的问题。
1. OTA介绍:
OTA
全称 over the air , OTA 升级是 Android 系统提供的标准软件升级方式。 它功能强大,提供了完全升级、增量升级模式,可以通过 SD 卡升级,也可以通过网络升级。在系统升级中,主要要做的就是在本地编译出完整包和差分包,放到服务器供用户选择。
2.
OTA包的制作:
完整包就是变异整个系统生成的OTA包,大小可能在几百M左右,但是它相对于OTA差分包来说更加的稳定,差分包体积比较小,升级比较方便,这个就看用户自己的选择。在linux下,完整包的生成方法是:make
clean; make; make otapackage; 之后会在out/target/product/torsby 生成一个zip包:vargo_torsby-ota.zip,这就是一个完整包可以直接拿去升级。同时,也在out/target/product/torsby/obj/PACKAGING/target_files_intermediates这个目录生成一个用来编译差分包的包,我们可以先重命名为old.zip,然后把第二次的包命名为new.zip, 接下来就可以来生成差分包,在 build/tools/releasetools
目录下有个ota_from_target_files的系统自带脚本,在linux下:./build/tools/releasetools/ota_from_target_files -i ~/old.zip ~/new.zip ~/update.zip,就会在当前目录生成update.zip的差分包
, 注意要把两个ota包放在当前目录执行这句命令。那么这里的update.zip差分包必须在old.zip这个系统上升级,才能到new.zip这个版本。
3. 项目结构:
整个项目的的功能是用户从设置进入系统升级后,会自动请求服务器检查是否有版本需要更新,如果没有则进入一个提示界面:您的系统已经是最新!如果不是最新系统,那么会在界面显示当前系统版本号和最新的系统版本号,以及更多里面的版本更新日志,用户点击立即安装就会进入一个版本列表,上面是服务器返回的所有可更细版本,选择一个版本就可以进行安装更新。
代码的核心类就是 IradarUpdateSystemFragment.class, 他继承自PreferenceFragment 是为了和设置Settings的UI设计保持同步,然后它归属于IradarUpdateSystemActivity,所以真正的代码实现就在这个fragment中。在onCreate()方法中,首先进行actionBar和Preference的初始化,紧接着使用公司自己封装的网络框架RequestManager
来请求服务器获得最新版本,在这里要注意一点: 在使用RequestManager请求服务器之前要先初始化:
Options opts = new Options.Builder().enableNet().enablePush().build(); VargoHelper.Init(this,opts);
我把这个初始化放在自定义的OTAApplication中,但是为了保险起见还在等初始化一段时间后在调用RequestManager的请求方法,于是用handler来控制一下定时执行,300ms后再请求。整个请求过程用json传递数据,请求参数是getDeviceData()来获得,主要是当前的版本号和当前机器的DeviceId, RequestManager的使用不再累述使用大小功能号来请求服务器,同时绑定ResponseListener来获得请求结果,在onReceived()中拿到Response就是我们要的结果,而在其他几个方法中就是一些错误返回等等,我们也可以给出一些UI提示。这里要说明的就是:
RequestManager已经被特殊处理,可以直接在UI线程中调用,并且可以直接在结果中更新UI,我没有用handler。 responseCode这个参数就是来区分是否有版本更新,如果有更新的话就会把结果传到updatePreference()来更新我们的界面。
在这里还有一个就是”了解更多“这个Preference,是用来看更新日志的:点击后跳转到UpdateLogActivity, 他是一个窗口Activity的实现,用WebView.loadUrl()的方法加载一份更新日志。
那么界面更新完成之后,假如当前有版本可以更新,用户点击现在安装就会弹出一个版本列表,在此之前会有一个WIFI和电量判断,我们规定是必须连接WIFI并且电量不低于50%的情况下才能继续更新,检查WIFI是否开启的代码:
// 检查当前网络是否为WIFI private Boolean isWifiNet(){ ConnectivityManager connectionManager = (ConnectivityManager)context.getSystemService(context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo(); if(networkInfo==null){ return false; } else{ String netState = networkInfo.getTypeName(); if(netState.equals("WIFI")){ return true; } else return false; } }
检查当前电量是否低于50%的代码:
// 直接获取现在的电量 private boolean getBattery(){ String s = ""; boolean isOk = false; try { fr = new FileReader(file); BufferedReader br = new BufferedReader(fr); if((s=br.readLine())!=null){ if(Integer.parseInt(s)>49){ isOk = true; Log.d(LOG_TAG, "当前电量是>>>>>>>>>"+s); } else{ isOk = false; } } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return isOk; }
在这里说明一点: 获取电量的常规方法是绑定一个广播,在电量变化时会接到ACTION_BATTERY_CHANGED 的系统广播,但这个存在的问题就是没有即时性,用户点击按钮后就应该获取到广播,因此采用上面的方法:在android系统中 这个文件"/sys/class/power_supply/battery/capacity" 其实就存放了当前电量,直接new File()把它读出来!
当电量和WIFI都满足条件后就可以开始下载安装系统版本了。下载这一块我采用系统自带的DownloadManager框架,android原生系统就是用的这个框架,有已经封装好的通知栏,功能还是很完善,下面详述DownloadManager在本项目中的用法:
DownloadManager是一个下载管理类,在OnCreate()中获取:.
manger=(DownloadManager)getActivity().getSystemService(context.DOWNLOAD_SERVICE) ;
DownloadManager.Request是一个下载任务,我们可以对它具体设定一些下载参数,比如:通知栏是否可见,网络限定,下载目录等等,设定好了之后调用DownloadManager.enqueue方法就开始下载,它返回一个ID就是当前下载任务的ID ,由此可知它支持多任务!在后面的代码可以看到,把这个ID设为全局变量,通过这个ID能查询到当前下载进度:
DownloadManager.Request down=new DownloadManager.Request (Uri.parse(url)); //down.addRequestHeader(header, value); down.setNotificationVisibility(android.app.DownloadManager.Request.VISIBILITY_VISIBLE); down.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); down.setTitle(getActivity().getResources().getString(R.string.down_title)); if(getFile(OtaConstant.romName).exists()){ boolean isDelete = getFile(OtaConstant.romName).delete(); Log.d(LOG_TAG, "原来的update.zip删除?>>>>>>>"+ isDelete); } down.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, OtaConstant.romName); downloadId = manger.enqueue(down); SharedPreferences sharedPreferences = getActivity().getSharedPreferences("ota", Context.MODE_PRIVATE); //私有数据 Editor editor = sharedPreferences.edit();//获取编辑器 editor.putLong("downloadId", downloadId); editor.commit();//提交修改
到这里其实就已经开始下载了,但要更新我们的进度条,监听某个ID的下载进度,是使用ContentResolver()监听一个系统的URI, 记得要在onDestroy()解绑!:
context.getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true,downloadObserver);
// 监听下载进度 class DownloadChangeObserver extends ContentObserver { public DownloadChangeObserver(){ super(handler); } @Override public void onChange(boolean selfChange) { updateView(); Log.d(LOG_TAG, "监听到 正在下载"); } } public void updateView() { SharedPreferences sharedPreferences = getActivity().getSharedPreferences("ota", Context.MODE_PRIVATE); downloadId = sharedPreferences.getLong("downloadId", -1L); Log.d(LOG_TAG, "重新进入OTA 继续下载id>>>>>>>"+downloadId); if(downloadId!=-1){ int[] bytesAndStatus = getBytesAndStatus(downloadId); handler.sendMessage(handler.obtainMessage(0,bytesAndStatus)); } } public int[] getBytesAndStatus(long downloadId) { int[] bytesAndStatus = new int[] { -1, -1, 0 ,0}; DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId); Cursor c = null; try { c = manger.query(query); if (c != null && c.moveToFirst()) { bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)); bytesAndStatus[3] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_REASON)); Log.d(LOG_TAG, " COLUMN_STATUS>>>>>>>>>"+bytesAndStatus[2] + "COLUMN_REASON>>>>>>>>> "+bytesAndStatus[3] ); } } finally { if (c != null) { c.close(); } } return bytesAndStatus; }
在上面的代码中可以看出在下载过程中把一些下载信息比如:下载开始,暂停,正在下载... 当前下载大小 ,总大小等等,用handler发出来更新UI. 这里有个问题,就是我不太清楚下载状态 DownloadManager.COLUMN_STATUS 中的暂停和失败会在什么情况触发,有时下载中我直接断网关机,再开机他能接着下载,有时候他直接提示下载失败,不会再接着下载,我到现在还没能完全控制。还有一个,它是支持断点续传的,但是我没找到如何手动暂停,只能在一些情况它自己暂停!
还要处理的就是后台下载中,哪怕是关机重启,我重新进入软件,进度条要能够接着当前下载来显示。于是我在开始下载时马上用SharedPreferences保存了ID, 并且在开始一个下载前先清空了ID,在下次进入后就判断这个ID是否有值:有就说明是正在下载,没有数值(其实代码给的-1默认值)就是一次新下载。整个下载过程就是这样了,在实践中基本都能够顺利下载。完成后会在SD卡的DOWMLOAD文件夹中看到update.zip这个更新包,安装方法:
// 执行安装更新包 private void installRom(){ // 比对MD5 判断rom的完整性 Log.d(LOG_TAG, "下载的文件的md5是>>>>>>>>> "+Utils.getFileMD5(getFile(OtaConstant.romName))); SharedPreferences sp = context.getSharedPreferences("ota", 0); String romMd5 = sp.getString("md5", " "); Log.d(LOG_TAG, "服务器给定的md5是>>>>>>>>> "+romMd5); if(Utils.getFileMD5(getFile(OtaConstant.romName)).equalsIgnoreCase(romMd5)){ try { RecoverySystem.installPackage(context, getFile(OtaConstant.romName)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else{ Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.system_broken), 1).show(); getActivity().finish(); } if(startInstall!=null){ startInstall.dismiss(); } }
上面代码,在安装前其实有一个安装包MD5的校验,下载来的安装包必须和服务器给我的信息一致,才能安装否则就说明文件损坏,需要重新下载!安装OTA包的核心就这一句: RecoverySystem.installPackage(context, getFile(OtaConstant.romName)); 它的过程没有详细了解,过程也是不可控的,安装中如果出现问题应该就是安装包本身的问题,还有一个常见错误就是:提示找不到SD卡挂载路径!这个一般是系统recovery的问题,重新刷个recovery,关于这个详细的执行过程没有深入了解。
OTA还有问题就是:我们不能指望用户在当前界面等待下载,完成安装,这个安装很有可能是在后台完成的,因此必须把这个安装过程放在service里面,就是代码中的InstallService这个类。但是后面的实践告诉我,仅仅把安装放在service是不够的,因为在下载过程中有可能失败,必须给出失败提示,所以整个下载进度过程都应该放在service里面, 这个是当时没有考虑清楚,那么其实fragment里面其实是不需要管下载和安装的,这部分代码还没有精简。还有一个就是手工安装和自动安装,项目要求是下载完成后15开始自动安装,或者用户直接点击马上安装,这部分用handler控制一下吧,加一个FLAG标注一下手动自动,不要让两个安装都进行了.下面是下载失败的自定义通知栏的实现:
<pre name="code" class="java">private void shwoNotify(Context context){ Log.d(LOG_TAG, "开始show 一个状态栏"); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); RemoteViews view_custom = new RemoteViews(getPackageName(), R.layout.view_custom); view_custom.setImageViewResource(R.id.custom_icon, R.drawable.update); view_custom.setTextViewText(R.id.tv_custom_title, context.getResources().getString(R.string.service_prompt)); view_custom.setTextViewText(R.id.tv_custom_content, context.getResources().getString(R.string.system_broken)); mBuilder = new Builder(this); mBuilder.setContent(view_custom) .setWhen(System.currentTimeMillis()) .setContentIntent(getDefalutIntent(Notification.FLAG_AUTO_CANCEL)) .setWhen(System.currentTimeMillis())// .setTicker(context.getResources().getString(R.string.service_prompt)) .setPriority(Notification.PRIORITY_DEFAULT)// .setOngoing(false)// .setSmallIcon(R.drawable.update); Notification notify = mBuilder.build(); notify.contentView = view_custom; mNotificationManager.notify(notifyId, notify); Log.d(LOG_TAG, "结束show 一个状态栏"); } public PendingIntent getDefalutIntent(int flags){ Intent intent = new Intent(); intent.setClassName("cn.com.vargo.ota", "cn.com.vargo.ota.IradarUpdateSystemActivity"); PendingIntent pendingIntent= PendingIntent.getActivity(this, 1, intent, flags); return pendingIntent; }
最后附上Utils里面两个工具,一个是获取文件的MD5,一个是获取机器的MAC地址:
public class Utils { /** * 计算文件的MD5 * @param file * @return */ public static String getFileMD5(File file) { if (!file.isFile()) { return ""; } MessageDigest digest = null; FileInputStream in = null; byte buffer[] = new byte[1024]; int len; try { digest = MessageDigest.getInstance("MD5"); in = new FileInputStream(file); while ((len = in.read(buffer, 0, 1024)) != -1) { digest.update(buffer, 0, len); } in.close(); } catch (Exception e) { e.printStackTrace(); return null; } BigInteger bigInt = new BigInteger(1, digest.digest()); return bigInt.toString(16).toUpperCase(); } /** * // 获取mac地址: * @param context * @return */ public static String getMacAddress(Context context) { String macAddress = "000000000000"; try { WifiManager wifiMgr = (WifiManager) context .getSystemService(Context.WIFI_SERVICE); WifiInfo info = (null == wifiMgr ? null : wifiMgr .getConnectionInfo()); if (null != info) { if (!TextUtils.isEmpty(info.getMacAddress())) macAddress = info.getMacAddress().replace(":", ""); else return macAddress; } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return macAddress; } return macAddress; } }
4. 待解决:
从上面描述总结看出,还有几个问题待完善:
第一个,是DownloadManager支持断点续传,但是不知如何手动暂停。它具体在神马情况下给出暂停或失败状态,还不是很确定
第二个,就是OTA包的安装过程没有深入去研究,以及下载和安装应该全部移出fragment 只放在service。