项目实战: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。



  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值