diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt index a3426671..e755ea6a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt @@ -50,7 +50,6 @@ import com.sukisu.ultra.ui.theme.CardConfig import java.io.File import java.text.SimpleDateFormat import java.util.* -import java.util.zip.ZipInputStream enum class FlashingStatus { FLASHING, @@ -493,26 +492,13 @@ private fun TopBar( suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String { return withContext(Dispatchers.IO) { try { - val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri)) - var entry = zipInputStream.nextEntry - var name = context.getString(R.string.unknown_module) - - while (entry != null) { - if (entry.name == "module.prop") { - val reader = java.io.BufferedReader(java.io.InputStreamReader(zipInputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("name=") == true) { - name = line.substringAfter("=") - break - } - } - break - } - entry = zipInputStream.nextEntry + if (uri == Uri.EMPTY) { + return@withContext context.getString(R.string.unknown_module) } - zipInputStream.close() - name + if (!ModuleUtils.isUriAccessible(context, uri)) { + return@withContext context.getString(R.string.unknown_module) + } + ModuleUtils.extractModuleName(context, uri) } catch (_: Exception) { context.getString(R.string.unknown_module) } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index 924272e8..2d074ae4 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -4,6 +4,7 @@ import android.app.Activity.* import android.content.Context import android.content.Intent import android.net.Uri +import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -64,10 +65,7 @@ import okhttp3.OkHttpClient import com.sukisu.ultra.ui.util.ModuleModify import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.viewmodel.ModuleViewModel -import java.io.BufferedReader -import java.io.InputStreamReader import java.util.concurrent.TimeUnit -import java.util.zip.ZipInputStream import androidx.core.content.edit import com.sukisu.ultra.R import com.sukisu.ultra.ui.theme.CardConfig.cardElevation @@ -97,38 +95,21 @@ fun ModuleScreen(navigator: DestinationsNavigator) { scope.launch { val clipData = data.clipData if (clipData != null) { - // 处理多选结果 val selectedModules = mutableListOf() val selectedModuleNames = mutableMapOf() - suspend fun processUri(uri: Uri) { - val moduleName = withContext(Dispatchers.IO) { - try { - val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri)) - var entry = zipInputStream.nextEntry - var name = context.getString(R.string.unknown_module) - - while (entry != null) { - if (entry.name == "module.prop") { - val reader = BufferedReader(InputStreamReader(zipInputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("name=") == true) { - name = line.substringAfter("=") - break - } - } - break - } - entry = zipInputStream.nextEntry - } - name - } catch (e: Exception) { - context.getString(R.string.unknown_module) + fun processUri(uri: Uri) { + try { + if (!ModuleUtils.isUriAccessible(context, uri)) { + return } + ModuleUtils.takePersistableUriPermission(context, uri) + val moduleName = ModuleUtils.extractModuleName(context, uri) + selectedModules.add(uri) + selectedModuleNames[uri] = moduleName + } catch (e: Exception) { + Log.e("ModuleScreen", "Error while processing URI: $uri, Error: ${e.message}") } - selectedModules.add(uri) - selectedModuleNames[uri] = moduleName } for (i in 0 until clipData.itemCount) { @@ -136,7 +117,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) { processUri(uri) } - // 显示确认对话框 + if (selectedModules.isEmpty()) { + snackBarHost.showSnackbar("Unable to access selected module files") + return@launch + } + val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ") val confirmResult = confirmDialog.awaitConfirm( title = context.getString(R.string.module_install), @@ -146,49 +131,42 @@ fun ModuleScreen(navigator: DestinationsNavigator) { ) if (confirmResult == ConfirmResult.Confirmed) { - // 批量安装模块 - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules))) - viewModel.markNeedRefresh() - } - } else { - // 单个文件安装逻辑 - val uri = data.data ?: return@launch - val moduleName = withContext(Dispatchers.IO) { try { - val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri)) - var entry = zipInputStream.nextEntry - var name = context.getString(R.string.unknown_module) - - while (entry != null) { - if (entry.name == "module.prop") { - val reader = BufferedReader(InputStreamReader(zipInputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("name=") == true) { - name = line.substringAfter("=") - break - } - } - break - } - entry = zipInputStream.nextEntry - } - name + // 批量安装模块 + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules))) + viewModel.markNeedRefresh() } catch (e: Exception) { - context.getString(R.string.unknown_module) + Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}") + snackBarHost.showSnackbar("Error while installing module: ${e.message}") } } + } else { + val uri = data.data ?: return@launch + // 单个安装模块 + try { + if (!ModuleUtils.isUriAccessible(context, uri)) { + snackBarHost.showSnackbar("Unable to access selected module files") + return@launch + } - val confirmResult = confirmDialog.awaitConfirm( - title = context.getString(R.string.module_install), - content = context.getString(R.string.module_install_confirm, moduleName), - confirm = context.getString(R.string.install), - dismiss = context.getString(R.string.cancel) - ) + ModuleUtils.takePersistableUriPermission(context, uri) - if (confirmResult == ConfirmResult.Confirmed) { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) - viewModel.markNeedRefresh() + val moduleName = ModuleUtils.extractModuleName(context, uri) + + val confirmResult = confirmDialog.awaitConfirm( + title = context.getString(R.string.module_install), + content = context.getString(R.string.module_install_confirm, moduleName), + confirm = context.getString(R.string.install), + dismiss = context.getString(R.string.cancel) + ) + + if (confirmResult == ConfirmResult.Confirmed) { + navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) + viewModel.markNeedRefresh() + } + } catch (e: Exception) { + Log.e("ModuleScreen", "Error processing a single URI: $uri, Error: ${e.message}") + snackBarHost.showSnackbar("Error processing module file: ${e.message}") } } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt new file mode 100644 index 00000000..7c7adc39 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt @@ -0,0 +1,105 @@ +package com.sukisu.ultra.ui.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import java.io.BufferedReader +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.util.zip.ZipInputStream +import com.sukisu.ultra.R +import android.util.Log +import java.io.IOException + +object ModuleUtils { + private const val TAG = "ModuleUtils" + + fun extractModuleName(context: Context, uri: Uri): String { + if (uri == Uri.EMPTY) { + Log.e(TAG, "The supplied URI is empty") + return context.getString(R.string.unknown_module) + } + + return try { + Log.d(TAG, "Start extracting module names from URIs: $uri") + + // 从URI路径中提取文件名 + val fileName = uri.lastPathSegment?.let { path -> + val lastSlash = path.lastIndexOf('/') + if (lastSlash != -1 && lastSlash < path.length - 1) { + path.substring(lastSlash + 1) + } else { + path + } + }?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module) + + var formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.()\\u4e00-\\u9fa5]"), "").trim() + var moduleName = formattedFileName + + try { + // 打开ZIP文件输入流 + val inputStream = context.contentResolver.openInputStream(uri) + if (inputStream == null) { + Log.e(TAG, "Unable to get input stream from URI: $uri") + return formattedFileName + } + + val zipInputStream = ZipInputStream(inputStream) + var entry = zipInputStream.nextEntry + + // 遍历ZIP文件中的条目,查找module.prop文件 + while (entry != null) { + if (entry.name == "module.prop") { + val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8)) + var line: String? + var nameFound = false + while (reader.readLine().also { line = it } != null) { + if (line?.startsWith("name=") == true) { + moduleName = line.substringAfter("=") + moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.()\\u4e00-\\u9fa5]"), "").trim() + nameFound = true + break + } + } + break + } + entry = zipInputStream.nextEntry + } + zipInputStream.close() + Log.d(TAG, "Successfully extracted module name: $moduleName") + moduleName + } catch (e: IOException) { + Log.e(TAG, "Error reading ZIP file: ${e.message}") + formattedFileName + } + } catch (e: Exception) { + Log.e(TAG, "Exception when extracting module name: ${e.message}") + context.getString(R.string.unknown_module) + } + } + + // 验证URI是否有效并可访问 + fun isUriAccessible(context: Context, uri: Uri): Boolean { + if (uri == Uri.EMPTY) return false + + return try { + val inputStream = context.contentResolver.openInputStream(uri) + inputStream?.close() + inputStream != null + } catch (e: Exception) { + Log.e(TAG, "The URI is inaccessible: $uri, Error: ${e.message}") + false + } + } + + // 获取URI的持久权限 + fun takePersistableUriPermission(context: Context, uri: Uri) { + try { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + context.contentResolver.takePersistableUriPermission(uri, flags) + Log.d(TAG, "Persistent permissions for URIs have been obtained: $uri") + } catch (e: Exception) { + Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}") + } + } +} \ No newline at end of file