一、 說明:
本樣本是在上一個樣本(Android應用自身升級)的基礎上完成的。環境配置也同上一個demo一樣。只是增加了一些功能用來檢測Android系統中所有需要升級的應用程式,並從伺服器上下載更新。
二、 功能需求說明:
a) 檢測出Android系統中所有已安裝的應用(區別與Android系統內建的應用),並獲得每個應用的資訊。
b) 根據上一步獲得的系統中已安裝的應用資訊,通過http串連tomcat7伺服器,檢測每個應用的代碼版本並與當前應用的代碼版本進行比較,然後將需要更新的應用顯示 在ListView中。
c) 在ListView的onItemClick事件中提示是否下載並更新當前的應用。
d) 監聽系統的程式安裝和替換廣播,當收到系統中有程式安裝或替換時,重新整理當前的ListView視圖以及Activity的標題(Activity的標題用於提示當前需要更新的應用程式數量)。
三、 應用需求的前提假設:
a) 伺服器端的配置:
在tomcat7伺服器的根目錄下建立AppUpdate目錄,作為更新程式訪問的根目錄。
b) Apk檔案的下載路徑的標準假設:
在搜尋到系統所有使用者安裝的應用後,需要訪問伺服器中對應的每一個應用的version.json設定檔,進行代碼版本的比較。該搜尋路徑的標準規則我們假定為:
系統當前安裝的每一個應用對應的新版本所在的路徑為:
http://10.0.2.2:8080/AppUpdate/應用程式名稱/version.json,其中的應用程式名稱與當前安裝的應用程式名稱相對應,鑒於應用程式名稱中可能包含‘.’和空格(目前只發現這兩 種情況),我們將應用程式名稱中所有的‘.’和空格都以底線代替。例如:
我們搜到系統中當前已安裝的應用程式名稱為Sample Soft Keyboard的應用(包含空格),伺服器中對應的version.json檔案的遠程路徑為:
http://10.0.2.2:8080/AppUpdate/Sample_Soft_Keyboard/version.json,(Sample,Soft,Keyboard之間有底線),對應的新的Apk檔案的遠程路徑為:
http://10.0.2.2:8080/AppUpdate/Sample_Soft_Keyboard/Sample_Soft_Keyboard.apk
四、 流程以及關鍵技術說明:
1) 首先要掃描出系統所有的應用資訊,並過濾掉系統內建的應用,只儲存使用者安裝的應用。程式碼範例:
public void scanNeedUpdateApp(){List<PackageInfo> appPackage = getPackageManager().getInstalledPackages(0);//獲得系統所有應用的安裝包資訊 for(int i=0; i<appPackage.size(); i++){ PackageInfo packageInfo = appPackage.get(i); ApplicationInfo tmpAppInfo = new ApplicationInfo(); tmpAppInfo.appName = packageInfo.applicationInfo.loadLabel(getPackageManager()).toString(); tmpAppInfo.packageName = packageInfo.packageName; tmpAppInfo.versionName = packageInfo.versionName; tmpAppInfo.versionCode = packageInfo.versionCode; tmpAppInfo.appIcon = packageInfo.applicationInfo.loadIcon(getPackageManager()); //只添加非系統應用 if((packageInfo.applicationInfo.flags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0){ String appName = tmpAppInfo.appName.toString().replace('.', ' '); String VerJSONPath =webServicePath + appName +"/" +"version.json"; VerJSONPath = VerJSONPath.replaceAll(" ", "_");//拼接對應的version.json的訪問路徑 System.out.println(VerJSONPath); try { JSONObject jsonObj = GetNewVersionCode.getVersionJSON(VerJSONPath);if(jsonObj != null){Log.i("JSONnotNull","json 不為空白!");int newVersionCode = Integer.parseInt(jsonObj.getString("versionCode"));System.out.print("舊代碼版本");System.out.println(tmpAppInfo.versionCode);System.out.print("新代碼版本");System.out.println(newVersionCode);if(tmpAppInfo.versionCode < newVersionCode){tmpAppInfo.newVersionCode = newVersionCode;tmpAppInfo.newVersionName = jsonObj.getString("versionName");needUpdateList.add(tmpAppInfo);//將第三方的應用添加到列表中}}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();} } }}
說明: 獲得已安裝的應用程式資訊 可以通過getPackageManager()方法獲得
Public abstract PackageManager getPackageManager(), 然後將所有已安裝的包資訊放入List<PackageInfo>泛型中。方法如下
Public abstract List<PackageInfo> getInstalledPackages(int flags)
過濾第三方應用,所有的系統應用的flag標誌為FALG_SYSTEM(值為1),第三方的應用為flag標誌的值為0
2) 填充ListView的adapter資料集,並重新整理ListView,程式碼範例如下:
//填充ListViewpublic void fillListView(){ListView newUpdateListView = (ListView)findViewById(R.id.listview); appAdapter =new NeedUpdateListAdapter(this,needUpdateList);//填充adapter資料集 newUpdateListView.setDividerHeight(5); if(!appAdapter.isEmpty()){ int needUpdateCount = appAdapter.getCount(); setTitle("當前發現"+needUpdateCount + "款應用需要升級!"); newUpdateListView.setAdapter(appAdapter);//填充ListView newUpdateListView.setOnItemClickListener(this);//設定ListView中Item的單擊事件 }else{ setTitle("未發現需要升級的應用!"); }}
3) 註冊一個Handler用於在ListView重新整理之後,重新整理ListView所在的Activity的標題(用於顯示當前剩餘的需要更新的應用數量)程式碼範例:
//註冊refreshTitleHandler,用於在廣播接收中更新Activity的標題public void registerRefreshTitleHandler(){refreshTitleHandler = new Handler(){ public void handleMessage(Message msg){ switch(msg.what){ case 0:setTitle("當前發現"+appAdapter.getCount()+"款應用需要升級");break; } super.handleMessage(msg); } };}
4) 監聽系統中應用程式的安裝和替換廣播,並在廣播接收中重新整理ListView
說明:因為在新版本的應用下載更新時會進入系統內建的安裝程式,所以我們要監聽系統發送的有應用程式安裝或者替換的廣播
(”android.intent.action.PACKAGE_REPLACED”),並在廣播接收函數onReceiver()中處理重新整理ListView視圖,這就涉及到不同的類中更新UI介面的問題。在這裡我們的 解決方案是在接收到廣播重新整理ListView後向Activity的Handle發送一個訊息,用於更新Activity的標題。(Handle我們設定成全域靜態方便引用)。程式碼範例如下:
public class RefreshListViewBroadcastReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {// TODO Auto-generated method stubLog.i("Broadcast", "我是廣播");if(intent.getAction().equals("android.intent.action.PACKAGE_REPLACED")){//擷取被替換的包名,因為getDataString()返回的值包含了“package:“,所以要從第八的位置開始截取String replacedPackageName =intent.getDataString().substring(8);if(!context.getPackageName().equals(replacedPackageName)){Log.i("curPackName",context.getPackageName());System.out.print(replacedPackageName);Log.i("replacedPack",replacedPackageName);if(CheckUpdateAllActivity.appAdapter.remove(replacedPackageName)){//重新整理主Activity的TitltMessage message = new Message();message.what = 0;CheckUpdateAllActivity.refreshTitleHandler.sendMessage(message);}}}}}
進一步說明:廣播接收的註冊方式有兩種,一種是靜態註冊(在xml檔案中),另一種是動態註冊(在代碼中),二者的區別在於:生命週期不一樣。若以靜態方式註冊 的廣播,在第一次註冊之後就與其所在的應用程式無關了,即在應用程式退出後,系統仍然能接受到該廣播(若在應用程式退出後有該廣播發出)。以動態方式註冊的廣 播與程式有關,即程式退出後,就無法處理對應的廣播了。樣本中接受系統程式是否被替換的廣播會監聽自身的替換(重新RunAs),所以我們要屏蔽掉自身的替換,如
果不屏蔽的話在開始RunAs自己後(監聽了自己)會找不到程式中的appAdapter(因為程式還沒開始運行)而報出null 指標異常的現象。所以我們根據上下文(context)來 獲得當前應用的包名,並與此時被替換的包名(通過intent來獲得)作比較來過濾掉自身的監聽。
5) ListView的Item單擊事件。ListView中顯示的Item代表可升級的應用程式,若使用者單擊Item項後,彈出是否更新的對話方塊。
//ListView中Item的單擊事件public void onItemClick(AdapterView<?> parent, View view, int position, long id) {// TODO Auto-generated method stubApplicationInfo clickedItemInfo = (ApplicationInfo) appAdapter.getItem(position);curClickItemAppName = clickedItemInfo.appName;StringBuffer sb = new StringBuffer();Log.i("adapterVerName",clickedItemInfo.versionName);sb.append("目前的版本:" + "\n");sb.append("版本名稱:" + clickedItemInfo.versionName);sb.append("版本代碼:" + clickedItemInfo.versionCode + "\n");sb.append("發現新版本:" + "\n");sb.append("版本名稱:" + clickedItemInfo.newVersionName);sb.append("版本代碼:" + clickedItemInfo.newVersionCode + "\n");sb.append("是否更新?");Dialog dialog = new AlertDialog.Builder(CheckUpdateAllActivity.this).setTitle("軟體更新").setMessage(sb.toString()).setPositiveButton("立即更新", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubif(curClickItemAppName != ""){String appName = curClickItemAppName.replace('.', ' ');String loadUrl = webServicePath + appName +"/" +appName +".apk";loadUrl = loadUrl.replaceAll(" ", "_");Log.i("LoadUrl", loadUrl);downLoadApkFile(loadUrl, appName);}else{curClickItemAppName = "";}}}).setNegativeButton("暫不更新", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubcurClickItemAppName = "";}}).create();dialog.show();}
說明:被單擊的Item對應的應用程式資訊的擷取通過getItem(position)來完成,該函數返回的物件類型即Adapter中儲存的資料類型,強轉一下就可以了。之後就是按照 我們事先約定的新版本Apk的遠端存取路徑規則來拼接Apk的下載路徑了。
6) 下載新版本的apk檔案。這一步沒什麼好說的了,上一篇已經很詳細了(Android單個應用自身的升級)樣本如下:
//下載新的apk應用檔案protected void downLoadApkFile(final String url, final String appName) {// TODO Auto-generated method stubpBar = new ProgressDialog(CheckUpdateAllActivity.this);pBar.setTitle("正在下載");pBar.setMessage("請稍候...");pBar.setProgressStyle(ProgressDialog.STYLE_SPINNER);pBar.show();new Thread(){public void run(){HttpClient httpClient = new DefaultHttpClient();HttpGet httpGet = new HttpGet(url);HttpResponse httpResponse;try {httpResponse = httpClient.execute(httpGet);if(httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK){ HttpEntity httpEntity = httpResponse.getEntity();InputStream is = httpEntity.getContent();FileOutputStream fos = null;if(is !=null){File file = new File(Environment.getExternalStorageDirectory(),appName+".apk");fos = new FileOutputStream(file);byte[] buf = new byte[1024];int ch = -1;do{ch = is.read(buf);if(ch <= 0)break;fos.write(buf, 0, ch);}while(true);is.close();fos.close();haveDownLoad(appName + ".apk");}else{throw new RuntimeException("isStream is null");}}else if(httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND){//404未找到相應的檔案Looper.prepare();Toast toast = Toast.makeText(CheckUpdateAllActivity.this, "未找到對應的Apk檔案!", 1);pBar.cancel();toast.show();Looper.loop();}} catch (ClientProtocolException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}.start();}
說明:簡單的http通訊的步驟:1.建立HttpClient用戶端。2.建立訪問路徑HttpGet3.請求串連HttpResponse。通過http請求下載遠端的檔案時,有可能在所給的訪問路徑下 沒有需要的檔案,在這裡請求狀態通過HttpResponse.getStatusLine().getStatusCode()來獲得。常見的請求回應碼為200(請求成功),404(未找到對應的檔案)。另 外我們在下載子線程中去取消了下載進度條(下載完成後),涉及了在子線程中去更新UI,這樣違反了Android系統中UI單執行緒模式的原則,是不被允許的。所以要用
Looper或者Handle來處理。此處用了Looper。
7) 下載完成後提示是否安裝,當選擇取消後刪除sdcard中下載的Apk檔案。
//下載完成 關閉進度條,並提示是否安裝protected void haveDownLoad(final String fileName) {// TODO Auto-generated method stubpBar.cancel();//下載完成取消進度條haveDownHandler.post(new Runnable(){@Overridepublic void run() {// TODO Auto-generated method stubDialog installDialog = new AlertDialog.Builder(CheckUpdateAllActivity.this).setTitle("下載完成").setMessage("是否安裝新的應用").setPositiveButton("確定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubinstallNewApk(fileName);}}).setNegativeButton("取消", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubFile downLoadApk = new File(Environment.getExternalStorageDirectory(),fileName);if(downLoadApk.exists()){downLoadApk.delete();}}}).create();installDialog.show();}});}
8) 調用系統內建的安裝程式進行安裝。如果想採用靜默安裝,網上大俠們說要修改源碼才可以。
//安裝下載後的應用程式private void installNewApk(final String fileName) {// TODO Auto-generated method stubIntent intent = new Intent(Intent.ACTION_VIEW);intent.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory(),fileName)),"application/vnd.android.package-archive");startActivity(intent);}
9) JSON檔案解析類。該類中封裝了一個靜態方法getVersionJSON()用於獲得遠端version.json檔案資訊,返回物件類型為JSONObject,便於解析出每一個應用對應的 versionCode資訊。樣本如下:
public class GetNewVersionCode {public static JSONObject getVersionJSON(String VerJSONPath) throws ClientProtocolException, IOException, JSONException{StringBuilder VerJSON = new StringBuilder();HttpClient client = new DefaultHttpClient();HttpParams httpParams = client.getParams();HttpConnectionParams.setConnectionTimeout(httpParams, 3000);HttpConnectionParams.setSoTimeout(httpParams, 5000);HttpResponse response;response = client.execute(new HttpGet(VerJSONPath));//請求成功System.out.print("連結請求碼:");System.out.println(response.getStatusLine().getStatusCode());if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){Log.i("ConOK","連結成功");HttpEntity entity = response.getEntity();if(entity != null){BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8"), 8192);String line = null;while((line = reader.readLine()) != null){VerJSON.append(line+"\n");}reader.close();JSONArray verJSONArray = new JSONArray(VerJSON.toString());if(verJSONArray.length() > 0){JSONObject obj = verJSONArray.getJSONObject(0);return obj;}}Log.i("ContFail","擷取JSONObject失敗!");return null;}Log.i("ConFail","連結失敗!");return null;}}
10) 與ListView綁定的Adapter類,該類中最主要的方法是getCount()和getView()用於繪製ListView。在這裡重寫該類的構造方法,以便於在Adapter中儲存我們需要的類型 (ApplicationInfo)。樣本如下:
public class NeedUpdateListAdapter extends BaseAdapter {Context context;ArrayList<ApplicationInfo> needUpdateList = new ArrayList<ApplicationInfo>();public NeedUpdateListAdapter(Context context, ArrayList<ApplicationInfo> newNeedUpdateList){this.context = context;needUpdateList.clear();for(int i = 0; i<newNeedUpdateList.size(); i++){needUpdateList.add(newNeedUpdateList.get(i));}}@Overridepublic int getCount() {// TODO Auto-generated method stubreturn needUpdateList.size();}@Overridepublic Object getItem(int position) {// TODO Auto-generated method stubreturn needUpdateList.get(position);}@Overridepublic long getItemId(int position) {// TODO Auto-generated method stubreturn position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {// TODO Auto-generated method stubView newView = convertView;final ApplicationInfo appItem = needUpdateList.get(position);if(newView == null){LayoutInflater vi = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);newView = vi.inflate(R.layout.check_update_list_item, null);//newView.setClickable(true);加上此句ListViewi點擊無響應,不知道是為什麼}TextView appName = (TextView)newView.findViewById(R.id.appName);ImageView appIcon=(ImageView)newView.findViewById(R.id.icon);if(appName != null)appName.setText(appItem.appName);if(appIcon != null)appIcon.setImageDrawable(appItem.appIcon);return newView;}public boolean remove(String packageName){boolean flag = false;for(int i = 0; i < needUpdateList.size(); i++){if(needUpdateList.get(i).packageName.equals(packageName)){needUpdateList.remove(i);flag = true;Log.i("RemovePack", packageName);notifyDataSetChanged();}}if(flag){flag = false;return true;}return false;}public void removeAll(){needUpdateList.clear();notifyDataSetChanged();}}
說明:當每繪製一項時就會調用一次getView()方法,就會裝載一次我們建立的布局檔案一次(check_update_list_item.xml),在getView方法中設定布局檔案中的組件 (TextView和ImageView)就能得到我們想要的視圖。另外添加兩個方法remove()和removeAll()用於ListView的重新整理(當我們下載安裝新版本後)。其中 NotifyDataSetChanged()用於自動重新整理ListView。
11) 應用程式資訊類,主要用於儲存應用程式的一些資訊,如當前應用的程式名,包名,版本名稱,版本代碼,表徵圖,所搜尋到的新版本的版本名稱,新的版本代碼。
public class ApplicationInfo {public String appName = "";public String packageName = "";public String versionName = "";public String newVersionName = "";public int versionCode = 0;public int newVersionCode = 0;public Drawable appIcon = null;}
源碼下載串連:本篇源碼下載
下面是一些:
將系統所有已安裝的應用添加到ListView中
點擊更新某一個應用
下載新應用
下載完成提示安裝
進入系統安裝
更新完成後重新整理ListView和Activity標題
當在遠程伺服器中沒有找到對應的apk檔案則提示錯誤。