fix: v1.2.1 - resolve updater crash, add scrollable changelog and release title
This commit is contained in:
1 parent
96ec5e950e
commit
ab63f506c8
6 files changed
+190
-49
No files matched your search
@@ -4,7 +4,7 @@
|
||||
**ppSR & Escape Penacony** เป็นแอปพลิเคชัน Android แบบ All-in-One ที่รวมทั้ง **ตัวรันเซิร์ฟเวอร์จำลอง ppSR (Embedded Python 3.14)**, **ตัวอัปเดตเซิร์ฟเวอร์ตรงจากต้นทาง AFN**, **ระบบวาร์ปแผนที่ 86 ฉาก**, และ **เครื่องมือปรับแต่งเซิร์ฟเวอร์** ไว้ในแอปเดียวจบ **โดยไม่ต้องใช้สิทธิ์ Root, ไม่ต้องใช้ Shizuku และไม่ต้องติดตั้งแอป com.afn.ppSR แยกต่างหาก**
|
||||
|
||||
- **Application ID / Package**: `com.catlas.escapepenacony`
|
||||
- **เวอร์ชันล่าสุด (Current Version)**: `v1.2.0`
|
||||
- **เวอร์ชันล่าสุด (Current Version)**: `v1.2.1`
|
||||
- **สถาปัตยกรรม (Architecture)**: Native Android (Kotlin + Jetpack Compose + Material Design 3 + Embedded Chaquopy Python 3.14)
|
||||
- **Target ABI**: `arm64-v8a` (Android 8.0+ / API 26+)
|
||||
|
||||
@@ -119,12 +119,21 @@ Escape-penacony-standalone/
|
||||
|
||||
ไฟล์ APK ที่บิลด์เสร็จแล้วจะอยู่ที่:
|
||||
- `app/build/outputs/apk/release/app-release.apk`
|
||||
- หรือตัวสำเนาที่ root โฟลเดอร์: `Escape-penacony-standalone-v1.2.0.apk`
|
||||
- หรือตัวสำเนาที่ root โฟลเดอร์: `Escape-penacony-standalone-v1.2.1.apk`
|
||||
|
||||
---
|
||||
|
||||
## 📜 บันทึกการเปลี่ยนแปลง (Changelog)
|
||||
|
||||
### v1.2.1 (Build 9)
|
||||
- **App Updater Crash Fix:** แก้ไขปัญหาแอปปิดตัวลงกะทันหัน (Force Close/Crash) ขณะกดอัปเดตแอป:
|
||||
- เพิ่มสิทธิ์ `REQUEST_INSTALL_PACKAGES` ใน `AndroidManifest.xml` ให้สอดคล้องกับมาตรการความปลอดภัยบน Android 8.0 - 15
|
||||
- ขยายการกำหนดค่า `FileProvider` (`file_paths.xml`) รองรับ Storage Path ทั้ง internal files, cache, external storage ครอบคลุมมือถือทุกรุ่น
|
||||
- ปรับปรุง `AppUpdateManager` ให้รองรับ HTTP Redirects แบบ Multi-hop (เช่น GitHub Assets CDN) และตรวจสอบความสมบูรณ์ของไฟล์ APK ก่อนติดตั้ง
|
||||
- ครอบ Intent Launch ด้วย Error Handling แบบปลอดภัย พร้อมแจ้งเตือนผู้ใช้หากยังไม่ได้เปิดสิทธิ์ Unknown Sources แทนการหยุดทำงานของแอป
|
||||
- **Scrollable Changelog:** ปรับกล่องข้อความบันทึกการเปลี่ยนแปลงในหน้าต่างอัปเดตให้สามารถเลื่อนอ่าน (Vertical Scroll) ได้อย่างอิสระ ไม่ถูกตัดบรรทัด
|
||||
- **Release Title Display:** แสดงหัวข้อการอัปเดต (Release Title) ชัดเจนทั้งในหน้าต่างแจ้งเตือนและแผง Updater Tab
|
||||
|
||||
### v1.2.0 (Build 8)
|
||||
- **New Feature - EX Eidolon (3 EX Rank) Manager:** เพิ่ม UI หน้าต่างปรับแต่งและเปิด/ปิดระบบ 3 EX Eidolon สำหรับตัวละครพิเศษ:
|
||||
- JingYuan (จิ่งหยวน) [Avatar ID: 1204]
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.catlas.escapepenacony"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 8
|
||||
versionName = "v1.2.0"
|
||||
versionCode = 9
|
||||
versionName = "v1.2.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name="com.chaquo.python.android.PyApplication"
|
||||
|
||||
@@ -129,28 +129,65 @@ object AppUpdateManager {
|
||||
onProgress: (Int) -> Unit
|
||||
): Result<File> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val url = URL(downloadUrl)
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = 15000
|
||||
readTimeout = 15000
|
||||
setRequestProperty("User-Agent", "Escape-Penacony-Android-App")
|
||||
var currentUrl = downloadUrl
|
||||
var conn: HttpURLConnection? = null
|
||||
var redirects = 0
|
||||
val maxRedirects = 6
|
||||
|
||||
while (redirects < maxRedirects) {
|
||||
val u = URL(currentUrl)
|
||||
val c = (u.openConnection() as HttpURLConnection).apply {
|
||||
instanceFollowRedirects = true
|
||||
connectTimeout = 15000
|
||||
readTimeout = 25000
|
||||
setRequestProperty("User-Agent", "Escape-Penacony-Android-App")
|
||||
}
|
||||
|
||||
val code = c.responseCode
|
||||
if (code == HttpURLConnection.HTTP_MOVED_PERM ||
|
||||
code == HttpURLConnection.HTTP_MOVED_TEMP ||
|
||||
code == HttpURLConnection.HTTP_SEE_OTHER ||
|
||||
code == 307 || code == 308
|
||||
) {
|
||||
val location = c.getHeaderField("Location")
|
||||
?: throw java.io.IOException("Redirect with no Location header")
|
||||
c.disconnect()
|
||||
currentUrl = if (location.startsWith("http://") || location.startsWith("https://")) {
|
||||
location
|
||||
} else {
|
||||
URL(u, location).toString()
|
||||
}
|
||||
redirects++
|
||||
} else if (code in 200..299) {
|
||||
conn = c
|
||||
break
|
||||
} else {
|
||||
c.disconnect()
|
||||
throw java.io.IOException("HTTP error $code: ${c.responseMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
val fileLength = conn.contentLength
|
||||
val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) ?: context.filesDir
|
||||
val apkFile = File(dir, "Escape-penacony-update.apk")
|
||||
val finalConn = conn ?: throw java.io.IOException("Too many redirects: $redirects")
|
||||
val fileLength = finalConn.contentLength
|
||||
val updateDir = File(context.cacheDir, "updates").apply { mkdirs() }
|
||||
val apkFile = File(updateDir, "Escape-penacony-update.apk")
|
||||
if (apkFile.exists()) apkFile.delete()
|
||||
|
||||
conn.inputStream.use { input ->
|
||||
finalConn.inputStream.use { input ->
|
||||
FileOutputStream(apkFile).use { output ->
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
val buffer = ByteArray(16 * 1024)
|
||||
var total: Long = 0
|
||||
var count: Int
|
||||
var lastProgress = -1
|
||||
while (input.read(buffer).also { count = it } != -1) {
|
||||
total += count
|
||||
if (fileLength > 0) {
|
||||
val progress = ((total * 100) / fileLength).toInt()
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress(progress)
|
||||
val progress = ((total * 100) / fileLength).toInt().coerceIn(0, 100)
|
||||
if (progress != lastProgress) {
|
||||
lastProgress = progress
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
output.write(buffer, 0, count)
|
||||
@@ -158,36 +195,56 @@ object AppUpdateManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (!apkFile.exists() || apkFile.length() == 0L) {
|
||||
throw java.io.IOException("Downloaded APK file is empty")
|
||||
}
|
||||
|
||||
Result.success(apkFile)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun promptInstallApk(context: Context, apkFile: File) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||
val settingsIntent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(settingsIntent)
|
||||
return
|
||||
fun promptInstallApk(context: Context, apkFile: File): Result<Unit> {
|
||||
return try {
|
||||
if (!apkFile.exists() || apkFile.length() == 0L) {
|
||||
return Result.failure(IllegalStateException("Downloaded APK file is empty or missing"))
|
||||
}
|
||||
}
|
||||
|
||||
val apkUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
apkFile
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||
try {
|
||||
val settingsIntent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(settingsIntent)
|
||||
} catch (e: Exception) {
|
||||
val fallbackIntent = Intent(Settings.ACTION_SECURITY_SETTINGS).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(fallbackIntent)
|
||||
}
|
||||
return Result.failure(SecurityException("Please allow installing unknown apps from this source in Settings, then click Update again."))
|
||||
}
|
||||
}
|
||||
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val apkUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
apkFile
|
||||
)
|
||||
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(installIntent)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
context.startActivity(installIntent)
|
||||
}
|
||||
|
||||
fun isNewerVersion(current: String, latest: String): Boolean {
|
||||
|
||||
@@ -12,8 +12,10 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
@@ -461,7 +463,16 @@ fun MainScreen() {
|
||||
}
|
||||
isDownloadingApk = false
|
||||
res.onSuccess { apkFile ->
|
||||
AppUpdateManager.promptInstallApk(context, apkFile)
|
||||
AppUpdateManager.promptInstallApk(context, apkFile).onFailure { err ->
|
||||
snackbarHostState.showSnackbar(
|
||||
tr(
|
||||
currentLanguage,
|
||||
"Unable to launch installer: ${err.message}",
|
||||
"ไม่สามารถเปิดการติดตั้งได้: ${err.message}",
|
||||
"无法启动安装程序: ${err.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
snackbarHostState.showSnackbar(
|
||||
tr(currentLanguage, "Download failed: ${it.message}", "ดาวน์โหลดไม่สำเร็จ: ${it.message}", "下载失败: ${it.message}")
|
||||
@@ -928,20 +939,56 @@ fun MainScreen() {
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
tr(currentLanguage, "Current: ${BuildConfig.VERSION_NAME} -> Latest: ${release.tagName}", "เวอร์ชันปัจจุบัน: ${BuildConfig.VERSION_NAME} -> ล่าสุด: ${release.tagName}", "当前版本: ${BuildConfig.VERSION_NAME} -> 最新: ${release.tagName}"),
|
||||
tr(
|
||||
currentLanguage,
|
||||
"Current: ${BuildConfig.VERSION_NAME} -> Latest: ${release.tagName}",
|
||||
"เวอร์ชันปัจจุบัน: ${BuildConfig.VERSION_NAME} -> ล่าสุด: ${release.tagName}",
|
||||
"当前版本: ${BuildConfig.VERSION_NAME} -> 最新: ${release.tagName}"
|
||||
),
|
||||
fontSize = 13.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
if (release.changelog.isNotBlank()) {
|
||||
if (release.releaseTitle.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
release.changelog,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray,
|
||||
maxLines = 6,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
text = release.releaseTitle,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
if (release.changelog.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = tr(currentLanguage, "Changelog:", "บันทึกการเปลี่ยนแปลง:", "更新日志:"),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFFA0AEC0)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
val changelogScrollState = rememberScrollState()
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 60.dp, max = 220.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF141721)),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(changelogScrollState)
|
||||
.padding(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = release.changelog,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFD1D5DB),
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isDownloadingApk) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
LinearProgressIndicator(
|
||||
@@ -951,7 +998,12 @@ fun MainScreen() {
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
tr(currentLanguage, "Downloading APK: $apkDownloadProgress%", "กำลังดาวน์โหลดไฟล์ APK: $apkDownloadProgress%", "正在下载安装包: $apkDownloadProgress%"),
|
||||
tr(
|
||||
currentLanguage,
|
||||
"Downloading APK: $apkDownloadProgress%",
|
||||
"กำลังดาวน์โหลดไฟล์ APK: $apkDownloadProgress%",
|
||||
"正在下载安装包: $apkDownloadProgress%"
|
||||
),
|
||||
fontSize = 11.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
@@ -970,7 +1022,16 @@ fun MainScreen() {
|
||||
isDownloadingApk = false
|
||||
res.onSuccess { apkFile ->
|
||||
showAppUpdateDialog = false
|
||||
AppUpdateManager.promptInstallApk(context, apkFile)
|
||||
AppUpdateManager.promptInstallApk(context, apkFile).onFailure { err ->
|
||||
snackbarHostState.showSnackbar(
|
||||
tr(
|
||||
currentLanguage,
|
||||
"Unable to launch installer: ${err.message}",
|
||||
"ไม่สามารถเปิดการติดตั้งได้: ${err.message}",
|
||||
"无法启动安装程序: ${err.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
snackbarHostState.showSnackbar(
|
||||
tr(currentLanguage, "Download failed: ${it.message}", "ดาวน์โหลดไม่สำเร็จ: ${it.message}", "下载失败: ${it.message}")
|
||||
@@ -1785,6 +1846,16 @@ fun UpdaterTab(
|
||||
color = PurplePrimary
|
||||
)
|
||||
}
|
||||
if (appReleaseInfo != null && appReleaseInfo.releaseTitle.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = appReleaseInfo.releaseTitle,
|
||||
fontSize = 11.sp,
|
||||
color = Color.LightGray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-files-path name="downloads" path="Download/" />
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="internal_files" path="." />
|
||||
<cache-path name="internal_cache" path="." />
|
||||
<external-path name="external" path="." />
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-cache-path name="external_cache" path="." />
|
||||
</paths>
|
||||
Reference in new issue
Block a user