android中的版本更新是每一个APP的标配,记得最早的时候还是使用HttpUrlConnection+Handler来实现,如今时过境迁,特别是随着OkHttp、RxJava的流行,HttpUrlConnection+Handler的慢慢就用的少了,特别是在Android 7.0的手机上,系统对读取手机文件做了更进一步的限制,基于种种原因,最近又将版本更新做了一次整理,记录下来以防止后面遗忘。废话不多说,先上效果。
这个效果是在点击立即更新之后,关闭AlertDialog,在后台下载,同时在通知栏显示下载进度(通知栏权限已打开)。
这个效果是在点击立即更新之后,不关闭AlertDialog,直接显示下载进度。
具体来说,大致的流程如下:
进入正文
在正式编码之前,我们需要多方位思考,理清思路再编码,基于版本更新,首先我们需要知道几个点:
(1)下载的时候必须要有进度通知,能够很清晰明了知道下载了多少。
(2)有些时候我们需要对版本进行强制更新,所以这个功能必须有。
(3)当用户下载完成后但是一不小心没有点安装,那么下次再次更新的时候应该是直接安装(APK必须要下载到本地),而不是再次从网络获取。
(4)更新安装成功后最好能够自动删除原来的apk文件。
当然,在实际的开发过程中,可能还有一些更为详细的需求,这个就根据实际情况来定了。了解需求之后,接下来我们看看应该怎么去编写。
首先创建版本更新的类并声明相对应的变量
/**
* author: zhoufan
* data: 2021/8/10 10:04
* content: 实现App的版本更新
*/
public class UpdateVersion {
/**
* 上下文
*/
private Context mContext;
/**
* 版本号
*/
private int mVersionCode;
/**
* 版本名称
*/
private String mVersionName;
/**
* 更新提示内容
*/
private String mUpdateContent;
/**
* 更新的APK对应的网络地址
*/
private String mUpdateAPKUrl;
/**
* 更新的类型 1:正常更新,2:强制更新
*/
private int mUpdateStatus = 1;
/**
* 更新的时候是否在前台开启进度条
*/
private boolean isShowUpdateDialog;
/**
* 构造函数
*/
public UpdateVersion(Context context, int versionCode, String versionName, String updateContent, String updateAPKUrl, int updateStatus, boolean isShowUpdateDialog) {
this.mContext = context;
this.mVersionCode = versionCode;
this.mVersionName = versionName;
this.mUpdateContent = updateContent;
this.mUpdateAPKUrl = updateAPKUrl;
this.mUpdateStatus = updateStatus;
this.isShowUpdateDialog = isShowUpdateDialog;
}
}
这里面声明了是否强制更新以及是否开启前台进度条,后面会使用到这两个参数。接下来就是我们的弹框了
/**
* 弹出Dialog显示更新对话框
*/
private void createDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.bklistDialog);
View updateView = LayoutInflater.from(mContext).inflate(R.layout.dialog_update_version, null);
initDialogView(updateView);
builder.setView(updateView);
mDialog = builder.create();
// 判断是否为强制更新
if (mUpdateStatus == 2) {
mDialog.setCanceledOnTouchOutside(false);
mUpdateCancel.setVisibility(View.GONE);
mDialog.setOnKeyListener(keyListener);
mDialog.setCancelable(false);
} else {
mDialog.setCanceledOnTouchOutside(true);
}
// 显示
mDialog.show();
}
/**
* 获取View的控件并添加点击响应事件
*/
private void initDialogView(View updateView) {
TextView newVersionName = updateView.findViewById(R.id.new_version_value);
TextView newVersionContent = updateView.findViewById(R.id.update_content);
mUpdateProgressLayout = updateView.findViewById(R.id.update_download_layout);
mUpdateProgressbar = updateView.findViewById(R.id.update_download_progressbar);
mUpdateDownloadNumber = updateView.findViewById(R.id.update_download_number);
mUpdateCancel = updateView.findViewById(R.id.cancel);
mUpdateSure = updateView.findViewById(R.id.sure);
String versionName = "V" + mVersionName;
newVersionName.setText(versionName);
if (!TextUtils.isEmpty(mUpdateContent)) {
newVersionContent.setText(mUpdateContent);
} else {
newVersionContent.setText(mContext.getResources().getString(R.string.updateNewVersion));
}
mUpdateSure.setText(mContext.getResources().getString(R.string.new_update));
}
在这里我们就需要判断是否为强制更新了,因为强制更新的话就表示我们在AlertDialog弹出后无法被关闭,同时我们的取消按钮也要被隐藏掉。
开始显示我们的AlertDialog
public void showUpdateDialog() {
// 设置下载的安装路径
mSavePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "Android/data/" + mContext.getPackageName() + File.separator + "apk";
mSaveFileName = mSavePath + File.separator + mVersionCode + ".apk";
createDialog();
// 取消更新
mUpdateCancel.setOnClickListener(v -> {
if (mDialog != null && mDialog.isShowing())
mDialog.dismiss();
});
// 确认更新
mUpdateSure.setOnClickListener(v -> {
File file = new File(mSaveFileName);
if (file.exists()) {
// apk文件已经存在,直接安装
installApk(mSaveFileName);
} else {
// 后台开启下载功能,下载完毕后自动更新
if (isShowUpdateDialog) {
mUpdateProgressLayout.setVisibility(View.VISIBLE);
} else {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
}
}
startDownloadApk();
}
});
}
首先确定好我们的安装路径,这里我是放置在我的项目的根目录下,然后相应的处理取消和立即更新的事件,取消对应的比较简单,直接将弹框关闭就好,立即更新稍微复杂一点,分为几步:
(1)首先判断要更新的apk文件存在不存在,如果存在的话直接安装apk文件即可。
(2)如果不存在的话,需要判断是在通知栏显示下载进度还是在界面直接显示下载进度。
(3)最后开启下载。
开始下载
/**
* 使用OkHttp来进行网络下载
*/
private void startDownloadApk() {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url(mUpdateAPKUrl).get().build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
MyToast.showCenterSortToast(mContext, mContext.getString(R.string.download_fail));
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
//储存下载文件的目录
File dir = new File(mSavePath);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(mSaveFileName);
try {
is = response.body().byteStream();
long total = response.body().contentLength();
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
if (mUpdateProgressLayout.getVisibility() == View.VISIBLE) {
//下载中更新进度条
Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
mUpdateProgressbar.setProgress(integer);
String currentPercent = integer + "%";
mUpdateDownloadNumber.setText(currentPercent);
});
} else {
Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
updateNotification(progress);
});
}
}
fos.flush();
//下载完成
Observable.just(100).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> installApk(mSaveFileName));
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
开启下载这一块我采用的就是最新的OkHttp+RxJava,这个就不用多说了,一看就懂。最后当下载完成之后就可以直接安装APK了。
安装APK
/**
* 安装apk
*/
private void installApk(String filePath) {
if (mUpdateProgressLayout.getVisibility() == View.GONE) {
NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(1);
}
File file = new File(filePath);
if (Build.VERSION.SDK_INT >= 24) {//判读版本是否在7.0以上
String authority = mContext.getPackageName() + ".fileProvider";
Uri apkUri = FileProvider.getUriForFile(mContext, authority, file);
Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
mContext.startActivity(install);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
mContext.startActivity(intent);
}
}
安装APK稍微复杂一点,因为在android 7.0手机上面,系统做了一个更改。
在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictModeAPI 政策禁止在您的应用外部公开 file://URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content://URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。
这是android 官方的解释,什么意思呢?简单来说就是在android 7.0手机上面,原理的Uri模式玩不转了,系统建议采用FileProvider来实现。那么FileProvider怎么使用呢?也很简单。
FileProvider使用步骤
(1)在res目录下新建xml文件夹,在xml文件夹下面新建一个provider_paths.xml文件。
(2)在provider_paths.xml文件里面声明
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path
name="apk" // 名字随便取
path="" /> // path为空代表的是整个文件目录
</paths>
</resources>
(3)在清单文件里面进行声明
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.steven.sunworld.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
注意:authorities为包名+fileProvider,中间用 . 来连接,如上所示。
(4)在使用FileProvider.getUriForFile(mContext, authority, file);的时候authority的取值必须和你在清单文件里面设置的一致,file就是你要操作的文件。
到这里,版本更新的所有环节都基本结束了,最后贴上完整代码
/**
* author: zhoufan
* data: 2021/8/10 10:04
* content: 实现App的版本更新
*/
public class UpdateVersion {
/**
* 上下文
*/
private Context mContext;
/**
* 版本号
*/
private int mVersionCode;
/**
* 版本名称
*/
private String mVersionName;
/**
* 更新提示内容
*/
private String mUpdateContent;
/**
* 更新的APK对应的网络地址
*/
private String mUpdateAPKUrl;
/**
* 更新的类型 1:正常更新,2:强制更新
*/
private int mUpdateStatus = 1;
/**
* 更新的时候是否在前台开启进度条
*/
private boolean isShowUpdateDialog;
/**
* 更新弹出的确认更新确认框
*/
private AlertDialog mDialog;
/**
* APK下载之后保存的地址
*/
private String mSavePath;
/**
* 保存的文件名
*/
private String mSaveFileName;
/**
* 当前下载的进度
*/
private int mDownloadProgress;
private RelativeLayout mUpdateProgressLayout;
private ProgressBar mUpdateProgressbar;
private TextView mUpdateDownloadNumber;
private TextView mUpdateCancel;
private TextView mUpdateSure;
public UpdateVersion(Context context, int versionCode, String versionName, String updateContent, String updateAPKUrl, int updateStatus, boolean isShowUpdateDialog) {
this.mContext = context;
this.mVersionCode = versionCode;
this.mVersionName = versionName;
this.mUpdateContent = updateContent;
this.mUpdateAPKUrl = updateAPKUrl;
this.mUpdateStatus = updateStatus;
this.isShowUpdateDialog = isShowUpdateDialog;
}
public void showUpdateDialog() {
// 设置下载的安装路径
mSavePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "Android/data/" + mContext.getPackageName() + File.separator + "apk";
mSaveFileName = mSavePath + File.separator + mVersionCode + ".apk";
createDialog();
// 取消更新
mUpdateCancel.setOnClickListener(v -> {
if (mDialog != null && mDialog.isShowing())
mDialog.dismiss();
});
// 确认更新
mUpdateSure.setOnClickListener(v -> {
File file = new File(mSaveFileName);
if (file.exists()) {
// apk文件已经存在,直接安装
installApk(mSaveFileName);
} else {
// 后台开启下载功能,下载完毕后自动更新
if (isShowUpdateDialog) {
mUpdateProgressLayout.setVisibility(View.VISIBLE);
} else {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
}
}
startDownloadApk();
}
});
}
/**
* 弹出Dialog显示更新对话框
*/
private void createDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.bklistDialog);
View updateView = LayoutInflater.from(mContext).inflate(R.layout.dialog_update_version, null);
initDialogView(updateView);
builder.setView(updateView);
mDialog = builder.create();
// 判断是否为强制更新
if (mUpdateStatus == 2) {
mDialog.setCanceledOnTouchOutside(false);
mUpdateCancel.setVisibility(View.GONE);
mDialog.setOnKeyListener(keyListener);
mDialog.setCancelable(false);
} else {
mDialog.setCanceledOnTouchOutside(true);
}
// 显示
mDialog.show();
}
/**
* 获取View的控件并添加点击响应事件
*/
private void initDialogView(View updateView) {
TextView newVersionName = updateView.findViewById(R.id.new_version_value);
TextView newVersionContent = updateView.findViewById(R.id.update_content);
mUpdateProgressLayout = updateView.findViewById(R.id.update_download_layout);
mUpdateProgressbar = updateView.findViewById(R.id.update_download_progressbar);
mUpdateDownloadNumber = updateView.findViewById(R.id.update_download_number);
mUpdateCancel = updateView.findViewById(R.id.cancel);
mUpdateSure = updateView.findViewById(R.id.sure);
String versionName = "V" + mVersionName;
newVersionName.setText(versionName);
if (!TextUtils.isEmpty(mUpdateContent)) {
newVersionContent.setText(mUpdateContent);
} else {
newVersionContent.setText(mContext.getResources().getString(R.string.updateNewVersion));
}
mUpdateSure.setText(mContext.getResources().getString(R.string.new_update));
}
/**
* 禁用返回键
*/
private DialogInterface.OnKeyListener keyListener = (dialog, keyCode, event) -> keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0;
/**
* 安装apk
*/
private void installApk(String filePath) {
if (mUpdateProgressLayout.getVisibility() == View.GONE) {
NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(1);
}
File file = new File(filePath);
if (Build.VERSION.SDK_INT >= 24) {//判读版本是否在7.0以上
String authority = mContext.getPackageName() + ".fileProvider";
Uri apkUri = FileProvider.getUriForFile(mContext, authority, file);
Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
mContext.startActivity(install);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
mContext.startActivity(intent);
}
}
/**
* 使用OkHttp来进行网络下载
*/
private void startDownloadApk() {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url(mUpdateAPKUrl).get().build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
MyToast.showCenterSortToast(mContext, mContext.getString(R.string.download_fail));
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
//储存下载文件的目录
File dir = new File(mSavePath);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(mSaveFileName);
try {
is = response.body().byteStream();
long total = response.body().contentLength();
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
if (mUpdateProgressLayout.getVisibility() == View.VISIBLE) {
//下载中更新进度条
Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
mUpdateProgressbar.setProgress(integer);
String currentPercent = integer + "%";
mUpdateDownloadNumber.setText(currentPercent);
});
} else {
Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
updateNotification(progress);
});
}
}
fos.flush();
//下载完成
Observable.just(100).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> installApk(mSaveFileName));
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
/**
* 通知栏更新
*/
private void updateNotification(int progress) {
if (NotificationManagerCompat.from(mContext).areNotificationsEnabled()) {
if (progress > mDownloadProgress) {
mDownloadProgress = progress;
NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel("1", "update", NotificationManager.IMPORTANCE_HIGH);
notificationChannel.setSound(null, null);
notificationChannel.enableLights(false);
notificationChannel.setLightColor(Color.RED);
notificationChannel.setShowBadge(false);
notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
manager.createNotificationChannel(notificationChannel);
}
// 创建自定义的样式布局
RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.download_progress_state_view);
// 在这里可以设置RemoteView的初始布局
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "1");
builder.setCustomContentView(remoteViews);
// 不可以自动取消
builder.setAutoCancel(false);
// 必须要设置,否则在android 10手机上面会闪退
builder.setSmallIcon(R.mipmap.ic_launcher_round);
// 设置通知的优先级
builder.setPriority(NotificationCompat.PRIORITY_MAX);
Notification nn = builder.build();
nn.contentView = remoteViews;
nn.icon = R.mipmap.ic_launcher;
remoteViews.setImageViewResource(R.id.download_progress_img, R.mipmap.ic_launcher);
String loadShow = mContext.getResources().getString(R.string.app_download_show);
remoteViews.setTextViewText(R.id.download_progress_name, loadShow);
remoteViews.setProgressBar(R.id.download_progressbar, 100, mDownloadProgress, false);
remoteViews.setTextViewText(R.id.download_progress_text, mDownloadProgress + "%");
manager.notify(1, nn);
}
}
}
}