前言:基本上,大部分的App都要求做版本更新。以前没有具体负责过这一模块的开发,大概的原理是知道的,一直以为很简单,没怎么理。但最近自己在做这个模块的时候,还踩了不少坑。记录一下:
按照之前自己接触到的版本更新,总结一下其主要分为三大步骤:
1.检测版本更新:检测应用新版本,一般是进入应用时调取服务器接口,获取最新应用版 本信息,和当前应用版本信息进行比较,如果当前应用不是最新版本,则下载最新应用并自动跳转至系统自带的安装界面
2. 下载apk并且安装:下载apk可以调用android SDK 自带的DownloadManager进行下载,但是看一些帖子说使用DownloadManager进行apk下载的时候,会有坑,有时候某些机型会出现“包解析错误”造成更新失败的问题,所以,一般会自定义下载apk并进行安装。
3. 调用系统安装:在android 7.0以前,可以使用下面的方法调起系统安装:
private void update_test(){
String mSavePath= Environment.getExternalStorageDirectory().getPath()+"/app-release.apk";
System.out.println("apk路径=====================》"+mSavePath);
File saveFile = new File(mSavePath);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(saveFile),"application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
其中,mSavePath表示的是apk下载之后在本地存储的路径。
但是从Android 7.0开始,为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访,传递软件包网域外的 file:// URI 可能给接收器留下无法访问的路径。因此,尝试传递 file:// URI 会触发 FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
FileProvider的基本使用方法:
1.必须在AndroidManifest.xml清单文件中注册provider:
其中,exported:要求必须为false,为true则会报安全异常。
grantUriPermissions:true,表示授予 URI 临时访问权限。
authorities 组件标识,按照android常用规则,都以包名开头,避免和其它应用发生冲突。
2.上面配置文件中 android:resource=”@xml/file_paths” 指的是当前组件引用 res/xml/file_paths.xml 这个文件。
我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件:
< external-path /> 代表的根目录:
Environment.getExternalStorageDirectory()
如果是< files-path /> 代表的根目录: Context.getFilesDir()
如果是< cache-path /> 代表的根目录: getCacheDir()
上述代码中path=””,是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。
3.使用FileProvider调用系统安装:
我们可以看到android 7.0以前的安装方式与7.0 以后的主要区别:
1)之前的Uri改成了有FileProvider创建一个content类型的Uri;
2)添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 来对目标应用临时授权该Uri所代表的文件。
同时,为了适配android 7.0之前与 7.0之后的机型,需要添加对Android版本判断的部分。
下面是从检查版本更新,到下载apk、调起系统安装的代码:
//从服务器获取apk版本与当前apk版本进行比较,若版本不一致,则提示更新
private void updateUI(JSONObject jsonObject) {
if (this.getView() != null) {
Button btn = this.getView().findViewById(R.id.updateBtn);
try {
final String version = jsonObject.getString("version");
String currentVersion = BuildConfig.VERSION_NAME;
final String url = jsonObject.getString("url");
if (version.compareTo(currentVersion) <= 0) {
btn.setVisibility(View.INVISIBLE);
} else {
btn.setVisibility(View.VISIBLE);
btn.setText(getString(R.string.update_app, version));
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//upgradeApp(url);
//upgradeAppByBrowser(url);
promptUpdating(url,version);
}
});
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
//点击更新之后,弹出确认框,通过用户的选择取消或者确定进行版本的更新与否
private void promptUpdating(final String url,String version){
String msg = getString(R.string.confirm_update_app,version);
new IOSDialog.Builder(getContext())
.setMessage(msg)
.setPositiveButton(R.string.confirm,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
upgradeApp(url);
}
})
.setNegativeButton(R.string.cancel, null).show();
}
//使用Okhttp进行apk的下载
private void upgradeApp(String url) {
int pos = url.lastIndexOf('/');
final String fileName = url.substring(pos + 1);
final KProgressHUD progressHUD = KProgressHUD.create(getContext())
.setStyle(KProgressHUD.Style.PIE_DETERMINATE)
.setLabel(getString(R.string.downloading))
.setMaxProgress(100)
.setCancellable(false)
.show();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
final Activity activity = getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progressHUD.dismiss();
Util.showIRToast(activity, false, activity.getString(R.string.network_exception));
}
});
}
}
//apk下载的回调
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
long target = response.body().contentLength();
InputStream in = response.body().byteStream();
byte[] buff = new byte[1024 * 4];
long downloaded = 0;
File downloads = null;
//7.0
if ( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N){
downloads = new File(getActivity().getFilesDir(), "downloads");
downloads.mkdir();
}else{
downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
//apk临时存储的本地位置
final File targetFile = new File(downloads, fileName);
if (targetFile.exists()) {
targetFile.delete();
}
targetFile.createNewFile();
OutputStream output = new FileOutputStream(targetFile);
while (true) {
int readed = in.read(buff);
if (readed == -1) {
break;
}
output.write(buff, 0, readed);
//write buff
downloaded += readed;
final int p = (int) (downloaded * 100 / target);
final Activity activity = getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progressHUD.setProgress(p);
}
});
}
}
output.flush();
output.close();
Log.e("123","文件大小:"+targetFile.length());
final Activity activity = getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progressHUD.dismiss();
launchInstallation(targetFile);
}
});
}
} else {
final Activity activity = getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progressHUD.dismiss();
Util.showIRToast(activity, false, activity.getString(R.string.network_exception));
}
});
}
}
}
}
);
}
//调起系统安装界面,进行安装
private void launchInstallation(File targetFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
// Uri uri = Uri.fromFile(targetFile);
Uri uri = null;
//android 版本的判断
if ( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N){
uri = FileProvider.getUriForFile(getContext(),
getString(R.string.file_provider_authority),
targetFile);
}else{
uri = Uri.fromFile(targetFile);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
startActivity(intent);
}
至此,基本上是完成看版本的更新。
但是,在实际测试的时候,发现这样的处理方式还是存在问题:有些机型比如:小米 Redmi 4x在安装更新完成之后,点击“打开”但程序无法启动;更新下载apk完成之后,点击“取消”进行取消安装,这样的处理方式会报异常;还有一些进行在下载完成apk之后进行安装并完成安装之后,没显示“完成”、“打开”选择,基于这些问题,所以需要进行改进,如下:
//调起系统安装界面,进行安装
private void launchInstallation(File targetFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
// Uri uri = Uri.fromFile(targetFile);
Uri uri = null;
//android 版本的判断
if ( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N){
uri = FileProvider.getUriForFile(getContext(),
getString(R.string.file_provider_authority),
targetFile);
}else{
uri = Uri.fromFile(targetFile);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
startActivity(intent);
private void launchInstallation(File targetFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
// Uri uri = Uri.fromFile(targetFile);
Uri uri = null;
if ( android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N){
uri = FileProvider.getUriForFile(getContext(),
getString(R.string.file_provider_authority),
targetFile);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}else{
uri = Uri.fromFile(targetFile);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivityForResult(intent, 0);// 如果用户取消安装的话,
}
//系统安装回调
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
System.out.println("AboutUsFragment.onActivityResult requestCode=============>"+requestCode+" =======resultCode===================>"+resultCode);
//若用户在更新下载apk之后,点击了“确定”,将安装进程杀死
if (resultCode!=0){
android.os.Process.killProcess(android.os.Process.myPid());
}
}
即:在Intent跳转时,使用startActivityForResult(intent, 0);方法,获取到安装回调的结果,而根据回调的结果,可以进行对应的处理:当用户选择“确定”,将安装进程杀死,这样就解决了以上出现的问题。
但是,这样处理之后,可能还是会有一个问题:更新安装完成之后,选择【打开】、【完成】对应用的影响。当【打开】,进入到应用时,按Home键,之后再从桌面点击icon启动应用时,应用会从首启动页进行启动,没有按照我们期望的那样回到之前的页面;而【完成】则没有这样的问题。
根据网上很多人提供的方法,很显然是因为重新执行了应用的首启动项的onCreate()方法引起的,所有,找打了问题的原因,相应的可以在onCreate()方法中做处理:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/**
* Android安装apk后点击[完成]和[打开]的区别 {泪(xuè)的教训}
1、点击[打开]-->安装apk后直接点击[打开]启动app,按home键后再次点击app icon启动应用时会出现每次都从MAIN Activity重新启动,而不是进入原来界面的问题(某些机型、系统存在此问题);
2、点击[完成]-->点击完成后,从桌面点击app icon启动应用则不会出现[1]中的问题;
*/
if (!isTaskRoot()){
Intent intent = getIntent();
String action = intent.getAction();
if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {
finish();
return;
}
}
}
这个方法可以解决部分机型因为【打开】而引起的Home重新启动问题,但是并不是所有的机型都能生效,大家有什么比较好的方案,可以一起交流。
参考博客:https://blog.csdn.net/mq2856992713/article/details/72858249