项目实战:OTA系统升级

对之前做的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。

时间: 2024-11-05 22:59:05

项目实战:OTA系统升级的相关文章

angularJs项目实战!01:模块划分和目录组织

近日来我有幸主导了一个典型的web app开发.该项目从产品层次来说是个典型的CRUD应用,故而我毫不犹豫地采用了grunt + boilerplate + angularjs + bootstrap + D3 + requirejs 的架构来实现它.angularjs早在去年6月份我就有所接触,将它应用在实验室项目的个别页面中,11月份在新浪的时候也将其推荐给了所在云事业部项目组.项目组老大程辉等人都是很有技术敏感性的人,大胆地采纳了我的建议,将之应用于原本使用dojo开发的项目前端模块上.然

Asp.Net Core 项目实战之权限管理系统(7) 组织机构、角色、用户权限

0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有 1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端 2 Asp.Net Core 项目实战之权限管理系统(2) 功能及实体设计 3 Asp.Net Core 项目实战之权限管理系统(3) 通过EntityFramework Core使用PostgreSQL 4 Asp.Net Core 项目实战之权限管理系统(4) 依赖注入.仓储.服务的多项目分层实现 5 Asp.Net Core 项目实

【ASP.NET实战教程】ASP.NET实战教程大集合,各种项目实战集合

[ASP.NET实战教程]ASP.NET实战教程大集合,各种项目实战集合,希望大家可以好好学习教程中,有的比较老了,但是一直很经典!!!!论坛中很多小伙伴说.net没有实战教程学习,所以小编连夜搜集整理出一些比较好的教程,望君好好珍惜,资源不易,且保持,且珍惜直接上资源截图: 下载地址[回复可见]:http://www.fu83.cn/thread-282-1-1.html 感觉文章写的好,一定要回复 推荐哦!!!

基于Hadoop离线大数据分析平台项目实战

基于Hadoop离线大数据分析平台项目实战  课程学习入口:http://www.xuetuwuyou.com/course/184 课程出自学途无忧网:http://www.xuetuwuyou.com 课程简介: 某购物电商网站数据分析平台,分为收集数据.数据分析和数据展示三大层面.其中数据分析主要依据大数据Hadoop生态系统常用组件进行处理,此项目真实的展现了大数据在企业中实际应用. 课程内容 (1)文件收集框架 Flume ①Flume 设计架构.原理(三大组件) ②Flume 初步使

Vue2+VueRouter2+webpack 构建项目实战(二):目录以及文件结构

通过上一篇博文<Vue2+VueRouter2+webpack 构建项目实战(一):准备工作>,我们已经新建好了一个基于vue+webpack的项目.本篇文章详细介绍下项目的结构. 项目目录以及文件结构 如图所示: 如上图所示,自动构建的vue项目的结构就是这样. 目录/文件 说明 build 这个是我们最终发布的时候会把代码发布在这里,在开发阶段,我们基本不用管. config 配置目录,默认配置没有问题,所以我们也不用管 node_modules 项目开发依赖的一些模块 src 开发目录(

【WEB API项目实战干货系列】- 导航篇(十足干货分享)

小分享:我有几张阿里云优惠券,用券购买或者升级阿里云相应产品最多可以优惠五折!领券地址:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=ohmepe03 在今天移动互联网的时代,作为攻城师的我们,谁不想着只写一套API就可以让我们的Web, Android APP, IOS APP, iPad APP, Hybired APP, H5 Web共用共同的逻辑呢? [WEB API项目实战干货系列]

Asp.Net Core 项目实战之权限管理系统(6) 功能管理

0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有 1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端 2 Asp.Net Core 项目实战之权限管理系统(2) 功能及实体设计 3 Asp.Net Core 项目实战之权限管理系统(3) 通过EntityFramework Core使用PostgreSQL 4 Asp.Net Core 项目实战之权限管理系统(4) 依赖注入.仓储.服务的多项目分层实现 5 Asp.Net Core 项目实

【无私分享:ASP.NET CORE 项目实战(第九章)】创建区域Areas,添加TagHelper

目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 在Asp.net Core VS2015中,我们发现还有很多不太简便的地方,比如右击添加视图,转到试图页等功能图不见了,虽然我们可以通过工具栏的自定义命令,把这两个右击菜单添加上,但是貌似是灰色的不能用. 其实,这样也好,通过手动创建,更让我们深刻的理解MVC以及路由之间的关系,很多人认为底层的东西是高大上的,比如一提到汇编,很多人感觉牛的不行不行的,其实术业有专攻,做程序的感觉搞核电的很牛,搞核电的同样也感觉做程序的很牛

【无私分享:ASP.NET CORE 项目实战(第二章)】添加EF上下文对象,添加接口、实现类以及无处不在的依赖注入(DI)

目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 上一章,我们介绍了安装和新建控制器.视图,这一章我们来创建个数据模型,并且添加接口和实现类. 添加EF上下文对象 按照我们以前的习惯,我们还是新建几个文件夹 Commons:存放帮助类 Domians:数据模型 Services:接口和实现类 我们在Domains文件夹下添加一个类库 Domain 我们新建一个类 ApplicationDbContext 继承 DbContext 1 using Microsoft.Ent