Android包下载安装流程

发布时间 2023-10-08 14:36:40作者: Tears_fg

背景

应用上线前,必不可少的需要应用升级操作,android选择的是在应用内升级,这里选择系统自带的downloadManager进行操作。实现应用内升级及通知栏升级进度显示。
我们首先需要给应用添加存储权限和允许应用安装包的权限。

1.添加权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

2.版本号判断

判断当前版本号是否需要升级
@JvmStatic //Java使用该方法
fun checkVersion(apkInfo: UpgradeBean?): Boolean {
   if (apkInfo == null) {
      return false
   }
   if (apkInfo.isUpdate == false) {
      return false
   }
   val oldVersion = AppUtils.getAppVersionName().filter { it.isDigit() }
      .toInt()//本地版本号
   //本地版本号 = BaseApplication.getContext().getPackageManager().getPackageInfo(BaseApplication.getContext().getPackageName(), PackageManager.GET_CONFIGURATIONS).versionCode

   val version = if (!apkInfo.latestVersion.isNullOrEmpty()) apkInfo.latestVersion.filter { it.isDigit() }
      ?.toInt() ?: -1
   else -1
   //filter过滤器过滤字符,isDigit()只提取数字,防止其他字符混入
   if (version > oldVersion) {
      return true
   }
   return false
}

3.申请存储权限

6.0以上存储需要申请写入权限,这里使用的XXpermission,代码仓库地址:https://github.com/getActivity/XXPermissions
 //1.申请权限
            XXPermissions.with(mContext)
                // 申请单个权限
                .permission(Permission.WRITE_EXTERNAL_STORAGE)
                .request(object : OnPermissionCallback {

                    override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
                        upgradeBean ?: return
                        UpgradeManager.upgradeApk(mContext, upgradeBean!!, processCallBack = { bean ->
                            if (upgradeBean?.updateType == "NORMAL" && bean.status == ProgressState.download_start) {
                                ToastUtils.showShort("正在下载中...")
                                dismissCallback.invoke()
                                dismiss()
                                return@upgradeApk
                            }
                            bind.tvUpgrade.isEnabled = false
                            bind.tvUpgrade.setSolidColor(KBSkinUtil.getColor(KBColor.primary, 0.3f))
                            bind.tvUpgrade.text = "下载中..."

                            when (bean.status) {
                                ProgressState.download_start -> {
                                    progressBar.isShow(true)
                                    progressBar.setProgress(0f)
                                }

                                ProgressState.downloading -> {
                                    progressBar.setProgress(bean.progress.toFloat())
                                }

                                ProgressState.download_success -> {
                                    progressBar.isShow(false)
                                    progressBar.setProgress(0f)
//                                    dismiss()
                                    bind.tvUpgrade.isEnabled = true
                                    bind.tvUpgrade.setSolidColor(KBSkinUtil.getColor(KBColor.primary))
                                    bind.tvUpgrade.text = "立即更新"
                                }

                                else -> {
                                    //失败
                                    progressBar.isShow(false)
                                    progressBar.setProgress(0f)
                                    bind.tvUpgrade.isEnabled = true
                                    bind.tvUpgrade.setSolidColor(KBSkinUtil.getColor(KBColor.primary))
                                    bind.tvUpgrade.text = "立即更新"
                                }
                            }
                        })
                    }

                    override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
                        if (doNotAskAgain) {
                            // 如果是被永久拒绝就跳转到应用权限系统设置页面
                            XXPermissions.startPermissionActivity(context, permissions)
                        } else {
                            toast("获取存储权限失败")
                        }
                    }
                })
        }
    }
需要注意的是,在android10以上的设备需要适配分区存储,如果暂未适配,请在manifast文件中添加
android:requestLegacyExternalStorage="true"
在android11以上必须适配分区存储,andorid11以上系统对手机存储进行了分区,写入权限已经不存在,应用只能在自己私有目录及系统分配的公共目录(例如,download)进行读取操作。但是android10及以下还是需要申请动态存储权限。

