Apply Spotless formatting to Java and Kotlin files

This commit is contained in:
世界
2026-01-17 16:58:26 +08:00
parent 3c9ab19466
commit 9c820a3400
170 changed files with 4496 additions and 5318 deletions

View File

@@ -25,7 +25,7 @@ object RootInstaller {
handle.service.installPackage(
pfd,
apkFile.length(),
android.os.Process.myUserHandle().hashCode()
android.os.Process.myUserHandle().hashCode(),
)
}
}
@@ -69,10 +69,7 @@ object RootInstaller {
}
}
private class RootServiceHandle(
val connection: ServiceConnection,
val service: IRootService
) : java.io.Closeable {
private class RootServiceHandle(val connection: ServiceConnection, val service: IRootService) : java.io.Closeable {
override fun close() {
Handler(Looper.getMainLooper()).post {
RootService.unbind(connection)

View File

@@ -10,9 +10,7 @@ import android.content.pm.PackageInstaller as AndroidPackageInstaller
object SystemPackageInstaller {
fun canSystemSilentInstall(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
}
fun canSystemSilentInstall(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
fun install(context: Context, apkFile: File) {
val packageInstaller = context.packageManager.packageInstaller
@@ -38,7 +36,7 @@ object SystemPackageInstaller {
context,
sessionId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)
session.commit(pendingIntent.intentSender)

View File

@@ -15,10 +15,7 @@ import io.nekohasekai.sfa.update.UpdateState
import io.nekohasekai.sfa.update.UpdateTrack
import java.util.concurrent.TimeUnit
class UpdateWorker(
private val appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
class UpdateWorker(private val appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
companion object {
private const val WORK_NAME = "AutoUpdate"
@@ -37,7 +34,8 @@ class UpdateWorker(
.build()
val workRequest = PeriodicWorkRequestBuilder<UpdateWorker>(
24, TimeUnit.HOURS
24,
TimeUnit.HOURS,
)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
@@ -46,7 +44,7 @@ class UpdateWorker(
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
workRequest
workRequest,
)
Log.d(TAG, "Auto update scheduled")
}

View File

@@ -4,6 +4,12 @@ import android.os.Bundle;
import android.os.IInterface;
public interface IIntentReceiver extends IInterface {
void performReceive(Intent intent, int resultCode, String data, Bundle extras,
boolean ordered, boolean sticky, int sendingUser);
void performReceive(
Intent intent,
int resultCode,
String data,
Bundle extras,
boolean ordered,
boolean sticky,
int sendingUser);
}

View File

@@ -7,8 +7,14 @@ import android.os.IInterface;
public interface IIntentSender extends IInterface {
void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options);
void send(
int code,
Intent intent,
String resolvedType,
IBinder whitelistToken,
IIntentReceiver finishedReceiver,
String requiredPermission,
Bundle options);
abstract class Stub extends Binder implements IIntentSender {
public static IIntentSender asInterface(IBinder binder) {

View File

@@ -7,7 +7,12 @@ import android.os.RemoteException;
public interface IPackageInstaller extends IInterface {
int createSession(PackageInstaller.SessionParams params, String installerPackageName, String installerAttributionTag, int userId) throws RemoteException;
int createSession(
PackageInstaller.SessionParams params,
String installerPackageName,
String installerAttributionTag,
int userId)
throws RemoteException;
IPackageInstallerSession openSession(int sessionId) throws RemoteException;

View File

@@ -6,10 +6,8 @@ import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -34,7 +32,8 @@ public final class RemotePreferences implements SharedPreferences {
private final String mGroup;
private final Lock mLock = new ReentrantLock();
private final Map<String, Object> mMap = new ConcurrentHashMap<>();
private final Map<OnSharedPreferenceChangeListener, Object> mListeners = Collections.synchronizedMap(new WeakHashMap<>());
private final Map<OnSharedPreferenceChangeListener, Object> mListeners =
Collections.synchronizedMap(new WeakHashMap<>());
private volatile boolean isDeleted = false;
@@ -114,7 +113,8 @@ public final class RemotePreferences implements SharedPreferences {
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
mListeners.remove(listener);
}
@@ -197,7 +197,9 @@ public final class RemotePreferences implements SharedPreferences {
changes.addAll(mDelete);
changes.addAll(mMap.keySet());
for (String key : changes) {
mListeners.keySet().forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
mListeners
.keySet()
.forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
}
Bundle bundle = new Bundle();

View File

@@ -7,7 +7,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -22,7 +21,12 @@ public final class XposedProvider extends ContentProvider {
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
public Cursor query(
@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
return null;
}
@@ -39,12 +43,17 @@ public final class XposedProvider extends ContentProvider {
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
public int delete(
@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
public int update(
@NonNull Uri uri,
@Nullable ContentValues values,
@Nullable String selection,
@Nullable String[] selectionArgs) {
return 0;
}

View File

@@ -3,10 +3,8 @@ package io.github.libxposed.service;
import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -16,7 +14,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
@SuppressWarnings("unused")
public final class XposedService {
public final static class ServiceException extends RuntimeException {
public static final class ServiceException extends RuntimeException {
ServiceException(String message) {
super(message);
}
@@ -26,43 +24,38 @@ public final class XposedService {
}
}
private final static Map<OnScopeEventListener, IXposedScopeCallback> scopeCallbacks = new WeakHashMap<>();
private static final Map<OnScopeEventListener, IXposedScopeCallback> scopeCallbacks =
new WeakHashMap<>();
/**
* Callback interface for module scope request.
*/
/** Callback interface for module scope request. */
public interface OnScopeEventListener {
/**
* Callback when the request notification / window prompted.
*
* @param packageName Package name of requested app
*/
default void onScopeRequestPrompted(String packageName) {
}
default void onScopeRequestPrompted(String packageName) {}
/**
* Callback when the request is approved.
*
* @param packageName Package name of requested app
*/
default void onScopeRequestApproved(String packageName) {
}
default void onScopeRequestApproved(String packageName) {}
/**
* Callback when the request is denied.
*
* @param packageName Package name of requested app
*/
default void onScopeRequestDenied(String packageName) {
}
default void onScopeRequestDenied(String packageName) {}
/**
* Callback when the request is timeout or revoked.
*
* @param packageName Package name of requested app
*/
default void onScopeRequestTimeout(String packageName) {
}
default void onScopeRequestTimeout(String packageName) {}
/**
* Callback when the request is failed.
@@ -70,11 +63,13 @@ public final class XposedService {
* @param packageName Package name of requested app
* @param message Error message
*/
default void onScopeRequestFailed(String packageName, String message) {
}
default void onScopeRequestFailed(String packageName, String message) {}
private IXposedScopeCallback asInterface() {
return scopeCallbacks.computeIfAbsent(this, (listener) -> new IXposedScopeCallback.Stub() {
return scopeCallbacks.computeIfAbsent(
this,
(listener) ->
new IXposedScopeCallback.Stub() {
@Override
public void onScopeRequestPrompted(String packageName) {
listener.onScopeRequestPrompted(packageName);
@@ -104,28 +99,21 @@ public final class XposedService {
}
public enum Privilege {
/**
* Unknown privilege value.
*/
/** Unknown privilege value. */
FRAMEWORK_PRIVILEGE_UNKNOWN,
/**
* The framework is running as root.
*/
/** The framework is running as root. */
FRAMEWORK_PRIVILEGE_ROOT,
/**
* The framework is running in a container with a fake system_server.
*/
/** The framework is running in a container with a fake system_server. */
FRAMEWORK_PRIVILEGE_CONTAINER,
/**
* The framework is running as a different app, which may have at most shell permission.
*/
/** The framework is running as a different app, which may have at most shell permission. */
FRAMEWORK_PRIVILEGE_APP,
/**
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will be null and remote file is unsupported.
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will
* be null and remote file is unsupported.
*/
FRAMEWORK_PRIVILEGE_EMBEDDED
}
@@ -211,7 +199,9 @@ public final class XposedService {
public Privilege getFrameworkPrivilege() {
try {
int value = mService.getFrameworkPrivilege();
return (value >= 0 && value <= 3) ? Privilege.values()[value + 1] : Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
return (value >= 0 && value <= 3)
? Privilege.values()[value + 1]
: Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
} catch (RemoteException e) {
throw new ServiceException(e);
}
@@ -273,7 +263,9 @@ public final class XposedService {
*/
@NonNull
public SharedPreferences getRemotePreferences(@NonNull String group) {
return mRemotePrefs.computeIfAbsent(group, k -> {
return mRemotePrefs.computeIfAbsent(
group,
k -> {
try {
RemotePreferences instance = RemotePreferences.newInstance(this, k);
if (instance == null) {
@@ -300,7 +292,9 @@ public final class XposedService {
deletionLock.writeLock().lock();
try {
mService.deleteRemotePreferences(group);
mRemotePrefs.computeIfPresent(group, (k, v) -> {
mRemotePrefs.computeIfPresent(
group,
(k, v) -> {
v.setDeleted();
return null;
});

View File

@@ -2,9 +2,7 @@ package io.github.libxposed.service;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@@ -12,21 +10,17 @@ import java.util.Set;
@SuppressWarnings("unused")
public final class XposedServiceHelper {
/**
* Callback interface for Xposed service.
*/
/** Callback interface for Xposed service. */
public interface OnServiceListener {
/**
* Callback when the service is connected.<br/>
* Callback when the service is connected.<br>
* This method could be called multiple times if multiple Xposed frameworks exist.
*
* @param service Service instance
*/
void onServiceBind(@NonNull XposedService service);
/**
* Callback when the service is dead.
*/
/** Callback when the service is dead. */
void onServiceDied(@NonNull XposedService service);
}
@@ -52,7 +46,7 @@ public final class XposedServiceHelper {
}
/**
* Register a ServiceListener to receive service binders from Xposed frameworks.<br/>
* Register a ServiceListener to receive service binders from Xposed frameworks.<br>
* This method should only be called once.
*
* @param listener Listener to register

View File

@@ -18,8 +18,8 @@ import io.nekohasekai.sfa.bg.UpdateProfileWork
import io.nekohasekai.sfa.constant.Bugs
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.utils.HookStatusClient
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.vendor.Vendor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope

View File

@@ -8,7 +8,6 @@ import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import java.io.File
import java.io.FileNotFoundException
class WorkingDirectoryProvider : DocumentsProvider() {
@@ -47,7 +46,7 @@ class WorkingDirectoryProvider : DocumentsProvider() {
add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD,
)
add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher)
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
@@ -64,11 +63,7 @@ class WorkingDirectoryProvider : DocumentsProvider() {
return result
}
override fun queryChildDocuments(
parentDocumentId: String,
projection: Array<out String>?,
sortOrder: String?
): Cursor {
override fun queryChildDocuments(parentDocumentId: String, projection: Array<out String>?, sortOrder: String?): Cursor {
val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
val parent = getFileForDocId(parentDocumentId)
parent.listFiles()?.forEach { file ->
@@ -77,21 +72,13 @@ class WorkingDirectoryProvider : DocumentsProvider() {
return result
}
override fun openDocument(
documentId: String,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor {
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor {
val file = getFileForDocId(documentId)
val accessMode = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(file, accessMode)
}
override fun createDocument(
parentDocumentId: String,
mimeType: String,
displayName: String
): String {
override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String {
val parent = getFileForDocId(parentDocumentId)
val file = File(parent, displayName)

View File

@@ -8,8 +8,8 @@ import android.os.Build
import android.util.Log
import android.widget.Toast
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.vendor.PackageQueryManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -21,10 +21,7 @@ class AppChangeReceiver : BroadcastReceiver() {
private const val TAG = "AppChangeReceiver"
}
override fun onReceive(
context: Context,
intent: Intent,
) {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "onReceive: ${intent.action}")
if (!Settings.perAppProxyEnabled) {
Log.d(TAG, "per app proxy disabled")

View File

@@ -12,10 +12,7 @@ import kotlinx.coroutines.withContext
class BootReceiver : BroadcastReceiver() {
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(
context: Context,
intent: Intent,
) {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
}

View File

@@ -28,13 +28,13 @@ import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.SystemProxyStatus
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.MainActivity
import io.nekohasekai.sfa.constant.Action
import io.nekohasekai.sfa.constant.Alert
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.hasPermission
import io.nekohasekai.sfa.compose.MainActivity
import io.nekohasekai.sfa.vendor.Vendor
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -44,10 +44,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
class BoxService(
private val service: Service,
private val platformInterface: PlatformInterface,
) : CommandServerHandler {
class BoxService(private val service: Service, private val platformInterface: PlatformInterface) : CommandServerHandler {
companion object {
private const val PROFILE_UPDATE_INTERVAL = 15L * 60 * 1000 // 15 minutes in milliseconds
private const val TAG = "BoxService"
@@ -81,10 +78,7 @@ class BoxService(
private var receiverRegistered = false
private val receiver =
object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent,
) {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Action.SERVICE_CLOSE -> {
stopService()
@@ -316,10 +310,7 @@ class BoxService(
}
}
private suspend fun stopAndAlert(
type: Alert,
message: String? = null,
) {
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
Settings.startedByUser = false
withContext(Dispatchers.Main) {
if (receiverRegistered) {
@@ -368,9 +359,7 @@ class BoxService(
return Service.START_NOT_STICKY
}
internal fun onBind(): IBinder {
return binder
}
internal fun onBind(): IBinder = binder
internal fun onDestroy() {
binder.close()

View File

@@ -13,7 +13,6 @@ import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -134,11 +133,7 @@ object DebugInfoExporter {
return count
}
private fun addLogEntries(
zip: ZipOutputStream,
warnings: MutableList<String>,
context: Context,
): Int {
private fun addLogEntries(zip: ZipOutputStream, warnings: MutableList<String>, context: Context): Int {
var count = 0
if (streamCommandToZip(zip, "logs/logcat.txt", warnings, listOf("logcat", "-d", "-b", "all")) != null) count++
if (streamCommandToZip(zip, "logs/dmesg.txt", warnings, listOf("dmesg")) != null) count++
@@ -185,11 +180,7 @@ object DebugInfoExporter {
}
}
private fun addSystemEntries(
zip: ZipOutputStream,
warnings: MutableList<String>,
packageName: String,
): Int {
private fun addSystemEntries(zip: ZipOutputStream, warnings: MutableList<String>, packageName: String): Int {
var count = 0
if (streamCommandToZip(zip, "system/getprop.txt", warnings, listOf("getprop")) != null) count++
if (streamCommandToZip(zip, "system/uname.txt", warnings, listOf("uname", "-a")) != null) count++
@@ -214,23 +205,24 @@ object DebugInfoExporter {
"system/packages_pm.txt",
warnings,
listOf("pm", "list", "packages", "-f"),
) != null) count++
) != null
) {
count++
}
}
if (streamCommandToZip(
zip,
"system/dumpsys_package_${packageName}.txt",
"system/dumpsys_package_$packageName.txt",
warnings,
listOf("dumpsys", "package", packageName),
) != null) count++
) != null
) {
count++
}
return count
}
private fun addFileEntry(
zip: ZipOutputStream,
file: File,
entryName: String,
warnings: MutableList<String>,
): Boolean {
private fun addFileEntry(zip: ZipOutputStream, file: File, entryName: String, warnings: MutableList<String>): Boolean {
if (!file.isFile) {
warnings.add("missing file: ${file.path}")
return false
@@ -262,18 +254,14 @@ object DebugInfoExporter {
zip.closeEntry()
}
private data class CommandResult(
val exitCode: Int,
val bytes: Long,
)
private data class CommandResult(val exitCode: Int, val bytes: Long)
private fun streamCommandToZip(
zip: ZipOutputStream,
entryName: String,
warnings: MutableList<String>,
command: List<String>,
): CommandResult? {
return try {
): CommandResult? = try {
val process = ProcessBuilder(command).redirectErrorStream(true).start()
val entry = ZipEntry(entryName)
zip.putNextEntry(entry)
@@ -298,15 +286,8 @@ object DebugInfoExporter {
runCatching { zip.closeEntry() }
null
}
}
private fun buildError(
stage: String,
detail: String,
throwable: Throwable?,
warnings: List<String>,
outputPath: String?,
): String {
private fun buildError(stage: String, detail: String, throwable: Throwable?, warnings: List<String>, outputPath: String?): String {
val sb = StringBuilder()
sb.append("stage=").append(stage).append('\n')
if (!outputPath.isNullOrBlank()) {

View File

@@ -60,7 +60,8 @@ object DefaultNetworkListener {
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null
val pendingRequests = arrayListOf<NetworkMessage.Get>()
for (message in channel) when (message) {
for (message in channel) {
when (message) {
is NetworkMessage.Start -> {
if (listeners.isEmpty()) register()
listeners[message.key] = message.listener
@@ -79,8 +80,10 @@ object DefaultNetworkListener {
}
is NetworkMessage.Stop ->
if (listeners.isNotEmpty() && // was not empty
listeners.remove(message.key) != null && listeners.isEmpty()
if (listeners.isNotEmpty() &&
// was not empty
listeners.remove(message.key) != null &&
listeners.isEmpty()
) {
network = null
unregister()
@@ -109,31 +112,31 @@ object DefaultNetworkListener {
}
}
}
}
suspend fun start(
key: Any,
listener: (Network?) -> Unit,
) = networkActor.send(
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(
NetworkMessage.Start(
key,
listener,
),
)
suspend fun get(): Network = if (fallback) @TargetApi(23) {
suspend fun get(): Network = if (fallback) {
@TargetApi(23)
Application.connectivity.activeNetwork
?: error("missing default network") // failed to listen, return current if available
} else NetworkMessage.Get().run {
} else {
NetworkMessage.Get().run {
networkActor.send(this)
response.await()
}
}
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
private object Callback : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) =
runBlocking {
override fun onAvailable(network: Network) = runBlocking {
networkActor.send(
NetworkMessage.Put(
network,
@@ -141,16 +144,12 @@ object DefaultNetworkListener {
)
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
// it's a good idea to refresh capabilities
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
}
override fun onLost(network: Network) =
runBlocking {
override fun onLost(network: Network) = runBlocking {
networkActor.send(
NetworkMessage.Lost(
network,

View File

@@ -40,9 +40,7 @@ object DefaultNetworkMonitor {
checkDefaultInterfaceUpdate(defaultNetwork)
}
private fun checkDefaultInterfaceUpdate(
newNetwork: Network?
) {
private fun checkDefaultInterfaceUpdate(newNetwork: Network?) {
val listener = listener ?: return
if (newNetwork != null) {
val interfaceName =
@@ -61,5 +59,4 @@ object DefaultNetworkMonitor {
listener.updateDefaultInterface("", -1, false, false)
}
}
}

View File

@@ -19,15 +19,10 @@ import kotlin.coroutines.suspendCoroutine
object LocalResolver : LocalDNSTransport {
private const val RCODE_NXDOMAIN = 3
override fun raw(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
override fun raw(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
@RequiresApi(Build.VERSION_CODES.Q)
override fun exchange(
ctx: ExchangeContext,
message: ByteArray,
) {
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
return runBlocking {
val defaultNetwork = DefaultNetworkMonitor.require()
suspendCoroutine { continuation ->
@@ -35,10 +30,7 @@ object LocalResolver : LocalDNSTransport {
ctx.onCancel(signal::cancel)
val callback =
object : DnsResolver.Callback<ByteArray> {
override fun onAnswer(
answer: ByteArray,
rcode: Int,
) {
override fun onAnswer(answer: ByteArray, rcode: Int) {
if (rcode == 0) {
ctx.rawSuccess(answer)
} else {
@@ -70,11 +62,7 @@ object LocalResolver : LocalDNSTransport {
}
}
override fun lookup(
ctx: ExchangeContext,
network: String,
domain: String,
) {
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
return runBlocking {
val defaultNetwork = DefaultNetworkMonitor.require()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -84,10 +72,7 @@ object LocalResolver : LocalDNSTransport {
val callback =
object : DnsResolver.Callback<Collection<InetAddress>> {
@Suppress("ThrowableNotThrown")
override fun onAnswer(
answer: Collection<InetAddress>,
rcode: Int,
) {
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
if (rcode == 0) {
ctx.success(
(answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }

View File

@@ -2,7 +2,6 @@ package io.nekohasekai.sfa.bg;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -14,14 +13,16 @@ public class LogEntry implements Parcelable {
public final int level;
public final long timestamp;
@NonNull
public final String source;
@NonNull
public final String message;
@Nullable
public final String stackTrace;
@NonNull public final String source;
@NonNull public final String message;
@Nullable public final String stackTrace;
public LogEntry(int level, long timestamp, @NonNull String source, @NonNull String message, @Nullable String stackTrace) {
public LogEntry(
int level,
long timestamp,
@NonNull String source,
@NonNull String message,
@Nullable String stackTrace) {
this.level = level;
this.timestamp = timestamp;
this.source = source;
@@ -51,7 +52,8 @@ public class LogEntry implements Parcelable {
return 0;
}
public static final Creator<LogEntry> CREATOR = new Creator<>() {
public static final Creator<LogEntry> CREATOR =
new Creator<>() {
@Override
public LogEntry createFromParcel(Parcel in) {
return new LogEntry(in);

View File

@@ -2,12 +2,10 @@ package io.nekohasekai.sfa.bg;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class PackageEntry implements Parcelable {
@NonNull
public final String packageName;
@NonNull public final String packageName;
public PackageEntry(@NonNull String packageName) {
this.packageName = packageName;
@@ -27,7 +25,8 @@ public class PackageEntry implements Parcelable {
return 0;
}
public static final Creator<PackageEntry> CREATOR = new Creator<>() {
public static final Creator<PackageEntry> CREATOR =
new Creator<>() {
@Override
public PackageEntry createFromParcel(Parcel in) {
return new PackageEntry(in);

View File

@@ -21,7 +21,6 @@ import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import java.util.ArrayList;
import java.util.List;
@@ -106,7 +105,8 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable {
if (i < n) {
dest.writeInt(0);
final int start = i;
Binder retriever = new Binder() {
Binder retriever =
new Binder() {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {

View File

@@ -27,9 +27,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
interface PlatformInterfaceWrapper : PlatformInterface {
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
return true
}
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
override fun autoDetectInterfaceControl(fd: Int) {
}
@@ -38,9 +36,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
error("invalid argument")
}
override fun useProcFS(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
}
override fun useProcFS(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
@RequiresApi(Build.VERSION_CODES.Q)
override fun findConnectionOwner(
@@ -136,13 +132,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return InterfaceArray(interfaces.iterator())
}
override fun underNetworkExtension(): Boolean {
return false
}
override fun underNetworkExtension(): Boolean = false
override fun includeAllNetworks(): Boolean {
return false
}
override fun includeAllNetworks(): Boolean = false
override fun clearDNSCache() {
}
@@ -161,9 +153,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return WIFIState(ssid, wifiInfo.bssid)
}
override fun localDNSTransport(): LocalDNSTransport? {
return LocalResolver
}
override fun localDNSTransport(): LocalDNSTransport? = LocalResolver
@OptIn(ExperimentalEncodingApi::class)
override fun systemCertificates(): StringIterator {
@@ -182,15 +172,10 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return StringArray(certificates.iterator())
}
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) :
NetworkInterfaceIterator {
override fun hasNext(): Boolean {
return iterator.hasNext()
}
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) : NetworkInterfaceIterator {
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): LibboxNetworkInterface {
return iterator.next()
}
override fun next(): LibboxNetworkInterface = iterator.next()
}
class StringArray(private val iterator: Iterator<String>) : StringIterator {
@@ -199,22 +184,16 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return 0
}
override fun hasNext(): Boolean {
return iterator.hasNext()
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): String = iterator.next()
}
override fun next(): String {
return iterator.next()
}
}
private fun InterfaceAddress.toPrefix(): String {
return if (address is Inet6Address) {
private fun InterfaceAddress.toPrefix(): String = if (address is Inet6Address) {
"${Inet6Address.getByAddress(address.address).hostAddress}/$networkPrefixLength"
} else {
"${address.hostAddress}/$networkPrefixLength"
}
}
private val NetworkInterface.flags: Int
@SuppressLint("SoonBlockedPrivateApi")

View File

@@ -4,14 +4,12 @@ import android.app.Service
import android.content.Intent
import io.nekohasekai.libbox.Notification
class ProxyService : Service(), PlatformInterfaceWrapper {
class ProxyService :
Service(),
PlatformInterfaceWrapper {
private val service = BoxService(this, this)
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
) = service.onStartCommand()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand()
override fun onBind(intent: Intent) = service.onBind()

View File

@@ -25,7 +25,7 @@ object RootClient {
Shell.setDefaultBuilder(
Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setTimeout(10)
.setTimeout(10),
)
}
@@ -95,6 +95,7 @@ object RootClient {
val svc = bindService()
return try {
val slice = svc.getInstalledPackages(flags, userId)
@Suppress("UNCHECKED_CAST")
val list = slice.list as List<PackageInfo>
list

View File

@@ -1,13 +1,12 @@
package io.nekohasekai.sfa.bg
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
import android.content.Intent
import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.ParcelFileDescriptor
import com.topjohnwu.superuser.ipc.RootService
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
import java.io.IOException
class RootServer : RootService() {
@@ -17,10 +16,7 @@ class RootServer : RootService() {
stopSelf()
}
override fun getInstalledPackages(
flags: Int,
userId: Int
): ParceledListSlice<PackageInfo> {
override fun getInstalledPackages(flags: Int, userId: Int): ParceledListSlice<PackageInfo> {
val allPackages = PrivilegedServiceUtils.getInstalledPackages(flags, userId)
return ParceledListSlice(allPackages)
}
@@ -30,16 +26,12 @@ class RootServer : RootService() {
PrivilegedServiceUtils.installPackage(apk, size, userId)
}
override fun exportDebugInfo(outputPath: String?): String {
return DebugInfoExporter.export(
override fun exportDebugInfo(outputPath: String?): String = DebugInfoExporter.export(
this@RootServer,
outputPath!!,
BuildConfig.APPLICATION_ID
BuildConfig.APPLICATION_ID,
)
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onBind(intent: Intent): IBinder = binder
}

View File

@@ -43,9 +43,7 @@ class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub
}
}
override fun getStatus(): Int {
return (status.value ?: Status.Stopped).ordinal
}
override fun getStatus(): Int = (status.value ?: Status.Stopped).ordinal
override fun registerCallback(callback: IServiceCallback) {
callbacks.register(callback)

View File

@@ -18,11 +18,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class ServiceConnection(
private val context: Context,
callback: Callback,
private val register: Boolean = true,
) : ServiceConnection {
class ServiceConnection(private val context: Context, callback: Callback, private val register: Boolean = true) : ServiceConnection {
companion object {
private const val TAG = "ServiceConnection"
}
@@ -66,10 +62,7 @@ class ServiceConnection(
Log.d(TAG, "request reconnect")
}
override fun onServiceConnected(
name: ComponentName,
binder: IBinder,
) {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IService.Stub.asInterface(binder)
this.service = service
try {
@@ -98,10 +91,7 @@ class ServiceConnection(
interface Callback {
fun onServiceStatusChanged(status: Status)
fun onServiceAlert(
type: Alert,
message: String?,
) {
fun onServiceAlert(type: Alert, message: String?) {
}
}
@@ -110,10 +100,7 @@ class ServiceConnection(
callback.onServiceStatusChanged(Status.values()[status])
}
override fun onServiceAlert(
type: Int,
message: String?,
) {
override fun onServiceAlert(type: Int, message: String?) {
callback.onServiceAlert(Alert.values()[type], message)
}
}

View File

@@ -16,8 +16,8 @@ import androidx.lifecycle.MutableLiveData
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.compose.MainActivity
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.MainActivity
import io.nekohasekai.sfa.constant.Action
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.Settings
@@ -27,10 +27,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext
class ServiceNotification(
private val status: MutableLiveData<Status>,
private val service: Service,
) : BroadcastReceiver(), CommandClient.Handler {
class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) :
BroadcastReceiver(),
CommandClient.Handler {
companion object {
private const val notificationId = 1
private const val notificationChannel = "service"
@@ -82,10 +81,7 @@ class ServiceNotification(
}
}
fun show(
lastProfileName: String,
@StringRes contentTextId: Int,
) {
fun show(lastProfileName: String, @StringRes contentTextId: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel(
NotificationChannel(
@@ -132,10 +128,7 @@ class ServiceNotification(
)
}
override fun onReceive(
context: Context,
intent: Intent,
) {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_SCREEN_ON -> {
commandClient.connect()

View File

@@ -8,7 +8,9 @@ import androidx.annotation.RequiresApi
import io.nekohasekai.sfa.constant.Status
@RequiresApi(24)
class TileService : TileService(), ServiceConnection.Callback {
class TileService :
TileService(),
ServiceConnection.Callback {
private val connection = ServiceConnection(this, this)
override fun onServiceStatusChanged(status: Status) {

View File

@@ -59,10 +59,7 @@ class UpdateProfileWork {
}
}
class UpdateTask(
appContext: Context,
params: WorkerParameters,
) : CoroutineWorker(appContext, params) {
class UpdateTask(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
var selectedProfileUpdated = false
val remoteProfiles =

View File

@@ -15,18 +15,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class VPNService : VpnService(), PlatformInterfaceWrapper {
class VPNService :
VpnService(),
PlatformInterfaceWrapper {
companion object {
private const val TAG = "VPNService"
}
private val service = BoxService(this, this)
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
) = service.onStartCommand()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand()
override fun onBind(intent: Intent): IBinder {
val binder = super.onBind(intent)

View File

@@ -16,34 +16,29 @@ import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.UnfoldLess
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import dev.jeziellago.compose.markdowntext.MarkdownText
import androidx.compose.material3.Badge
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Job
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -53,9 +48,9 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
@@ -69,9 +64,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -81,6 +79,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.jeziellago.compose.markdowntext.MarkdownText
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.BuildConfig
@@ -91,25 +90,24 @@ import io.nekohasekai.sfa.compose.base.GlobalEventBus
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
import io.nekohasekai.sfa.compose.base.UiEvent
import io.nekohasekai.sfa.compose.component.ServiceStatusBar
import io.nekohasekai.sfa.compose.component.UptimeText
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
import io.nekohasekai.sfa.compose.component.UptimeText
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
import io.nekohasekai.sfa.compose.navigation.ProfileRoutes
import io.nekohasekai.sfa.compose.navigation.SFANavHost
import io.nekohasekai.sfa.compose.navigation.Screen
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
import io.nekohasekai.sfa.compose.topbar.TopBarController
import io.nekohasekai.sfa.compose.screen.dashboard.CardGroup
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
import io.nekohasekai.sfa.compose.theme.SFATheme
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
import io.nekohasekai.sfa.compose.topbar.TopBarController
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
import io.nekohasekai.sfa.constant.Alert
import io.nekohasekai.sfa.constant.ServiceMode
import io.nekohasekai.sfa.constant.Status
@@ -119,10 +117,13 @@ import io.nekohasekai.sfa.ktx.launchCustomTab
import io.nekohasekai.sfa.update.UpdateState
import io.nekohasekai.sfa.vendor.Vendor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : ComponentActivity(), ServiceConnection.Callback {
class MainActivity :
ComponentActivity(),
ServiceConnection.Callback {
private val connection = ServiceConnection(this, this)
private lateinit var dashboardViewModel: DashboardViewModel
private var currentServiceStatus by mutableStateOf(Status.Stopped)
@@ -253,8 +254,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
}
}
private suspend fun prepare() =
withContext(Dispatchers.Main) {
private suspend fun prepare() = withContext(Dispatchers.Main) {
try {
val intent = VpnService.prepare(this@MainActivity)
if (intent != null) {
@@ -388,8 +388,11 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
text = {
MarkdownText(
markdown = stringResource(
if (BuildConfig.FLAVOR == "play") R.string.check_update_prompt_play
else R.string.check_update_prompt_github
if (BuildConfig.FLAVOR == "play") {
R.string.check_update_prompt_play
} else {
R.string.check_update_prompt_github
},
),
style = MaterialTheme.typography.bodyMedium,
)
@@ -534,7 +537,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
@Suppress("UNCHECKED_CAST")
return GroupsViewModel(dashboardViewModel.commandClient) as T
}
}
},
)
} else {
null
@@ -909,7 +912,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
@Suppress("UNCHECKED_CAST")
return GroupsViewModel(dashboardViewModel.commandClient) as T
}
}
},
)
val groupsUiState by groupsViewModel.uiState.collectAsState()
val allCollapsed = groupsUiState.expandedGroups.isEmpty()
@@ -943,12 +946,16 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
if (groupsUiState.groups.isNotEmpty()) {
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
Icon(
imageVector = if (allCollapsed) Icons.Default.UnfoldMore
else Icons.Default.UnfoldLess,
contentDescription = if (allCollapsed)
imageVector = if (allCollapsed) {
Icons.Default.UnfoldMore
} else {
Icons.Default.UnfoldLess
},
contentDescription = if (allCollapsed) {
stringResource(R.string.expand_all)
else
stringResource(R.string.collapse_all),
} else {
stringResource(R.string.collapse_all)
},
)
}
}
@@ -1032,10 +1039,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
connection.reconnect()
}
override fun onServiceAlert(
type: Alert,
message: String?,
) {
override fun onServiceAlert(type: Alert, message: String?) {
when (type) {
Alert.RequestLocationPermission -> {
return requestLocationPermission()
@@ -1071,11 +1075,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
}
@Composable
private fun ServiceAlertDialog(
alertType: Alert,
message: String?,
onDismiss: () -> Unit,
) {
private fun ServiceAlertDialog(alertType: Alert, message: String?, onDismiss: () -> Unit) {
val title =
when (alertType) {
Alert.RequestNotificationPermission -> stringResource(R.string.notification_permission_title)
@@ -1106,10 +1106,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
}
@Composable
private fun LocationPermissionDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
private fun LocationPermissionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.location_permission_title)) },
@@ -1128,10 +1125,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
}
@Composable
private fun BackgroundLocationPermissionDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
private fun BackgroundLocationPermissionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.location_permission_title)) },

View File

@@ -56,10 +56,7 @@ abstract class BaseViewModel<State, Event> : ViewModel() {
sendGlobalEvent(UiEvent.ErrorMessage(message))
}
protected fun launch(
onError: ((Throwable) -> Unit)? = null,
block: suspend CoroutineScope.() -> Unit,
) {
protected fun launch(onError: ((Throwable) -> Unit)? = null, block: suspend CoroutineScope.() -> Unit) {
val errorHandler =
CoroutineExceptionHandler { _, throwable ->
onError?.invoke(throwable) ?: sendError(throwable)

View File

@@ -29,7 +29,5 @@ object GlobalEventBus {
* Try to emit an event without suspending.
* Returns true if the event was emitted successfully.
*/
fun tryEmit(event: UiEvent): Boolean {
return _events.tryEmit(event)
}
fun tryEmit(event: UiEvent): Boolean = _events.tryEmit(event)
}

View File

@@ -19,11 +19,7 @@ import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R
@Composable
fun SelectableMessageDialog(
title: String,
message: String,
onDismiss: () -> Unit,
) {
fun SelectableMessageDialog(title: String, message: String, onDismiss: () -> Unit) {
val clipboard = LocalClipboardManager.current
val context = LocalContext.current
val scrollState = rememberScrollState()

View File

@@ -8,8 +8,4 @@ sealed class UiState<out T> {
data class Error(val exception: Throwable, val message: String? = null) : UiState<Nothing>()
}
data class BaseUiState<T>(
val isLoading: Boolean = false,
val data: T? = null,
val error: String? = null,
)
data class BaseUiState<T>(val isLoading: Boolean = false, val data: T? = null, val error: String? = null)

View File

@@ -164,10 +164,7 @@ fun ServiceStatusBar(
}
@Composable
private fun StatusItem(
text: String,
modifier: Modifier = Modifier,
) {
private fun StatusItem(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
@@ -178,10 +175,7 @@ private fun StatusItem(
}
@Composable
fun UptimeText(
startTime: Long,
modifier: Modifier = Modifier,
) {
fun UptimeText(startTime: Long, modifier: Modifier = Modifier) {
var currentTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(startTime) {

View File

@@ -27,11 +27,7 @@ import org.kodein.emoji.EmojiTemplateCatalog
import org.kodein.emoji.all
@Composable
fun UpdateAvailableDialog(
updateInfo: UpdateInfo,
onDismiss: () -> Unit,
onUpdate: () -> Unit,
) {
fun UpdateAvailableDialog(updateInfo: UpdateInfo, onDismiss: () -> Unit, onUpdate: () -> Unit) {
val context = LocalContext.current
val emojiCatalog = remember { EmojiTemplateCatalog(Emoji.all()) }

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
@@ -24,10 +23,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@Composable
fun QRCodeDialog(
bitmap: Bitmap,
onDismiss: () -> Unit,
) {
fun QRCodeDialog(bitmap: Bitmap, onDismiss: () -> Unit) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false),

View File

@@ -33,6 +33,7 @@ class QRSBitmapGenerator(
private val actualBufferSize = bufferSize.coerceAtMost(frames.size)
private val bitmapBuffer = arrayOfNulls<Bitmap>(actualBufferSize)
private var generationJob: Job? = null
@Volatile
private var currentFrameIndex = 0
private var generatedUpTo = -1

View File

@@ -1,9 +1,9 @@
package io.nekohasekai.sfa.compose.component.qr
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -58,11 +58,7 @@ import io.nekohasekai.sfa.qrs.QRSEncoder
import kotlinx.coroutines.delay
@Composable
fun QRSDialog(
profileData: ByteArray,
profileName: String,
onDismiss: () -> Unit,
) {
fun QRSDialog(profileData: ByteArray, profileName: String, onDismiss: () -> Unit) {
val context = LocalContext.current
val configuration = LocalConfiguration.current
val isTablet = configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)

View File

@@ -40,8 +40,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
@@ -54,18 +54,14 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanViewModel
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
import kotlin.math.max
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun QRScanSheet(
onDismiss: () -> Unit,
onScanResult: (QRScanResult) -> Unit,
viewModel: QRScanViewModel = viewModel(),
) {
fun QRScanSheet(onDismiss: () -> Unit, onScanResult: (QRScanResult) -> Unit, viewModel: QRScanViewModel = viewModel()) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
@@ -74,12 +70,12 @@ fun QRScanSheet(
var hasPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
PackageManager.PERMISSION_GRANTED,
)
}
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
contract = ActivityResultContracts.RequestPermission(),
) { isGranted ->
if (isGranted) {
hasPermission = true
@@ -113,7 +109,7 @@ fun QRScanSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.9f)
.fillMaxHeight(0.9f),
) {
Row(
modifier = Modifier
@@ -138,44 +134,44 @@ fun QRScanSheet(
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
onDismissRequest = { showMenu = false },
) {
DropdownMenuItem(
text = {
Text(
(if (uiState.useFrontCamera) "" else " ") +
stringResource(R.string.profile_add_scan_use_front_camera)
stringResource(R.string.profile_add_scan_use_front_camera),
)
},
onClick = {
viewModel.toggleFrontCamera(lifecycleOwner)
showMenu = false
}
},
)
DropdownMenuItem(
text = {
Text(
(if (uiState.torchEnabled) "" else " ") +
stringResource(R.string.profile_add_scan_enable_torch)
stringResource(R.string.profile_add_scan_enable_torch),
)
},
onClick = {
viewModel.toggleTorch()
showMenu = false
}
},
)
if (uiState.vendorAnalyzerAvailable) {
DropdownMenuItem(
text = {
Text(
(if (uiState.useVendorAnalyzer) "" else " ") +
stringResource(R.string.profile_add_scan_use_vendor_analyzer)
stringResource(R.string.profile_add_scan_use_vendor_analyzer),
)
},
onClick = {
viewModel.toggleVendorAnalyzer()
showMenu = false
}
},
)
}
}
@@ -185,7 +181,7 @@ fun QRScanSheet(
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.weight(1f),
) {
if (hasPermission) {
CameraPreview(
@@ -201,7 +197,7 @@ fun QRScanSheet(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
@@ -214,7 +210,7 @@ fun QRScanSheet(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(
@@ -228,18 +224,18 @@ fun QRScanSheet(
Text(
text = "${minOf(99, (progress * 100).toInt())}%",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
),
color = Color.White
color = Color.White,
)
}
Text(
text = "QRS",
style = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold
fontWeight = FontWeight.Bold,
),
color = Color.White,
modifier = Modifier.offset(y = (-88).dp)
modifier = Modifier.offset(y = (-88).dp),
)
}
}
@@ -257,7 +253,7 @@ fun QRScanSheet(
TextButton(onClick = { viewModel.dismissError() }) {
Text(stringResource(android.R.string.ok))
}
}
},
)
}
}
@@ -294,7 +290,7 @@ private fun CameraPreview(
}
}
}
}
},
)
Canvas(modifier = Modifier.fillMaxSize()) {
@@ -309,11 +305,7 @@ private fun CameraPreview(
}
}
private fun mapCropAreaToPreview(
area: QRCodeCropArea,
viewWidth: Float,
viewHeight: Float,
): Rect? {
private fun mapCropAreaToPreview(area: QRCodeCropArea, viewWidth: Float, viewHeight: Float): Rect? {
if (viewWidth <= 0f || viewHeight <= 0f) return null
val rotation = ((area.rotationDegrees % 360) + 360) % 360

View File

@@ -1,18 +1,12 @@
package io.nekohasekai.sfa.compose.model
import androidx.compose.runtime.Immutable
import io.nekohasekai.sfa.ktx.toList
import io.nekohasekai.libbox.Connection as LibboxConnection
import io.nekohasekai.libbox.ProcessInfo as LibboxProcessInfo
import io.nekohasekai.sfa.ktx.toList
@Immutable
data class ProcessInfo(
val processId: Long,
val userId: Int,
val userName: String,
val processPath: String,
val packageName: String,
) {
data class ProcessInfo(val processId: Long, val userId: Int, val userName: String, val processPath: String, val packageName: String) {
companion object {
fun from(processInfo: LibboxProcessInfo?): ProcessInfo? {
if (processInfo == null) return null
@@ -68,16 +62,13 @@ data class Connection(
return true
}
private fun performSearchPlain(content: String): Boolean {
return destination.contains(content, ignoreCase = true) ||
private fun performSearchPlain(content: String): Boolean = destination.contains(content, ignoreCase = true) ||
domain.contains(content, ignoreCase = true) ||
outbound.contains(content, ignoreCase = true) ||
rule.contains(content, ignoreCase = true) ||
processInfo?.packageName?.contains(content, ignoreCase = true) == true
}
private fun performSearchType(type: String, value: String): Boolean {
return when (type) {
private fun performSearchType(type: String, value: String): Boolean = when (type) {
"network" -> network.equals(value, ignoreCase = true)
"inbound" -> inbound.contains(value, ignoreCase = true)
"inbound.type" -> inboundType.equals(value, ignoreCase = true)
@@ -92,11 +83,9 @@ data class Connection(
"chain" -> chain.any { it.contains(value, ignoreCase = true) }
else -> false
}
}
companion object {
fun from(connection: LibboxConnection): Connection {
return Connection(
fun from(connection: LibboxConnection): Connection = Connection(
id = connection.id,
inbound = connection.inbound,
inboundType = connection.inboundType,
@@ -122,5 +111,4 @@ data class Connection(
processInfo = ProcessInfo.from(connection.processInfo),
)
}
}
}

View File

@@ -24,12 +24,7 @@ data class Group(
}
@Immutable
data class GroupItem(
val tag: String,
val type: String,
val urlTestTime: Long,
val urlTestDelay: Int,
) {
data class GroupItem(val tag: String, val type: String, val urlTestTime: Long, val urlTestDelay: Int) {
constructor(item: OutboundGroupItem) : this(
item.tag,
item.type,

View File

@@ -10,11 +10,7 @@ import androidx.compose.material.icons.filled.SwapVert
import androidx.compose.ui.graphics.vector.ImageVector
import io.nekohasekai.sfa.R
sealed class Screen(
val route: String,
@StringRes val titleRes: Int,
val icon: ImageVector,
) {
sealed class Screen(val route: String, @StringRes val titleRes: Int, val icon: ImageVector) {
object Dashboard : Screen(
route = "dashboard",
titleRes = R.string.title_dashboard,

View File

@@ -1,10 +1,6 @@
package io.nekohasekai.sfa.compose.navigation
data class NewProfileArgs(
val importName: String? = null,
val importUrl: String? = null,
val qrsData: ByteArray? = null,
)
data class NewProfileArgs(val importName: String? = null, val importUrl: String? = null, val qrsData: ByteArray? = null)
object ProfileRoutes {
const val NewProfile = "profile/new"

View File

@@ -12,18 +12,20 @@ import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsRoute
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsRoute
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
import io.nekohasekai.sfa.compose.screen.log.HookLogScreen
import io.nekohasekai.sfa.compose.screen.log.LogScreen
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
import io.nekohasekai.sfa.compose.screen.profile.EditProfileRoute
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen
@@ -31,8 +33,6 @@ import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
import io.nekohasekai.sfa.compose.screen.settings.SettingsScreen
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
private val slideInFromRight: AnimatedContentTransitionScope<*>.() -> androidx.compose.animation.EnterTransition = {
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300))

View File

@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
@@ -60,6 +59,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp

View File

@@ -124,10 +124,7 @@ class NewProfileViewModel(application: Application) : AndroidViewModel(applicati
_uiState.update { it.copy(autoUpdateInterval = intValue.coerceAtLeast(15)) }
}
fun setImportUri(
uri: Uri,
fileName: String?,
) {
fun setImportUri(uri: Uri, fileName: String?) {
_uiState.update {
it.copy(
importUri = uri,

View File

@@ -23,8 +23,7 @@ class ProfileImportHandler(private val context: Context) {
}
sealed class QRCodeParseResult {
data class RemoteProfile(val name: String, val host: String, val url: String) :
QRCodeParseResult()
data class RemoteProfile(val name: String, val host: String, val url: String) : QRCodeParseResult()
data class LocalProfile(val name: String) : QRCodeParseResult()
@@ -43,8 +42,7 @@ class ProfileImportHandler(private val context: Context) {
data class Error(val message: String) : UriParseResult()
}
suspend fun importFromUri(uri: Uri): ImportResult =
withContext(Dispatchers.IO) {
suspend fun importFromUri(uri: Uri): ImportResult = withContext(Dispatchers.IO) {
try {
val data =
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
@@ -80,8 +78,7 @@ class ProfileImportHandler(private val context: Context) {
}
}
suspend fun parseUri(uri: Uri): UriParseResult =
withContext(Dispatchers.IO) {
suspend fun parseUri(uri: Uri): UriParseResult = withContext(Dispatchers.IO) {
try {
val data =
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
@@ -112,8 +109,7 @@ class ProfileImportHandler(private val context: Context) {
}
}
suspend fun parseQRCode(data: String): QRCodeParseResult =
withContext(Dispatchers.IO) {
suspend fun parseQRCode(data: String): QRCodeParseResult = withContext(Dispatchers.IO) {
try {
// Check if it's a sing-box remote profile import link
if (data.startsWith("sing-box://import-remote-profile")) {
@@ -157,8 +153,7 @@ class ProfileImportHandler(private val context: Context) {
}
}
suspend fun importFromQRCode(data: String): ImportResult =
withContext(Dispatchers.IO) {
suspend fun importFromQRCode(data: String): ImportResult = withContext(Dispatchers.IO) {
try {
// Check if it's a sing-box remote profile import link
if (data.startsWith("sing-box://import-remote-profile")) {
@@ -194,8 +189,7 @@ class ProfileImportHandler(private val context: Context) {
}
}
suspend fun parseQRSData(data: ByteArray): QRSParseResult =
withContext(Dispatchers.IO) {
suspend fun parseQRSData(data: ByteArray): QRSParseResult = withContext(Dispatchers.IO) {
try {
val content = try {
Libbox.decodeProfileContent(data)
@@ -210,8 +204,7 @@ class ProfileImportHandler(private val context: Context) {
}
}
suspend fun importFromQRSData(data: ByteArray): ImportResult =
withContext(Dispatchers.IO) {
suspend fun importFromQRSData(data: ByteArray): ImportResult = withContext(Dispatchers.IO) {
try {
val content = try {
Libbox.decodeProfileContent(data)
@@ -259,10 +252,7 @@ class ProfileImportHandler(private val context: Context) {
return ImportResult.Success(profile)
}
private suspend fun importRemoteProfile(
name: String,
url: String,
): ImportResult {
private suspend fun importRemoteProfile(name: String, url: String): ImportResult {
val typedProfile =
TypedProfile().apply {
type = TypedProfile.Type.Remote
@@ -297,14 +287,12 @@ class ProfileImportHandler(private val context: Context) {
?: "Remote Profile"
}
private fun extractHostFromUrl(url: String): String {
return try {
private fun extractHostFromUrl(url: String): String = try {
val uri = Uri.parse(url)
uri.host ?: url
} catch (e: Exception) {
url
}
}
private fun getFileNameFromUri(uri: Uri): String {
var filename = "Imported Profile"
@@ -354,10 +342,7 @@ class ProfileImportHandler(private val context: Context) {
}
}
private suspend fun importJsonConfiguration(
jsonContent: String,
profileName: String,
): ImportResult {
private suspend fun importJsonConfiguration(jsonContent: String, profileName: String): ImportResult {
return try {
// Validate the JSON configuration using sing-box
try {

View File

@@ -1,5 +1,6 @@
package io.nekohasekai.sfa.compose.screen.connections
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -10,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -32,14 +32,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R
@@ -286,10 +286,7 @@ fun ConnectionDetailsScreen(
}
@Composable
private fun DetailSection(
title: String,
content: @Composable ColumnScope.() -> Unit,
) {
private fun DetailSection(title: String, content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
@@ -317,12 +314,7 @@ private fun DetailSection(
}
@Composable
private fun DetailRow(
label: String,
value: String,
monospace: Boolean = false,
valueColor: Color = MaterialTheme.colorScheme.onSurface,
) {
private fun DetailRow(label: String, value: String, monospace: Boolean = false, valueColor: Color = MaterialTheme.colorScheme.onSurface) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -346,20 +338,10 @@ private fun DetailRow(
}
@Composable
private fun rememberBounceBlockingNestedScrollConnection(
scrollState: ScrollState
): NestedScrollConnection = remember(scrollState) {
private fun rememberBounceBlockingNestedScrollConnection(scrollState: ScrollState): NestedScrollConnection = remember(scrollState) {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (available.y < 0) available else Offset.Zero
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return if (available.y < 0) available else Velocity.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
}
}

View File

@@ -45,16 +45,13 @@ import androidx.compose.ui.unit.dp
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.model.Connection
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private fun Drawable.toBitmap(): Bitmap {
if (this is BitmapDrawable) return bitmap
val bitmap = Bitmap.createBitmap(
intrinsicWidth.coerceAtLeast(1),
intrinsicHeight.coerceAtLeast(1),
Bitmap.Config.ARGB_8888
Bitmap.Config.ARGB_8888,
)
val canvas = Canvas(bitmap)
setBounds(0, 0, canvas.width, canvas.height)
@@ -62,10 +59,7 @@ private fun Drawable.toBitmap(): Bitmap {
return bitmap
}
data class AppInfo(
val icon: ImageBitmap,
val label: String,
)
data class AppInfo(val icon: ImageBitmap, val label: String)
@Composable
private fun rememberAppInfo(packageName: String): AppInfo? {
@@ -86,12 +80,7 @@ private fun rememberAppInfo(packageName: String): AppInfo? {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ConnectionItem(
connection: Connection,
onClick: () -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
fun ConnectionItem(connection: Connection, onClick: () -> Unit, onClose: () -> Unit, modifier: Modifier = Modifier) {
var showContextMenu by remember { mutableStateOf(false) }
val packageName = connection.processInfo?.packageName?.takeIf { it.isNotEmpty() }
val appInfo = packageName?.let { rememberAppInfo(it) }

View File

@@ -18,11 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
@@ -54,15 +49,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.compose.model.Connection
import io.nekohasekai.sfa.compose.model.ConnectionSort
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.model.Connection
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -118,7 +118,7 @@ fun ConnectionsPage(
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
}
},
)
},
)
@@ -230,7 +230,7 @@ fun ConnectionsPage(
stringResource(R.string.close_search)
} else {
stringResource(R.string.search)
}
},
)
},
onClick = {
@@ -433,20 +433,10 @@ fun ConnectionsScreen(
}
@Composable
private fun rememberBounceBlockingNestedScrollConnection(
lazyListState: LazyListState
): NestedScrollConnection = remember(lazyListState) {
private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (available.y < 0) available else Offset.Zero
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return if (available.y < 0) available else Velocity.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
}
}

View File

@@ -6,14 +6,13 @@ import io.nekohasekai.libbox.Connections
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.compose.base.BaseViewModel
import io.nekohasekai.sfa.compose.base.ScreenEvent
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.ktx.toList
import io.nekohasekai.sfa.compose.model.Connection
import io.nekohasekai.sfa.compose.model.ConnectionSort
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.ktx.toList
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -22,6 +21,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicLong
data class ConnectionsUiState(
val connections: List<Connection> = emptyList(),
@@ -38,7 +38,9 @@ sealed class ConnectionsEvent : ScreenEvent {
data object AllConnectionsClosed : ConnectionsEvent()
}
class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler {
class ConnectionsViewModel :
BaseViewModel<ConnectionsUiState, ConnectionsEvent>(),
CommandClient.Handler {
private val commandClient = CommandClient(
viewModelScope,
CommandClient.ConnectionType.Connections,
@@ -62,7 +64,7 @@ class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>
combine(
AppLifecycleObserver.isForeground,
_isVisible,
_serviceStatus
_serviceStatus,
) { foreground, visible, status ->
Triple(foreground, visible, status)
}.collect { (foreground, visible, status) ->

View File

@@ -1,7 +1,6 @@
package io.nekohasekai.sfa.compose.screen.dashboard
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
@@ -14,8 +13,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
@@ -44,12 +43,7 @@ import io.nekohasekai.sfa.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ClashModeCard(
modes: List<String>,
selectedMode: String,
onModeSelected: (String) -> Unit,
modifier: Modifier = Modifier,
) {
fun ClashModeCard(modes: List<String>, selectedMode: String, onModeSelected: (String) -> Unit, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
) {
@@ -127,11 +121,7 @@ fun ClashModeCard(
}
@Composable
private fun ModeDropdown(
modes: List<String>,
selectedMode: String,
onModeSelected: (String) -> Unit,
) {
private fun ModeDropdown(modes: List<String>, selectedMode: String, onModeSelected: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxWidth()) {

View File

@@ -24,11 +24,7 @@ import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R
@Composable
fun ConnectionsCard(
connectionsIn: String,
connectionsOut: String,
modifier: Modifier = Modifier,
) {
fun ConnectionsCard(connectionsIn: String, connectionsOut: String, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
) {

View File

@@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -37,10 +36,7 @@ import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.constant.Status
import kotlinx.coroutines.launch
data class CardRenderItem(
val cards: List<CardGroup>,
val isRow: Boolean,
)
data class CardRenderItem(val cards: List<CardGroup>, val isRow: Boolean)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -307,11 +303,7 @@ fun processCardsForRendering(
* This function is only relevant when the service is running.
* Note: Profiles card is always available and should not use this function.
*/
fun isCardAvailableWhenServiceRunning(
cardGroup: CardGroup,
uiState: DashboardUiState,
): Boolean {
return when (cardGroup) {
fun isCardAvailableWhenServiceRunning(cardGroup: CardGroup, uiState: DashboardUiState): Boolean = when (cardGroup) {
CardGroup.ClashMode -> uiState.clashModeVisible
CardGroup.UploadTraffic -> uiState.trafficVisible
CardGroup.DownloadTraffic -> uiState.trafficVisible
@@ -319,5 +311,4 @@ fun isCardAvailableWhenServiceRunning(
CardGroup.Connections -> uiState.trafficVisible
CardGroup.SystemProxy -> uiState.systemProxyVisible
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety
}
}

View File

@@ -27,11 +27,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.RestartAlt
import io.nekohasekai.sfa.compat.animateItemCompat
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.Cable
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Route
import androidx.compose.material.icons.outlined.SettingsEthernet
@@ -65,6 +63,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compat.animateItemCompat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -96,12 +95,12 @@ fun DashboardSettingsBottomSheet(
var dragOffset by remember { mutableStateOf(0f) }
val density = LocalDensity.current
fun onMove(
fromIndex: Int,
toIndex: Int,
) {
if (fromIndex != toIndex && fromIndex >= 0 && toIndex >= 0 &&
fromIndex < reorderedList.size && toIndex < reorderedList.size
fun onMove(fromIndex: Int, toIndex: Int) {
if (fromIndex != toIndex &&
fromIndex >= 0 &&
toIndex >= 0 &&
fromIndex < reorderedList.size &&
toIndex < reorderedList.size
) {
val newList = reorderedList.toMutableList()
val item = newList.removeAt(fromIndex)

View File

@@ -114,16 +114,15 @@ data class DashboardUiState(
),
val showCardSettingsDialog: Boolean = false,
) {
data class DeprecatedNote(
val message: String,
val migrationLink: String?,
)
data class DeprecatedNote(val message: String, val migrationLink: String?)
}
// DashboardViewModel now only uses UiEvent for all events
// No need for DashboardEvent anymore as all events are handled globally
class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandClient.Handler {
class DashboardViewModel :
BaseViewModel<DashboardUiState, UiEvent>(),
CommandClient.Handler {
private val _serviceStatus = MutableStateFlow(Status.Stopped)
val serviceStatus: StateFlow<Status> = _serviceStatus.asStateFlow()
@@ -395,10 +394,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
}
}
fun moveProfile(
from: Int,
to: Int,
) {
fun moveProfile(from: Int, to: Int) {
val currentProfiles = currentState.profiles.toMutableList()
if (from < to) {
@@ -614,10 +610,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
}
}
override fun initializeClashMode(
modeList: List<String>,
currentMode: String,
) {
override fun initializeClashMode(modeList: List<String>, currentMode: String) {
viewModelScope.launch(Dispatchers.Main) {
updateState {
copy(
@@ -702,8 +695,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
}
// Helper functions for serialization
private fun getDefaultItemOrder() =
listOf(
private fun getDefaultItemOrder() = listOf(
CardGroup.UploadTraffic,
CardGroup.DownloadTraffic,
CardGroup.Debug,
@@ -766,11 +758,9 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
private fun cardGroupToString(card: CardGroup): String = card.name
private fun stringToCardGroup(name: String): CardGroup? {
return try {
private fun stringToCardGroup(name: String): CardGroup? = try {
CardGroup.valueOf(name)
} catch (e: IllegalArgumentException) {
null
}
}
}

View File

@@ -24,11 +24,7 @@ import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R
@Composable
fun DebugCard(
memory: String,
goroutines: String,
modifier: Modifier = Modifier,
) {
fun DebugCard(memory: String, goroutines: String, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
) {

View File

@@ -24,12 +24,7 @@ import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.LineChart
@Composable
fun DownloadTrafficCard(
downlink: String,
downlinkTotal: String,
downlinkHistory: List<Float>,
modifier: Modifier = Modifier,
) {
fun DownloadTrafficCard(downlink: String, downlinkTotal: String, downlinkHistory: List<Float>, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
) {

View File

@@ -26,10 +26,10 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.UnfoldLess
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.UnfoldLess
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -58,19 +58,19 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.model.Group
import io.nekohasekai.sfa.compose.model.GroupItem
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.utils.CommandClient
@OptIn(ExperimentalMaterial3Api::class)
@@ -401,12 +401,7 @@ private fun ProxyGroupItem(
}
@Composable
private fun ProxyItemsList(
items: List<GroupItem>,
selectedTag: String,
isSelectable: Boolean,
onItemSelected: (String) -> Unit,
) {
private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
val itemsPerRow = 2
val chunkedItems =
remember(items) {
@@ -450,13 +445,7 @@ private fun ProxyItemsList(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ProxyChip(
item: GroupItem,
isSelected: Boolean,
isSelectable: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
// Use simpler, faster animations
val animatedElevation by animateFloatAsState(
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
@@ -566,11 +555,7 @@ private fun ProxyChip(
}
@Composable
private fun ProxyLatencyBadge(
delay: Int,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
// Direct color calculation without animation for better performance
val colorScheme = MaterialTheme.colorScheme
val latencyColor =
@@ -624,15 +609,9 @@ private fun ProxyLatencyBadge(
}
@Composable
private fun rememberBounceBlockingNestedScrollConnection(
lazyListState: LazyListState
): NestedScrollConnection = remember(lazyListState) {
private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// Only block upward scroll (y < 0) at bottom to prevent sheet expansion
// Allow downward scroll (y > 0) at top to let sheet collapse
return if (available.y < 0) available else Offset.Zero

View File

@@ -6,6 +6,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -42,9 +43,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.toArgb
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -52,6 +51,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight

View File

@@ -1,5 +1,6 @@
package io.nekohasekai.sfa.compose.screen.dashboard
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -16,7 +17,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -29,11 +29,7 @@ import io.nekohasekai.sfa.compose.util.ProfileIcons
import io.nekohasekai.sfa.database.Profile
@Composable
fun ProfileSelectorButton(
selectedProfile: Profile?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
fun ProfileSelectorButton(selectedProfile: Profile?, onClick: () -> Unit, modifier: Modifier = Modifier) {
Surface(
onClick = onClick,
modifier = modifier.fillMaxWidth().height(48.dp),

View File

@@ -5,6 +5,7 @@ import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -21,33 +22,30 @@ import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.DataObject
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.IosShare
import androidx.compose.material.icons.filled.QrCode2
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.DataObject
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.outlined.CreateNewFolder
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.FileUpload
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.toArgb
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -55,6 +53,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -64,11 +63,11 @@ import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.ProfileContent
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.component.qr.QRCodeDialog
import io.nekohasekai.sfa.compose.component.qr.QRScanSheet
import io.nekohasekai.sfa.compose.component.qr.QRSDialog
import io.nekohasekai.sfa.compose.component.qr.QRScanSheet
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
import io.nekohasekai.sfa.compose.screen.configuration.ProfileImportHandler
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
import io.nekohasekai.sfa.compose.util.QRCodeGenerator
import io.nekohasekai.sfa.compose.util.RelativeTimeFormatter
import io.nekohasekai.sfa.database.Profile

View File

@@ -23,12 +23,7 @@ import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R
@Composable
fun SystemProxyCard(
enabled: Boolean,
isSwitching: Boolean,
onToggle: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
fun SystemProxyCard(enabled: Boolean, isSwitching: Boolean, onToggle: (Boolean) -> Unit, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
) {

View File

@@ -24,12 +24,7 @@ import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.LineChart
@Composable
fun UploadTrafficCard(
uplink: String,
uplinkTotal: String,
uplinkHistory: List<Float>,
modifier: Modifier = Modifier,
) {
fun UploadTrafficCard(uplink: String, uplinkTotal: String, uplinkHistory: List<Float>, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
) {

View File

@@ -54,9 +54,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.model.Group
import io.nekohasekai.sfa.compose.model.GroupItem
import io.nekohasekai.sfa.constant.Status
@Composable
fun GroupsScreen(
@@ -294,12 +294,7 @@ private fun ProxyGroupCard(
}
@Composable
private fun ProxyItemsList(
items: List<GroupItem>,
selectedTag: String,
isSelectable: Boolean,
onItemSelected: (String) -> Unit,
) {
private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
// Cache the chunked items to avoid re-chunking on every recomposition
val itemsPerRow = 2
val chunkedItems =
@@ -344,13 +339,7 @@ private fun ProxyItemsList(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ProxyChip(
item: GroupItem,
isSelected: Boolean,
isSelectable: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
// Use simpler, faster animations
val animatedElevation by animateFloatAsState(
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
@@ -460,11 +449,7 @@ private fun ProxyChip(
}
@Composable
private fun ProxyLatencyBadge(
delay: Int,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
// Direct color calculation without animation for better performance
val colorScheme = MaterialTheme.colorScheme
val latencyColor =

View File

@@ -5,10 +5,10 @@ import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.OutboundGroup
import io.nekohasekai.sfa.compose.base.BaseViewModel
import io.nekohasekai.sfa.compose.base.ScreenEvent
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.model.Group
import io.nekohasekai.sfa.compose.model.GroupItem
import io.nekohasekai.sfa.compose.model.toList
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient
import kotlinx.coroutines.Dispatchers
@@ -28,9 +28,9 @@ sealed class GroupsEvent : ScreenEvent {
data class GroupSelected(val groupTag: String, val itemTag: String) : GroupsEvent()
}
class GroupsViewModel(
private val sharedCommandClient: CommandClient? = null,
) : BaseViewModel<GroupsUiState, GroupsEvent>(), CommandClient.Handler {
class GroupsViewModel(private val sharedCommandClient: CommandClient? = null) :
BaseViewModel<GroupsUiState, GroupsEvent>(),
CommandClient.Handler {
private val commandClient: CommandClient
private val isUsingSharedClient: Boolean
@@ -154,10 +154,7 @@ class GroupsViewModel(
}
}
fun selectGroupItem(
groupTag: String,
itemTag: String,
) {
fun selectGroupItem(groupTag: String, itemTag: String) {
// Check if this is actually a different selection
val currentGroup = uiState.value.groups.find { it.tag == groupTag }
if (currentGroup?.selected == itemTag) {

View File

@@ -15,7 +15,9 @@ import java.util.LinkedList
import java.util.concurrent.atomic.AtomicLong
@OptIn(FlowPreview::class)
abstract class BaseLogViewModel : ViewModel(), LogViewerViewModel {
abstract class BaseLogViewModel :
ViewModel(),
LogViewerViewModel {
protected val _uiState = MutableStateFlow(LogUiState())
override val uiState: StateFlow<LogUiState> = _uiState.asStateFlow()
@@ -119,9 +121,7 @@ abstract class BaseLogViewModel : ViewModel(), LogViewerViewModel {
.joinToString("\n")
}
override fun getAllLogsText(): String {
return _uiState.value.logs.joinToString("\n") { AnsiColorUtils.stripAnsi(it.entry.message) }
}
override fun getAllLogsText(): String = _uiState.value.logs.joinToString("\n") { AnsiColorUtils.stripAnsi(it.entry.message) }
protected fun updateDisplayedLogs() {
val currentState = _uiState.value

View File

@@ -3,16 +3,9 @@ package io.nekohasekai.sfa.compose.screen.log
import androidx.compose.ui.text.AnnotatedString
import io.nekohasekai.sfa.constant.Status
data class LogEntryData(
val level: LogLevel,
val message: String,
)
data class LogEntryData(val level: LogLevel, val message: String)
data class ProcessedLogEntry(
val id: Long,
val entry: LogEntryData,
val annotatedString: AnnotatedString,
)
data class ProcessedLogEntry(val id: Long, val entry: LogEntryData, val annotatedString: AnnotatedString)
enum class LogLevel(val label: String, val priority: Int) {
Default("Default", 7),

View File

@@ -1,9 +1,9 @@
package io.nekohasekai.sfa.compose.screen.log
import android.content.ClipData
import android.os.Build
import android.content.res.Configuration
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts

View File

@@ -13,7 +13,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.LinkedList
class LogViewModel : BaseLogViewModel(), CommandClient.Handler {
class LogViewModel :
BaseLogViewModel(),
CommandClient.Handler {
companion object {
private val maxLines = 3000
}

View File

@@ -4,9 +4,9 @@ import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -53,13 +53,13 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
import io.nekohasekai.sfa.compose.shared.PackageCache
import io.nekohasekai.sfa.compose.shared.SortMode
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.vendor.PackageQueryManager
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
@@ -68,11 +68,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
private data class LoadResult(
val packages: List<PackageCache>,
val selectedUids: Set<Int>,
)
private data class LoadResult(val packages: List<PackageCache>, val selectedUids: Set<Int>)
private const val VPN_SERVICE_PERMISSION = "android.permission.BIND_VPN_SERVICE"
@@ -126,7 +122,8 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
val hasManagement = permissions.any { it in managementPermissions }
val isSelf = packageCache.packageName == context.packageName
val hasVpnService =
!isSelf && (
!isSelf &&
(
permissions.any { it == VPN_SERVICE_PERMISSION } ||
packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true
)
@@ -138,11 +135,9 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
}
}
fun buildPackageList(newUids: Set<Int>): Set<String> {
return newUids.mapNotNull { uid ->
fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
packages.find { it.uid == uid }?.packageName
}.toSet()
}
fun updateCurrentPackages(filterQuery: String) {
currentPackages =

View File

@@ -239,9 +239,13 @@ fun EditProfileContentScreen(
}
// Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y - Redo
(
modifierPressed && event.isShiftPressed && event.key == Key.Z ||
modifierPressed && event.key == Key.Y
) && !uiState.isReadOnly -> {
modifierPressed &&
event.isShiftPressed &&
event.key == Key.Z ||
modifierPressed &&
event.key == Key.Y
) &&
!uiState.isReadOnly -> {
viewModel.redo()
true
}

View File

@@ -38,11 +38,7 @@ data class EditProfileContentUiState(
val profileName: String = "", // Add profile name
)
class EditProfileContentViewModel(
private val profileId: Long,
initialProfileName: String = "",
initialIsReadOnly: Boolean = false,
) : ViewModel() {
class EditProfileContentViewModel(private val profileId: Long, initialProfileName: String = "", initialIsReadOnly: Boolean = false) : ViewModel() {
private val _uiState =
MutableStateFlow(
EditProfileContentUiState(
@@ -56,10 +52,7 @@ class EditProfileContentViewModel(
private var editor: ManualScrollTextProcessor? = null
private var configCheckJob: Job? = null
fun setEditor(
textProcessor: ManualScrollTextProcessor,
isReadOnly: Boolean = false,
) {
fun setEditor(textProcessor: ManualScrollTextProcessor, isReadOnly: Boolean = false) {
val isNewEditor = editor != textProcessor
editor = textProcessor
textProcessor.resumeAutoScroll()
@@ -89,18 +82,12 @@ class EditProfileContentViewModel(
// Customize text selection to remove Cut and Paste options
textProcessor.customSelectionActionModeCallback =
object : android.view.ActionMode.Callback {
override fun onCreateActionMode(
mode: android.view.ActionMode?,
menu: android.view.Menu?,
): Boolean {
override fun onCreateActionMode(mode: android.view.ActionMode?, menu: android.view.Menu?): Boolean {
// Allow the action mode to be created
return true
}
override fun onPrepareActionMode(
mode: android.view.ActionMode?,
menu: android.view.Menu?,
): Boolean {
override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: android.view.Menu?): Boolean {
// Remove editing-related menu items, keep only Copy and Select All
menu?.let { m ->
// Remove all editing-related items
@@ -116,10 +103,7 @@ class EditProfileContentViewModel(
return true
}
override fun onActionItemClicked(
mode: android.view.ActionMode?,
item: android.view.MenuItem?,
): Boolean {
override fun onActionItemClicked(mode: android.view.ActionMode?, item: android.view.MenuItem?): Boolean {
// Let the default implementation handle allowed actions (copy, select all)
return false
}

View File

@@ -13,11 +13,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
@Composable
fun EditProfileRoute(
profileId: Long,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
) {
fun EditProfileRoute(profileId: Long, onNavigateBack: () -> Unit, modifier: Modifier = Modifier) {
if (profileId == -1L) {
LaunchedEffect(Unit) {
onNavigateBack()

View File

@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
@@ -61,6 +60,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle

View File

@@ -183,13 +183,11 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
private fun checkHasChanges(state: EditProfileUiState): Boolean {
return state.name != state.originalName ||
private fun checkHasChanges(state: EditProfileUiState): Boolean = state.name != state.originalName ||
state.icon != state.originalIcon ||
state.remoteUrl != state.originalRemoteUrl ||
state.autoUpdate != state.originalAutoUpdate ||
state.autoUpdateInterval != state.originalAutoUpdateInterval
}
fun saveChanges() {
val state = _uiState.value
@@ -343,10 +341,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
fun saveExportToUri(
context: Context,
uri: Uri,
) {
fun saveExportToUri(context: Context, uri: Uri) {
val content = pendingExportContent ?: return
viewModelScope.launch(Dispatchers.IO) {

View File

@@ -37,11 +37,7 @@ import io.nekohasekai.sfa.compose.util.ProfileIcon
import io.nekohasekai.sfa.compose.util.ProfileIcons
@Composable
fun IconSelectionDialog(
currentIconId: String?,
onIconSelected: (String?) -> Unit,
onDismiss: () -> Unit,
) {
fun IconSelectionDialog(currentIconId: String?, onIconSelected: (String?) -> Unit, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier =
@@ -110,12 +106,7 @@ fun IconSelectionDialog(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun IconOption(
icon: ProfileIcon?,
label: String,
isSelected: Boolean,
onClick: () -> Unit,
) {
private fun IconOption(icon: ProfileIcon?, label: String, isSelected: Boolean, onClick: () -> Unit) {
Card(
modifier =
Modifier

View File

@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -60,6 +59,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -74,11 +74,7 @@ import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IconSelectionScreen(
currentIconId: String?,
onIconSelected: (String?) -> Unit,
onNavigateBack: () -> Unit,
) {
fun IconSelectionScreen(currentIconId: String?, onIconSelected: (String?) -> Unit, onNavigateBack: () -> Unit) {
var searchQuery by remember { mutableStateOf("") }
var selectedCategory by remember { mutableStateOf<String?>(null) }
var viewMode by remember { mutableStateOf(IconViewMode.CATEGORIES) }
@@ -436,11 +432,7 @@ fun IconSelectionScreen(
}
@Composable
private fun CategoryList(
categories: List<IconCategory>,
currentIconId: String?,
onCategoryClick: (IconCategory) -> Unit,
) {
private fun CategoryList(categories: List<IconCategory>, currentIconId: String?, onCategoryClick: (IconCategory) -> Unit) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
@@ -456,11 +448,7 @@ private fun CategoryList(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CategoryCard(
category: IconCategory,
hasSelectedIcon: Boolean,
onClick: () -> Unit,
) {
private fun CategoryCard(category: IconCategory, hasSelectedIcon: Boolean, onClick: () -> Unit) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
@@ -517,11 +505,7 @@ private fun CategoryCard(
}
@Composable
private fun IconGrid(
icons: List<ProfileIcon>,
currentIconId: String?,
onIconClick: (ProfileIcon) -> Unit,
) {
private fun IconGrid(icons: List<ProfileIcon>, currentIconId: String?, onIconClick: (ProfileIcon) -> Unit) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 72.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -539,11 +523,7 @@ private fun IconGrid(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun IconGridItem(
icon: ProfileIcon,
isSelected: Boolean,
onClick: () -> Unit,
) {
private fun IconGridItem(icon: ProfileIcon, isSelected: Boolean, onClick: () -> Unit) {
Card(
onClick = onClick,
modifier =

View File

@@ -4,7 +4,6 @@ import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import com.blacksquircle.ui.editorkit.widget.TextProcessor;
public class ManualScrollTextProcessor extends TextProcessor {
@@ -117,7 +116,8 @@ public class ManualScrollTextProcessor extends TextProcessor {
}
if (userDragging) {
if (downSelectionStart >= 0 && (selStart != downSelectionStart || selEnd != downSelectionEnd)) {
if (downSelectionStart >= 0
&& (selStart != downSelectionStart || selEnd != downSelectionEnd)) {
restoringSelection = true;
int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart;
setSelection(downSelectionStart, targetEnd);

View File

@@ -6,9 +6,9 @@ import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -54,7 +54,6 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -66,9 +65,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
@@ -79,13 +76,13 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
import io.nekohasekai.sfa.compose.shared.PackageCache
import io.nekohasekai.sfa.compose.shared.SortMode
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.vendor.PackageQueryManager
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
import kotlinx.coroutines.Dispatchers
@@ -97,16 +94,9 @@ import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile
private data class LoadResult(
val proxyMode: Int,
val packages: List<PackageCache>,
val selectedUids: Set<Int>,
)
private data class LoadResult(val proxyMode: Int, val packages: List<PackageCache>, val selectedUids: Set<Int>)
private data class ScanProgress(
val current: Int,
val max: Int,
)
private data class ScanProgress(val current: Int, val max: Int)
private sealed class ScanResult {
data object Empty : ScanResult()
@@ -139,11 +129,9 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
var scanProgress by remember { mutableStateOf<ScanProgress?>(null) }
var scanResult by remember { mutableStateOf<ScanResult?>(null) }
fun buildPackageList(newUids: Set<Int>): Set<String> {
return newUids.mapNotNull { uid ->
fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
packages.find { it.uid == uid }?.packageName
}.toSet()
}
fun updateCurrentPackages(filterQuery: String) {
currentPackages =

View File

@@ -14,12 +14,7 @@ data class QRCodeCropArea(
)
object QRCodeSmartCrop {
fun findCropArea(
yData: ByteArray,
width: Int,
height: Int,
rotationDegrees: Int,
): QRCodeCropArea? {
fun findCropArea(yData: ByteArray, width: Int, height: Int, rotationDegrees: Int): QRCodeCropArea? {
val minDim = min(width, height)
if (minDim <= 0) return null
@@ -94,14 +89,7 @@ object QRCodeSmartCrop {
return bestArea
}
private data class CropComponent(
val minX: Int,
val minY: Int,
val maxX: Int,
val maxY: Int,
val count: Int,
val score: Float,
)
private data class CropComponent(val minX: Int, val minY: Int, val maxX: Int, val maxY: Int, val count: Int, val score: Float)
private fun findBestComponent(
yData: ByteArray,
@@ -233,13 +221,7 @@ object QRCodeSmartCrop {
return best
}
private fun buildCropArea(
component: CropComponent,
step: Int,
width: Int,
height: Int,
rotationDegrees: Int,
): QRCodeCropArea? {
private fun buildCropArea(component: CropComponent, step: Int, width: Int, height: Int, rotationDegrees: Int): QRCodeCropArea? {
val left = component.minX * step
val top = component.minY * step
val right = min(width, (component.maxX + 1) * step)

View File

@@ -83,7 +83,7 @@ class QRScanViewModel(application: Application) : AndroidViewModel(application)
_uiState.update {
it.copy(
vendorAnalyzerAvailable = vendorAnalyzer != null,
useVendorAnalyzer = vendorAnalyzer != null
useVendorAnalyzer = vendorAnalyzer != null,
)
}
}
@@ -196,7 +196,7 @@ class QRScanViewModel(application: Application) : AndroidViewModel(application)
lifecycleOwner,
cameraSelector,
preview,
analysis
analysis,
)
val maxZoom = camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f
_uiState.update { it.copy(maxZoomRatio = maxZoom, zoomRatio = 1f) }

View File

@@ -109,12 +109,10 @@ class ZxingQRCodeAnalyzer(
return yData
}
private fun tryDecode(bitmap: BinaryBitmap): Result? {
return try {
private fun tryDecode(bitmap: BinaryBitmap): Result? = try {
qrCodeReader.decode(bitmap)
} catch (_: NotFoundException) {
qrCodeReader.reset()
null
}
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Log
import android.provider.Settings as AndroidSettings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -29,7 +28,6 @@ import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SystemUpdateAlt
import androidx.compose.material3.Switch
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Badge
import androidx.compose.material3.Card
@@ -42,6 +40,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -53,8 +52,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -62,22 +59,25 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.navigation.NavController
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.update.UpdateCheckException
import io.nekohasekai.sfa.update.UpdateState
import io.nekohasekai.sfa.update.UpdateTrack
import io.nekohasekai.sfa.vendor.Vendor
import io.nekohasekai.sfa.utils.HookStatusClient
import io.nekohasekai.sfa.vendor.Vendor
import io.nekohasekai.sfa.xposed.XposedActivation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import android.provider.Settings as AndroidSettings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -136,7 +136,8 @@ fun AppSettingsScreen(navController: NavController) {
isMethodAvailable = success
silentInstallError = if (success) {
null
} else when (silentInstallMethod) {
} else {
when (silentInstallMethod) {
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
@@ -144,6 +145,7 @@ fun AppSettingsScreen(navController: NavController) {
}
}
}
}
if (showTrackDialog) {
UpdateTrackDialog(
@@ -224,12 +226,14 @@ fun AppSettingsScreen(navController: NavController) {
isMethodAvailable = success
silentInstallError = if (success) {
null
} else when (method) {
} else {
when (method) {
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
else -> context.getString(R.string.silent_install_verify_failed, method)
}
}
}
},
onDismiss = { showInstallMethodMenu = false },
)
@@ -478,12 +482,14 @@ fun AppSettingsScreen(navController: NavController) {
isMethodAvailable = success
silentInstallError = if (success) {
null
} else when (silentInstallMethod) {
} else {
when (silentInstallMethod) {
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
}
}
}
} else {
silentInstallError = null
}
@@ -510,11 +516,13 @@ fun AppSettingsScreen(navController: NavController) {
Text(
if (xposedActivated) {
stringResource(R.string.install_method_root)
} else when (silentInstallMethod) {
} else {
when (silentInstallMethod) {
"PACKAGE_INSTALLER" -> stringResource(R.string.install_method_package_installer)
"SHIZUKU" -> stringResource(R.string.install_method_shizuku)
"ROOT" -> stringResource(R.string.install_method_root)
else -> silentInstallMethod
}
},
style = MaterialTheme.typography.bodyMedium,
)
@@ -597,7 +605,7 @@ fun AppSettingsScreen(navController: NavController) {
.clickable {
val intent = Intent(
AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:${context.packageName}")
Uri.parse("package:${context.packageName}"),
)
context.startActivity(intent)
},

View File

@@ -1,5 +1,10 @@
package io.nekohasekai.sfa.compose.screen.settings
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.provider.DocumentsContract
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -11,11 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.provider.DocumentsContract
import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.DeleteForever
@@ -343,7 +343,7 @@ private fun openInFileManager(context: Context) {
Toast.makeText(
context,
context.getString(R.string.no_file_manager),
Toast.LENGTH_SHORT
Toast.LENGTH_SHORT,
).show()
}
}

View File

@@ -1,7 +1,6 @@
package io.nekohasekai.sfa.compose.screen.settings
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -61,18 +60,18 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.navigation.NavController
import io.nekohasekai.sfa.R
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.base.GlobalEventBus
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
import io.nekohasekai.sfa.compose.base.UiEvent
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.utils.DetectionResult
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.utils.HookStatusClient
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.utils.VpnDetectionTest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -122,7 +121,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
var messageDialogMessage by remember { mutableStateOf("") }
val saveFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/zip")
contract = ActivityResultContracts.CreateDocument("application/zip"),
) { uri ->
val file = exportedFile
if (uri != null && file != null) {
@@ -231,8 +230,11 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
CircularProgressIndicator(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(12.dp))
Text(
if (exportError != null) exportError!!
else stringResource(R.string.exporting)
if (exportError != null) {
exportError!!
} else {
stringResource(R.string.exporting)
},
)
}
},
@@ -273,7 +275,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.cache",
file
file,
)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "application/zip"
@@ -283,7 +285,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
context.startActivity(Intent.createChooser(intent, null))
showExportSuccessDialog = false
exportedFile = null
}
},
) {
Text(stringResource(R.string.menu_share))
}
@@ -293,7 +295,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
onClick = {
val file = exportedFile ?: return@TextButton
saveFileLauncher.launch(file.name)
}
},
) {
Text(stringResource(R.string.save))
}
@@ -448,7 +450,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
}
val timestamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val outZip = File(exportBase, "sing-box-lsposed-debug-${timestamp}.zip")
val outZip = File(exportBase, "sing-box-lsposed-debug-$timestamp.zip")
exportCancelled = false
exportError = null
showExportProgressDialog = true
@@ -469,7 +471,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
messageDialogTitle = context.getString(R.string.error_title)
messageDialogMessage = context.getString(
R.string.privilege_settings_export_debug_failed,
failure
failure,
)
showMessageDialog = true
}
@@ -621,7 +623,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
} else {
RoundedCornerShape(12.dp)
}
},
),
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
@@ -662,7 +664,6 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
),
)
}
}
}
@@ -730,7 +731,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
} else {
RoundedCornerShape(12.dp)
}
},
),
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
@@ -847,11 +848,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
}
@Composable
private fun SelfTestDialog(
isRunning: Boolean,
result: DetectionResult?,
onDismiss: () -> Unit,
) {
private fun SelfTestDialog(isRunning: Boolean, result: DetectionResult?, onDismiss: () -> Unit) {
val notDetectedText = stringResource(R.string.privilege_settings_hide_test_not_detected)
AlertDialog(

View File

@@ -54,14 +54,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.NavController
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.bg.RootClient
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
import io.nekohasekai.sfa.vendor.PackageQueryManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -272,32 +272,44 @@ fun ProfileOverrideScreen(navController: NavController) {
Text(
stringResource(R.string.per_app_proxy_package_query_mode),
style = MaterialTheme.typography.bodyLarge,
color = if (modeEnabled) Color.Unspecified
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
color = if (modeEnabled) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
},
)
},
supportingContent = {
Text(
if (useRootMode) "ROOT" else "Shizuku",
style = MaterialTheme.typography.bodyMedium,
color = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
color = if (modeEnabled) {
MaterialTheme.colorScheme.onSurfaceVariant
} else {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha)
},
)
},
leadingContent = {
Icon(
imageVector = Icons.Outlined.Tune,
contentDescription = null,
tint = if (modeEnabled) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
tint = if (modeEnabled) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
},
)
},
trailingContent = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
contentDescription = null,
tint = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
tint = if (modeEnabled) {
MaterialTheme.colorScheme.onSurfaceVariant
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
},
)
},
modifier = Modifier
@@ -601,7 +613,7 @@ fun ProfileOverrideScreen(navController: NavController) {
Toast.makeText(
context,
R.string.root_access_denied,
Toast.LENGTH_LONG
Toast.LENGTH_LONG,
).show()
}
}

View File

@@ -63,10 +63,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ServiceSettingsScreen(
navController: NavController,
serviceConnection: ServiceConnection? = null,
) {
fun ServiceSettingsScreen(navController: NavController, serviceConnection: ServiceConnection? = null) {
OverrideTopBar {
TopAppBar(
title = { Text(stringResource(R.string.service)) },

View File

@@ -15,15 +15,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterAlt
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material3.Badge
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -49,15 +48,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.update.UpdateState
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
import io.nekohasekai.sfa.utils.HookStatusClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View File

@@ -7,20 +7,12 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
internal data class TopBarEntry(
val key: Any,
val content: @Composable () -> Unit,
)
internal data class TopBarEntry(val key: Any, val content: @Composable () -> Unit)
class TopBarController internal constructor(
private val state: MutableState<List<TopBarEntry>>,
) {
class TopBarController internal constructor(private val state: MutableState<List<TopBarEntry>>) {
val current: (@Composable () -> Unit)? get() = state.value.lastOrNull()?.content
fun set(
key: Any,
content: @Composable () -> Unit,
) {
fun set(key: Any, content: @Composable () -> Unit) {
state.value = state.value.filterNot { it.key == key } + TopBarEntry(key, content)
}

View File

@@ -9,10 +9,7 @@ import androidx.compose.material.icons.sharp.*
import androidx.compose.material.icons.twotone.*
import androidx.compose.ui.graphics.vector.ImageVector
data class IconCategory(
val name: String,
val icons: List<ProfileIcon>,
)
data class IconCategory(val name: String, val icons: List<ProfileIcon>)
object MaterialIconsLibrary {
val categories =
@@ -416,20 +413,16 @@ object MaterialIconsLibrary {
),
)
fun getAllIcons(): List<ProfileIcon> {
return categories.flatMap { it.icons }
}
fun getAllIcons(): List<ProfileIcon> = categories.flatMap { it.icons }
fun getIconById(id: String?): ImageVector? {
if (id == null) return null
return getAllIcons().find { it.id == id }?.icon
}
fun getCategoryForIcon(iconId: String): String? {
return categories.find { category ->
fun getCategoryForIcon(iconId: String): String? = categories.find { category ->
category.icons.any { it.id == iconId }
}?.name
}
fun searchIcons(query: String): List<ProfileIcon> {
val lowercaseQuery = query.lowercase()

View File

@@ -5,11 +5,7 @@ import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.ui.graphics.vector.ImageVector
import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
data class ProfileIcon(
val id: String,
val icon: ImageVector,
val label: String,
)
data class ProfileIcon(val id: String, val icon: ImageVector, val label: String)
object ProfileIcons {
// Use the complete Material Icons library with all available icons
@@ -26,13 +22,9 @@ object ProfileIcons {
return Icons.AutoMirrored.Default.InsertDriveFile
}
fun getCategoryForIcon(iconId: String): String? {
return MaterialIconsLibrary.getCategoryForIcon(iconId)
}
fun getCategoryForIcon(iconId: String): String? = MaterialIconsLibrary.getCategoryForIcon(iconId)
fun searchIcons(query: String): List<ProfileIcon> {
return MaterialIconsLibrary.searchIcons(query)
}
fun searchIcons(query: String): List<ProfileIcon> = MaterialIconsLibrary.searchIcons(query)
fun getCategories() = MaterialIconsLibrary.categories
}

View File

@@ -87,12 +87,7 @@ object QRCodeGenerator {
}
}
fun generate(
content: String,
size: Int = 512,
foregroundColor: Int = Color.BLACK,
backgroundColor: Int = Color.WHITE,
): Bitmap {
fun generate(content: String, size: Int = 512, foregroundColor: Int = Color.BLACK, backgroundColor: Int = Color.WHITE): Bitmap {
val writer = QRCodeWriter()
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size)

View File

@@ -11,10 +11,7 @@ object RelativeTimeFormatter {
* Formats a date as relative time for recent dates (within 7 days)
* or as full date/time for older dates.
*/
fun format(
context: Context,
date: Date?,
): String {
fun format(context: Context, date: Date?): String {
if (date == null) return ""
val now = System.currentTimeMillis()
@@ -59,10 +56,7 @@ object RelativeTimeFormatter {
* Formats a date as short relative time for compact displays.
* Uses shorter format like "2h" instead of "2 hours ago".
*/
fun formatShort(
context: Context,
date: Date?,
): String {
fun formatShort(context: Context, date: Date?): String {
if (date == null) return ""
val now = System.currentTimeMillis()

View File

@@ -5,9 +5,6 @@ import io.nekohasekai.sfa.compose.util.ProfileIcon
/**
* Represents a category of Material Icons following Google's official taxonomy
*/
data class IconCategory(
val name: String,
val icons: List<ProfileIcon>,
) {
data class IconCategory(val name: String, val icons: List<ProfileIcon>) {
val size: Int get() = icons.size
}

View File

@@ -52,16 +52,12 @@ object MaterialIconsLibrary {
/**
* Get all icons from all categories
*/
fun getAllIcons(): List<ProfileIcon> {
return categories.flatMap { it.icons }
}
fun getAllIcons(): List<ProfileIcon> = categories.flatMap { it.icons }
/**
* Get an icon by its ID
*/
fun getIconById(id: String): ImageVector? {
return getAllIcons().find { it.id == id }?.icon
}
fun getIconById(id: String): ImageVector? = getAllIcons().find { it.id == id }?.icon
/**
* Get the category name for a given icon ID
@@ -91,22 +87,16 @@ object MaterialIconsLibrary {
/**
* Get icons by category name
*/
fun getIconsByCategory(categoryName: String): List<ProfileIcon> {
return categories.find { it.name.equals(categoryName, ignoreCase = true) }?.icons
fun getIconsByCategory(categoryName: String): List<ProfileIcon> = categories.find { it.name.equals(categoryName, ignoreCase = true) }?.icons
?: emptyList()
}
/**
* Get total number of icons in the library
*/
fun getTotalIconCount(): Int {
return categories.sumOf { it.icons.size }
}
fun getTotalIconCount(): Int = categories.sumOf { it.icons.size }
/**
* Get category names
*/
fun getCategoryNames(): List<String> {
return categories.map { it.name }
}
fun getCategoryNames(): List<String> = categories.map { it.name }
}

Some files were not shown because too many files have changed in this diff Show More