Android(安卓)自动检测版本并升级
如何在应用启动时检查版本更新对应用进行升级,解决的方案有多种。本文将阐述利用Android 自带的DownloadManager进行下载apk文件,已经下载成功后自动安装。DownloadManager在后台下载文件时会有进度条的,所以省去了开发进度条的麻烦。
思路:在AndroidManifest.xml文件中定义了versionCode以及versionName,称为旧的,如果使用AndroidStudio开发的话,这些东西都移到了build.gradle文件中。而在服务器端,也要配置(通常在配置文件中)一个版本号以及对应的版本名称,称为最新的,此外还必须配置一个apk文件的下载路径Url,当然还可以配置一些升级或更新的内容。然后在应用启动时,从服务器获取该最新的信息,可以用一个对象封装这些信息。然后对比新旧版本号,若新版本号大于旧版本号,则弹出对话框让用户进行确认升级,确认后用DownloadMananger进行下载获取的Url。DownloadManager在下载完成后会发一个通知给android系统,所以需要定义一个BroadcastReceiver来接收该通知,在onReceive()方法中将该apk进行安装。这样就实现了该功能。
有些设备是没有SD卡的,所以需要妥善设置apk的下载存放位置。
1.为了完成上述工作,定义如下一个辅助类.
ApplicationHelper.java
package com.jykj.departure.util;import android.app.ActivityManager;import android.app.ActivityManager.RunningTaskInfo;import android.content.Context;import android.content.Intent;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import android.content.pm.PackageManager.NameNotFoundException;import android.net.ConnectivityManager;import android.net.NetworkInfo;import android.net.Uri;import android.os.Environment;import android.provider.Settings;import android.telephony.TelephonyManager;import java.io.File;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.Date;import java.util.Locale;public class ApplicationHelper { public static final CharSequence TOAST_NO_NET = "很遗憾,木有网络"; //判断某个文件是否为apk文件 public static boolean isApkFile(String fileName){ fileName = fileName.toLowerCase(); int idx = fileName.lastIndexOf(".apk"); return idx >=0; } /** * 获得AndroidManifest.xml中的versionCode * * @param context 上下文 * @return int */ public static int getLocalVersionCode(Context context) { try { PackageInfo pi = context.getPackageManager().getPackageInfo( context.getPackageName(), 0); return pi.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 0; } /** * 获得AndroidManifest.xml中的versionName * * @param context 上下文 * @return str */ public static String getLocalVersionName(Context context) { try { PackageInfo pi = context.getPackageManager().getPackageInfo( context.getPackageName(), 0); return pi.versionName; } catch (NameNotFoundException e) { e.printStackTrace(); } return null; } /** * 安装apk文件 * * @param context 上下文 * @param uri uri */ public static void installApk(Context context, Uri uri) { Intent installIntent = new Intent(Intent.ACTION_VIEW); installIntent.setDataAndType(uri, "application/vnd.android.package-archive"); installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(installIntent); } /** * 卸载apk程序 * * @param context 上下文 * @param uri uri */ public static void uninstallApk(Context context, Uri uri, String packageName) { Uri packageURI = Uri.parse("package:" + packageName); Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI); context.startActivity(uninstallIntent); } /** * 判断是否有网络连接 * * @param context 上下文 * @return bool */ public static boolean isNetworkConnected(Context context) { if (context != null) { ConnectivityManager mConnectivityManager = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo mNetworkInfo = mConnectivityManager .getActiveNetworkInfo(); if (mNetworkInfo != null && mNetworkInfo.isConnected() && mNetworkInfo.getState() == NetworkInfo.State.CONNECTED) { return mNetworkInfo.isAvailable(); } } return false; }}
2.从服务器获取的信息的封装类
Apkinfo.java
package com.sudoku.jack.sudoku.model;import java.io.Serializable;public class Apkinfo implements Serializable{ private String versionName; private int versionCode; private String apkName; private String upgradeContent; private String apkURL; public String getVersionName() { return versionName; } public void setVersionName(String versionName) { this.versionName = versionName; } public int getVersionCode() { return versionCode; } public void setVersionCode(int versionCode) { this.versionCode = versionCode; } /** * @return 如 Express-v1.0.3.apk */ public String getApkName() { return apkName; } public void setApkName(String apkName) { this.apkName = apkName; } public String getUpgradeContent() { return upgradeContent; } public void setUpgradeContent(String upgradeContent) { this.upgradeContent = upgradeContent; } /** * @return apk下载地址URL */ public String getApkURL() { return apkURL; } public void setApkURL(String apkURL) { this.apkURL = apkURL; }}
3.将服务器端的信息JSON格式解析为ApkInfo对象
ParseJSON.java
import com.sudoku.jack.sudoku.model.Apkinfo;import org.json.JSONException;import org.json.JSONObject;public class ParseJSON { public static final String RetCode = "RetCode"; public static final String RetMsg = "RetMsg"; public static final String ResultObj = "ResultObj"; // 状态码 public static final String CODE_SUCCESS = "0000"; public static Apkinfo parseToApkinfo(@NonNull JSONObject jo) throws JSONException { String url = jo.getString("apkURL"); if(url==null||url.isEmpty()){ return null; } Apkinfo a = new Apkinfo(); a.setVersionCode(jo.getInt("apkVer")); a.setApkURL(url); Log.e("TAG","ParseToApkInfo version code:"+ a.getVersionCode()); if(!jo.isNull("apkUpgradeContent")) a.setUpgradeContent(jo.getString("apkUpgradeContent")); else a.setUpgradeContent("请务必更新以确保功能使用稳定!"); // 解析apkURL int index = url.lastIndexOf("/"); String name = url.substring(index + 1); Log.e("TAG","ParseToApkInfo URL:" + url); a.setApkName(name);// Express-v1.0.3.apk int idxStart = name.lastIndexOf("v"); if(idxStart == -1) idxStart = name.lastIndexOf("V"); if(idxStart ==-1) idxStart = name.lastIndexOf("-"); if(idxStart ==-1) idxStart = name.lastIndexOf("_"); int idxEnd = name.lastIndexOf(".apk"); if(idxEnd==-1) idxEnd = name.lastIndexOf("."); try{ String version = name.substring(idxStart+1, idxEnd); Log.e("TAG","ParseToApkInfo version:"+ version); a.setVersionName(version); }catch (Exception e){ e.printStackTrace(); } return a; }}
4.从服务器获取信息的任务类
GetNewestVersionTask.java
package com.sudoku.jack.sudoku.task;import android.app.AlertDialog;import android.app.ProgressDialog;import android.content.Context;import android.content.DialogInterface;import android.net.Uri;import android.os.AsyncTask;import android.widget.Toast;import com.sudoku.jack.sudoku.model.Apkinfo;import com.sudoku.jack.sudoku.util.Helper;import com.sudoku.jack.sudoku.util.HttpHelper;import com.sudoku.jack.sudoku.util.ParseJSON;import org.json.JSONException;import org.json.JSONObject;/** * 获取最新版本的apk信息 的AsyncTask * * @author Administrator */public class GetNewestVersionTask extends AsyncTask { private ProgressDialog pd; private Context mContext; private boolean mShowProgress; private String exceptionInfo;//后台任务处理出现的异常信息 /** * 构造方法:获取最新版本的apk信息 的AsyncTask * * @param context 启动任务的activity * @param showProgress 是否显示进度条 */ public GetNewestVersionTask(Context context, boolean showProgress) { mContext = context; mShowProgress = showProgress; } @Override protected void onPreExecute() { pd = new ProgressDialog(mContext); if (mShowProgress) { pd.setProgressStyle(ProgressDialog.STYLE_SPINNER); pd.setTitle("检查更新"); pd.setMessage("获取数据中,请稍后..."); pd.show(); } } @Override protected String doInBackground(Void... params) { /* String inputParams = WebServiceHelper.getInputParam(WebServiceHelper .getNewestVersionParams(Helper.getIMEI(mContext))); try { return WebServiceHelper.getService(inputParams, WebServiceHelper.S_getNewestVersion); } catch (IOException | XmlPullParserException e) { e.printStackTrace(); exceptionInfo = "获取最新版本时出现异常:" + e.getMessage(); return null; } */ // 以下是用来测试的,实际使用时应使用上面的代码来获取信息,可以是http请求或者Web Service String url = "http://ftp.daumkakao.com/eclipse/tools/pdt/downloads/pdt-Update-3.6.0.201509151953.zip"; JSONObject obj = new JSONObject(); try { obj.put("RetCode", "0000"); obj.put("RetMsg", ""); JSONObject apk = new JSONObject(); apk.put("apkVer", 2); apk.put("apkURL", url); obj.put("ResultObj", apk); } catch (JSONException e) { e.printStackTrace(); } return obj.toString(); } @Override protected void onPostExecute(String result) { if (mShowProgress&&pd!=null) pd.cancel(); if (result == null) { Toast.makeText(mContext, exceptionInfo, Toast.LENGTH_SHORT).show(); return; } try { JSONObject obj = new JSONObject(result); if (ParseJSON.RESULT_SUCCESS.equals(obj.getString(ParseJSON.RetCode))) { JSONObject jo = obj.getJSONObject(ParseJSON.ResultObj); final Apkinfo apkinfo = ParseJSON.parseToApkinfo(jo); //CacheHelper.setCacheApkinfo(apkinfo); if(apkinfo==null || ApplicationHelper.getLocalVersionCode(mContext)>=apkinfo.getVersionCode()) { if (mShowProgress) { Toast.makeText(mContext, "已经是最新版本!", Toast.LENGTH_SHORT).show(); } }else{ String message = "版本升级:"+ ApplicationHelper.getLocalVersionName(mContext) + "->" + apkinfo.getVersionName() + "\n"; message += apkinfo.getUpgradeContent(); new AlertDialog.Builder(mContext).setTitle("检测到新版本"). setMessage(message).setPositiveButton("立即更新", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //检查是否已经下载过,直接安装 Uri uri = ApplicationHelper.checkFileExist(apkinfo.getApkName()); if (uri != null) { if(ApplicationHelper.isApkFile(apkinfo.getApkName())) ApplicationHelper.installApk(mContext, uri); } else { HttpHelper.download(mContext, apkinfo.getApkName(), Uri.parse(apkinfo.getApkURL())); } } }).setNegativeButton("下次再说", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }).create().show(); } } else { Toast.makeText(mContext, obj.getString(ParseJSON.RetMsg), Toast.LENGTH_SHORT).show(); } } catch (JSONException e) { e.printStackTrace(); Toast.makeText(mContext, e.getMessage(), Toast.LENGTH_SHORT).show(); } }}
5.下载完成后需要一个广播接收器,用来自动安装文件
DownloadCompleteReceiver.java
import android.app.DownloadManager;import android.content.BroadcastReceiver;import android.content.ContentResolver;import android.content.Context;import android.content.Intent;import android.database.Cursor;import android.net.Uri;import android.util.Log;import com.jykj.departure.util.ApplicationHelper;import java.io.File;/** * DownloadManager下载完后 ,DOWNLOAD_SERVICE 会发送广播提示下载完成 */public class DownloadCompleteReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) { /** * The download manager is a system service that handles * long-running HTTP downloads. */ DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);// 从下载服务获取下载管理器 DownloadManager.Query query = new DownloadManager.Query(); long downid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1); //downloadManager.getUriForDownloadedFile(downid); query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);// 设置过滤状态:成功 query.setFilterById(downid); Cursor c = downloadManager.query(query);// 查询以前下载过的‘成功文件’ if (c!=null&&c.moveToFirst()) {// 移动到最新下载的文件 String fileUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); Log.e("TAG","COLUMN_LOCAL_URI:"+fileUri); int fileNameIdx = c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME); String fileName = c.getString(fileNameIdx); File file = new File(fileName); Log.e("TAG", "======文件名称=====:" + fileName); if(ApplicationHelper.isApkFile(fileName)) ApplicationHelper.installApk(context, Uri.parse("file://"+fileName)); c.close(); } // 安装 } }}
6.最后别忘了在AndroidManifest.xml文件中加上如下配置
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <receiver android:name=".receiver.DownloadBroadcastReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.DOWNLOAD_COMPLETE" /> intent-filter> <intent-filter> <action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED" /> intent-filter> receiver>
注意:android:exported=”true” ,如果设置为false,该接收器将接收不到广播。
两个权限也必不可少。
7.利用DownloadManager下载apk文件
HttpHelper.java
package com.jykj.departure.util;import android.app.DownloadManager;import android.app.DownloadManager.Request;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.net.Uri;import android.os.Environment;import android.util.Log;import android.widget.Toast;import java.io.BufferedReader;import java.io.DataOutputStream;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.URL;import java.net.URLEncoder;import java.util.Map;import java.util.Map.Entry;/** * 通用的与Http相关的辅助类 */public class HttpHelper { private static String JSESSIONID; //定义一个静态的字段,保存sessionID 例如JSESSIONID=AD5F5C9EEB16C71EC3725DBF209F6178 /** * 利用android 下载管理器 下载文件 * * @param context 上下文 * @param fileName 文件名 * @param uri * http或https * @return 如果已经下载过返回-1,否则返回Download ID */ public static long download(Context context, String fileName, Uri uri) { Toast.makeText(context, "已经转入后台下载!", Toast.LENGTH_SHORT).show(); DownloadManager manager = (DownloadManager) context .getSystemService(Context.DOWNLOAD_SERVICE); // 初始化下载管理器 Request request = new Request(uri);// 创建请求 request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);// 设置允许使用的网络类型,这里是移动网络和wifi都可以 request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); request.setAllowedOverRoaming(false);// 漫游 /*request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, fileName);*/ //判断是否有SD卡,如果有设置路径,没有则使用默认路径 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName); /* File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),fileName); request.setDestinationUri(Uri.fromFile(file));*/ return manager.enqueue(request);// 将下载请求放入队列 } /** * http POST 请求 * * @param urlString * http请求 * @param content * 请求正文,正文内容其实跟get的URL中'?'后的参数字符串一致 * @return str * @throws IOException IOException */ public static String requestPost(String urlString, String content) throws IOException { System.out.println("session:"+JSESSIONID+",URL:"+urlString+"?"+content); URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(6 * 1000); // Read from the connection. Default is true. conn.setDoInput(true); // Output to the connection. Default is // false, set to true because post // method must write something to the // connection // 设置是否向connection输出,因为这个是post请求,参数要放在 // http正文内,因此需要设为true conn.setDoOutput(true); // Post cannot use caches conn.setUseCaches(false); // Set the post method. Default is GET conn.setRequestMethod("POST"); // This method takes effects to every instances of this class. URLConnection.setFollowRedirects是static函数,作用于所有的URLConnection对象。 // connection.setFollowRedirects(true); // This methods only takes effacts to this instance. conn.setInstanceFollowRedirects(true); // Set the content type to urlencoded,because we will write some URL-encoded content to the // connection. Settings above must be set before connect! // 配置本次连接的Content-type,配置为application/x-www-form-urlencoded的 // 意思是正文是urlencoded编码过的form参数,下面我们可以看到我们对正文内容使用URLEncoder.encode // 进行编码 conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); if(null != JSESSIONID){ conn.setRequestProperty("Cookie",JSESSIONID); } // 连接,从postUrl.openConnection()至此的配置必须要在connect之前完成, // 要注意的是connection.getOutputStream会隐含的进行connect。 conn.connect(); DataOutputStream out = new DataOutputStream(conn.getOutputStream()); // The URL-encoded contend DataOutputStream.writeBytes将字符串中的16位的unicode字符以8位的字符形式写道流里面 out.writeBytes(content); out.flush(); out.close(); // flush and close //System.out.println(conn.getResponseMessage()); BufferedReader reader = new BufferedReader(new InputStreamReader( conn.getInputStream(), "utf-8"));// 设置编码,否则中文乱码 String line; StringBuilder sb = new StringBuilder(); while ((line = reader.readLine()) != null) { sb.append(line); } String cookieval = conn.getHeaderField("set-cookie"); System.out.println("set-cookie:"+cookieval); if(cookieval != null) { JSESSIONID = cookieval.substring(0, cookieval.indexOf(";")); } reader.close(); conn.disconnect(); return sb.toString(); } /** * http Post请求 ,参考 {@link #requestPost(String,String) requestPost(String,String)} * * @param urlString * 请求url * @param map * 封装了正文内容的map * @return str * @throws IOException IOException * */ public static String requestPost(String urlString, Map map) throws IOException { if(map==null) return requestPost(urlString); // 正文,正文内容其实跟get的URL中'?'后的参数字符串一致 // String content = // "key=j0r53nmbbd78x7m1pqml06u2&type=1&toemail=cngolon@gmail.com" // + "&activatecode=" + URLEncoder.encode("中国聚龙", "utf-8"); String content = ""; for (Entry en : map.entrySet()) { String key = en.getKey(); String value = URLEncoder.encode(en.getValue(), "utf-8"); if (!content.isEmpty()) { content += "&"; } content += key + "=" + value; } return requestPost(urlString, content); } /** * http Post请求,不带content ,参考 {@link #requestPost(String,String) requestPost(String,String)} * @param urlString 请求url * @return str * @throws IOException IOException */ public static String requestPost(String urlString) throws IOException { return requestPost(urlString, ""); } /** * http GET 请求 * * @param url url * @return str * @throws IOException IOException */ public static String requestGet(String url) throws IOException { URL getUrl = new URL(url); // 根据拼凑的URL,打开连接,URL.openConnection函数会根据URL的类型, // 返回不同的URLConnection子类的对象,这里URL是一个http,因此实际返回的是HttpURLConnection HttpURLConnection connection = (HttpURLConnection) getUrl .openConnection(); connection.setConnectTimeout(6 * 1000); // 进行连接,但是实际上get request要在下一句的connection.getInputStream()函数中才会真正发到 // 服务器 if(null != JSESSIONID){ connection.setRequestProperty("Cookie", JSESSIONID); } connection.connect(); // 取得输入流,并使用Reader读取 BufferedReader reader = new BufferedReader(new InputStreamReader( connection.getInputStream(), "utf-8"));// 设置编码,否则中文乱码 String line; StringBuilder sb = new StringBuilder(); while ((line = reader.readLine()) != null) { sb.append(line); } String cookieval = connection.getHeaderField("set-cookie"); System.out.println("set-cookie:"+cookieval); if(cookieval != null) { JSESSIONID = cookieval.substring(0, cookieval.indexOf(";")); } reader.close(); // 断开连接 connection.disconnect(); return sb.toString(); } /** * 读取图片 * @param url 图片url * @return Bitmap * @throws IOException IOException */ public static Bitmap getBitmap(String url) throws IOException { // 获得连接 HttpURLConnection conn = (HttpURLConnection) new URL(url) .openConnection(); // 设置超时时间为6000毫秒,conn.setConnectionTiem(0);表示没有时间限制 conn.setConnectTimeout(6000); // 连接设置获得数据流 conn.setDoInput(true); // 不使用缓存 conn.setUseCaches(false); // 这句可有可无,没有影响 conn.connect(); // 得到数据流 InputStream is = conn.getInputStream(); // 解析得到图片 Bitmap bitMap = BitmapFactory.decodeStream(is); is.close(); return bitMap; }}
8.使用
在登录的Activity或者主Activity的onCreate()方法中进行调用,如
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ... //检查版本更新 new GetNewestVersionTask(this,false).execute(); }
这么一个小小的功能却要如此折腾。。。
*309. 《道德经》第六十九章2 悟道之人有三宝
原文:我恒有三宝,持而保之:一曰慈,二曰俭,三曰不敢为天下先。夫慈,故能勇;俭,故能广;不敢为天下先,故能为成事长。
译文:悟道之人一直有三个宝贝,好好地拿在手里:第一是利他,第二是降低自己的欲望,第三是不能把自己的利益放在天下人的前面。因为心怀慈悲,所以做事勇敢;因为对自己节俭,所以机会广;不敢把自己的利益放在前面,所以能被推举为做事的首领。
先天下之忧而忧,后天下之乐而乐,这是真正做事的人的品格。
更多相关文章
- linux学习笔记《一.烧写篇_android》
- android 从第三方app打开方式添加自己的app
- 转:Activity_dialog效果
- android Thumbnail攻略
- Android(安卓)RRO机制的运用-----google开机向导客制化
- android中的Handler(1)
- Mac下用Charles实现Android(安卓)http和https抓包
- SSDP协议的Android实现以及使用
- Android文件目录结构