0%

轻松实现APP自动检测更新

概述:为了以快速并且节约的方式让APP更新版本,通常需要在APP内增加自动检测更新新版本的功能。

运行截图:

)))

实现:4个步骤

1.在服务端放置存储版本信息的文件

一般以json格式保存必要的信息:apk文件下载地址、版本号、更新内容

1
2
3
4
5
6

{
"url":"http://crazyfzw.github.io/demo/auto-update-version/new-version-v2.0.apk",
"versionCode":2,
"updateMessage":"[1]新增视频弹幕功能<br/>[2]优化离线缓存功能<br/>[3]增强了稳定性"
}

2.继承AsyncTask创建一个异步任务去下载版本信息文件

从服务器取得版本信息,与本地apk对比版本号,判断是否有更新,若有,则以Dialog让用户选择是否更新,若用户选择更新,则调用服务去完成apk文件的下载

CheckVersionInfoTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/**
*
*/
public class CheckVersionInfoTask extends AsyncTask<Void, Void, String> {

private static final String TAG = "CheckVersionInfoTask";
private ProgressDialog dialog;
private Context mContext;
private boolean mShowProgressDialog;
private static final String VERSION_INFO_URL = "http://crazyfzw.github.io/demo/auto-update-version/update.json";

public CheckVersionInfoTask(Context context, boolean showProgressDialog) {
this.mContext = context;
this.mShowProgressDialog = showProgressDialog;

}

//初始化显示Dialog
protected void onPreExecute() {
if (mShowProgressDialog) {
dialog = new ProgressDialog(mContext);
dialog.setMessage(mContext.getString(R.string.check_new_version));
dialog.show();
}
}

//在后台任务(子线程)中检查服务器的版本信息
@Override
protected String doInBackground(Void... params) {
return getVersionInfo(VERSION_INFO_URL);
}


//后台任务执行完毕后,解除Dialog并且解析return返回的结果
@Override
protected void onPostExecute(String result) {

if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}

if (!TextUtils.isEmpty(result)) {
parseJson(result);
}
}


/**
* 从服务器取得版本信息
* {
"url":"http://crazyfzw.github.io/demo/auto-update-version/new-version-v2.0.apk",
"versionCode":2,
"updateMessage":"[1]新增视频弹幕功能<br/>[2]优化离线缓存功能<br/>[3]增强了稳定性"
}
* @return
*/
public String getVersionInfo(String urlStr){
HttpURLConnection uRLConnection = null;
InputStream is = null;
BufferedReader buffer = null;
String result = null;
try {
URL url = new URL(urlStr);
uRLConnection = (HttpURLConnection) url.openConnection();
uRLConnection.setRequestMethod("GET");
is = uRLConnection.getInputStream();
buffer = new BufferedReader(new InputStreamReader(is));
StringBuilder strBuilder = new StringBuilder();
String line;
while ((line = buffer.readLine()) != null) {
strBuilder.append(line);
}
result = strBuilder.toString();
} catch (Exception e) {
Log.e(TAG, "http post error");
} finally {
if (buffer != null) {
try {
buffer.close();
} catch (IOException ignored) {
}
}
if (is != null) {
try {
is.close();
} catch (IOException ignored) {

}
}
if (uRLConnection != null) {
uRLConnection.disconnect();
}
}
return result;
}

/**
*
* @param result
*/
private void parseJson(String result) {
try {
JSONObject obj = new JSONObject(result);
String apkUrl = obj.getString("url"); //APK下载路径
String updateMessage = obj.getString("updateMessage");//版本更新说明
int apkCode = obj.getInt("versionCode"); //新版APK对于的版本号

//取得已经安装在手机的APP的版本号 versionCode
int versionCode = getCurrentVersionCode();

//对比版本号判断是否需要更新
if (apkCode > versionCode) {

showDialog(updateMessage, apkUrl);

} else if (mShowProgressDialog) {
Toast.makeText(mContext, mContext.getString(R.string.there_no_new_version), Toast.LENGTH_SHORT).show();
}

} catch (JSONException e) {
Log.e(TAG, "parse json error");
}
}

/**
* 取得当前版本号
* @return
*/
public int getCurrentVersionCode() {

try {
return mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException ignored) {
}
return 0;
}


/**
* 显示对话框提示用户有新版本,并且让用户选择是否更新版本
* @param content
* @param downloadUrl
*/
public void showDialog(String content, final String downloadUrl) {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(R.string.dialog_choose_update_title);
builder.setMessage(Html.fromHtml(content))
.setPositiveButton(R.string.dialog_btn_confirm_download, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
//下载apk文件
goToDownloadApk(downloadUrl);
}
})
.setNegativeButton(R.string.dialog_btn_cancel_download, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
}
});

AlertDialog dialog = builder.create();
//点击对话框外面,对话框不消失
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}

/**
* 用intent启用DownloadService服务去下载AKP文件
* @param downloadUrl
*/
private void goToDownloadApk(String downloadUrl) {
Intent intent = new Intent(mContext, DownloadApkService.class);
intent.putExtra("apkUrl", downloadUrl);
mContext.startService(intent);
}
}

注:其中用到的 StorageUtils.getCacheDirectory(context) 是用于取得应用在手机缓存目录

StorageUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

