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