Android app自动更新总结

1.配置:

1.1 AndroidManifest.xml中添加权限和FileProvider:

  1. ——————————————————————————————————————–
  2. <uses-permission android:name=“android.permission.INTERNET”/>
  3. <uses-permission android:name=“android.permission.READ_EXTERNAL_STORAGE”/>
  4. <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE”/>
  5. <uses-permission android:name=“android.permission.REQUEST_INSTALL_PACKAGES” />
  6. ——————————————————————————————————————–
  7. <provider
  8. android:name=“androidx.core.content.FileProvider”
  9. android:authorities=“com.fengzhi.wuyemanagement.fileprovider”
  10. android:grantUriPermissions=“true”
  11. android:exported=“false”>
  12. <meta-data
  13. android:name=“android.support.FILE_PROVIDER_PATHS”
  14. android:resource=“@xml/file_paths” />
  15. </provider>

1.2 新建文件(路径:res\xml\file_paths.xml):

  1. <paths>
  2. <external-path path=“.” name=“external_storage_root” />
  3. </paths>

1.3 (app的)build.gradle:

  1. implementation “com.lzy.net:okgo:3.0.4”//okgo 网络请求
  2. implementation ‘com.google.code.gson:gson:2.8.2’//gson
  3. implementation “org.permissionsdispatcher:permissionsdispatcher:4.3.1”//权限
  4. annotationProcessor “org.permissionsdispatcher:permissionsdispatcher-processor:4.3.1”//权限

2.这里以点击按钮进行更新为例:

2.1 核心代码:

  1. private int version;
  2. /* 更新进度条 */
  3. private ProgressBar mProgress;
  4. private AlertDialog mDownloadDialog;
  5. ——————————————————————————————————————–
  6. //点击按钮,检查权限,,,检查更新的方法
  7. @NeedsPermission({Manifest.permission.READ_EXTERNAL_STORAGE,
  8. Manifest.permission.WRITE_EXTERNAL_STORAGE,
  9. Manifest.permission.REQUEST_INSTALL_PACKAGES})
  10. protected void checkUpdate() {
  11. showLoadingDialog(“检测更新中…”);
  12. version = AppUpdateUtil.getAppVersionCode(this);//检查当前版本号
  13. // 调用方法,,,接口的具体实现,接收传过来的参数,再调自己的方法,
  14. requestAppUpdate(version, new DataRequestListener<UpdateAppBean>() {
  15. @Override
  16. public void success(UpdateAppBean data) {
  17. // 返回的json,getStatus为0时,去下载apk文件,这里是下载apk文件的方法
  18. updateApp(data.getData().getApk_url());
  19. }
  20. @Override
  21. public void fail(String msg) {
  22. // 返回的json,getStatus为1时,提示:”已是*新版本!”
  23. SToast(msg);
  24. dismissLoadingDialog();
  25. }
  26. });
  27. }
  28. //检查版本号,*次请求(post),,,UpdateAppBean根据服务器返回生成
  29. private void requestAppUpdate(int version, final DataRequestListener<UpdateAppBean> listener) {
  30. OkGo.<String>post(Const.HOST_URL + Const.UPDATEAPP).params(“version”, version).execute(new StringCallback() {
  31. @Override
  32. public void onSuccess(Response<String> response) {
  33. Gson gson = new Gson();
  34. UpdateAppBean updateAppBean = gson.fromJson(response.body(), UpdateAppBean.class);
  35. if (updateAppBean.getStatus() == 0) {
  36. listener.success(updateAppBean);
  37. } else {
  38. listener.fail(updateAppBean.getMsg());
  39. }
  40. }
  41. @Override
  42. public void onError(Response<String> response) {
  43. listener.fail(“服务器连接失败”);
  44. dismissLoadingDialog();
  45. }
  46. });
  47. }
  48. //如果有新版本,提示有新的版本,然后下载apk文件
  49. private void updateApp(String apk_url) {
  50. dismissLoadingDialog();
  51. DialogUtils.getInstance().showDialog(this, “发现新的版本,是否下载更新?”,
  52. new DialogUtils.DialogListener() {
  53. @Override
  54. public void positiveButton() {
  55. downloadApp(apk_url);
  56. }
  57. });
  58. }
  59. //下载apk文件并跳转(第二次请求,get)
  60. private void downloadApp(String apk_url) {
  61. OkGo.<File>get(apk_url).tag(this).execute(new FileCallback() {
  62. @Override
  63. public void onSuccess(Response<File> response) {
  64. String filePath = response.body().getAbsolutePath();
  65. Intent intent = IntentUtil.getInstallAppIntent(mContext, filePath);
  66. // 测试过这里必须用startactivity,不能用stratactivityforresult
  67. mContext.startActivity(intent);
  68. dismissLoadingDialog();
  69. mDownloadDialog.dismiss();
  70. mDownloadDialog=null;
  71. }
  72. @Override
  73. public void downloadProgress(Progress progress) {
  74. // showDownloadDialog();
  75. // mProgress.setProgress((int) (progress.fraction * 100));
  76. if (mDownloadDialog == null) {
  77. // 构造软件下载对话框
  78. AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
  79. builder.setTitle(“正在更新”);
  80. // 给下载对话框增加进度条
  81. final LayoutInflater inflater = LayoutInflater.from(mContext);
  82. View v = inflater.inflate(R.layout.item_progress, null);
  83. mProgress = (ProgressBar) v.findViewById(R.id.update_progress);
  84. builder.setView(v);
  85. mDownloadDialog = builder.create();
  86. mDownloadDialog.setCancelable(false);
  87. mDownloadDialog.show();
  88. }
  89. mProgress.setProgress((int) (progress.fraction * 100));
  90. }
  91. });
  92. }

