xpak兼容更多的apks安装

This commit is contained in:
yangfei
2025-05-15 16:31:15 +08:00
parent c60b317125
commit b19b6960ac
12 changed files with 170 additions and 478 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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) {
}
}
}
}

View File

@ -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
}
}

View File

@ -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 -> {

View File

@ -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 {

View File

@ -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

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -4,5 +4,6 @@ import java.io.File
data class XApkUnZipEntry(
val id: String,
val file: File
val file: File,
val isXapkApks: Boolean,
)

View File

@ -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(),

View File

@ -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
}
}