一、需求
- 进入 APK 时,弹框提示升级
- 进入 APK 后,点击升级按钮提示升级
二、升级流程
- 访问服务器端最新 APK 的版本 server_version
- 获取本地已安装 APK 的版本 local_version
- 对比 server_version 和 local_version 的大小
- 若 server_version > local_version,则弹框提示升级
- 点击弹框中的取消按钮,不升级
- 点击弹框中的升级按钮,开始下载服务器端最新 APK
- 弹框提示下载进度
- 下载完成,进入安装界面
- 点击安装,升级完成
三、升级案例
3.1 服务端
准备:
- 记得添加 gson 的 jar 包到 path 路径
- 把服务器的 APK 放到 WebContent 目录下
开始:
1、使用 Eclipse 创建 Dynamic Web Project
2、项目名称 UpdateApk
3、项目 UpdateApk 目录结构
4、右键 src–New–Servlet,创建 CheckVersionServlet(用于获取服务器 APK 版本)和UpdateApkServlet(用户下载更新 APK)
5、CheckVersionServlet.java
@WebServlet("/CheckVersionServlet")
public class CheckVersionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter out = response.getWriter();
// 服务器版本
float serverVersion = ApkInfo.SERVER_VERSION;
Gson gson = new Gson();
ApkModel apkModel = new ApkModel();
apkModel.setData(serverVersion);
String json = gson.toJson(apkModel);
out.println(json);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
6、UpdateApkServlet
@WebServlet("/UpdateApkServlet")
public class UpdateApkServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置响应类型
response.setContentType("text/html;charset=UTF-8");
// 获取请求参数 user_version,对应客户端 APK 版本
String user_version_name = request.getParameter("user_version");
float user_version;
try {
// 请求参数 user_version 对应的字符串转为 float 类型的值
user_version = Float.valueOf(user_version_name);
} catch (NullPointerException | NumberFormatException e) { // 数字转换异常,则请求参数 user_version 格式无效,不能转为 float 类型
PrintWriter out = response.getWriter();
ApkModel apkModel = new ApkModel();
apkModel.setData("请求参数 user_version 格式无效,不能转为 float 类型");
Gson gson = new Gson();
String json = gson.toJson(apkModel);
out.println(json);
// 请求参数 user_version 格式无效,直接返回
return;
}
// 请求参数 user_version 格式有效,可转为 float 类型
// 服务器 APK 版本
float server_version = ApkInfo.SERVER_VERSION;
// 客户端 APK 版本小于服务器 APK 版本
if (user_version < server_version) {
// 服务器 APK 的路径
String apkPath = getServletContext().getRealPath("/");
// 服务器 APK 的名称
String apkName = ApkInfo.SERVER_APK_NAME;
// 根据 APK 所在路径创建 APK 文件
File apkFile = new File(apkPath + File.separator + apkName);
// 文件存在
if (apkFile.exists()) {
// 在下载框默认显示的文件名
String downloadFilename = ApkInfo.SERVER_APK_NAME;
// 设置在下载框默认显示的文件名
response.setHeader("Content-Disposition", "attachment;filename=" + downloadFilename);
// 设置文件长度
response.setContentLength((int) apkFile.length());
// 指定返回对象是文件流
response.setContentType("application/octet-stream");
// 获取 APK 文件的输入流
InputStream is = new FileInputStream(apkFile);
BufferedInputStream bis = new BufferedInputStream(is);
// 缓冲
byte[] buffer = new byte[bis.available()];
// 读到缓冲中
bis.read(buffer);
// 获取服务端的输出流
ServletOutputStream os = response.getOutputStream();
// 把缓冲写到输出流中
os.write(buffer);
os.flush();
// 关闭各种流
is.close();
bis.close();
os.close();
}
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
7、ApkModel
public class ApkModel {
/**
* 服务器返回给客户端的信息,可能是字符串,也可能是其它数据类型
*/
private Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
}
- ApkInfo
public class ApkInfo {
// 服务器 APK 的版本,假设为 2.0
public static final float SERVER_VERSION = 2.0f;
// 服务器 APK 的名称,和 WebContent 下 APK 文件名一致
public static final String SERVER_APK_NAME = "new.apk";
}
3.2 客户端
准备:
获取本机 IP:打开 cmd 命令行窗口,输入 ipconfig 后回车,或通过 ‘打开"网络和 Intent"设置’—查看网络属性—IPv4地址查看
获取服务器最新 APK 版本接口:http://你的 IP 地址:8080/UpdateApk/CheckVersionServlet
下载服务器最新 APK 接口:http://你的 IP 地址:8080/UpdateApk/UpdateApkServlet?user_version=x.x(x.x,如1.0)
使用自定义的 HttpUtils 网络访问框架访问网络,可用使用其它的方式访问网络
开始:
1、访问网络,获取服务器最新 APK 的版本,若有最新版本且版本大于已安装版本,则弹框提示升级
/**
* 检测 APK 是否需要升级
*/
private void checkUpdate() {
/**
* 请求网络
*/
HttpUtils.with(this)
.url("http://你的 IP 地址:8080/UpdateApk/CheckVersionServlet")
.execute(new HttpCallBack<ApkInfoModel>() {
@Override
public void onSuccess(final ApkInfoModel apkInfoModel) {
Log.e("tag", "local: " + ApkInfoUtils.getLocalApkVersion(MainActivity.this) + ", server: " + apkInfoModel.getData());
// 成功回调
if (apkInfoModel != null && ApkInfoUtils.isNeedUpdate(MainActivity.this, apkInfoModel.getData())) {
AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
.setMessage("当前有新版本,是否进行升级?")
.setNegativeButton("否", null)
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 使用 ApkDownLoader 下载 APK
ApkDownLoader.init(MainActivity.this).downLoadApk(apkInfoModel.getData(), "http://你的 IP 地址:8080/UpdateApk
/UpdateApkServlet");
}
}).create();
dialog.show();
} else {
Toast.makeText(MainActivity.this, "不需要升级", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(Exception e) {
// 失败回调
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
2、ApkDownLoader
public class ApkDownLoader {
// APK 下载文件村路径(当前指定 APK 文件路径为 SD 卡根目录下 download 目录,当 Android 版本大于 7.0 时,必须使用 FileProvider 获取文件访问的权限,需要在下面 provider_paths.xml 中设置 path 的值为 download)
public static final String SD_FOLDER = Environment.getExternalStorageDirectory() + "/download";
// APK 下载文件
private static File mApkFile;
// 下载进度提示框
private static ProgressDialog pd;
// 最大进度
private static final int MAX_PROGRESS = 100;
// Handler:下载中,显示进度消息
private static final int MSG_SHOW_PROGRESS = 1;
// Handler:下载完成,安装 APK 消息
private static final int MSG_INSTALL_APK = 2;
// Handler 处理下载消息
private MyHandler handler;
// 上下文
private Context mContext;
private ApkDownLoader(Context context) {
this.mContext = context;
}
/**
* 初始化 ApkDownLoader
* @param context 上下文
* @return ApkDownLoader
*/
public static ApkDownLoader init(Context context) {
return new ApkDownLoader(context);
}
/**
* @param serverApkVersion
* @param url
*/
public void downLoadApk(float serverApkVersion, final String url) {
showProgressDialog();
handler = new MyHandler(this);
downloadFile(mContext, url);
}
/**
* 显示下载进度框
*/
private void showProgressDialog() {
pd = new ProgressDialog(mContext);
pd.setCancelable(false); // 不能取消
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage("正在下载安装包,请稍后");
pd.setTitle("版本升级");
pd.setMax(MAX_PROGRESS); // 最大进度
pd.create();
pd.show();
}
private void downloadFile(final Context context, String url) {
/**
* 请求网络
*/
HttpUtils.with(mContext).get()
.url(url)
.addParam("user_version", "" + ApkInfo.getLocalApkVersion(context))
.execute(new IEngineCallBack() {
@Override
public void onSuccess(Response result) {
// SD 卡可用,下载最新 APK
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
InputStream is = null;
OutputStream os = null;
// 成功回调
try {
// 获取输入流
is = result.body().byteStream();
// 创建最新 APK 文件
mApkFile = new File(SD_FOLDER, "new.apk");
// APK 保存所在目录不存在
if (!mApkFile.getParentFile().exists()) {
// 创建 APK 保存所在目录
mApkFile.getParentFile().mkdirs();
}
os = new FileOutputStream(mApkFile);
int len;
int total = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
total += len;
// 通过 handler 发送消息,更新下载进度
Message msg = Message.obtain();
msg.what = MSG_SHOW_PROGRESS;
msg.arg1 = total;
handler.sendMessage(msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭输出流
IOExceptionCloser.close(os);
// 关闭输入流
IOExceptionCloser.close(is);
}
} else {
// 提示 SD 卡不可用
HttpUtils.handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "SD 卡不可用", Toast.LENGTH_SHORT).show();
}
});
}
}
@Override
public void onError(Exception e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
/**
* Handler 处理下载消息
*/
private static class MyHandler extends Handler {
private WeakReference<ApkDownLoader> reference;
public MyHandler(ApkDownLoader apkDownLoader) {
reference = new WeakReference<ApkDownLoader>(apkDownLoader);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (reference == null || reference.get() == null) {
return;
}
switch (msg.what) {
case MSG_SHOW_PROGRESS:
// 显示下载进度通知
reference.get().showDownloadProgress(msg.arg1);
case MSG_INSTALL_APK:
// 安装 APK
reference.get().installApk();
break;
}
}
};
/**
* 安装 APK
*/
private void installApk() {
Intent intent = new Intent(Intent.ACTION_VIEW);
File file = new File(mApkFile.getAbsolutePath());
Uri uri;
// Android 版本大于 7.0,需要使用 FileProvider 获取文件访问的权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(mContext, "你的应用包名.provider", file);
}
// Android 版本小于 7.0,直接使用文件即可
else {
uri = Uri.fromFile(file);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动安装界面进行安装
mContext.startActivity(intent);
}
/**
* 显示下载进度
* @param progress 当前进度,最大进度为 100
*/
private void showDownloadProgress(int progress) {
// 设置当前下载进度
pd.setProgress(progress);
// 进度为 100,下载完成
if (progress == MAX_PROGRESS) {
pd.setMessage("下载完毕");
// 发送下载完成消息
handler.sendEmptyMessage(MSG_INSTALL_APK);
}
}
}
3、ApkInfoModel
public class ApkInfoModel {
private float data;
public float getData() {
return data;
}
}
4、ApkInfoUtils
public class ApkInfoUtils {
/**
* 获取本地 APK 版本
* @param context 上下文
* @return 本地 APK 版本
*/
public static float getLocalApkVersion(Context context) {
try {
String localPackage = context.getPackageName();
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(localPackage, 0);
return Float.valueOf(packageInfo.versionName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}
/**
* APK 是否需要升级
* @param context 上下文
* @param serverApkVersion 服务器端 APK 版本号
* @return
*/
public static boolean isNeedUpdate(Context context, float serverApkVersion) {
float localApkVersion = getLocalApkVersion(context);
if (serverApkVersion > localApkVersion) {
return true;
}
return false;
}
}
- IOExceptionCloser
public class IOExceptionCloser {
public static void close(Closeable io) {
if (io != null) {
try {
io.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意
1、Android 7.0 开始需要使用 FileProvider 来获取其他应用的文件访问权限,需要在 AndroidManifest.xml 文件的 application 标签内注册一个 provider
<provider
android:authorities="你的应用包名.provider" // 必须保证唯一
android:name="android.support.v4.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
2、provider_paths.xml:存放在 res/xml 下
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_path"
path="download"/> // path 不写任何内容,默认路径为 SD 卡根目录
</paths>
3、注册相关权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
关于 Android 6.0 运行时权限的处理找个 demo 看一看,然后用一用就好了
至此,APK 升级安装的功能就基本实现了