4.包下载

下载包之前先判断包存不存在,这里可以使用后端返回包的md5值来进行检测包的完整性安装。
使用DownloadManager可配置在通知栏自定义titile和描述。
fun upgradeApk(context: Context, upgradeInfo: UpgradeBean, processCallBack: (data: ProgressBean) -> Unit = { data -> }) {
   //设置apk下载地址:本机存储的download文件夹下
   val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
   //找到该路径下的对应名称的apk文件,有可能已经下载过了
   val file = File(dir, "kqiu-v${upgradeInfo.latestVersion}.apk")

   //开辟线程
   MainScope().launch {
      val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
      // 1、判断是否下载过apk
      if (file.exists() && false) {
         val authority: String = context.applicationContext.packageName + ".fileprovider"
         // "content://" 类型的uri   --将"file://"类型的uri变成"content://"类型的uri
         val uri = FileProvider.getUriForFile(context, authority, file)
         // 5、安装apk, content://和file://路径都需要
         requestPermission(context, uri, file)
      } else {
         // 2、DownloadManager配置
         val request = DownloadManager.Request(Uri.parse(encodeGB(upgradeInfo.resURL)))  //处理中文下载地址
         // 设置下载路径和下载的apk名称
         request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "kqiu-v${upgradeInfo.latestVersion}.apk")
         request.setTitle("K球")
         request.setDescription("下载中.....")
         // 下载时在通知栏内显示下载进度条
         request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
         // 设置MIME类型,即设置下载文件的类型, 如果下载的是android文件, 设置为application/vnd.android.package-archive
         request.setMimeType("application/vnd.android.package-archive")
         // 3、获取到下载id
         downloadId = downloadManager.enqueue(request)
         processCallBack.invoke(ProgressBean(ProgressState.download_start, 0))

         // 开辟IO线程
         MainScope().launch(Dispatchers.IO) {
            // 4、动态更新下载进度
            val success = checkDownloadProgress(
               downloadManager,
               downloadId,
               processCallBack,
               file
            )
            MainScope().launch {
               if (success) {
                  processCallBack.invoke(ProgressBean(ProgressState.download_success, 100))
                  // 下载文件"content://"类型的uri ,DownloadManager通过downloadId
                  val uri = downloadManager.getUriForDownloadedFile(downloadId)
                  // 通过downLoadId查询下载的apk文件转成"file://"类型的uri
                  val file = queryDownloadedApk(context, downloadId)
                  // 5、安装apk
                  requestPermission(context, uri, file)
               } else {
                  ToastUtil.showNormal("下载失败")
                  if (file.exists()) {
                     // 当不需要的时候,清除之前的下载文件,避免浪费用户空间
                     file.delete()
                  }
                  // 删除下载任务和文件
                  downloadManager.remove(downloadId)
                  // 隐藏进度条显示按钮,重新下载
                  processCallBack.invoke(ProgressBean(ProgressState.download_fail, 0))
               }
               cancel()
            }
            cancel()
         }
      }
      cancel()
   }
}
查询包下载进度。
private fun queryDownloadedApk(context: Context, downloadId: Long): File? {
   var targetApkFile: File? = null
   val downloader = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
   if (downloadId != -1L) {
      val query = DownloadManager.Query()
      query.setFilterById(downloadId)
      query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)
      val cur: Cursor? = downloader.query(query)
      if (cur != null) {
         if (cur.moveToFirst()) {
            val uriString: String =
               cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
            if (!TextUtils.isEmpty(uriString)) {
               targetApkFile = Uri.parse(uriString).path?.let { File(it) }
            }
         }
         cur.close()
      }
   }
   return targetApkFile
}
处理下载地址为中文编码的问题
private fun encodeGB(downloadUrl: String): String? {
   //转换中文编码
   val split = downloadUrl.split("/".toRegex()).toTypedArray()
   for (i in 1 until split.size) {
      try {
         split[i] = URLEncoder.encode(split[i], "GB2312")
      } catch (e: UnsupportedEncodingException) {
         e.printStackTrace()
      }
      split[0] = split[0] + "/" + split[i]
   }
   split[0] = split[0].replace("\\+".toRegex(), "%20") //处理空格
   return split[0]
}

