如何机智地实现在线更新APK

Android 6.0 以后出现的运行时权限,对于用户来说的确是好事,但也未必是好事。
不过,对于开发来说,那可真不是好事。

对于用户来说:

  • 可以自主控制我能否给你想要的权限,看似拥有选择自由。
  • 但,好程序需要权限你不给,肯定有些功能你用不了;对于流氓软件,你不给权限还不能用;你说选择自由么?
  • 流氓软件真想要盗取用户的隐私,用户是防不胜防的。其实,对于用户最重要的是,选择一个值得信赖的软件。

对于开发来说:

  • 应该想得比较多的就是,我该怎么实现用户拒绝了权限,也能提供类似的功能呢!

这一波,我想谈谈如何机智的实现在线更新APK,在用户不给存储权限的情况下。希望能帮助像我一样走了弯路的朋友。


存储目录介绍

  • 系统目录
Method Result
Environment.getDataDirectory() /data
Environment.getDownloadCacheDirectory() /cache
Environment.getRootDirectory() /system
  • 外部存储目录
Method Result
Environment.getExternalStorageDirectory() /storage/sdcard0
Environment.getExternalStoragePublicDirectory(DIRECTORY_ALARMS) /storage/sdcard0/Alarms
Environment.getExternalStoragePublicDirectory(DIRECTORY_DCIM) /storage/sdcard0/DCIM
Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) /storage/sdcard0/Download
Environment.getExternalStoragePublicDirectory(DIRECTORY_MOVIES) /storage/sdcard0/Movies
Environment.getExternalStoragePublicDirectory(DIRECTORY_MUSIC) /storage/sdcard0/Music
Environment.getExternalStoragePublicDirectory(DIRECTORY_NOTIFICATIONS) /storage/sdcard0/Notifications
Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES) /storage/sdcard0/Pictures
Environment.getExternalStoragePublicDirectory(DIRECTORY_PODCASTS) /storage/sdcard0/Podcasts
Environment.getExternalStoragePublicDirectory(DIRECTORY_RINGTONES) /storage/sdcard0/Ringtones
  • 应用程序目录
Method Result
getCacheDir() /data/data/package/cache
getFilesDir() /data/data/package/files
getFilesDir().getParent() /data/data/package
  • 应用外部存储目录
Method Result
getExternalCacheDir() /storage/sdcard0/Android/data/package/cache
getExternalFilesDir(null) /storage/sdcard0/Android/data/package/files
getExternalFilesDir(DIRECTORY_ALARMS) /storage/sdcard0/Android/data/package/files/Alarms
getExternalFilesDir(DIRECTORY_DCIM) /storage/sdcard0/Android/data/package/files/DCIM
getExternalFilesDir(DIRECTORY_DOWNLOADS) /storage/sdcard0/Android/data/package/files/Download
getExternalFilesDir(DIRECTORY_MOVIES) /storage/sdcard0/Android/data/package/files/Movies
getExternalFilesDir(DIRECTORY_MUSIC) /storage/sdcard0/Android/data/package/files/Music
getExternalFilesDir(DIRECTORY_NOTIFICATIONS) /storage/sdcard0/Android/data/package/files/Notifications
getExternalFilesDir(DIRECTORY_PICTURES) /storage/sdcard0/Android/data/package/files/Pictures
getExternalFilesDir(DIRECTORY_PODCASTS) /storage/sdcard0/Android/data/package/files/Podcasts
getExternalFilesDir(DIRECTORY_RINGTONES) /storage/sdcard0/Android/data/package/files/Ringtones

APK 存储目录分析

需要权限的存储目录

  • 外部存储目录
  • 应用外部存储目录

即:读取SD卡的都需要存储权限。
既然前面说了,不要存储权限,那下载的APK就无法存储在该目录了,淘汰。

系统目录

额。这个目录也还是算了。除非你的apk 升级到root权限,或者所有用户的手机都是root过后的。

应用程序目录

也就只剩下它了,这个目录存储apk,安装能顺利么?

  • 问题:apk下载存储没问题,但安装等待一段时间后提示 “应用未安装”。
  • 什么原因呢:原来 我们下载的apk文件存储的地址,也是有权限问题的,对于安装程序来说,它是无法读取该目录下的apk。此权限问题非彼权限问题。
  • 如何解决呢?:执行以下代码修改权限即可。(注意事项:如果我们在data/data/package name/files/ 目录下又建立了新目录,然后把apk文件放在该新目录下。这个时候这个新目录的权限也要修改为777,不然只改apk文件的权限也是不行的。)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private Uri getApkUri(File apkFile) {
    //如果没有设置 SDCard 写权限,或者没有 SDCard,apk 文件保存在内存中,需要授予权限才能安装
    try {
    String[] command = {"chmod", "777", apkFile.toString()};
    ProcessBuilder builder = new ProcessBuilder(command);
    builder.start();
    } catch (IOException ignored) {
    }
    Uri uri = Uri.fromFile(apkFile);
    return uri;
    }

    APK 安装兼容Android7.0及以上

  • 在Manifest文件中指定provider

    • 创建UpdateApkFileProvider文件
      1
      2
      3
      4
      import android.support.v4.content.FileProvider;

      public class UpdateApkFileProvider extends FileProvider {
      }
    • 创建update_apk_paths.xml文件
      元素必须包含一到多个子元素。这些子元素用于指定共享文件的目录路径,必须是这些元素之一:
      • files-path :内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;
      • cache-path :内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;
      • external-path :外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;
      • external-files-path :外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;
      • external-cache-path :外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?xml version="1.0" encoding="utf-8"?>
    <paths>
    <external-path
    path="."
    name="apk_update" />
    <external-files-path
    path="."
    name="apk_update"/>
    </paths>

    path 属性代表当前目录下的子目录路径,之所以需要指定目录,是为了帮助我们把当前path下访问受限的 file:// URI 转化为可以授权共享的 content:// URI。( . 表示当前目录,可只写某某子目录,比如:”/IMG””)

    • 配置provider
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
       <!--apk更新相关兼容android N-->
      <application>
      <provider
      android:name=".UpdateApkFileProvider"
      android:authorities="${applicationId}.update.provider"
      android:exported="false"
      android:grantUriPermissions="true">

      <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/update_apk_paths"/> <!-- 这里用到了上面的update_apk_paths.xml -->

      </provider>
      </application>
  • 安装一个apk

    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
    String authorities ="该authorities为自定义的provider UpdateApkFileProvider的authorities值";

    protected void installApk(File file) {
    if (file != null) {
    Uri uri = Uri.fromFile(file);
    Intent intent = new Intent(Intent.ACTION_VIEW);
    // 由于没有在Activity环境下启动Activity,设置下面的标签
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) { //判读版本是否在7.0及以上
    try {
    uri = FileProvider.getUriForFile(Activity, authorities, file);
    } catch (Exception e) {
    e.printStackTrace();
    }
    //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
    }else{
    intent.setDataAndType(uri,
    "application/vnd.android.package-archive");
    }
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
    Activity.startActivity(intent);
    }
    }

特别注意

  • 异常
    Package packageName new Target SDK 21 doesn’t support runtime permissions but the old target SDK 23 does.

  • 解释
    该异常是在更新apk的时候产生的,一般情况下遇到该问题,很难发现导致的原因,因为这个异常你还真不容易从错误日志发现。一看异常,应该大家都能明白。哎,原来是新版本的 target SDK 低于23,而旧版本的 target SDK 高于 23。所以需要特别注意:旧版本如果 target SDK 高于 23了,新版本想要再低于23是不可再能覆盖安装旧版本的。

相关参考