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

View File

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

View File

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

View File

@@ -4,6 +4,12 @@ import android.os.Bundle;
import android.os.IInterface; import android.os.IInterface;
public interface IIntentReceiver extends IInterface { public interface IIntentReceiver extends IInterface {
void performReceive(Intent intent, int resultCode, String data, Bundle extras, void performReceive(
boolean ordered, boolean sticky, int sendingUser); 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 { public interface IIntentSender extends IInterface {
void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, void send(
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options); int code,
Intent intent,
String resolvedType,
IBinder whitelistToken,
IIntentReceiver finishedReceiver,
String requiredPermission,
Bundle options);
abstract class Stub extends Binder implements IIntentSender { abstract class Stub extends Binder implements IIntentSender {
public static IIntentSender asInterface(IBinder binder) { public static IIntentSender asInterface(IBinder binder) {

View File

@@ -7,7 +7,12 @@ import android.os.RemoteException;
public interface IPackageInstaller extends IInterface { 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; IPackageInstallerSession openSession(int sessionId) throws RemoteException;

View File

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

View File

@@ -7,7 +7,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -22,7 +21,12 @@ public final class XposedProvider extends ContentProvider {
@Nullable @Nullable
@Override @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; return null;
} }
@@ -39,12 +43,17 @@ public final class XposedProvider extends ContentProvider {
} }
@Override @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; return 0;
} }
@Override @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; return 0;
} }

View File

@@ -3,10 +3,8 @@ package io.github.libxposed.service;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.RemoteException; import android.os.RemoteException;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -16,7 +14,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class XposedService { public final class XposedService {
public final static class ServiceException extends RuntimeException { public static final class ServiceException extends RuntimeException {
ServiceException(String message) { ServiceException(String message) {
super(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 { public interface OnScopeEventListener {
/** /**
* Callback when the request notification / window prompted. * Callback when the request notification / window prompted.
* *
* @param packageName Package name of requested app * @param packageName Package name of requested app
*/ */
default void onScopeRequestPrompted(String packageName) { default void onScopeRequestPrompted(String packageName) {}
}
/** /**
* Callback when the request is approved. * Callback when the request is approved.
* *
* @param packageName Package name of requested app * @param packageName Package name of requested app
*/ */
default void onScopeRequestApproved(String packageName) { default void onScopeRequestApproved(String packageName) {}
}
/** /**
* Callback when the request is denied. * Callback when the request is denied.
* *
* @param packageName Package name of requested app * @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. * Callback when the request is timeout or revoked.
* *
* @param packageName Package name of requested app * @param packageName Package name of requested app
*/ */
default void onScopeRequestTimeout(String packageName) { default void onScopeRequestTimeout(String packageName) {}
}
/** /**
* Callback when the request is failed. * Callback when the request is failed.
@@ -70,11 +63,13 @@ public final class XposedService {
* @param packageName Package name of requested app * @param packageName Package name of requested app
* @param message Error message * @param message Error message
*/ */
default void onScopeRequestFailed(String packageName, String message) { default void onScopeRequestFailed(String packageName, String message) {}
}
private IXposedScopeCallback asInterface() { private IXposedScopeCallback asInterface() {
return scopeCallbacks.computeIfAbsent(this, (listener) -> new IXposedScopeCallback.Stub() { return scopeCallbacks.computeIfAbsent(
this,
(listener) ->
new IXposedScopeCallback.Stub() {
@Override @Override
public void onScopeRequestPrompted(String packageName) { public void onScopeRequestPrompted(String packageName) {
listener.onScopeRequestPrompted(packageName); listener.onScopeRequestPrompted(packageName);
@@ -104,28 +99,21 @@ public final class XposedService {
} }
public enum Privilege { public enum Privilege {
/** /** Unknown privilege value. */
* Unknown privilege value.
*/
FRAMEWORK_PRIVILEGE_UNKNOWN, FRAMEWORK_PRIVILEGE_UNKNOWN,
/** /** The framework is running as root. */
* The framework is running as root.
*/
FRAMEWORK_PRIVILEGE_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, 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, 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 FRAMEWORK_PRIVILEGE_EMBEDDED
} }
@@ -211,7 +199,9 @@ public final class XposedService {
public Privilege getFrameworkPrivilege() { public Privilege getFrameworkPrivilege() {
try { try {
int value = mService.getFrameworkPrivilege(); 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) { } catch (RemoteException e) {
throw new ServiceException(e); throw new ServiceException(e);
} }
@@ -273,7 +263,9 @@ public final class XposedService {
*/ */
@NonNull @NonNull
public SharedPreferences getRemotePreferences(@NonNull String group) { public SharedPreferences getRemotePreferences(@NonNull String group) {
return mRemotePrefs.computeIfAbsent(group, k -> { return mRemotePrefs.computeIfAbsent(
group,
k -> {
try { try {
RemotePreferences instance = RemotePreferences.newInstance(this, k); RemotePreferences instance = RemotePreferences.newInstance(this, k);
if (instance == null) { if (instance == null) {
@@ -300,7 +292,9 @@ public final class XposedService {
deletionLock.writeLock().lock(); deletionLock.writeLock().lock();
try { try {
mService.deleteRemotePreferences(group); mService.deleteRemotePreferences(group);
mRemotePrefs.computeIfPresent(group, (k, v) -> { mRemotePrefs.computeIfPresent(
group,
(k, v) -> {
v.setDeleted(); v.setDeleted();
return null; return null;
}); });

View File

@@ -2,9 +2,7 @@ package io.github.libxposed.service;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set; import java.util.Set;
@@ -12,21 +10,17 @@ import java.util.Set;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class XposedServiceHelper { public final class XposedServiceHelper {
/** /** Callback interface for Xposed service. */
* Callback interface for Xposed service.
*/
public interface OnServiceListener { 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. * This method could be called multiple times if multiple Xposed frameworks exist.
* *
* @param service Service instance * @param service Service instance
*/ */
void onServiceBind(@NonNull XposedService service); void onServiceBind(@NonNull XposedService service);
/** /** Callback when the service is dead. */
* Callback when the service is dead.
*/
void onServiceDied(@NonNull XposedService service); 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. * This method should only be called once.
* *
* @param listener Listener to register * @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.constant.Bugs
import io.nekohasekai.sfa.utils.AppLifecycleObserver import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.utils.HookStatusClient import io.nekohasekai.sfa.utils.HookStatusClient
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.vendor.Vendor import io.nekohasekai.sfa.vendor.Vendor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,15 +19,10 @@ import kotlin.coroutines.suspendCoroutine
object LocalResolver : LocalDNSTransport { object LocalResolver : LocalDNSTransport {
private const val RCODE_NXDOMAIN = 3 private const val RCODE_NXDOMAIN = 3
override fun raw(): Boolean { override fun raw(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
override fun exchange( override fun exchange(ctx: ExchangeContext, message: ByteArray) {
ctx: ExchangeContext,
message: ByteArray,
) {
return runBlocking { return runBlocking {
val defaultNetwork = DefaultNetworkMonitor.require() val defaultNetwork = DefaultNetworkMonitor.require()
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
@@ -35,10 +30,7 @@ object LocalResolver : LocalDNSTransport {
ctx.onCancel(signal::cancel) ctx.onCancel(signal::cancel)
val callback = val callback =
object : DnsResolver.Callback<ByteArray> { object : DnsResolver.Callback<ByteArray> {
override fun onAnswer( override fun onAnswer(answer: ByteArray, rcode: Int) {
answer: ByteArray,
rcode: Int,
) {
if (rcode == 0) { if (rcode == 0) {
ctx.rawSuccess(answer) ctx.rawSuccess(answer)
} else { } else {
@@ -70,11 +62,7 @@ object LocalResolver : LocalDNSTransport {
} }
} }
override fun lookup( override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
ctx: ExchangeContext,
network: String,
domain: String,
) {
return runBlocking { return runBlocking {
val defaultNetwork = DefaultNetworkMonitor.require() val defaultNetwork = DefaultNetworkMonitor.require()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -84,10 +72,7 @@ object LocalResolver : LocalDNSTransport {
val callback = val callback =
object : DnsResolver.Callback<Collection<InetAddress>> { object : DnsResolver.Callback<Collection<InetAddress>> {
@Suppress("ThrowableNotThrown") @Suppress("ThrowableNotThrown")
override fun onAnswer( override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
answer: Collection<InetAddress>,
rcode: Int,
) {
if (rcode == 0) { if (rcode == 0) {
ctx.success( ctx.success(
(answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress } (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.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -14,14 +13,16 @@ public class LogEntry implements Parcelable {
public final int level; public final int level;
public final long timestamp; public final long timestamp;
@NonNull @NonNull public final String source;
public final String source; @NonNull public final String message;
@NonNull @Nullable public final String stackTrace;
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.level = level;
this.timestamp = timestamp; this.timestamp = timestamp;
this.source = source; this.source = source;
@@ -51,7 +52,8 @@ public class LogEntry implements Parcelable {
return 0; return 0;
} }
public static final Creator<LogEntry> CREATOR = new Creator<>() { public static final Creator<LogEntry> CREATOR =
new Creator<>() {
@Override @Override
public LogEntry createFromParcel(Parcel in) { public LogEntry createFromParcel(Parcel in) {
return new LogEntry(in); return new LogEntry(in);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,11 +18,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ServiceConnection( class ServiceConnection(private val context: Context, callback: Callback, private val register: Boolean = true) : ServiceConnection {
private val context: Context,
callback: Callback,
private val register: Boolean = true,
) : ServiceConnection {
companion object { companion object {
private const val TAG = "ServiceConnection" private const val TAG = "ServiceConnection"
} }
@@ -66,10 +62,7 @@ class ServiceConnection(
Log.d(TAG, "request reconnect") Log.d(TAG, "request reconnect")
} }
override fun onServiceConnected( override fun onServiceConnected(name: ComponentName, binder: IBinder) {
name: ComponentName,
binder: IBinder,
) {
val service = IService.Stub.asInterface(binder) val service = IService.Stub.asInterface(binder)
this.service = service this.service = service
try { try {
@@ -98,10 +91,7 @@ class ServiceConnection(
interface Callback { interface Callback {
fun onServiceStatusChanged(status: Status) fun onServiceStatusChanged(status: Status)
fun onServiceAlert( fun onServiceAlert(type: Alert, message: String?) {
type: Alert,
message: String?,
) {
} }
} }
@@ -110,10 +100,7 @@ class ServiceConnection(
callback.onServiceStatusChanged(Status.values()[status]) callback.onServiceStatusChanged(Status.values()[status])
} }
override fun onServiceAlert( override fun onServiceAlert(type: Int, message: String?) {
type: Int,
message: String?,
) {
callback.onServiceAlert(Alert.values()[type], message) 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.Libbox
import io.nekohasekai.libbox.StatusMessage import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.compose.MainActivity
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.MainActivity
import io.nekohasekai.sfa.constant.Action import io.nekohasekai.sfa.constant.Action
import io.nekohasekai.sfa.constant.Status import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.database.Settings
@@ -27,10 +27,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ServiceNotification( class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) :
private val status: MutableLiveData<Status>, BroadcastReceiver(),
private val service: Service, CommandClient.Handler {
) : BroadcastReceiver(), CommandClient.Handler {
companion object { companion object {
private const val notificationId = 1 private const val notificationId = 1
private const val notificationChannel = "service" private const val notificationChannel = "service"
@@ -82,10 +81,7 @@ class ServiceNotification(
} }
} }
fun show( fun show(lastProfileName: String, @StringRes contentTextId: Int) {
lastProfileName: String,
@StringRes contentTextId: Int,
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel( Application.notification.createNotificationChannel(
NotificationChannel( NotificationChannel(
@@ -132,10 +128,7 @@ class ServiceNotification(
) )
} }
override fun onReceive( override fun onReceive(context: Context, intent: Intent) {
context: Context,
intent: Intent,
) {
when (intent.action) { when (intent.action) {
Intent.ACTION_SCREEN_ON -> { Intent.ACTION_SCREEN_ON -> {
commandClient.connect() commandClient.connect()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,11 +19,7 @@ import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
@Composable @Composable
fun SelectableMessageDialog( fun SelectableMessageDialog(title: String, message: String, onDismiss: () -> Unit) {
title: String,
message: String,
onDismiss: () -> Unit,
) {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val context = LocalContext.current val context = LocalContext.current
val scrollState = rememberScrollState() 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 Error(val exception: Throwable, val message: String? = null) : UiState<Nothing>()
} }
data class BaseUiState<T>( data class BaseUiState<T>(val isLoading: Boolean = false, val data: T? = null, val error: String? = null)
val isLoading: Boolean = false,
val data: T? = null,
val error: String? = null,
)

View File

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

View File

@@ -27,11 +27,7 @@ import org.kodein.emoji.EmojiTemplateCatalog
import org.kodein.emoji.all import org.kodein.emoji.all
@Composable @Composable
fun UpdateAvailableDialog( fun UpdateAvailableDialog(updateInfo: UpdateInfo, onDismiss: () -> Unit, onUpdate: () -> Unit) {
updateInfo: UpdateInfo,
onDismiss: () -> Unit,
onUpdate: () -> Unit,
) {
val context = LocalContext.current val context = LocalContext.current
val emojiCatalog = remember { EmojiTemplateCatalog(Emoji.all()) } 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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -24,10 +23,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@Composable @Composable
fun QRCodeDialog( fun QRCodeDialog(bitmap: Bitmap, onDismiss: () -> Unit) {
bitmap: Bitmap,
onDismiss: () -> Unit,
) {
Dialog( Dialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false), properties = DialogProperties(usePlatformDefaultWidth = false),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,18 +12,20 @@ import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument 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.DashboardScreen
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel 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.HookLogScreen
import io.nekohasekai.sfa.compose.screen.log.LogScreen import io.nekohasekai.sfa.compose.screen.log.LogScreen
import io.nekohasekai.sfa.compose.screen.log.LogViewModel import io.nekohasekai.sfa.compose.screen.log.LogViewModel
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
import io.nekohasekai.sfa.compose.screen.profile.EditProfileRoute 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.AppSettingsScreen
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen 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.ServiceSettingsScreen
import io.nekohasekai.sfa.compose.screen.settings.SettingsScreen import io.nekohasekai.sfa.compose.screen.settings.SettingsScreen
import io.nekohasekai.sfa.constant.Status 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 = { private val slideInFromRight: AnimatedContentTransitionScope<*>.() -> androidx.compose.animation.EnterTransition = {
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300)) 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
@@ -60,6 +59,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp 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)) } _uiState.update { it.copy(autoUpdateInterval = intValue.coerceAtLeast(15)) }
} }
fun setImportUri( fun setImportUri(uri: Uri, fileName: String?) {
uri: Uri,
fileName: String?,
) {
_uiState.update { _uiState.update {
it.copy( it.copy(
importUri = uri, importUri = uri,

View File

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

View File

@@ -1,5 +1,6 @@
package io.nekohasekai.sfa.compose.screen.connections package io.nekohasekai.sfa.compose.screen.connections
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -32,14 +32,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset 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.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll 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.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
@@ -286,10 +286,7 @@ fun ConnectionDetailsScreen(
} }
@Composable @Composable
private fun DetailSection( private fun DetailSection(title: String, content: @Composable ColumnScope.() -> Unit) {
title: String,
content: @Composable ColumnScope.() -> Unit,
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -317,12 +314,7 @@ private fun DetailSection(
} }
@Composable @Composable
private fun DetailRow( private fun DetailRow(label: String, value: String, monospace: Boolean = false, valueColor: Color = MaterialTheme.colorScheme.onSurface) {
label: String,
value: String,
monospace: Boolean = false,
valueColor: Color = MaterialTheme.colorScheme.onSurface,
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -346,20 +338,10 @@ private fun DetailRow(
} }
@Composable @Composable
private fun rememberBounceBlockingNestedScrollConnection( private fun rememberBounceBlockingNestedScrollConnection(scrollState: ScrollState): NestedScrollConnection = remember(scrollState) {
scrollState: ScrollState
): NestedScrollConnection = remember(scrollState) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (available.y < 0) available else Offset.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
return 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.libbox.Libbox
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.model.Connection import io.nekohasekai.sfa.compose.model.Connection
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private fun Drawable.toBitmap(): Bitmap { private fun Drawable.toBitmap(): Bitmap {
if (this is BitmapDrawable) return bitmap if (this is BitmapDrawable) return bitmap
val bitmap = Bitmap.createBitmap( val bitmap = Bitmap.createBitmap(
intrinsicWidth.coerceAtLeast(1), intrinsicWidth.coerceAtLeast(1),
intrinsicHeight.coerceAtLeast(1), intrinsicHeight.coerceAtLeast(1),
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888,
) )
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
setBounds(0, 0, canvas.width, canvas.height) setBounds(0, 0, canvas.width, canvas.height)
@@ -62,10 +59,7 @@ private fun Drawable.toBitmap(): Bitmap {
return bitmap return bitmap
} }
data class AppInfo( data class AppInfo(val icon: ImageBitmap, val label: String)
val icon: ImageBitmap,
val label: String,
)
@Composable @Composable
private fun rememberAppInfo(packageName: String): AppInfo? { private fun rememberAppInfo(packageName: String): AppInfo? {
@@ -86,12 +80,7 @@ private fun rememberAppInfo(packageName: String): AppInfo? {
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ConnectionItem( fun ConnectionItem(connection: Connection, onClick: () -> Unit, onClose: () -> Unit, modifier: Modifier = Modifier) {
connection: Connection,
onClick: () -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
var showContextMenu by remember { mutableStateOf(false) } var showContextMenu by remember { mutableStateOf(false) }
val packageName = connection.processInfo?.packageName?.takeIf { it.isNotEmpty() } val packageName = connection.processInfo?.packageName?.takeIf { it.isNotEmpty() }
val appInfo = packageName?.let { rememberAppInfo(it) } 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.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check 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.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import io.nekohasekai.sfa.R 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.ConnectionSort
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.constant.Status import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.model.Connection
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -118,7 +118,7 @@ fun ConnectionsPage(
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all) ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active) ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed) ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
} },
) )
}, },
) )
@@ -230,7 +230,7 @@ fun ConnectionsPage(
stringResource(R.string.close_search) stringResource(R.string.close_search)
} else { } else {
stringResource(R.string.search) stringResource(R.string.search)
} },
) )
}, },
onClick = { onClick = {
@@ -433,20 +433,10 @@ fun ConnectionsScreen(
} }
@Composable @Composable
private fun rememberBounceBlockingNestedScrollConnection( private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
lazyListState: LazyListState
): NestedScrollConnection = remember(lazyListState) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (available.y < 0) available else Offset.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
return 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.libbox.Libbox
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
import io.nekohasekai.sfa.compose.base.ScreenEvent 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.Connection
import io.nekohasekai.sfa.compose.model.ConnectionSort import io.nekohasekai.sfa.compose.model.ConnectionSort
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter 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.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient import io.nekohasekai.sfa.utils.CommandClient
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -22,6 +21,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicLong
data class ConnectionsUiState( data class ConnectionsUiState(
val connections: List<Connection> = emptyList(), val connections: List<Connection> = emptyList(),
@@ -38,7 +38,9 @@ sealed class ConnectionsEvent : ScreenEvent {
data object AllConnectionsClosed : ConnectionsEvent() data object AllConnectionsClosed : ConnectionsEvent()
} }
class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler { class ConnectionsViewModel :
BaseViewModel<ConnectionsUiState, ConnectionsEvent>(),
CommandClient.Handler {
private val commandClient = CommandClient( private val commandClient = CommandClient(
viewModelScope, viewModelScope,
CommandClient.ConnectionType.Connections, CommandClient.ConnectionType.Connections,
@@ -62,7 +64,7 @@ class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>
combine( combine(
AppLifecycleObserver.isForeground, AppLifecycleObserver.isForeground,
_isVisible, _isVisible,
_serviceStatus _serviceStatus,
) { foreground, visible, status -> ) { foreground, visible, status ->
Triple(foreground, visible, status) Triple(foreground, visible, status)
}.collect { (foreground, visible, status) -> }.collect { (foreground, visible, status) ->

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -37,10 +36,7 @@ import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.constant.Status import io.nekohasekai.sfa.constant.Status
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class CardRenderItem( data class CardRenderItem(val cards: List<CardGroup>, val isRow: Boolean)
val cards: List<CardGroup>,
val isRow: Boolean,
)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -307,11 +303,7 @@ fun processCardsForRendering(
* This function is only relevant when the service is running. * This function is only relevant when the service is running.
* Note: Profiles card is always available and should not use this function. * Note: Profiles card is always available and should not use this function.
*/ */
fun isCardAvailableWhenServiceRunning( fun isCardAvailableWhenServiceRunning(cardGroup: CardGroup, uiState: DashboardUiState): Boolean = when (cardGroup) {
cardGroup: CardGroup,
uiState: DashboardUiState,
): Boolean {
return when (cardGroup) {
CardGroup.ClashMode -> uiState.clashModeVisible CardGroup.ClashMode -> uiState.clashModeVisible
CardGroup.UploadTraffic -> uiState.trafficVisible CardGroup.UploadTraffic -> uiState.trafficVisible
CardGroup.DownloadTraffic -> uiState.trafficVisible CardGroup.DownloadTraffic -> uiState.trafficVisible
@@ -320,4 +312,3 @@ fun isCardAvailableWhenServiceRunning(
CardGroup.SystemProxy -> uiState.systemProxyVisible CardGroup.SystemProxy -> uiState.systemProxyVisible
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety 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.Icons
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.RestartAlt 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.BugReport
import androidx.compose.material.icons.outlined.Cable import androidx.compose.material.icons.outlined.Cable
import androidx.compose.material.icons.outlined.Download 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.Person
import androidx.compose.material.icons.outlined.Route import androidx.compose.material.icons.outlined.Route
import androidx.compose.material.icons.outlined.SettingsEthernet 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.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compat.animateItemCompat
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -96,12 +95,12 @@ fun DashboardSettingsBottomSheet(
var dragOffset by remember { mutableStateOf(0f) } var dragOffset by remember { mutableStateOf(0f) }
val density = LocalDensity.current val density = LocalDensity.current
fun onMove( fun onMove(fromIndex: Int, toIndex: Int) {
fromIndex: Int, if (fromIndex != toIndex &&
toIndex: Int, fromIndex >= 0 &&
) { toIndex >= 0 &&
if (fromIndex != toIndex && fromIndex >= 0 && toIndex >= 0 && fromIndex < reorderedList.size &&
fromIndex < reorderedList.size && toIndex < reorderedList.size toIndex < reorderedList.size
) { ) {
val newList = reorderedList.toMutableList() val newList = reorderedList.toMutableList()
val item = newList.removeAt(fromIndex) val item = newList.removeAt(fromIndex)

View File

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

View File

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

View File

@@ -24,12 +24,7 @@ import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.LineChart import io.nekohasekai.sfa.compose.LineChart
@Composable @Composable
fun DownloadTrafficCard( fun DownloadTrafficCard(downlink: String, downlinkTotal: String, downlinkHistory: List<Float>, modifier: Modifier = Modifier) {
downlink: String,
downlinkTotal: String,
downlinkHistory: List<Float>,
modifier: Modifier = Modifier,
) {
Card( Card(
modifier = modifier.fillMaxWidth(), 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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.ExpandMore
import androidx.compose.material.icons.filled.Speed 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.Card
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api 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.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R 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.Group
import io.nekohasekai.sfa.compose.model.GroupItem 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 import io.nekohasekai.sfa.utils.CommandClient
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -401,12 +401,7 @@ private fun ProxyGroupItem(
} }
@Composable @Composable
private fun ProxyItemsList( private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
items: List<GroupItem>,
selectedTag: String,
isSelectable: Boolean,
onItemSelected: (String) -> Unit,
) {
val itemsPerRow = 2 val itemsPerRow = 2
val chunkedItems = val chunkedItems =
remember(items) { remember(items) {
@@ -450,13 +445,7 @@ private fun ProxyItemsList(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ProxyChip( private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
item: GroupItem,
isSelected: Boolean,
isSelectable: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
// Use simpler, faster animations // Use simpler, faster animations
val animatedElevation by animateFloatAsState( val animatedElevation by animateFloatAsState(
targetValue = if (isSelected) 6.dp.value else 1.dp.value, targetValue = if (isSelected) 6.dp.value else 1.dp.value,
@@ -566,11 +555,7 @@ private fun ProxyChip(
} }
@Composable @Composable
private fun ProxyLatencyBadge( private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
delay: Int,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
// Direct color calculation without animation for better performance // Direct color calculation without animation for better performance
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val latencyColor = val latencyColor =
@@ -624,15 +609,9 @@ private fun ProxyLatencyBadge(
} }
@Composable @Composable
private fun rememberBounceBlockingNestedScrollConnection( private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
lazyListState: LazyListState
): NestedScrollConnection = remember(lazyListState) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
// Only block upward scroll (y < 0) at bottom to prevent sheet expansion // Only block upward scroll (y < 0) at bottom to prevent sheet expansion
// Allow downward scroll (y > 0) at top to let sheet collapse // Allow downward scroll (y > 0) at top to let sheet collapse
return if (available.y < 0) available else Offset.Zero 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.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -42,9 +43,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.toArgb
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -52,6 +51,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,9 +4,9 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.nekohasekai.sfa.R 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.AppSelectionCard
import io.nekohasekai.sfa.compose.shared.PackageCache import io.nekohasekai.sfa.compose.shared.PackageCache
import io.nekohasekai.sfa.compose.shared.SortMode import io.nekohasekai.sfa.compose.shared.SortMode
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages 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.utils.PrivilegeSettingsClient
import io.nekohasekai.sfa.vendor.PackageQueryManager import io.nekohasekai.sfa.vendor.PackageQueryManager
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
@@ -68,11 +68,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Locale 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" 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 hasManagement = permissions.any { it in managementPermissions }
val isSelf = packageCache.packageName == context.packageName val isSelf = packageCache.packageName == context.packageName
val hasVpnService = val hasVpnService =
!isSelf && ( !isSelf &&
(
permissions.any { it == VPN_SERVICE_PERMISSION } || permissions.any { it == VPN_SERVICE_PERMISSION } ||
packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true 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> { fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
return newUids.mapNotNull { uid ->
packages.find { it.uid == uid }?.packageName packages.find { it.uid == uid }?.packageName
}.toSet() }.toSet()
}
fun updateCurrentPackages(filterQuery: String) { fun updateCurrentPackages(filterQuery: String) {
currentPackages = currentPackages =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
package io.nekohasekai.sfa.compose.screen.settings 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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material.icons.outlined.DeleteForever
@@ -343,7 +343,7 @@ private fun openInFileManager(context: Context) {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.no_file_manager), context.getString(R.string.no_file_manager),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
} }

View File

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

View File

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

View File

@@ -63,10 +63,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ServiceSettingsScreen( fun ServiceSettingsScreen(navController: NavController, serviceConnection: ServiceConnection? = null) {
navController: NavController,
serviceConnection: ServiceConnection? = null,
) {
OverrideTopBar { OverrideTopBar {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.service)) }, 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.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew 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.Code
import androidx.compose.material.icons.outlined.Description import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.FilterAlt
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Settings 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.Tune
import androidx.compose.material.icons.outlined.AdminPanelSettings
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.update.UpdateState import io.nekohasekai.sfa.update.UpdateState
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
import io.nekohasekai.sfa.utils.HookStatusClient import io.nekohasekai.sfa.utils.HookStatusClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -7,20 +7,12 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
internal data class TopBarEntry( internal data class TopBarEntry(val key: Any, val content: @Composable () -> Unit)
val key: Any,
val content: @Composable () -> Unit,
)
class TopBarController internal constructor( class TopBarController internal constructor(private val state: MutableState<List<TopBarEntry>>) {
private val state: MutableState<List<TopBarEntry>>,
) {
val current: (@Composable () -> Unit)? get() = state.value.lastOrNull()?.content val current: (@Composable () -> Unit)? get() = state.value.lastOrNull()?.content
fun set( fun set(key: Any, content: @Composable () -> Unit) {
key: Any,
content: @Composable () -> Unit,
) {
state.value = state.value.filterNot { it.key == key } + TopBarEntry(key, content) 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.material.icons.twotone.*
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
data class IconCategory( data class IconCategory(val name: String, val icons: List<ProfileIcon>)
val name: String,
val icons: List<ProfileIcon>,
)
object MaterialIconsLibrary { object MaterialIconsLibrary {
val categories = val categories =
@@ -416,20 +413,16 @@ object MaterialIconsLibrary {
), ),
) )
fun getAllIcons(): List<ProfileIcon> { fun getAllIcons(): List<ProfileIcon> = categories.flatMap { it.icons }
return categories.flatMap { it.icons }
}
fun getIconById(id: String?): ImageVector? { fun getIconById(id: String?): ImageVector? {
if (id == null) return null if (id == null) return null
return getAllIcons().find { it.id == id }?.icon return getAllIcons().find { it.id == id }?.icon
} }
fun getCategoryForIcon(iconId: String): String? { fun getCategoryForIcon(iconId: String): String? = categories.find { category ->
return categories.find { category ->
category.icons.any { it.id == iconId } category.icons.any { it.id == iconId }
}?.name }?.name
}
fun searchIcons(query: String): List<ProfileIcon> { fun searchIcons(query: String): List<ProfileIcon> {
val lowercaseQuery = query.lowercase() 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 androidx.compose.ui.graphics.vector.ImageVector
import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
data class ProfileIcon( data class ProfileIcon(val id: String, val icon: ImageVector, val label: String)
val id: String,
val icon: ImageVector,
val label: String,
)
object ProfileIcons { object ProfileIcons {
// Use the complete Material Icons library with all available icons // Use the complete Material Icons library with all available icons
@@ -26,13 +22,9 @@ object ProfileIcons {
return Icons.AutoMirrored.Default.InsertDriveFile return Icons.AutoMirrored.Default.InsertDriveFile
} }
fun getCategoryForIcon(iconId: String): String? { fun getCategoryForIcon(iconId: String): String? = MaterialIconsLibrary.getCategoryForIcon(iconId)
return MaterialIconsLibrary.getCategoryForIcon(iconId)
}
fun searchIcons(query: String): List<ProfileIcon> { fun searchIcons(query: String): List<ProfileIcon> = MaterialIconsLibrary.searchIcons(query)
return MaterialIconsLibrary.searchIcons(query)
}
fun getCategories() = MaterialIconsLibrary.categories fun getCategories() = MaterialIconsLibrary.categories
} }

View File

@@ -87,12 +87,7 @@ object QRCodeGenerator {
} }
} }
fun generate( fun generate(content: String, size: Int = 512, foregroundColor: Int = Color.BLACK, backgroundColor: Int = Color.WHITE): Bitmap {
content: String,
size: Int = 512,
foregroundColor: Int = Color.BLACK,
backgroundColor: Int = Color.WHITE,
): Bitmap {
val writer = QRCodeWriter() val writer = QRCodeWriter()
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size) 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) * Formats a date as relative time for recent dates (within 7 days)
* or as full date/time for older dates. * or as full date/time for older dates.
*/ */
fun format( fun format(context: Context, date: Date?): String {
context: Context,
date: Date?,
): String {
if (date == null) return "" if (date == null) return ""
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@@ -59,10 +56,7 @@ object RelativeTimeFormatter {
* Formats a date as short relative time for compact displays. * Formats a date as short relative time for compact displays.
* Uses shorter format like "2h" instead of "2 hours ago". * Uses shorter format like "2h" instead of "2 hours ago".
*/ */
fun formatShort( fun formatShort(context: Context, date: Date?): String {
context: Context,
date: Date?,
): String {
if (date == null) return "" if (date == null) return ""
val now = System.currentTimeMillis() 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 * Represents a category of Material Icons following Google's official taxonomy
*/ */
data class IconCategory( data class IconCategory(val name: String, val icons: List<ProfileIcon>) {
val name: String,
val icons: List<ProfileIcon>,
) {
val size: Int get() = icons.size val size: Int get() = icons.size
} }

View File

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

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