Compare commits

...

25 Commits

Author SHA1 Message Date
e855f59ae4 Merge branch 'feat/GHZSCY-7642' of git.shanqu.cc:halo/android/assistant-android into feat/GHZSCY-7642 2025-05-15 16:50:08 +08:00
9ea4b1367e Revert .gitlab-ci.yml 2025-05-15 16:49:29 +08:00
e44c11349b feat: 替换正式环境2.0.0插件url 2025-05-15 16:49:29 +08:00
a0ea9f9f44 feat: https://jira.shanqu.cc/browse/GHZSCY-7638 替换新版插件 2025-05-15 16:49:29 +08:00
082d3297f7 feat: 修复用户没有下载新的畅玩游戏,插件更新没有触发的问题。 https://jira.shanqu.cc/browse/GHZSCY-7642 2025-05-15 16:49:29 +08:00
f81055ae7f feat: 汉化打包测试 https://jira.shanqu.cc/browse/GHZSCY-7642 2025-05-15 16:49:29 +08:00
8e3de4e441 feat: 优化汉化UI 2025-05-15 16:49:29 +08:00
7cf66c4362 feat: 畅玩汉化功能 2025-05-15 16:49:29 +08:00
d514e4ce1b Merge branch 'fix/GHZSCY-7994-pr' into 'dev'
fix:【光环助手】开屏自有广告显示问题 https://jira.shanqu.cc/browse/GHZSCY-7994

See merge request halo/android/assistant-android!2194
2025-05-15 16:44:58 +08:00
9d0f75a882 fix:【光环助手】开屏自有广告显示问题 https://jira.shanqu.cc/browse/GHZSCY-7994 2025-05-15 16:44:58 +08:00
6c8c2f2340 Revert .gitlab-ci.yml 2025-05-15 16:43:15 +08:00
8d7afea0f6 feat: 替换正式环境2.0.0插件url 2025-05-15 16:40:36 +08:00
aff6cbccdc Merge branch 'feat/GHZSCY-7948' into 'dev'
xpak兼容更多的apks安装

See merge request halo/android/assistant-android!2193
2025-05-15 16:31:15 +08:00
b19b6960ac xpak兼容更多的apks安装 2025-05-15 16:31:15 +08:00
c60b317125 Merge branch 'feat/GHZSCY-7492' into 'dev'
feat: 游戏礼包新增领取条件 https://jira.shanqu.cc/browse/GHZSCY-7492

See merge request halo/android/assistant-android!2192
2025-05-15 15:32:39 +08:00
4fbb9d0c88 feat: 游戏礼包新增领取条件 https://jira.shanqu.cc/browse/GHZSCY-7492 2025-05-15 15:12:20 +08:00
f039d71562 Merge branch 'feat/install-external' into 'dev'
feat: 从SD卡兼容所有文件管理权限

See merge request halo/android/assistant-android!2191
2025-05-15 14:52:45 +08:00
09e439f143 feat: https://jira.shanqu.cc/browse/GHZSCY-7638 替换新版插件 2025-05-13 15:20:46 +08:00
c23a78a546 feat: https://jira.shanqu.cc/browse/GHZSCY-7638 https://jira.shanqu.cc/browse/GHZSCY-7879 https://jira.shanqu.cc/browse/GHZSCY-7972 打包测试 2025-05-13 14:45:21 +08:00
d3351ec0b6 feat: 读取汉化接口配置 https://jira.shanqu.cc/browse/GHZSCY-7642 2025-04-08 16:34:19 +08:00
e769c0e5f5 fix: 修复初始化汉化进程导致unity游戏卡屏的问题 https://jira.shanqu.cc/browse/GHZSCY-7642 2025-04-07 10:08:35 +08:00
bb3a849671 feat: 修复用户没有下载新的畅玩游戏,插件更新没有触发的问题。 https://jira.shanqu.cc/browse/GHZSCY-7642 2025-03-27 16:08:26 +08:00
4ed1e1e64a feat: 汉化打包测试 https://jira.shanqu.cc/browse/GHZSCY-7642 2025-03-27 13:44:54 +08:00
9f4eaa67fa feat: 优化汉化UI 2025-03-27 10:48:08 +08:00
655173482a feat: 畅玩汉化功能 2025-03-25 14:45:55 +08:00
26 changed files with 369 additions and 534 deletions

View File

@ -157,3 +157,4 @@ oss-upload&send-email:
only:
- dev
- release

View File

