6.6.3 - Alpha5 - APK 文件类型信息及媒体文件类型信息优化对话框显示效率及信息展示逻辑

This commit is contained in:
SuperMonster003
2025-05-21 20:05:39 +08:00
parent ad34b03013
commit 3d8e3e28eb
7 changed files with 152 additions and 127 deletions

View File

@@ -3,6 +3,10 @@ package org.autojs.autojs.extension
import android.view.View
import android.widget.TextView
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.autojs.autojs.theme.ThemeColorHelper
import org.autojs.autojs.util.ClipboardUtils
import org.autojs.autojs.util.IntentUtils
@@ -36,16 +40,23 @@ object MaterialDialogExtensions {
}
}
fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, f: () -> String?) {
val textValue = f.invoke()
fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, textValue: String?) {
if (textView.text == context.getString(R.string.ellipsis_six)) {
textView.text = textValue.takeUnless { it.isNullOrBlank() } ?: context.getString(R.string.text_unknown)
}
this.makeTextCopyable(textView, textValue)
}
fun MaterialDialog.setCopyableText(textView: TextView, f: () -> String?) {
val textValue = f.invoke()
fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, scope: CoroutineScope, f: () -> String?) {
scope.launch(Dispatchers.IO) {
val textValue = f.invoke()
withContext(Dispatchers.Main) {
setCopyableTextIfAbsent(textView, textValue)
}
}
}
fun MaterialDialog.setCopyableText(textView: TextView, textValue: String?) {
textView.text = textValue.takeUnless { it.isNullOrBlank() } ?: context.getString(R.string.text_unknown)
this.makeTextCopyable(textView, textValue)
}

View File