2.2 DataRequestListener:

  1. public interface DataRequestListener<T> {
  2. //请求成功
  3. void success(T data);
  4. //请求失败
  5. void fail(String msg);
  6. }

2.3 AppUpdateUtil:

  1. /**
  2. * 获取App版本码
  3. *
  4. * @param context 上下文
  5. * @return App版本码
  6. */
  7. public static int getAppVersionCode(Context context) {
  8. return getAppVersionCode(context, context.getPackageName());
  9. }

2.4 IntentUtil:

  1. public class IntentUtil {
  2. /**
  3. * 获取安装App(支持7.0)的意图
  4. *
  5. * @param context
  6. * @param filePath
  7. * @return
  8. */
  9. public static Intent getInstallAppIntent(Context context, String filePath) {
  10. //apk文件的本地路径
  11. File apkfile = new File(filePath);
  12. if (!apkfile.exists()) {
  13. return null;
  14. }
  15. Intent intent = new Intent(Intent.ACTION_VIEW);
  16. Uri contentUri = FileUtil.getUriForFile(context, apkfile);
  17. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  18. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  19. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  20. }
  21. intent.setDataAndType(contentUri, “application/vnd.android.package-archive”);
  22. return intent;
  23. }

2.5 FileUtil:

  1. /**
  2. * 将文件转换成uri(支持7.0)
  3. *
  4. * @param mContext
  5. * @param file
  6. * @return
  7. */
  8. public static Uri getUriForFile(Context mContext, File file) {
  9. Uri fileUri = null;
  10. if (Build.VERSION.SDK_INT >= 24) {
  11. fileUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + “.fileprovider”, file);
  12. } else {
  13. fileUri = Uri.fromFile(file);
  14. }
  15. return fileUri;
  16. }

 

Android 7.0 文件权限

1 FileProvider方式

这是谷歌官方推荐的解决方案。即使用FileProvider来生成一个content://格式的URI。具体实现方式如下:

  1. manifest声明
    在manifest中声明一个provider。name(即类名)为android.support.v4.content.FileProvider。
  1. <manifest>
  2. <application>
  3. <provider
  4. android:name=“android.support.v4.content.FileProvider”
  5. android:authorities=“com.mydomain.fileprovider”
  6. android:exported=“false”
  7. android:grantUriPermissions=“true”>
  8. <meta-data
  9. android:name=“android.support.FILE_PROVIDER_PATHS”
  10. android:resource=“@xml/file_paths” />
  11. </provider>
  12. </application>
  13. </manifest>

其中authorities可以自定义。为了避免和其它app冲突,*好带上自己app的包名。file_paths.xml中编写该Provider对外提供文件的目录。文件放置在res/xml/下。
2.编写file_paths.xml
文件格式如下:

  1. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  2. <files-path name=“my_images” path=“images/”/>
  3. </paths>

内部的element可以是files-pathcache-pathexternal-pathexternal-files-pathexternal-cache-path,分别对应Context.getFilesDir(),Context.getCacheDir(),Environment.getExternalStorageDirectory(),Context.getExternalFilesDir(),Context.getExternalCacheDir()等几个方法。后来翻看源码发现还有一个没有写进文档的,但是也可以使用的element,是root-path,直接对应文件系统根目录。不过既然没有写进文档中,其实还是有将来移除的可能的。使用的话需要注意一下风险。

3.在Java代码当中使用
以分享一个图片为例:

  1. File file = …; //要分享的图片文件
  2. Uri uri = FileProvider.getUriForFile(context, “com.mydomain.fileprovider”, file); //第二个参数是manifest中定义的`authorities`
  3. Intent intent = new Intent(Intent.ACTION_SEND);
  4. intent.setType(“image/*”);
  5. intent.putExtra(Intent.EXTRA_TITLE, title);
  6. intent.putExtra(Intent.EXTRA_TEXT, text);
  7. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //这一步很重要。给目标应用一个临时的授权。
  8. startActivity(intent); //或者其它*终处理方式

2 VmPolicy方式

以上方法固然是推荐使用的,正确的方法。但是我在实际开发中遇到这样的问题。某些应用(此处点名新浪微博)根本无法理解一个指向文件的content://格式的URI。新浪微博接收到这类URI之后,无法加载图片,并会在点击发送微博时崩溃。
另一方面,新浪微博对权限管理的处理采取了一种比较流氓的方式。它会在启动时申请文件读写权限,而如果拒*该权限的话,居然就直接退出了。我反正是不信什么需要文件权限来放缓存放数据的说辞。放缓存放数据有着一堆不需要权限的目录可用。但是这样一来,我们其实是不需要担心传递一个file://格式URI过去而对方没有权限的。
话说回来,如何解决这一问题呢?我在调研的时候观察到严格模式的一个方法:StrictMode.VmPolicy.Builder.detectFileUriExposure()。顾名思义,调用这个方法就会检测FileUriExposure这件事。这个方法其实从API18就有了,是不是有可能在API24变成了默认选项呢?
在Application.onCreate加入如下代码,置入一个不设防的VmPolicy:

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  2. StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
  3. StrictMode.setVmPolicy(builder.build());
  4. }

再用旧的方式直接把file://格式的URI发送出去。居然成功了,没有再抛出FileUriExposedException。

3 小结

*终我采取的综合方案是,先使用PackageManager.checkPermission检测对方的app有没有取得文件读写权限。如果有的话,给对方发送file://格式URI。如果没有的话,给对方发送FileProvider生成的URI并临时授权。
原本一个有标准解决方案的问题,因为某些应用不遵循规范而需要做更多的调研和workaround。实在是麻烦。希望可以帮助到遇到同样问题的人。