@ -609,32 +609,32 @@ object AdDelegateHelper {
), null, null
)
adImage.visibleIf(true)
ImageUtils.displayWithCallback(adImage, ad.img, true, object : BaseControllerListener<ImageInfo>() {
override fun onSubmit(id: String?, callerContext: Any?) {
super.onSubmit(id, callerContext)
adImage.post {
ownerSplashAdLoadTime = System.currentTimeMillis()
NewFlatLogUtils.logSplashAdLoad(ad.id)
}
}
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
isOwnerSplashAdShown = true
adImage.post {
NewFlatLogUtils.logSplashAdShow(ad.id, System.currentTimeMillis() - ownerSplashAdLoadTime)
}
}
override fun onFailure(id: String?, throwable: Throwable?) {
super.onFailure(id, throwable)
NewFlatLogUtils.logSplashAdFail(ad.id, "启动广告图加载失败")
}
})
if (ad.isImageType) {
adVideo.visibleIf(false)
adImage.visibleIf(true)
ImageUtils.displayWithCallback(adImage, ad.img, true, object : BaseControllerListener<ImageInfo>() {
override fun onSubmit(id: String?, callerContext: Any?) {
super.onSubmit(id, callerContext)
adImage.post {
ownerSplashAdLoadTime = System.currentTimeMillis()
NewFlatLogUtils.logSplashAdLoad(ad.id)
}
}
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
isOwnerSplashAdShown = true
adImage.post {
NewFlatLogUtils.logSplashAdShow(ad.id, System.currentTimeMillis() - ownerSplashAdLoadTime)
}
}
override fun onFailure(id: String?, throwable: Throwable?) {
super.onFailure(id, throwable)
NewFlatLogUtils.logSplashAdFail(ad.id, "启动广告图加载失败")
}
})
} else {
adImage.visibleIf(false)
adVideo.startPlay(ad.video.url)
}
startAdContainer.setOnClickListener {

View File

@ -324,7 +324,19 @@ public class LibaoUtils {
libaoBtn.setText(com.gh.gamecenter.feature.R.string.libao_finish);
libaoBtn.setBackgroundResource(R.drawable.button_border_round_gray);
libaoBtn.setTextColor(context.getResources().getColor(com.gh.gamecenter.common.R.color.button_gray));
libaoBtn.setOnClickListener(v -> ToastUtils.toast("兑换码领取已结束"));
libaoBtn.setOnClickListener(v ->
{
ToastUtils.toast("兑换码领取已结束");
SensorsBridge.trackEvent("GameGiftDraw",
"gift_type", "兑换码",
"game_name", libaoEntity.getGame().getName(),
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", sourceEntrance,
"button_name", libaoBtn.getText().toString()
);
});
} else {
libaoBtn.setText(R.string.libao_copy);
libaoBtn.setTextColor(ExtensionsKt.toColor(com.gh.gamecenter.common.R.color.white, context));
@ -338,6 +350,23 @@ public class LibaoUtils {
libaoEntity.getGame().getId(),
libaoEntity.getGame().getName()
);
SensorsBridge.trackEvent("GameGiftDraw",
"gift_type", "兑换码",
"game_name", libaoEntity.getGame().getName(),
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", sourceEntrance,
"button_name", libaoBtn.getText().toString()
);
SensorsBridge.trackEvent("GameGiftDrawResult",
"gift_type", "兑换码",
"game_name", libaoEntity.getGame().getName(),
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", sourceEntrance
);
ExtensionsKt.copyTextAndToast(libaoEntity.getCode(), libaoEntity.getToast());
});
}
@ -346,6 +375,25 @@ public class LibaoUtils {
libaoBtn.setOnClickListener(v -> {
String btnStatus = libaoBtn.getText().toString();
String giftType;
if ("ling".equals(libaoEntity.getStatus()) || "linged".equals(libaoEntity.getStatus())) {
giftType = "普通礼包";
} else if ("copy".equals(libaoEntity.getReceiveMethod())) {
giftType = "兑换码";
} else {
giftType = "淘号礼包";
}
SensorsBridge.trackEvent("GameGiftDraw",
"gift_type", giftType,
"game_name", libaoEntity.getGame().getName(),
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", sourceEntrance,
"button_name", btnStatus
);
// 领取限制
CheckLoginUtils.checkLogin(context, "礼包详情-[" + btnStatus + "]", () -> {
if ("领取".equals(btnStatus) || "淘号".equals(btnStatus)) {
@ -408,27 +456,11 @@ public class LibaoUtils {
} else {
libaoLing(context, libaoBtn, libaoEntity, adapter, isInstallRequired, null, entrance, sourceEntrance, listener);
}
SensorsBridge.trackEvent("GameGiftDraw",
"gift_type", "普通礼包",
"game_name", libaoEntity.getGame().getName(),
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", sourceEntrance
);
break;
case "再淘":
case "再淘一个":
case "淘号":
libaoTao(context, libaoBtn, libaoEntity, isInstallRequired, adapter, status, entrance, sourceEntrance, listener);
SensorsBridge.trackEvent("GameGiftDraw",
"gift_type", "淘号礼包",
"game_name", libaoEntity.getGame().getName(),
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", sourceEntrance
);
break;
}
});
@ -515,6 +547,7 @@ public class LibaoUtils {
try {
JSONObject errorJson = new JSONObject(exception.response().errorBody().string());
String detail = errorJson.getString("detail").toLowerCase();
int code = errorJson.getInt("code");
switch (detail) {
case "coming":
Utils.toast(context, "礼包领取时间未开始");
@ -545,7 +578,13 @@ public class LibaoUtils {
Utils.toast(context, "淘号失败,稍后重试");
break;
default:
Utils.toast(context, "操作失败");
if (code == 403211) {
Utils.toast(context, "条件不符,请先提交游戏评价");
} else if (code == 403212) {
Utils.toast(context, "条件不符,请修改游戏评价");
} else {
Utils.toast(context, "操作失败");
}
break;
}
@ -663,6 +702,7 @@ public class LibaoUtils {
String string = exception.response().errorBody().string();
JSONObject errorJson = new JSONObject(string);
String detail = errorJson.getString("detail").toLowerCase();
int code = errorJson.getInt("code");
switch (detail) {
case "coming":
Utils.toast(context, "礼包领取时间未开始");
@ -702,7 +742,13 @@ public class LibaoUtils {
Utils.toast(context, "网络状态异常,请稍后再试");
break;
default:
Utils.toast(context, "操作失败");
if (code == 403211) {
Utils.toast(context, "条件不符,请先提交游戏评价");
} else if (code == 403212) {
Utils.toast(context, "条件不符,请修改游戏评价");
} else {
Utils.toast(context, "操作失败");
}
break;
}
} catch (Exception ex) {

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

@ -422,9 +422,66 @@ public class LibaoDetailAdapter extends BaseRecyclerAdapter<ViewHolder> {
ExtensionsKt.setDrawableEnd(holder.binding.libaodetailCondition, com.gh.gamecenter.common.R.drawable.ic_libao_activity_arrow, null, null);
holder.binding.libaodetailCondition.setCompoundDrawablePadding(DisplayUtils.dip2px(4F));
}
} else if (mLibaoDetailEntity.getReceiveCondition() != null) {
holder.binding.libaodetailCondition.setVisibility(View.VISIBLE);
holder.binding.libaodetailConditionDescTv.setVisibility(View.VISIBLE);
holder.binding.libaodetailCondition.setText("领取条件:");
holder.binding.libaodetailConditionDescTv.setText(
getConditionDescText(
mGameEntity.getName(),
mLibaoDetailEntity.getReceiveCondition().getStar(),
mLibaoDetailEntity.getReceiveCondition().getWords()
)
);
}
}
/**
* 根据评分和字数限制生成文案。
*
* @param gameName 游戏名称
* @param star 评分选项 (-1: 无限制, 5: 5星好评, 4: 4星及以上, 3: 3星及以上, 2: 2星及以上, 1: 1星及以上)
* @param words 字数限制 (-1: 无限制, n: 字数不少于n)
* @return 生成的文案
*/
private String getConditionDescText(String gameName, int star, int words) {
StringBuilder text = new StringBuilder();
text.append("发表《").append(gameName).append("");
if (words == -1) {
text.append("的游戏评价");
} else {
text.append("不少于").append(words).append("字的游戏评价");
}
if (star != -1) {
text.append("并给予");
switch (star) {
case 5:
text.append("5星好评");
break;
case 4:
text.append("4星及以上评分");
break;
case 3:
text.append("3星及以上评分");
break;
case 2:
text.append("2星及以上评分");
break;
case 1:
text.append("1星及以上评分");
break;
default:
//throw new IllegalArgumentException("Invalid star value: " + star); // 或者返回一个默认值,比如空字符串或错误消息
return ""; // or return a default string or throw an exception.
}
}
return text.toString();
}
public LibaoEntity getLibaoEntity() {
return mLibaoEntity;
}

View File

@ -13,6 +13,14 @@ class LibaoDetailEntity {
var des: String? = null
// 领取限制 发表游戏评价 (game_comment)
@SerializedName("receive_limit")
var receiveLimit: String? = ""
// 领取条件
@SerializedName("receive_condition")
var receiveCondition: Condition ? = null
@SerializedName("new_des")
var newDes: String? = null
@ -24,4 +32,10 @@ class LibaoDetailEntity {
@SerializedName("me")
var me: MeEntity? = null
class Condition(
// 评分,-1/5/4/3/2/1 => 无限制、5星好评、4星及以上评分、3星及以上评分、2星及以上评分、1星及以上评分
val star: Int = 0,
// 字数,-1/n => 无限制、数量
val words: Int = 0
)
}

View File

@ -253,7 +253,7 @@ class GameLibaoAdapter(
null,
true,
"游戏详情",
"游戏详情"
"礼包列表页"
) {
adapter.notifyItemChanged(position)
}

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

@ -251,6 +251,8 @@ class Libao3FragmentAdapter extends BaseRecyclerAdapter<RecyclerView.ViewHolder>
String giftType;
if ("ling".equals(libaoEntity.getStatus()) || "linged".equals(libaoEntity.getStatus())) {
giftType = "普通礼包";
} else if ("copy".equals(libaoEntity.getReceiveMethod())) {
giftType = "兑换码";
} else {
giftType = "淘号礼包";
}
@ -262,7 +264,8 @@ class Libao3FragmentAdapter extends BaseRecyclerAdapter<RecyclerView.ViewHolder>
"game_id", libaoEntity.getGame().getId(),
"gift_id", libaoEntity.getId(),
"gift_name", libaoEntity.getName(),
"source_entrance", "礼包中心-存号箱"
"source_entrance", "礼包中心-存号箱",
"button_name", holder.binding.libaoBtnStatus.getText().toString()
);
SensorsBridge.trackEvent("GameGiftDrawResult",

View File

@ -82,7 +82,6 @@ public class LibaoDetailFragment extends ToolbarFragment implements LibaoDetailA
private LibaoDetailAdapter mAdapter;
private LibaoEntity mLibaoEntity;
private DownloadEntity mDownloadEntity;
private GameEntity mGameEntity;
private boolean mIsScroll;
@ -129,7 +128,6 @@ public class LibaoDetailFragment extends ToolbarFragment implements LibaoDetailA
if (url.equals(downloadEntity.getUrl())) {
if (!"pause".equals(DownloadManager.getInstance().
getStatus(downloadEntity.getUrl()))) {
mDownloadEntity = downloadEntity;
DetailDownloadUtils.updateViewHolder(getDetailViewHolder());
}
}

View File

@ -1460,6 +1460,8 @@ object VHelper {
}
}
}
// 启动游戏时也执行下载插件逻辑,修复用户没有下载新的畅玩游戏,插件更新没有触发的问题。
initVaPlugin(gameId)
} catch (e: Exception) {
ToastUtils.toast(e.localizedMessage ?: "")
}

View File

@ -73,15 +73,35 @@
android:textColor="@color/text_primary"
android:textSize="16sp" />
<TextView
android:id="@+id/libaodetail_condition"
android:layout_width="wrap_content"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:lineSpacingExtra="6dp"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:visibility="gone" />
android:layout_marginTop="12dp">
<TextView
android:id="@+id/libaodetail_condition"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="6dp"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:visibility="gone"
tools:text="领取条件:"
tools:visibility="visible" />
<TextView
android:id="@+id/libaodetail_condition_desc_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/libaodetail_condition"
android:lineSpacingExtra="2dp"
android:textColor="@color/theme"
android:textSize="12sp"
android:visibility="gone"
tools:text="领取条件字段配置的文案色值为text_theme领取条件字段配置的文案色值为text_theme"
tools:visibility="visible" />
</RelativeLayout>
<TextView
android:id="@+id/libaodetail_time_start"

View File

@ -16,6 +16,7 @@
android:visibility="gone"
app:actualImageScaleType="centerCrop"
app:fadeDuration="0"
android:background="@color/ui_background"
app:layout_constraintBottom_toTopOf="@id/startAdIcpContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

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

View File

@ -8,7 +8,7 @@ plugins {
android {
// 在 DEFAULT_PLUGIN_URL 中提取插件版本号
def defaultPluginUrl = "https://app-static.796697.com/va/plugin/2024/10/17/1.0.3_64_1729148903919.zip"
def defaultPluginUrl = "https://app-static.796697.com/va/plugin/2025/05/15/2.0.0_64_1747292406020.zip"
def versionText = defaultPluginUrl.substring(defaultPluginUrl.lastIndexOf("/") + 1)
def pluginVersionPattern = ~/^\d+.\d+.\d+/
def matcher = versionText =~ pluginVersionPattern

View File

@ -79,6 +79,7 @@ def optionalVaModules = [
'va-common' : 'vasdk/common',
'va-core' : 'vasdk/core',
'va-feature-realname': 'vasdk/feature/realname-window',
'va-feature-translate': 'vasdk/feature/translate',
'va-library-commons' : 'vasdk/commons',
'va-library-network' : 'vasdk/library/network',
'va-archive' : 'vasdk/archive',

2
vasdk

Submodule vasdk updated: fc59639f20...950989924d