Add module download error alerts and optimize update checking logic
- Add a formatting string for the update list - Fix module update failures caused by spaces and other non Linux readable characters. Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
@@ -66,6 +66,7 @@ import com.sukisu.ultra.ui.theme.getCardColors
|
|||||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
@@ -443,6 +444,7 @@ private fun ModuleList(
|
|||||||
val downloadingText = stringResource(R.string.module_downloading)
|
val downloadingText = stringResource(R.string.module_downloading)
|
||||||
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
||||||
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
||||||
|
val downloadErrorText = stringResource(R.string.module_download_error)
|
||||||
|
|
||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
@@ -453,12 +455,20 @@ private fun ModuleList(
|
|||||||
downloadUrl: String,
|
downloadUrl: String,
|
||||||
fileName: String
|
fileName: String
|
||||||
) {
|
) {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = okhttp3.Request.Builder()
|
||||||
|
.url(changelogUrl)
|
||||||
|
.header("User-Agent", "SukiSU-Ultra/2.0")
|
||||||
|
.build()
|
||||||
|
|
||||||
val changelogResult = loadingDialog.withLoading {
|
val changelogResult = loadingDialog.withLoading {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
OkHttpClient().newCall(
|
client.newCall(request).execute().body!!.string()
|
||||||
okhttp3.Request.Builder().url(changelogUrl).build()
|
|
||||||
).execute().body!!.string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,6 +517,11 @@ private fun ModuleList(
|
|||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "$downloadErrorText: $errorMsg", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -823,14 +838,6 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.Outlined.PlayArrow,
|
imageVector = Icons.Outlined.PlayArrow,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// text = stringResource(R.string.action),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,14 +856,6 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
// text = stringResource(R.string.open)
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,14 +874,6 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
//if (!module.hasActionScript || !module.hasWebUi) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
// text = stringResource(R.string.module_update)
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,7 +882,7 @@ fun ModuleItem(
|
|||||||
onClick = { onUninstallClicked(module) },
|
onClick = { onUninstallClicked(module) },
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||||
) {
|
) {
|
||||||
if (!module.remove) {
|
if (!module.remove) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -906,15 +897,6 @@ fun ModuleItem(
|
|||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
|
|
||||||
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,23 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val TAG = "DownloadUtil"
|
||||||
|
private val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
|
||||||
|
private const val MAX_RETRY_COUNT = 3
|
||||||
|
private const val RETRY_DELAY_MS = 3000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -26,8 +36,10 @@ fun download(
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
description: String,
|
description: String,
|
||||||
onDownloaded: (Uri) -> Unit = {},
|
onDownloaded: (Uri) -> Unit = {},
|
||||||
onDownloading: () -> Unit = {}
|
onDownloading: () -> Unit = {},
|
||||||
|
onError: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
Log.d(TAG, "Start Download: $url")
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
val query = DownloadManager.Query()
|
val query = DownloadManager.Query()
|
||||||
@@ -49,6 +61,13 @@ fun download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val downloadFile = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
if (downloadFile.exists()) {
|
||||||
|
downloadFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
val request = DownloadManager.Request(url.toUri())
|
val request = DownloadManager.Request(url.toUri())
|
||||||
.setDestinationInExternalPublicDir(
|
.setDestinationInExternalPublicDir(
|
||||||
@@ -59,66 +78,206 @@ fun download(
|
|||||||
.setMimeType("application/zip")
|
.setMimeType("application/zip")
|
||||||
.setTitle(fileName)
|
.setTitle(fileName)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
|
.addRequestHeader("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.setAllowedOverMetered(true)
|
||||||
|
.setAllowedOverRoaming(true)
|
||||||
|
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||||
|
|
||||||
downloadManager.enqueue(request)
|
try {
|
||||||
|
val downloadId = downloadManager.enqueue(request)
|
||||||
|
Log.d(TAG, "Successful launch of the download,ID: $downloadId")
|
||||||
|
monitorDownload(context, downloadManager, downloadId, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Download startup failure", e)
|
||||||
|
onError("Download startup failure: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun monitorDownload(
|
||||||
|
context: Context,
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
downloadId: Long,
|
||||||
|
url: String,
|
||||||
|
fileName: String,
|
||||||
|
description: String,
|
||||||
|
onDownloaded: (Uri) -> Unit,
|
||||||
|
onDownloading: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
retryCount: Int = 0
|
||||||
|
) {
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
|
||||||
|
var lastProgress = -1
|
||||||
|
var stuckCounter = 0
|
||||||
|
|
||||||
|
val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
Log.d(TAG, "Download Successfully: $localUri")
|
||||||
|
onDownloaded(localUri.toUri())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_FAILED -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||||
|
Log.d(TAG, "Download failed with reason code: $reason")
|
||||||
|
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
Log.d(TAG, "Attempts to re download, number of retries: ${retryCount + 1}")
|
||||||
|
handler.postDelayed({
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
}, RETRY_DELAY_MS)
|
||||||
|
} else {
|
||||||
|
onError("Download failed, please check network connection or storage space")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val downloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||||
|
|
||||||
|
if (totalBytes > 0) {
|
||||||
|
val progress = (downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
if (progress == lastProgress) {
|
||||||
|
stuckCounter++
|
||||||
|
if (stuckCounter > 30) {
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
Log.d(TAG, "Download stalled and restarted")
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastProgress = progress
|
||||||
|
stuckCounter = 0
|
||||||
|
Log.d(TAG, "Download progress: $progress% ($downloadedBytes/$totalBytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.post(runnable)
|
||||||
|
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
|
||||||
|
if (id == downloadId) {
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
|
||||||
|
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
onDownloaded(localUri.toUri())
|
||||||
|
} else {
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
download(context!!, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
} else {
|
||||||
|
onError("Download failed, please try again later")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context?.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context,
|
||||||
|
receiver,
|
||||||
|
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkNewVersion(): LatestVersionInfo {
|
fun checkNewVersion(): LatestVersionInfo {
|
||||||
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
||||||
val defaultValue = LatestVersionInfo()
|
val defaultValue = LatestVersionInfo()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
|
val client = okhttp3.OkHttpClient.Builder()
|
||||||
.use { response ->
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
if (!response.isSuccessful) {
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
return defaultValue
|
.build()
|
||||||
}
|
|
||||||
val body = response.body?.string()
|
|
||||||
if (body == null) {
|
|
||||||
Log.d("CheckUpdate", "Response body is null")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
Log.d("CheckUpdate", "Response body: $body")
|
|
||||||
val json = org.json.JSONObject(body)
|
|
||||||
|
|
||||||
// 直接从 tag_name 提取版本号(如 v1.1)
|
val request = okhttp3.Request.Builder()
|
||||||
val tagName = json.optString("tag_name", "")
|
.url(url)
|
||||||
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
|
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
// 从 body 字段获取更新日志(保留换行符)
|
client.newCall(request).execute().use { response ->
|
||||||
val changelog = json.optString("body")
|
if (!response.isSuccessful) {
|
||||||
.replace("\\r\\n", "\n") // 转换换行符
|
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
||||||
|
return defaultValue
|
||||||
val assets = json.getJSONArray("assets")
|
|
||||||
for (i in 0 until assets.length()) {
|
|
||||||
val asset = assets.getJSONObject(i)
|
|
||||||
val name = asset.getString("name")
|
|
||||||
if (!name.endsWith(".apk")) continue
|
|
||||||
|
|
||||||
// 修改正则表达式,只匹配 SukiSU 和版本号
|
|
||||||
val regex = Regex("SukiSU.*_(\\d+)-release")
|
|
||||||
val matchResult = regex.find(name)
|
|
||||||
if (matchResult == null) {
|
|
||||||
Log.d("CheckUpdate", "No match found in $name, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val versionCode = matchResult.groupValues[1].toInt()
|
|
||||||
|
|
||||||
val downloadUrl = asset.getString("browser_download_url")
|
|
||||||
return LatestVersionInfo(
|
|
||||||
versionCode,
|
|
||||||
downloadUrl,
|
|
||||||
changelog,
|
|
||||||
versionName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Log.d("CheckUpdate", "No valid apk asset found, returning default value")
|
|
||||||
defaultValue
|
|
||||||
}
|
}
|
||||||
|
val body = response.body?.string()
|
||||||
|
if (body == null) {
|
||||||
|
Log.d("CheckUpdate", "Return data is null")
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
Log.d("CheckUpdate", "Return data: $body")
|
||||||
|
val json = org.json.JSONObject(body)
|
||||||
|
|
||||||
|
// 直接从 tag_name 提取版本号(如 v1.1)
|
||||||
|
val tagName = json.optString("tag_name", "")
|
||||||
|
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
|
||||||
|
|
||||||
|
// 从 body 字段获取更新日志(保留换行符)
|
||||||
|
val changelog = json.optString("body")
|
||||||
|
.replace("\\r\\n", "\n") // 转换换行符
|
||||||
|
|
||||||
|
val assets = json.getJSONArray("assets")
|
||||||
|
for (i in 0 until assets.length()) {
|
||||||
|
val asset = assets.getJSONObject(i)
|
||||||
|
val name = asset.getString("name")
|
||||||
|
if (!name.endsWith(".apk")) continue
|
||||||
|
|
||||||
|
val regex = Regex("SukiSU.*_(\\d+)-release")
|
||||||
|
val matchResult = regex.find(name)
|
||||||
|
if (matchResult == null) {
|
||||||
|
Log.d("CheckUpdate", "No matches found: $name, skip over")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val versionCode = matchResult.groupValues[1].toInt()
|
||||||
|
|
||||||
|
val downloadUrl = asset.getString("browser_download_url")
|
||||||
|
return LatestVersionInfo(
|
||||||
|
versionCode,
|
||||||
|
downloadUrl,
|
||||||
|
changelog,
|
||||||
|
versionName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d("CheckUpdate", "No valid APK resource found, return default value")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
}.getOrDefault(defaultValue)
|
}.getOrDefault(defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||||
DisposableEffect(context) {
|
DisposableEffect(context) {
|
||||||
@@ -158,4 +317,3 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ import org.json.JSONArray
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ModuleViewModel : ViewModel() {
|
class ModuleViewModel : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ModuleViewModel"
|
private const val TAG = "ModuleViewModel"
|
||||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||||
|
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModuleInfo(
|
class ModuleInfo(
|
||||||
@@ -117,6 +119,10 @@ class ModuleViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sanitizeVersionString(version: String): String {
|
||||||
|
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
|
||||||
|
}
|
||||||
|
|
||||||
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
||||||
val empty = Triple("", "", "")
|
val empty = Triple("", "", "")
|
||||||
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
||||||
@@ -126,19 +132,32 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val result = kotlin.runCatching {
|
val result = kotlin.runCatching {
|
||||||
val url = m.updateJson
|
val url = m.updateJson
|
||||||
Log.i(TAG, "checkUpdate url: $url")
|
Log.i(TAG, "checkUpdate url: $url")
|
||||||
val response = okhttp3.OkHttpClient()
|
|
||||||
.newCall(
|
val client = okhttp3.OkHttpClient.Builder()
|
||||||
okhttp3.Request.Builder()
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
.url(url)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.build()
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
).execute()
|
.build()
|
||||||
|
|
||||||
|
val request = okhttp3.Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
Log.d(TAG, "checkUpdate code: ${response.code}")
|
Log.d(TAG, "checkUpdate code: ${response.code}")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body?.string() ?: ""
|
response.body?.string() ?: ""
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "checkUpdate failed: ${response.message}")
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}.getOrDefault("")
|
}.getOrElse { e ->
|
||||||
|
Log.e(TAG, "checkUpdate exception", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "checkUpdate result: $result")
|
Log.i(TAG, "checkUpdate result: $result")
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -149,7 +168,8 @@ class ModuleViewModel : ViewModel() {
|
|||||||
JSONObject(result)
|
JSONObject(result)
|
||||||
}.getOrNull() ?: return empty
|
}.getOrNull() ?: return empty
|
||||||
|
|
||||||
val version = updateJson.optString("version", "")
|
var version = updateJson.optString("version", "")
|
||||||
|
version = sanitizeVersionString(version)
|
||||||
val versionCode = updateJson.optInt("versionCode", 0)
|
val versionCode = updateJson.optInt("versionCode", 0)
|
||||||
val zipUrl = updateJson.optString("zipUrl", "")
|
val zipUrl = updateJson.optString("zipUrl", "")
|
||||||
val changelog = updateJson.optString("changelog", "")
|
val changelog = updateJson.optString("changelog", "")
|
||||||
|
|||||||
@@ -366,4 +366,5 @@
|
|||||||
<string name="check_log">Please check the log</string>
|
<string name="check_log">Please check the log</string>
|
||||||
<string name="installing_module">Module being installed %1$d/%2$d</string>
|
<string name="installing_module">Module being installed %1$d/%2$d</string>
|
||||||
<string name="module_failed_count">%d Failed to install a new module</string>
|
<string name="module_failed_count">%d Failed to install a new module</string>
|
||||||
|
<string name="module_download_error">Module download failed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user