diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts index d8fa4434..ba20f7fb 100644 --- a/V2rayNG/app/build.gradle.kts +++ b/V2rayNG/app/build.gradle.kts @@ -153,6 +153,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.lifecycle.runtime.compose) + implementation(libs.coil.compose) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt index 219e35e9..3440ee6e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt @@ -1,11 +1,8 @@ package com.v2ray.ang.dto -import android.graphics.drawable.Drawable - data class AppInfo( val appName: String, val packageName: String, - val appIcon: Drawable, val isSystemApp: Boolean, var isSelected: Int ) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerActivity.kt index 26d7246c..c4391af0 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerActivity.kt @@ -187,7 +187,7 @@ fun AppPickerScreen( AppListItem( appName = app.appName, packageName = app.packageName, - icon = app.appIcon, + icon = null, checked = checked, onCheckedChange = { onToggleApp(app.packageName) } ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt index b56c526e..c04c8362 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt @@ -120,13 +120,9 @@ class AppPickerViewModel(application: Application) : BaseViewModel(application) } private fun createSpecialItemUnidentified(context: Context): AppInfo { - val icon = context.getDrawable(android.R.drawable.ic_menu_help) - ?: context.getDrawable(android.R.drawable.sym_def_app_icon) - ?: error("No fallback drawable available") return AppInfo( appName = context.getString(R.string.app_picker_unknown_app), packageName = AppConfig.UNIDENTIFIED_PACKAGE, - appIcon = icon, isSystemApp = false, isSelected = 0 ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Components.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Components.kt index ff695448..95d60e8d 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Components.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/compose/Components.kt @@ -1,14 +1,10 @@ package com.v2ray.ang.ui.compose -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -45,7 +41,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalHapticFeedback @@ -53,7 +48,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.v2ray.ang.util.AppIconFetcher import androidx.compose.ui.unit.sp import com.v2ray.ang.R import sh.calvin.reorderable.ReorderableCollectionItemScope @@ -163,6 +162,7 @@ fun AppListItem( onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier ) { + val context = LocalContext.current Row( modifier = modifier .fillMaxWidth() @@ -170,41 +170,26 @@ fun AppListItem( .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { - if (icon != null) { - when (icon) { - is Bitmap -> { - Image( - bitmap = icon.asImageBitmap(), - contentDescription = null, - modifier = Modifier.size(40.dp), - contentScale = ContentScale.Fit - ) - } - - is Int -> { - Image( - painter = painterResource(id = icon), - contentDescription = null, - modifier = Modifier.size(40.dp), - contentScale = ContentScale.Fit - ) - } - - is Drawable -> { - val bitmap = remember(icon) { createBitmapFromDrawable(icon) } - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier.size(40.dp), - contentScale = ContentScale.Fit - ) - } - - else -> { - Box(modifier = Modifier.size(40.dp)) - } + val model = remember(icon, packageName) { + if (icon != null) { + icon + } else { + val data = "appicon:$packageName" + ImageRequest.Builder(context) + .data(data) + .fetcherFactory(AppIconFetcher.Factory(context)) + .build() } } + + AsyncImage( + model = model, + contentDescription = null, + modifier = Modifier.size(40.dp), + contentScale = ContentScale.Fit, + error = painterResource(R.drawable.ic_image_24dp), + fallback = painterResource(R.drawable.ic_image_24dp) + ) Spacer(modifier = Modifier.width(16.dp)) Column(modifier = Modifier.weight(1f)) { Text( @@ -230,19 +215,6 @@ fun AppListItem( } } -private fun createBitmapFromDrawable(drawable: Drawable): Bitmap { - if (drawable is BitmapDrawable) { - return drawable.bitmap - } - val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 96 - val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: 96 - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - return bitmap -} - @Composable fun ItemDivider() { AppDivider(modifier = Modifier.padding(horizontal = 12.dp)) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/perappproxy/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/perappproxy/PerAppProxyActivity.kt index e1de5fe7..ceb3be5b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/perappproxy/PerAppProxyActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/perappproxy/PerAppProxyActivity.kt @@ -272,7 +272,7 @@ fun PerAppProxyScreen( AppListItem( appName = app.appName, packageName = app.packageName, - icon = app.appIcon, + icon = null, checked = checked, onCheckedChange = { onToggleApp(app.packageName) } ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppIconFetcher.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppIconFetcher.kt new file mode 100644 index 00000000..dbd06493 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppIconFetcher.kt @@ -0,0 +1,46 @@ +package com.v2ray.ang.util + +import android.content.Context +import coil.ImageLoader +import coil.decode.DataSource +import coil.fetch.DrawableResult +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.request.Options +import com.v2ray.ang.AppConfig + +class AppIconFetcher( + private val packageName: String, + private val context: Context +) : Fetcher { + override suspend fun fetch(): FetchResult? { + val drawable = try { + if (packageName == AppConfig.UNIDENTIFIED_PACKAGE) { + context.getDrawable(android.R.drawable.ic_menu_help) + ?: context.getDrawable(android.R.drawable.sym_def_app_icon) + } else { + context.packageManager.getApplicationIcon(packageName) + } + } catch (_: Exception) { + null + } ?: return null + + return DrawableResult( + drawable = drawable, + isSampled = false, + dataSource = DataSource.DISK + ) + } + + class Factory(private val context: Context) : Fetcher.Factory { + override fun create(data: Any, options: Options, imageLoader: ImageLoader): Fetcher? { + val dataString = data.toString() + + if (dataString.startsWith("appicon:")) { + val pkg = dataString.substringAfter("appicon:") + return AppIconFetcher(pkg, context) + } + return null + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt index 577698ea..697d619f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt @@ -24,10 +24,9 @@ object AppManagerUtil { val applicationInfo = pkg.applicationInfo ?: continue val appName = applicationInfo.loadLabel(packageManager).toString() - val appIcon = applicationInfo.loadIcon(packageManager) ?: continue val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0 - val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) + val appInfo = AppInfo(appName, pkg.packageName, isSystemApp, 0) apps.add(appInfo) } diff --git a/V2rayNG/gradle/libs.versions.toml b/V2rayNG/gradle/libs.versions.toml index d29bd827..bdf44297 100644 --- a/V2rayNG/gradle/libs.versions.toml +++ b/V2rayNG/gradle/libs.versions.toml @@ -19,6 +19,7 @@ mockitoMockitoInline = "5.2.0" camerax = "1.6.1" composeBom = "2026.06.01" reorderable = "3.1.0" +coil = "2.7.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" } @@ -54,6 +55,7 @@ androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleViewmodelKtx" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" }