xpak兼容更多的apks安装
This commit is contained in:
@ -64,8 +64,6 @@ object PackageInstaller {
|
||||
showUnzipToast: Boolean,
|
||||
ignoreAsVGame: Boolean,
|
||||
) {
|
||||
val pkgPath = downloadEntity.path
|
||||
val isXapk = XapkInstaller.XAPK_EXTENSION_NAME == pkgPath.getExtension()
|
||||
val isDownloadAsVGame = downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE) == Constants.VGAME
|
||||
|| downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE) == Constants.DUAL_DOWNLOAD_VGAME
|
||||
|
||||
@ -99,7 +97,7 @@ object PackageInstaller {
|
||||
context.startActivity(mainIntent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
} else {
|
||||
if (isXapk) {
|
||||
if (XapkInstaller.isXapk(downloadEntity)) {
|
||||
XapkInstaller.install(context, downloadEntity, showUnzipToast)
|
||||
} else {
|
||||
install(context, downloadEntity.isPlugin, downloadEntity.path, downloadEntity)
|
||||
@ -211,9 +209,6 @@ object PackageInstaller {
|
||||
pkgPath: String,
|
||||
sessionId: Int = -1
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return
|
||||
}
|
||||
val installer = context.packageManager.packageInstaller
|
||||
val session = installer.openSession(sessionId)
|
||||
// 监听安装回调的组件,可以是Activity、Service或者是BroadcastReceiver
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
package com.gh.common.xapk
|
||||
|
||||
import com.lightgame.download.DownloadEntity
|
||||
|
||||
interface IXapkUnzipListener {
|
||||
fun onProgress(downloadEntity: DownloadEntity, unzipPath: String, unzipSize: Long, unzipProgress: Long)
|
||||
|
||||
fun onNext(downloadEntity: DownloadEntity, unzipPath: String)
|
||||
|
||||
fun onCancel(downloadEntity: DownloadEntity)
|
||||
|
||||
fun onFailure(downloadEntity: DownloadEntity, exception: Exception)
|
||||
|
||||
fun onSuccess(downloadEntity: DownloadEntity)
|
||||
}
|
||||
@ -52,6 +52,12 @@ import com.gh.gamecenter.core.utils.SPUtils
|
||||
*
|
||||
* obb文件直接解压至根目录即可,无需操作文件
|
||||
* apk文件这解压的gh-files文件夹中
|
||||
*
|
||||
* update: 2025/4/29
|
||||
* 简单来说,有两种XAPK文件的格式:
|
||||
* XAPK = Android App Bundle(基础APK + 配置APK) 【downloadentity.format == xapk(apks)】
|
||||
* XAPK = APK文件 + OBB数据文件 【downloadentity.format == xapk】
|
||||
*
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object XapkInstaller : XApkUnZipCallback, XApkUnZipOutputFactory {
|
||||
@ -92,85 +98,90 @@ object XapkInstaller : XApkUnZipCallback, XApkUnZipOutputFactory {
|
||||
|
||||
private val mPendingSessionInfoMap = HashMap<String, XapkPendingSessionInfo>()
|
||||
|
||||
fun isXapk(downloadEntity: DownloadEntity) = XAPK_EXTENSION_NAME == downloadEntity.path.getExtension()
|
||||
|
||||
private fun isXapkApks(downloadEntity: DownloadEntity) = downloadEntity.format == Constants.XAPK_APKS_FORMAT
|
||||
|
||||
/**
|
||||
* precondition: assert_true(isXapk())
|
||||
*
|
||||
*/
|
||||
// 按并行解压
|
||||
@JvmStatic
|
||||
fun install(context: Context, downloadEntity: DownloadEntity, showUnzipToast: Boolean = false) {
|
||||
this.mContext = context
|
||||
|
||||
val filePath = downloadEntity.path
|
||||
if (XAPK_EXTENSION_NAME == filePath.getExtension()) {
|
||||
if (MiuiUtils.isMiui() && !MiuiUtils.isMiuiOptimizationDisabled() && downloadEntity.format == Constants.XAPK_APKS_FORMAT) {// 小米手机开启miui以后,需要引导用户关闭miui优化
|
||||
DialogHelper.showMiuiOptimizationWarning(
|
||||
context,
|
||||
downloadEntity,
|
||||
onHintClick = {
|
||||
val guides = Config.getNewApiSettingsEntity()?.install
|
||||
val miuiOptimizationGuide = guides?.guides?.findLast {
|
||||
it.type == GUIDE_TYPE_MIUI_OPTIMIZATION
|
||||
}
|
||||
if (miuiOptimizationGuide != null) {
|
||||
DirectUtils.directToLinkPage(
|
||||
context,
|
||||
miuiOptimizationGuide.link,
|
||||
MIUI_OPTIMIZATION_WARNING_DIALOG_ENTRANCE,
|
||||
""
|
||||
)
|
||||
}
|
||||
},
|
||||
onConfirmClick = {
|
||||
if (SystemUtils.isDevelopmentSettingsEnabled(context)) {
|
||||
context.startActivity(
|
||||
Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
it.markDismissByTouchInside()
|
||||
it.dismiss()
|
||||
} else {
|
||||
ToastUtils.showToast(context.getString(R.string.miui_open_adb_hint))
|
||||
}
|
||||
val isXapkApks = isXapkApks(downloadEntity)
|
||||
|
||||
if (isXapkApks && MiuiUtils.isMiui() && !MiuiUtils.isMiuiOptimizationDisabled()) {
|
||||
// 小米手机开启miui以后,需要引导用户关闭miui优化
|
||||
DialogHelper.showMiuiOptimizationWarning(
|
||||
context,
|
||||
downloadEntity,
|
||||
onHintClick = {
|
||||
val guides = Config.getNewApiSettingsEntity()?.install
|
||||
val miuiOptimizationGuide = guides?.guides?.findLast {
|
||||
it.type == GUIDE_TYPE_MIUI_OPTIMIZATION
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
PermissionHelper.checkManageAllFilesOrStoragePermissionBeforeAction(context) {
|
||||
val unzipAction = {
|
||||
DownloadManager.getInstance().getDownloadEntitySnapshot(downloadEntity.url, downloadEntity.gameId)
|
||||
?.let {
|
||||
unzipXapkFile(it)
|
||||
if (showUnzipToast) {
|
||||
Utils.toast(mContext, "解压过程请勿退出光环助手!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XAPK (apks) 格式,不在乎 obb 文件夹是否可读
|
||||
if (downloadEntity.format == Constants.XAPK_APKS_FORMAT) {
|
||||
unzipAction.invoke()
|
||||
return@checkManageAllFilesOrStoragePermissionBeforeAction
|
||||
}
|
||||
|
||||
// 以 file 的方式访问 obb 文件夹是否可行
|
||||
val isFileStyleObbFolderReadable =
|
||||
if (systemHasFlaw) {
|
||||
File(Environment.getExternalStorageDirectory().path, "\u200bAndroid/obb").list() != null
|
||||
if (miuiOptimizationGuide != null) {
|
||||
DirectUtils.directToLinkPage(
|
||||
context,
|
||||
miuiOptimizationGuide.link,
|
||||
MIUI_OPTIMIZATION_WARNING_DIALOG_ENTRANCE,
|
||||
""
|
||||
)
|
||||
}
|
||||
},
|
||||
onConfirmClick = {
|
||||
if (SystemUtils.isDevelopmentSettingsEnabled(context)) {
|
||||
context.startActivity(
|
||||
Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
it.markDismissByTouchInside()
|
||||
it.dismiss()
|
||||
} else {
|
||||
File(Environment.getExternalStorageDirectory().path, "Android/obb").list() != null
|
||||
ToastUtils.showToast(context.getString(R.string.miui_open_adb_hint))
|
||||
}
|
||||
|
||||
// 如果是文件夹风格的 obb 文件夹可读,或当前上下文不是 AppCompatActivity,或当前上下文已经被销毁,则直接解压
|
||||
if (isFileStyleObbFolderReadable
|
||||
|| context !is AppCompatActivity
|
||||
|| context.isFinishing
|
||||
) {
|
||||
unzipAction.invoke()
|
||||
} else {
|
||||
unzipWithCulpritHandled(context, downloadEntity.url, unzipAction)
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
PermissionHelper.checkManageAllFilesOrStoragePermissionBeforeAction(context) {
|
||||
val unzipAction = {
|
||||
DownloadManager.getInstance().getDownloadEntitySnapshot(downloadEntity.url, downloadEntity.gameId)
|
||||
?.let {
|
||||
unzipXapkFile(it, isXapkApks)
|
||||
if (showUnzipToast) {
|
||||
Utils.toast(mContext, "解压过程请勿退出光环助手!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XAPK (apks) 格式,不在乎 obb 文件夹是否可读
|
||||
if (isXapkApks) {
|
||||
unzipAction.invoke()
|
||||
return@checkManageAllFilesOrStoragePermissionBeforeAction
|
||||
}
|
||||
|
||||
// 以 file 的方式访问 obb 文件夹是否可行
|
||||
val isFileStyleObbFolderReadable =
|
||||
if (systemHasFlaw) {
|
||||
File(Environment.getExternalStorageDirectory().path, "\u200bAndroid/obb").list() != null
|
||||
} else {
|
||||
File(Environment.getExternalStorageDirectory().path, "Android/obb").list() != null
|
||||
}
|
||||
|
||||
// 如果是文件夹风格的 obb 文件夹可读,或当前上下文不是 AppCompatActivity,或当前上下文已经被销毁,则直接解压
|
||||
if (isFileStyleObbFolderReadable
|
||||
|| context !is AppCompatActivity
|
||||
|| context.isFinishing
|
||||
) {
|
||||
unzipAction.invoke()
|
||||
} else {
|
||||
unzipWithCulpritHandled(context, downloadEntity.url, unzipAction)
|
||||
}
|
||||
} else {
|
||||
throwExceptionInDebug("如果是Apk包请使用PackageInstaller进行安装")
|
||||
PackageInstaller.install(mContext, downloadEntity)
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,13 +261,8 @@ object XapkInstaller : XApkUnZipCallback, XApkUnZipOutputFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private fun unzipXapkFile(downloadEntity: DownloadEntity) {
|
||||
mXApkUnZipper.unzip(
|
||||
XApkUnZipEntry(
|
||||
downloadEntity.path,
|
||||
File(downloadEntity.path)
|
||||
)
|
||||
)
|
||||
private fun unzipXapkFile(downloadEntity: DownloadEntity, isXapkApks: Boolean) {
|
||||
XApkUnZipEntry(downloadEntity.path, File(downloadEntity.path), isXapkApks).let(mXApkUnZipper::unzip)
|
||||
mDownloadEntityMap[downloadEntity.path] = downloadEntity
|
||||
}
|
||||
|
||||
@ -469,13 +475,16 @@ object XapkInstaller : XApkUnZipCallback, XApkUnZipOutputFactory {
|
||||
|
||||
mPendingSessionInfoMap[downloadEntity.path] = XapkPendingSessionInfo(downloadEntity.path, sessionId)
|
||||
AppExecutor.ioExecutor.execute {// 有可能卡顿造成anr
|
||||
PackageInstaller.installMultiple(
|
||||
applicationContext,
|
||||
downloadEntity.packageName,
|
||||
downloadEntity.path,
|
||||
sessionId
|
||||
)
|
||||
NDataChanger.notifyDataChanged(downloadEntity)
|
||||
try {
|
||||
PackageInstaller.installMultiple(
|
||||
applicationContext,
|
||||
downloadEntity.packageName,
|
||||
downloadEntity.path,
|
||||
sessionId
|
||||
)
|
||||
NDataChanger.notifyDataChanged(downloadEntity)
|
||||
} catch (ignore: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,243 +0,0 @@
|
||||
package com.gh.common.xapk
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import com.gh.gamecenter.BuildConfig
|
||||
import com.gh.gamecenter.common.utils.*
|
||||
import com.gh.gamecenter.core.utils.MD5Utils
|
||||
import com.halo.assistant.HaloApp
|
||||
import com.lightgame.download.DownloadEntity
|
||||
import com.lightgame.utils.Utils
|
||||
import net.lingala.zip4j.progress.ProgressMonitor
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
|
||||
class XapkUnzipThread(
|
||||
private var mDownloadEntity: DownloadEntity,
|
||||
private var mUnzipListener: IXapkUnzipListener
|
||||
) : Thread() {
|
||||
|
||||
private val mDefaultBufferSize = 1024 * 1024
|
||||
|
||||
var canceled = false
|
||||
|
||||
override fun run() {
|
||||
super.run()
|
||||
try {
|
||||
val path = mDownloadEntity.path
|
||||
var unzipSize = 0L
|
||||
try {
|
||||
unzipSize = getUnzipSize(path)
|
||||
} catch (e: Exception) {
|
||||
planB()
|
||||
return
|
||||
}
|
||||
|
||||
val msg = FileUtils.isCanDownload(HaloApp.getInstance().application, unzipSize)
|
||||
if (!msg.isNullOrEmpty()) {
|
||||
// 空间不足应该不用刷新页面,保持不变就好
|
||||
Utils.toast(HaloApp.getInstance().application, "设备存储空间不足,请清理后重试!")
|
||||
mUnzipListener.onCancel(mDownloadEntity)
|
||||
return
|
||||
}
|
||||
var unzipProgress = 0L
|
||||
val absolutePath = Environment.getExternalStorageDirectory().absolutePath
|
||||
val xapkFile = File(path)
|
||||
|
||||
val unzipClosure: (zip: ZipFile) -> Unit = { zip ->
|
||||
for (zipEntry in zip.entries().asSequence()) {
|
||||
val outputFile = if (zipEntry.name.getExtension() == XapkInstaller.XAPK_DATA_EXTENSION_NAME) {
|
||||
File(absolutePath + File.separator + zipEntry.name)
|
||||
} else if (zipEntry.name.getExtension() == XapkInstaller.PACKAGE_EXTENSION_NAME) {
|
||||
// apk文件名称 = xapk文件名 + MD5(本身的文件名称) + ".apk"
|
||||
// 如 abc_com.gh.gamecenter.apk 这样的文件名,在使用浏览器安装时系统浏览器可能会抹掉文件类型,导致无法唤起下载完自动安装,这里去掉多个 "." 号,保证浏览器安装正常使用
|
||||
val fileName = xapkFile.nameWithoutExtension + "_" + MD5Utils.getContentMD5(zipEntry.name) + "." + XapkInstaller.PACKAGE_EXTENSION_NAME
|
||||
File(FileUtils.getDownloadPath(HaloApp.getInstance().application, fileName))
|
||||
} else continue // 暂时只需要解压xpk/obb文件
|
||||
|
||||
if (zipEntry.isDirectory) {
|
||||
if (!outputFile.exists()) {
|
||||
throwException("unzip create file path failure", !outputFile.mkdirs())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!outputFile.parentFile.exists()) {
|
||||
throwException("unzip create file path failure", !outputFile.parentFile.mkdirs())
|
||||
}
|
||||
|
||||
// 如果存在同名且大小一致,可以认为该文件已经解压缩(在不解压缩的情况下如果如果获取压缩文件的MD5????)
|
||||
if (!outputFile.exists()) {
|
||||
throwException("unzip create file failure", !outputFile.createNewFile())
|
||||
} else if (outputFile.length() != zipEntry.size) {
|
||||
throwException("unzip delete existing file failure", !outputFile.delete())
|
||||
throwException("unzip create file failure", !outputFile.createNewFile())
|
||||
} else {
|
||||
unzipProgress += zipEntry.size
|
||||
mUnzipListener.onProgress(mDownloadEntity, outputFile.path, unzipSize, unzipProgress)
|
||||
mUnzipListener.onNext(mDownloadEntity, outputFile.path)
|
||||
continue
|
||||
}
|
||||
|
||||
// unzip
|
||||
zip.getInputStream(zipEntry).use { input ->
|
||||
outputFile.outputStream().use { output ->
|
||||
val buffer = ByteArray(mDefaultBufferSize)
|
||||
var bytes = input.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
output.write(buffer, 0, bytes)
|
||||
unzipProgress += bytes
|
||||
bytes = input.read(buffer)
|
||||
if (canceled) {
|
||||
mUnzipListener.onCancel(mDownloadEntity)
|
||||
return@use
|
||||
} else {
|
||||
// 防止多次短时间内多次触发onProgress方法导致阻塞主线程(低端机会出现十分明显的卡顿)
|
||||
debounceActionWithInterval(-1, 500) {
|
||||
mUnzipListener.onProgress(
|
||||
mDownloadEntity,
|
||||
outputFile.path,
|
||||
unzipSize,
|
||||
unzipProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mUnzipListener.onNext(mDownloadEntity, outputFile.path)
|
||||
}
|
||||
}
|
||||
|
||||
// Kotlin 1.4.X 在安卓 4.4 以下使用 use 默认关闭 ZipFile 的流时会触发
|
||||
// java.lang.IncompatibleClassChangeError: interface not implemented 的 Error (Throwable)
|
||||
// 但实测是不影响解压的,所以这里换用 let 不关闭流,确保不闪退,并且不影响解压结果
|
||||
// 帮用户解压了,但游戏能不能安装就看天吧 (毕竟支持4.X的游戏也不多了)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
ZipFile(File(path)).use { unzipClosure.invoke(it) }
|
||||
} else {
|
||||
ZipFile(File(path)).let { unzipClosure.invoke(it) }
|
||||
}
|
||||
mUnzipListener.onSuccess(mDownloadEntity)
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) throw e
|
||||
mUnzipListener.onFailure(mDownloadEntity, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 部分未知情况(有可能是项目配置问题,有可能时Zip包破损)原生的ZipFile无法打开压缩包,则启动以下备用方案
|
||||
* 具体复现机型:小米 9(Android 9)
|
||||
* 具体复现的压缩包:太空竞速2 具体可以咨询陈思雨
|
||||
*
|
||||
* 实现方式请参考:https://github.com/srikanth-lingala/zip4j
|
||||
* 注意使用该库的ZipInputStream依然无法解压,提示头文件丢失
|
||||
*
|
||||
* 后续如果有需要可以直接使用该方法进行解压
|
||||
*/
|
||||
private fun planB() {
|
||||
val zipPath = mDownloadEntity.path
|
||||
|
||||
val absolutePath = Environment.getExternalStorageDirectory().absolutePath
|
||||
|
||||
val zipFile = net.lingala.zip4j.ZipFile(zipPath)
|
||||
val progressMonitor = zipFile.progressMonitor
|
||||
zipFile.isRunInThread = true
|
||||
|
||||
val fileHeaders = zipFile.fileHeaders
|
||||
var unzipSize = 0L
|
||||
for (fileHeader in fileHeaders) {
|
||||
unzipSize += fileHeader.uncompressedSize;
|
||||
}
|
||||
|
||||
var unzipProgress = 0L
|
||||
var completedSize = 0L
|
||||
|
||||
for (fileHeader in fileHeaders) {
|
||||
if (canceled) {
|
||||
mUnzipListener.onCancel(mDownloadEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// 暂时只需要解压xpk/obb文件
|
||||
val extension = fileHeader.fileName.getExtension()
|
||||
if (extension != XapkInstaller.XAPK_DATA_EXTENSION_NAME && extension != XapkInstaller.PACKAGE_EXTENSION_NAME) continue
|
||||
|
||||
|
||||
var unzipPath = ""
|
||||
if (extension == XapkInstaller.XAPK_DATA_EXTENSION_NAME) {
|
||||
unzipPath = absolutePath + File.separator + fileHeader.fileName
|
||||
|
||||
if (hasUnzipFile(unzipPath, fileHeader.uncompressedSize)) {
|
||||
mUnzipListener.onNext(mDownloadEntity, unzipPath)
|
||||
continue
|
||||
}
|
||||
|
||||
zipFile.extractFile(fileHeader.fileName, absolutePath)
|
||||
}
|
||||
|
||||
if (extension == XapkInstaller.PACKAGE_EXTENSION_NAME) {
|
||||
val downloadDir = FileUtils.getDownloadDir(HaloApp.getInstance().application)
|
||||
val unzipFileName = File(zipPath).nameWithoutExtension + "_" + fileHeader.fileName
|
||||
unzipPath = downloadDir + File.separator + unzipFileName
|
||||
|
||||
if (hasUnzipFile(unzipPath, fileHeader.uncompressedSize)) {
|
||||
mUnzipListener.onNext(mDownloadEntity, unzipPath)
|
||||
continue
|
||||
}
|
||||
|
||||
zipFile.extractFile(fileHeader.fileName, downloadDir, unzipFileName)
|
||||
}
|
||||
|
||||
throwExceptionInDebug("check unzipPath", unzipPath.isEmpty())
|
||||
|
||||
// 回调太频繁了,变态吗? 4K回调一次,还不能改,FUCK
|
||||
var filterCounter = 0
|
||||
val filterInterval = 1024 * 10
|
||||
while (progressMonitor.state != ProgressMonitor.State.READY) {
|
||||
filterCounter++
|
||||
if (filterCounter % filterInterval == 0) {
|
||||
unzipProgress = completedSize + progressMonitor.workCompleted
|
||||
mUnzipListener.onProgress(mDownloadEntity, unzipPath, unzipSize, unzipProgress)
|
||||
|
||||
if (canceled) {
|
||||
progressMonitor.isCancelAllTasks = true
|
||||
mUnzipListener.onCancel(mDownloadEntity)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
completedSize += fileHeader.uncompressedSize
|
||||
mUnzipListener.onNext(mDownloadEntity, unzipPath)
|
||||
}
|
||||
|
||||
mUnzipListener.onSuccess(mDownloadEntity)
|
||||
}
|
||||
|
||||
private fun hasUnzipFile(unzipPath: String, uncompressedSize: Long): Boolean {
|
||||
val unzipFile = File(unzipPath)
|
||||
if (unzipFile.exists() || unzipFile.length() == uncompressedSize) return true
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getUnzipSize(path: String): Long {
|
||||
var totalSize = 0L
|
||||
|
||||
val calculateSizeClosure: (zip: ZipFile) -> Unit = { zip ->
|
||||
for (entry in zip.entries()) {
|
||||
totalSize += entry.size
|
||||
}
|
||||
}
|
||||
|
||||
// Kotlin 1.4.X 在安卓 4.4 以下使用 use 默认ZipFile 的流时会触发
|
||||
// java.lang.IncompatibleClassChangeError: interface not implemented 的 Error (Throwable)
|
||||
// 实测是不影响解压,所以这里换用 let 不关闭流,确保不闪退
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
ZipFile(File(path)).use { calculateSizeClosure.invoke(it) }
|
||||
} else {
|
||||
ZipFile(File(path)).let { calculateSizeClosure.invoke(it) }
|
||||
}
|
||||
return totalSize
|
||||
}
|
||||
}
|
||||
@ -8,10 +8,9 @@ import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.gh.common.util.DirectUtils
|
||||
import com.gh.common.util.PackageInstaller
|
||||
import com.gh.common.util.PackageLauncher
|
||||
import com.gh.common.util.PackageUtils
|
||||
import com.gh.common.util.*
|
||||
import com.gh.common.xapk.XapkInstaller
|
||||
import com.gh.common.xapk.XapkUnzipStatus
|
||||
import com.gh.download.DownloadManager
|
||||
import com.gh.gamecenter.R
|
||||
import com.gh.gamecenter.ShellActivity
|
||||
@ -80,6 +79,15 @@ class SpecialDownloadDialogFragment : BaseDraggableDialogFragment() {
|
||||
DownloadStatus.done -> {
|
||||
downloadBtn?.setProgress(1.0F)
|
||||
downloadBtn?.setText(context?.getString(com.gh.gamecenter.feature.R.string.install) ?: "")
|
||||
val xapkStatus = downloadEntity.meta[XapkInstaller.XAPK_UNZIP_STATUS]
|
||||
when {
|
||||
(XapkUnzipStatus.SUCCESS.name == xapkStatus || XapkUnzipStatus.INSTALLED.name == xapkStatus) && XapkInstaller.isInstalling(downloadEntity.path) -> {
|
||||
downloadBtn?.setText(context?.getString(com.gh.gamecenter.feature.R.string.installing) ?: "")
|
||||
}
|
||||
XapkUnzipStatus.UNZIPPING.name == xapkStatus -> {
|
||||
downloadBtn?.setText(context?.getString(com.gh.gamecenter.feature.R.string.unzipping) ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DownloadStatus.cancel -> {
|
||||
|
||||
@ -12,6 +12,7 @@ import java.io.OutputStream
|
||||
class XApkUnZipTask internal constructor(
|
||||
val id: String,
|
||||
val file: File,
|
||||
val isXapkApks: Boolean,
|
||||
val useFlawToUnzip: Boolean,
|
||||
private val factory: XApkUnZipOutputFactory,
|
||||
private val callback: XApkUnZipTaskCallback
|
||||
@ -19,7 +20,7 @@ class XApkUnZipTask internal constructor(
|
||||
|
||||
private val progressRecorder = ProgressRecorder()
|
||||
|
||||
val apk = XApkFile(file, useFlawToUnzip)
|
||||
val apk = XApkFile(file, isXapkApks, useFlawToUnzip)
|
||||
|
||||
private var runningThread: Thread? = null
|
||||
|
||||
@ -46,7 +47,9 @@ class XApkUnZipTask internal constructor(
|
||||
|
||||
progressRecorder.start(manifest.totalSize)
|
||||
|
||||
factory.onCreateOBBOutput(apk).output(xAPkWriter, manifest)
|
||||
if(!isXapkApks) {
|
||||
factory.onCreateOBBOutput(apk).output(xAPkWriter, manifest)
|
||||
}
|
||||
|
||||
val apkOutput = factory.onCreateApkOutput(apk)
|
||||
|
||||
@ -73,7 +76,8 @@ class XApkUnZipTask internal constructor(
|
||||
fun write(
|
||||
fileName: String,
|
||||
output: OutputStream,
|
||||
bufferedSize: Int = DEFAULT_BUFFER_SIZE
|
||||
bufferedSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
onCopyFinish: (OutputStream) -> Unit = {}
|
||||
) {
|
||||
callback.onNext(this@XApkUnZipTask, fileName)
|
||||
with(apk.getInputStream(fileName)) {
|
||||
@ -92,6 +96,7 @@ class XApkUnZipTask internal constructor(
|
||||
bytesCopied += bytes
|
||||
bytes = read(buffer)
|
||||
}
|
||||
onCopyFinish(output)
|
||||
} catch (e: Throwable) {
|
||||
throw e
|
||||
} finally {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.gh.gamecenter.xapk
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.gh.gamecenter.xapk.core.*
|
||||
@ -15,8 +14,6 @@ class XApkUnZipper(
|
||||
private val useFlawToUnzip: Boolean = false
|
||||
) : XApkUnZipTaskCallback {
|
||||
|
||||
constructor(context: Context) : this(DefaultXApkUnZIpOutputFactory(context))
|
||||
|
||||
private val taskMap = mutableMapOf<String, XApkUnZipTask>()
|
||||
|
||||
private val callbacks = mutableListOf<XApkUnZipCallback>()
|
||||
@ -28,6 +25,7 @@ class XApkUnZipper(
|
||||
val task = XApkUnZipTask(
|
||||
id = entry.id,
|
||||
file = entry.file,
|
||||
isXapkApks = entry.isXapkApks,
|
||||
callback = this,
|
||||
useFlawToUnzip = useFlawToUnzip,
|
||||
factory = outputFactory
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package com.gh.gamecenter.xapk.core
|
||||
|
||||
import android.content.Context
|
||||
import com.gh.gamecenter.xapk.io.NonSplitApksOutput
|
||||
import com.gh.gamecenter.xapk.io.OBBFileOutput
|
||||
import com.gh.gamecenter.xapk.io.SplitApksOutput
|
||||
import com.gh.gamecenter.xapk.io.XApkFileOutput
|
||||
import com.gh.gamecenter.xapk.pi.IPackageInstaller
|
||||
|
||||
class DefaultXApkUnZIpOutputFactory(
|
||||
private val applicationContext: Context,
|
||||
) : XApkUnZipOutputFactory {
|
||||
|
||||
override fun onCreateOBBOutput(apk: XApkFile): XApkFileOutput<Unit> {
|
||||
return OBBFileOutput()
|
||||
}
|
||||
|
||||
override fun onCreateApkOutput(apk: XApkFile): XApkFileOutput<IPackageInstaller> =
|
||||
if (apk.manifest.isMultiApks) {
|
||||
SplitApksOutput(applicationContext)
|
||||
} else {
|
||||
NonSplitApksOutput()
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ import java.io.InputStream
|
||||
*
|
||||
* https://wiki.shanqu.cc/pages/viewpage.action?pageId=127762915#%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E6%BC%8F%E6%B4%9E
|
||||
*/
|
||||
class XApkFile(val file: File, val useFlawToUnzip: Boolean = false) {
|
||||
class XApkFile(val file: File, val isXapkApks: Boolean, val useFlawToUnzip: Boolean = false) {
|
||||
|
||||
companion object {
|
||||
const val MANIFEST_FILE_NAME = "manifest.json"
|
||||
@ -25,12 +25,19 @@ class XApkFile(val file: File, val useFlawToUnzip: Boolean = false) {
|
||||
* 安装配置信息
|
||||
*/
|
||||
val manifest by lazy {
|
||||
XApkManifest.fromInputStream(
|
||||
getManifestInputStream()
|
||||
).apply {
|
||||
if (useFlawToUnzip) {
|
||||
expansions?.forEach {
|
||||
it.installPath = it.installPath.replace("Android", "\u200BAndroid")
|
||||
if (isXapkApks) {
|
||||
// TODO: 目前apks安装只用到total_size和split_apks字段,如果需要其他字段,需要相应的构造。
|
||||
val apks = zipFile.fileHeaders.filter { it.fileName.endsWith(".apk") }.map { it.fileName }
|
||||
.map { XApkManifest.Apk(it, ""/*unused*/) }
|
||||
return@lazy XApkManifest(packageName = ""/*unused*/, totalSize = file.length(), splitApks = apks)
|
||||
} else {
|
||||
return@lazy XApkManifest.fromInputStream(
|
||||
getManifestInputStream()
|
||||
).apply {
|
||||
if (useFlawToUnzip) {
|
||||
expansions?.forEach {
|
||||
it.installPath = it.installPath.replace("Android", "\u200BAndroid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,48 +72,8 @@ class XApkFile(val file: File, val useFlawToUnzip: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安装配置文件信息
|
||||
*/
|
||||
fun getManifestFileInfo(): FileHeader = getFileInfo(MANIFEST_FILE_NAME)
|
||||
|
||||
/**
|
||||
* 开启安装配置输入流
|
||||
*/
|
||||
fun getManifestInputStream(): InputStream = getInputStream(MANIFEST_FILE_NAME)
|
||||
|
||||
/**
|
||||
* 获取图标文件信息
|
||||
*/
|
||||
fun getIconFileInfo(): FileHeader = getFileInfo(manifest.icon)
|
||||
|
||||
/**
|
||||
* 开启图标输入流
|
||||
*/
|
||||
fun getIconInputStream(): InputStream = getInputStream(manifest.icon)
|
||||
|
||||
/**
|
||||
* 获取应用安装包文件信息
|
||||
* @param apk Apk实体
|
||||
*/
|
||||
fun getApkFileInfo(apk: XApkManifest.Apk): FileHeader = getFileInfo(apk.file)
|
||||
|
||||
/**
|
||||
* 开启安装包输入流
|
||||
* @param apk Apk实体
|
||||
*/
|
||||
fun getApkInputStream(apk: XApkManifest.Apk): InputStream = getInputStream(apk.file)
|
||||
|
||||
/**
|
||||
* 获取OBB数据文件信息
|
||||
* @param expansion OBB实体
|
||||
*/
|
||||
fun getOBBFileInfo(expansion: XApkManifest.Expansion): FileHeader = getFileInfo(expansion.file)
|
||||
|
||||
/**
|
||||
* 开启OBB数据输入流
|
||||
* @param expansion OBB实体
|
||||
*/
|
||||
fun getOBBInputStream(expansion: XApkManifest.Expansion): InputStream = getInputStream(expansion.file)
|
||||
|
||||
}
|
||||
@ -4,5 +4,6 @@ import java.io.File
|
||||
|
||||
data class XApkUnZipEntry(
|
||||
val id: String,
|
||||
val file: File
|
||||
val file: File,
|
||||
val isXapkApks: Boolean,
|
||||
)
|
||||
@ -5,25 +5,10 @@ import com.google.gson.annotations.SerializedName
|
||||
import java.io.InputStream
|
||||
|
||||
data class XApkManifest(
|
||||
@SerializedName("xapk_version")
|
||||
val xApkVersion: String,
|
||||
@SerializedName("package_name")
|
||||
val packageName: String,
|
||||
val name: String,
|
||||
@SerializedName("locales_name")
|
||||
val localesName: Map<String, String>,
|
||||
@SerializedName("version_code")
|
||||
val versionCode: String,
|
||||
@SerializedName("version_name")
|
||||
val versionName: String,
|
||||
@SerializedName("min_sdk_version")
|
||||
val minSdkVersion: String,
|
||||
@SerializedName("target_sdk_version")
|
||||
val targetSdkVersion: String,
|
||||
val permissions: List<String>,
|
||||
@SerializedName("total_size")
|
||||
val totalSize: Long,
|
||||
val icon: String,
|
||||
val expansions: List<Expansion>? = null,
|
||||
@SerializedName("split_apks")
|
||||
val splitApks: List<Apk> = emptyList(),
|
||||
|
||||
@ -2,11 +2,9 @@ package com.gh.gamecenter.xapk.io
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller.Session
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.gh.gamecenter.xapk.XApkUnZipTask
|
||||
import com.gh.gamecenter.xapk.data.XApkManifest
|
||||
import com.gh.gamecenter.xapk.pi.IPackageInstaller
|
||||
@ -22,39 +20,47 @@ class SplitApksOutput(
|
||||
override fun output(
|
||||
writer: XApkUnZipTask.XApkWriter,
|
||||
manifest: XApkManifest
|
||||
): IPackageInstaller =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // 仅支持Android 5.0以上的设备
|
||||
SessionParams(
|
||||
SessionParams.MODE_FULL_INSTALL
|
||||
)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}.let {
|
||||
val installer = context
|
||||
.packageManager
|
||||
.packageInstaller
|
||||
// 创建安装会话并获取返回会话ID
|
||||
val sessionId = installer.createSession(it)
|
||||
// 开启安装会话
|
||||
val session = installer.openSession(sessionId)
|
||||
try {
|
||||
manifest.splitApks.forEach { apk ->
|
||||
// 创建一个输出流,将APK文件流输入到输入流当中,流名称可以自定义
|
||||
writer.write(
|
||||
apk.file,
|
||||
session.openWrite(apk.file, 0, -1),
|
||||
)
|
||||
): IPackageInstaller = SessionParams(SessionParams.MODE_FULL_INSTALL)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}.let {
|
||||
val installer = context
|
||||
.packageManager
|
||||
.packageInstaller
|
||||
|
||||
var session: PackageInstaller.Session? = null
|
||||
var sessionId: Int = INVALID_SESSION_ID
|
||||
try {
|
||||
|
||||
// 创建安装会话并获取返回会话ID
|
||||
sessionId = installer.createSession(it)
|
||||
// 开启安装会话
|
||||
session = installer.openSession(sessionId)
|
||||
|
||||
manifest.splitApks.forEach { apk ->
|
||||
// 创建一个输出流,将APK文件流输入到输入流当中,流名称可以自定义
|
||||
writer.write(
|
||||
apk.file,
|
||||
session.openWrite(apk.file, 0, -1),
|
||||
bufferedSize = DEFAULT_BUFFER_SIZE,
|
||||
onCopyFinish = { outputStream ->
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
session.close()
|
||||
} catch (e: Throwable) {
|
||||
session.abandon()
|
||||
throw e
|
||||
}
|
||||
sessionId
|
||||
)
|
||||
}
|
||||
.let(factory)
|
||||
} else {
|
||||
throw ApkOutputUnsupportedSplitApksException()
|
||||
} catch (e: Throwable) {
|
||||
session?.abandon()
|
||||
throw e
|
||||
} finally {
|
||||
session?.close()
|
||||
}
|
||||
sessionId
|
||||
}
|
||||
.let(factory)
|
||||
|
||||
companion object {
|
||||
private const val INVALID_SESSION_ID = -1
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user