@@ -109,7 +109,7 @@ object ColorInfoDialogManager {
private suspend fun TextView.bindWith(dialog: MaterialDialog, text: String) {
withContext(Dispatchers.Main) {
dialog.setCopyableText(this@bindWith) { text }
dialog.setCopyableText(this@bindWith, text)
}
}

View File

@@ -2,6 +2,8 @@ package org.autojs.autojs.ui.main.scripts
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_META_DATA
import android.os.Build
import android.view.LayoutInflater
@@ -14,7 +16,11 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.android.apksig.ApkVerifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import net.dongliu.apk.parser.ApkFile
import org.autojs.autojs.extension.MaterialDialogExtensions.makeSettingsLaunchable
@@ -38,6 +44,10 @@ object ApkInfoDialogManager {
fun showApkInfoDialog(context: Context, apkFile: File) {
val binding = ApkFileInfoDialogListItemBinding.inflate(LayoutInflater.from(context))
// Create an independent Scope for the Dialog, bind its lifecycle with the Dialog.
// zh-CN: 针对 Dialog 独立创建一个 Scope, 生命周期与 Dialog 绑定.
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val apkFilePath = apkFile.absolutePath
val packageManager = context.packageManager
@@ -63,101 +73,118 @@ object ApkInfoDialogManager {
.neutralColorRes(R.color.dialog_button_hint)
.onNegative { materialDialog, _ -> materialDialog.dismiss() }
.show()
CoroutineScope(Dispatchers.Main).launch {
val packageInfo = withContext(Dispatchers.IO) {
runCatching { packageManager.getPackageArchiveInfo(apkFilePath, GET_META_DATA) }.getOrNull()
.apply {
setOnDismissListener { scope.cancel() }
makeTextCopyable { it.titleView }
}
val applicationInfo = packageInfo?.applicationInfo
val packageName = packageInfo?.packageName
val versionName = packageInfo?.versionName
val versionCode = packageInfo?.let {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> it.longVersionCode
else -> @Suppress("DEPRECATION") it.versionCode.toLong()
scope.launch {
val apkInfoDeferred = async(Dispatchers.IO) { getApkInfo(apkFile) }
val packageInfoDeferred = async(Dispatchers.IO) { getPackageInfo(packageManager, apkFilePath) }
val firstPackageName: String? = select {
apkInfoDeferred.onAwait { it?.packageName }
packageInfoDeferred.onAwait { it?.packageName }
}
val packageName: String? = firstPackageName ?: when {
apkInfoDeferred.isCompleted -> packageInfoDeferred.await()?.packageName
else -> apkInfoDeferred.await()?.packageName
}
// @Hint by SuperMonster003 on Nov 27, 2024.
// ! Prioritize handling "installed version" to determine whether to display its content view.
// ! This is the only content view that needs to be considered before displaying the dialog.
// ! It is now safe to display the dialog immediately as all content views have placeholders.
// ! zh-CN:
// ! 优先处理 "已安装版本", 决定是否显示其内容视图.
// ! 这是唯一一个在显示对话框之前需要考虑的内容视图.
// ! 此时可立即安全显示对话框, 因所有内容视图均已完成占位.
val installedPackageName: String? = packageName?.also { pkg ->
AppUtils.getInstalledVersionInfo(pkg)?.let { versionInfo ->
binding.installedVersionParent.isVisible = true
dialog.setCopyableTextIfAbsent(
binding.installedVersionValue,
context.getString(R.string.text_full_version_info, versionInfo.versionName, versionInfo.versionCode),
)
}
}
val fileSize = withContext(Dispatchers.IO) { PFiles.getHumanReadableSize(apkFile.length()) }
restoreEssentialViews(binding, context)
updateGuidelines(binding)
val signatureScheme = withContext(Dispatchers.IO) { getApkSignatureInfo(apkFile) }
dialog.setCopyableTextIfAbsent(binding.packageNameValue, packageName)
dialog.setCopyableTextIfAbsent(binding.deviceSdkValue, "${Build.VERSION.SDK_INT}")
dialog.setCopyableTextIfAbsent(binding.fileSizeValue, this) { PFiles.getHumanReadableSize(apkFile.length()) }
dialog.setCopyableTextIfAbsent(binding.signatureSchemeValue, this) { getApkSignatureInfo(apkFile) }
withContext(Dispatchers.Main) {
// @Hint by SuperMonster003 on Nov 27, 2024.
// ! Prioritize handling "installed version" to determine whether to display its content view.
// ! This is the only content view that needs to be considered before displaying the dialog.
// ! It is now safe to display the dialog immediately as all content views have placeholders.
// ! zh-CN:
// ! 优先处理 "已安装版本", 决定是否显示其内容视图.
// ! 这是唯一一个在显示对话框之前需要考虑的内容视图.
// ! 此时可立即安全显示对话框, 因所有内容视图均已完成占位.
val installedPackageName: String? = packageName?.let { pkg ->
AppUtils.getInstalledVersionInfo(pkg)?.let { versionInfo ->
binding.installedVersionParent.isVisible = true
dialog.setCopyableTextIfAbsent(binding.installedVersionValue) { context.getString(R.string.text_full_version_info, versionInfo.versionName, versionInfo.versionCode) }
pkg /* returns as installedPackageName */
launch(Dispatchers.IO) {
val apkInfo = apkInfoDeferred.await()
withContext(Dispatchers.Main) {
dialog.setCopyableTextIfAbsent(binding.labelNameValue, apkInfo?.label)
dialog.setCopyableTextIfAbsent(binding.minSdkValue, apkInfo?.minSdkVersion?.toString())
dialog.setCopyableTextIfAbsent(binding.targetSdkValue, apkInfo?.targetSdkVersion?.toString())
if (apkInfo != null) dialog.getActionButton(DialogAction.NEUTRAL).let { neutralButton ->
neutralButton.isVisible = true
neutralButton.text = "Manifest"
neutralButton.setOnClickListener { DisplayManifestActivity.launch(context, apkInfo.manifestXml, apkInfo.usesPermissions) }
}
}
}
restoreEssentialViews(binding, context)
updateGuidelines(binding)
when {
versionName != null -> {
binding.versionPlaceholderLabel.text = context.getString(R.string.text_version)
dialog.setCopyableTextIfAbsent(binding.versionPlaceholderValue) { context.getString(R.string.text_full_version_info, versionName, versionCode) }
launch(Dispatchers.IO) {
val packageInfo = packageInfoDeferred.await()
val applicationInfo = packageInfo?.applicationInfo
val versionName = packageInfo?.versionName
val versionCode = packageInfo?.let {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> it.longVersionCode
else -> @Suppress("DEPRECATION") it.versionCode.toLong()
}
versionCode != null -> {
binding.versionPlaceholderLabel.text = context.getString(R.string.text_version_code)
dialog.setCopyableTextIfAbsent(binding.versionPlaceholderValue) { "$versionCode" }
}
else -> dialog.setCopyableTextIfAbsent(binding.versionPlaceholderValue) { null }
}
withContext(Dispatchers.Main) {
when {
versionName != null -> {
binding.versionPlaceholderLabel.text = context.getString(R.string.text_version)
dialog.setCopyableTextIfAbsent(
binding.versionPlaceholderValue,
context.getString(R.string.text_full_version_info, versionName, versionCode),
)
}
versionCode != null -> {
binding.versionPlaceholderLabel.text = context.getString(R.string.text_version_code)
dialog.setCopyableTextIfAbsent(binding.versionPlaceholderValue, "$versionCode")
}
else -> dialog.setCopyableTextIfAbsent(binding.versionPlaceholderValue, null)
}
dialog.setCopyableTextIfAbsent(binding.packageNameValue) { packageName }
dialog.setCopyableTextIfAbsent(binding.deviceSdkValue) { "${Build.VERSION.SDK_INT}" }
dialog.setCopyableTextIfAbsent(binding.fileSizeValue) { fileSize }
dialog.setCopyableTextIfAbsent(binding.signatureSchemeValue) { signatureScheme }
dialog.setIcon(applicationInfo?.apply {
sourceDir = apkFilePath
publicSourceDir = apkFilePath
}?.loadIcon(packageManager) ?: AppCompatResources.getDrawable(context, R.drawable.ic_packaging))
dialog.makeSettingsLaunchable({ it.iconView }, installedPackageName)
dialog.makeTextCopyable { it.titleView }
val apkInfo = getApkInfo(apkFile)
dialog.setCopyableTextIfAbsent(binding.labelNameValue) { apkInfo?.label }
dialog.setCopyableTextIfAbsent(binding.packageNameValue) { apkInfo?.packageName }
dialog.setCopyableTextIfAbsent(binding.minSdkValue) { apkInfo?.minSdkVersion?.toString() }
dialog.setCopyableTextIfAbsent(binding.targetSdkValue) { apkInfo?.targetSdkVersion?.toString() }
if (apkInfo != null) dialog.getActionButton(DialogAction.NEUTRAL).let { neutralButton ->
neutralButton.isVisible = true
neutralButton.text = "Manifest"
neutralButton.setOnClickListener { DisplayManifestActivity.launch(context, apkInfo.manifestXml, apkInfo.usesPermissions) }
dialog.setIcon(applicationInfo?.apply {
sourceDir = apkFilePath
publicSourceDir = apkFilePath
}?.loadIcon(packageManager) ?: AppCompatResources.getDrawable(context, R.drawable.ic_packaging))
dialog.makeSettingsLaunchable({ it.iconView }, installedPackageName)
}
}
}
}
private suspend fun getApkInfo(apkFile: File): ApkInfo? = withContext(Dispatchers.IO) {
runCatching {
ApkFile(apkFile).use { parser ->
val meta = runCatching { parser.apkMeta }.getOrNull()
val label = meta?.label
val packageName = meta?.packageName
val minSdkVersion = meta?.minSdkVersion?.toIntOrNull()
val targetSdkVersion = meta?.targetSdkVersion?.toIntOrNull()
val usesPermissions = meta?.usesPermissions ?: emptyList()
val manifestXml = parser.manifestXml
ApkInfo(label, packageName, minSdkVersion, targetSdkVersion, usesPermissions, manifestXml)
}
}.getOrNull()
}
private fun getApkInfo(apkFile: File): ApkInfo? = runCatching {
ApkFile(apkFile).use { parser ->
val meta = runCatching { parser.apkMeta }.getOrNull()
val label = meta?.label
val packageName = meta?.packageName
val minSdkVersion = meta?.minSdkVersion?.toIntOrNull()
val targetSdkVersion = meta?.targetSdkVersion?.toIntOrNull()
val usesPermissions = meta?.usesPermissions ?: emptyList()
val manifestXml = parser.manifestXml
ApkInfo(label, packageName, minSdkVersion, targetSdkVersion, usesPermissions, manifestXml)
}
}.getOrNull()
private fun getPackageInfo(packageManager: PackageManager, apkFilePath: String): PackageInfo? = runCatching {
packageManager.getPackageArchiveInfo(apkFilePath, GET_META_DATA)
}.getOrNull()
private fun restoreEssentialViews(binding: ApkFileInfoDialogListItemBinding, context: Context) {
listOf(

View File

@@ -100,7 +100,6 @@ abstract class BaseDisplayContentActivity : BaseActivity() {
// ! zh-CN: 事实上, 此处依然存疑.
// ! Reference: https://stackoverflow.com/questions/3866499/two-directional-scroll-view
// !
super.onTouchEvent(event) or view.onTouchEvent(event)
}
else -> super.onTouchEvent(event)

View File

@@ -11,8 +11,9 @@ import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.autojs.autojs.extension.MaterialDialogExtensions.makeTextCopyable
@@ -35,6 +36,10 @@ object MediaInfoDialogManager {
fun showMediaInfoDialog(context: Context, explorerItem: ExplorerItem) {
val binding = MediaFileInfoDialogListItemBinding.inflate(LayoutInflater.from(context))
// Create an independent Scope for the Dialog, bind its lifecycle with the Dialog.
// zh-CN: 针对 Dialog 独立创建一个 Scope, 生命周期与 Dialog 绑定.
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val dialog = MaterialDialog.Builder(context)
.title(explorerItem.name)
.customView(binding.root, false)
@@ -48,60 +53,42 @@ object MediaInfoDialogManager {
.neutralText(R.string.ellipsis_six)
.neutralColorRes(R.color.dialog_button_unavailable)
.show()
.apply { setOnDismissListener { scope.cancel() } }
CoroutineScope(Dispatchers.Main).launch {
scope.launch {
val mediaInfo = MediaInfo()
val filePath = explorerItem.path
val deferredVideoFormat = async(Dispatchers.IO) { mediaInfo(filePath, VIDEO, "Format") }
val deferredAudioFormat = async(Dispatchers.IO) { mediaInfo(filePath, AUDIO, "Format") }
val deferredContainerFormat = async(Dispatchers.IO) { mediaInfo(filePath, GENERAL, "Format") }
val deferredMediaInfoTrimmed = async(Dispatchers.IO) { mediaInfo.getMediaInfoTrimmed(explorerItem.path) }
val videoFormatDeferred = async(Dispatchers.IO) { mediaInfo(filePath, VIDEO, "Format") }
val audioFormatDeferred = async(Dispatchers.IO) { mediaInfo(filePath, AUDIO, "Format") }
val containerFormatDeferred = async(Dispatchers.IO) { mediaInfo(filePath, GENERAL, "Format") }
val mediaInfoTrimmedDeferred = async(Dispatchers.IO) { mediaInfo.getMediaInfoTrimmed(explorerItem.path) }
launch(Dispatchers.IO) {
mediaInfo(filePath, GENERAL, "FileSize/String").let {
binding.fileSizeValue.bindWith(dialog, it)
}
}
launch(Dispatchers.IO) {
mediaInfo(filePath, GENERAL, "FileSize/String").let { binding.fileSizeValue.bindWith(dialog, it) }
mediaInfo(filePath, GENERAL, "Duration/String").let { binding.durationValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, GENERAL, "Album").let { binding.albumValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, GENERAL, "Track").let { binding.trackNameValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, GENERAL, "Performer").let { binding.performerValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, AUDIO, "BitRate/String").let { binding.bitRateForAudioValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, VIDEO, "BitRate/String").let { binding.bitRateForVideoValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, GENERAL, "Track").let { binding.trackNameValue.bindWith(dialog, it) }
mediaInfo(filePath, GENERAL, "Performer").let { binding.performerValue.bindWith(dialog, it) }
mediaInfo(filePath, GENERAL, "Album").let { binding.albumValue.bindWith(dialog, it) }
run {
val width = mediaInfo(filePath, VIDEO, "Width")
val height = mediaInfo(filePath, VIDEO, "Height")
binding.resolutionValue.bindWith(dialog, if (width.isNotEmpty() && height.isNotEmpty()) "$width × $height" else "")
}
mediaInfo(filePath, VIDEO, "DisplayAspectRatio/String").let { binding.aspectRatioValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
mediaInfo(filePath, VIDEO, "FrameRate").let { binding.frameRateValue.bindWith(dialog, it) }
}
launch(Dispatchers.IO) {
val deferredVideoWidth = async(Dispatchers.IO) { mediaInfo(filePath, VIDEO, "Width") }
val deferredVideoHeight = async(Dispatchers.IO) { mediaInfo(filePath, VIDEO, "Height") }
val (width, height) = awaitAll(deferredVideoWidth, deferredVideoHeight)
binding.resolutionValue.bindWith(dialog, if (width.isNotEmpty() && height.isNotEmpty()) "$width × $height" else "")
}
when {
deferredVideoFormat.await().isNotEmpty() -> {
videoFormatDeferred.await().isNotEmpty() -> {
setViewsAsVideoPlaceholder(binding)
dialog.setIcon(R.drawable.ic_movie)
dialog.getActionButton(DialogAction.POSITIVE)
setDialogPlayable(dialog, context, explorerItem)
}
deferredAudioFormat.await().isNotEmpty() -> {
audioFormatDeferred.await().isNotEmpty() -> {
setViewsAsMediaPlaceholder(binding)
dialog.setIcon(R.drawable.ic_voice_note)
setDialogPlayable(dialog, context, explorerItem)
@@ -126,7 +113,7 @@ object MediaInfoDialogManager {
dialog.makeTextCopyable { it.titleView }
}
when (val containerFormat = deferredContainerFormat.await()) {
when (val containerFormat = containerFormatDeferred.await()) {
MEDIA_INFO_ERROR_OPENING_FILE -> {
dialog.setContent(MEDIA_INFO_ERROR_OPENING_FILE)
dialog.getActionButton(DialogAction.NEUTRAL).let {
@@ -136,12 +123,12 @@ object MediaInfoDialogManager {
}
else -> {
containerFormat.let { binding.containerFormatValue.bindWith(dialog, it) }
deferredAudioFormat.await().let { binding.audioFormatValue.bindWith(dialog, it) }
deferredVideoFormat.await().let { binding.videoFormatValue.bindWith(dialog, it) }
audioFormatDeferred.await().let { binding.audioFormatValue.bindWith(dialog, it) }
videoFormatDeferred.await().let { binding.videoFormatValue.bindWith(dialog, it) }
}
}
updateDialogNeutralButton(dialog, context, deferredMediaInfoTrimmed.await())
updateDialogNeutralButton(dialog, context, mediaInfoTrimmedDeferred.await())
}
}
@@ -255,7 +242,7 @@ object MediaInfoDialogManager {
private suspend fun TextView.bindWith(dialog: MaterialDialog, text: String) {
withContext(Dispatchers.Main) {
dialog.setCopyableText(this@bindWith) { text }
dialog.setCopyableText(this@bindWith, text)
}
}