final class StorageUtils {

private static final String TAG = "StorageUtils";
private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE";

private StorageUtils() {
}

/**
* Returns application cache directory. Cache directory will be created on SD card
* ("/Android/data/[app_package_name]/cache") if card is mounted and app has appropriate permission. Else -
* Android defines cache directory on device's file system.
*/
public static File getCacheDirectory(Context context) {
File appCacheDir = null;
if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && hasExternalStoragePermission(context)) {
appCacheDir = getExternalCacheDir(context);
}
if (appCacheDir == null) {
appCacheDir = context.getCacheDir();
}
if (appCacheDir == null) {
Log.w(TAG, "Can't define system cache directory! The app should be re-installed.");
}
return appCacheDir;
}

private static File getExternalCacheDir(Context context) {
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
if (!appCacheDir.exists()) {
if (!appCacheDir.mkdirs()) {
Log.w(TAG, "Unable to create external cache directory");
return null;
}
try {
new File(appCacheDir, ".nomedia").createNewFile();
} catch (IOException e) {
Log.i(TAG, "Can't create \".nomedia\" file in application external cache directory");
}
}
return appCacheDir;
}

private static boolean hasExternalStoragePermission(Context context) {
int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION);
return perm == PackageManager.PERMISSION_GRANTED;
}
}

3.创建服务(Service)完成apk文件的下载,下载完成后调用系统的安装程序完成安装

DownloadApkService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

public class DownloadApkService extends IntentService{

private static final int BUFFER_SIZE = 10 * 1024; //缓存大小
private static final String TAG = "DownloadService";

private static final int NOTIFICATION_ID = 0;
private NotificationManager mNotifyManager;
private NotificationCompat.Builder mBuilder;

public DownloadApkService() {
super("DownloadApkService");
}

/**
* 在onHandleIntent中下载apk文件
* @param intent
*/
@Override
protected void onHandleIntent(Intent intent) {

//初始化通知,用于显示下载进度
mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(this);
String appName = getString(getApplicationInfo().labelRes);
int icon = getApplicationInfo().icon;
mBuilder.setContentTitle(appName).setSmallIcon(icon);

String urlStr = intent.getStringExtra("apkUrl"); //从intent中取得apk下载路径

InputStream in = null;
FileOutputStream out = null;
try {
//建立下载连接
URL url = new URL(urlStr);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setDoOutput(false);
urlConnection.setConnectTimeout(10 * 1000);
urlConnection.setReadTimeout(10 * 1000);
urlConnection.setRequestProperty("Connection", "Keep-Alive");
urlConnection.setRequestProperty("Charset", "UTF-8");
urlConnection.setRequestProperty("Accept-Encoding", "gzip, deflate");
urlConnection.connect();

//以文件流读取数据
long bytetotal = urlConnection.getContentLength(); //取得文件长度
long bytesum = 0;
int byteread = 0;
in = urlConnection.getInputStream();
File dir = StorageUtils.getCacheDirectory(this); //取得应用缓存目录
String apkName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.length());//取得apK文件名
File apkFile = new File(dir, apkName);
out = new FileOutputStream(apkFile);
byte[] buffer = new byte[BUFFER_SIZE];

int limit = 0;
int oldProgress = 0;
while ((byteread = in.read(buffer)) != -1) {
bytesum += byteread;
out.write(buffer, 0, byteread);
int progress = (int) (bytesum * 100L / bytetotal);
// 如果进度与之前进度相等,则不更新,如果更新太频繁,则会造成界面卡顿
if (progress != oldProgress) {
updateProgress(progress);
}
oldProgress = progress;
}

// 下载完成,调用installAPK开始安装文件
installAPk(apkFile);
Log.d("调试","download apk finish");
mNotifyManager.cancel(NOTIFICATION_ID);

} catch (Exception e) {
Log.e(TAG, "download apk file error");
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {

}
}
if (in != null) {
try {
in.close();
} catch (IOException ignored) {

}
}
}
}

/**
* 实时更新下载进度条显示
* @param progress
*/
private void updateProgress(int progress) {
//"正在下载:" + progress + "%"
mBuilder.setContentText(this.getString(R.string.dialog_choose_update_content, progress)).setProgress(100, progress, false);
PendingIntent pendingintent = PendingIntent.getActivity(this, 0, new Intent(), PendingIntent.FLAG_CANCEL_CURRENT);
mBuilder.setContentIntent(pendingintent);
mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build());
}


/**
* 调用系统安装程序安装下载好的apk
* @param apkFile
*/
private void installAPk(File apkFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
//如果没有设置SDCard写权限,或者没有sdcard,apk文件保存在内存中,需要授予权限才能安装
try {
String[] command = {"chmod", "777", apkFile.toString()}; //777代表权限 rwxrwxrwx
ProcessBuilder builder = new ProcessBuilder(command);
builder.start();
} catch (IOException ignored) {
}
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}

4.最后一步,new并execute我们写好的异步任务就行了。

1
new CheckVersionInfoTask(MainActivity.this, true).execute();

注:记得在AndroidManifest.xml中生明权限及注册Service哦

1
2
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

本案例完整源码:https://github.com/crazyfzw/AppAutoCheckUpdate

用到的相关知识:

异步任务AsysTask的相关用法:郭霖的Android AsyncTask完全解析,带你从源码的角度彻底理解

自动启动线程执行耗时任务并会自动停止的服务 IntentService的用法:Android理解:IntentService