6.处理未知来源应用权限

Android 8.0 中未知应用安装权限的开关默认是被关闭的 ,需要用户手动打开允许【未知来源应用权限】才能够安装。在安装之前先判断是否有安装权限,有直接安装,没有,申请后安装。
private fun requestPermission(context: Context, uri: Uri, file: File?) {
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      val haveInstallPermission = context.packageManager.canRequestPackageInstalls();
      if (haveInstallPermission) {
         installAPK(context, uri, file)
      } else {
         XXPermissions.with(context)
            .permission(Permission.REQUEST_INSTALL_PACKAGES)
            //.interceptor(PermissionInterceptor())
            .request(object : OnPermissionCallback {
               override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
                  if (allGranted) {
                     installAPK(context, uri, file)
                  } else {
                     toast("获取安装权限失败")
                  }
               }

               override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
                  super.onDenied(permissions, doNotAskAgain)
                  if (doNotAskAgain) {
                     // 如果是被永久拒绝就跳转到应用权限系统设置页面
                     XXPermissions.startPermissionActivity(context, permissions)
                  } else {
                     toast("获取安装权限失败")
                  }
               }
            })
      }

   } else {
      installAPK(context, uri, file)
   }

}
实际测试中,华为手机申请了权限后也没弹出允许未知来源的系统提示,而小米手机在调用context.packageManager.canRequestPackageInstalls()后,返回为false,跳到允许未知来源的设置里。如果不申请权限,也可以安装,但是用户手动关闭未知来源的权限后,应用就安装不了了。

8.安装包

在android7.0以后,私有目录被限制访问,给其他应用传递 file:// URI 类型的Uri,可能会导致接受者无法访问该路径。 因此,在Android7.0中尝试传递 file:// URI 会触发 FileUriExposedException。
如果我们需要安装包,需要适配android7.0文件应用共享,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用  FileProvider类。
使用fileProvider的步骤大致如下:
1.在manifest清单文件中注册provider。
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="{packageName}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true"
    tools:replace="android:authorities">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"
        tools:replace="android:resource" />
</provider>
2.指定共享的目录。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-path
            name="camera_photos"
            path="" />

        <external-cache-path
            name="external-cache"
            path="" />

        <path>
            <root-path name="files_apk"
                path="/"/>
        </path>
    </paths>

</resources>
  • <files-path/>代表的根目录: Context.getFilesDir()
  • <external-path/>代表的根目录: Environment.getExternalStorageDirectory()
  • <cache-path/>代表的根目录: getCacheDir()
使用fileprovider。
android7.0以上通过FileProvider创建一个content类型的URI
val authority: String = context.applicationContext.packageName + ".fileprovider"
// "content://" 类型的uri   --将"file://"类型的uri变成"content://"类型的uri
val uri = FileProvider.getUriForFile(context, authority, file)
拿到uri进行安装包的请求。
private fun installAPK(context: Context, apkUri: Uri, apkFile: File?) {
   val intent = Intent()
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      //安卓7.0版本以上安装
      intent.action = Intent.ACTION_VIEW
      intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
      intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)//添加这一句表示对目标应用临时授权该Uri所代表的文件
      intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
   } else {
      //安卓6.0-7.0版本安装
      intent.action = Intent.ACTION_DEFAULT
      intent.addCategory(Intent.CATEGORY_DEFAULT)
      intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
      intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
      apkFile?.let {
         intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
      }
   }
   try {
      context.startActivity(intent)
   } catch (e: Exception) {
      e.printStackTrace()
   }
}

注意事项

以上,应用内安装包的流程就完成了,主要注意的点就是针对android各个版本的特性适配。
  1. android6.0动态权限
  2. android7.0文件共享适配
  3. android8.0未知来源权限适配
  4. android11.0分区存储适配