Apply Spotless formatting to Java and Kotlin files
This commit is contained in:
@@ -25,7 +25,7 @@ object RootInstaller {
|
||||
handle.service.installPackage(
|
||||
pfd,
|
||||
apkFile.length(),
|
||||
android.os.Process.myUserHandle().hashCode()
|
||||
android.os.Process.myUserHandle().hashCode(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,7 @@ object RootInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
private class RootServiceHandle(
|
||||
val connection: ServiceConnection,
|
||||
val service: IRootService
|
||||
) : java.io.Closeable {
|
||||
private class RootServiceHandle(val connection: ServiceConnection, val service: IRootService) : java.io.Closeable {
|
||||
override fun close() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
RootService.unbind(connection)
|
||||
|
||||
@@ -10,9 +10,7 @@ import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||
|
||||
object SystemPackageInstaller {
|
||||
|
||||
fun canSystemSilentInstall(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
}
|
||||
fun canSystemSilentInstall(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
fun install(context: Context, apkFile: File) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
@@ -38,7 +36,7 @@ object SystemPackageInstaller {
|
||||
context,
|
||||
sessionId,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
|
||||
)
|
||||
|
||||
session.commit(pendingIntent.intentSender)
|
||||
|
||||
@@ -15,10 +15,7 @@ import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdateWorker(
|
||||
private val appContext: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
class UpdateWorker(private val appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
|
||||
|
||||
companion object {
|
||||
private const val WORK_NAME = "AutoUpdate"
|
||||
@@ -37,7 +34,8 @@ class UpdateWorker(
|
||||
.build()
|
||||
|
||||
val workRequest = PeriodicWorkRequestBuilder<UpdateWorker>(
|
||||
24, TimeUnit.HOURS
|
||||
24,
|
||||
TimeUnit.HOURS,
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
|
||||
@@ -46,7 +44,7 @@ class UpdateWorker(
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
workRequest
|
||||
workRequest,
|
||||
)
|
||||
Log.d(TAG, "Auto update scheduled")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ import android.os.Bundle;
|
||||
import android.os.IInterface;
|
||||
|
||||
public interface IIntentReceiver extends IInterface {
|
||||
void performReceive(Intent intent, int resultCode, String data, Bundle extras,
|
||||
boolean ordered, boolean sticky, int sendingUser);
|
||||
void performReceive(
|
||||
Intent intent,
|
||||
int resultCode,
|
||||
String data,
|
||||
Bundle extras,
|
||||
boolean ordered,
|
||||
boolean sticky,
|
||||
int sendingUser);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,14 @@ import android.os.IInterface;
|
||||
|
||||
public interface IIntentSender extends IInterface {
|
||||
|
||||
void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
|
||||
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options);
|
||||
void send(
|
||||
int code,
|
||||
Intent intent,
|
||||
String resolvedType,
|
||||
IBinder whitelistToken,
|
||||
IIntentReceiver finishedReceiver,
|
||||
String requiredPermission,
|
||||
Bundle options);
|
||||
|
||||
abstract class Stub extends Binder implements IIntentSender {
|
||||
public static IIntentSender asInterface(IBinder binder) {
|
||||
|
||||
@@ -7,7 +7,12 @@ import android.os.RemoteException;
|
||||
|
||||
public interface IPackageInstaller extends IInterface {
|
||||
|
||||
int createSession(PackageInstaller.SessionParams params, String installerPackageName, String installerAttributionTag, int userId) throws RemoteException;
|
||||
int createSession(
|
||||
PackageInstaller.SessionParams params,
|
||||
String installerPackageName,
|
||||
String installerAttributionTag,
|
||||
int userId)
|
||||
throws RemoteException;
|
||||
|
||||
IPackageInstallerSession openSession(int sessionId) throws RemoteException;
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -34,7 +32,8 @@ public final class RemotePreferences implements SharedPreferences {
|
||||
private final String mGroup;
|
||||
private final Lock mLock = new ReentrantLock();
|
||||
private final Map<String, Object> mMap = new ConcurrentHashMap<>();
|
||||
private final Map<OnSharedPreferenceChangeListener, Object> mListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final Map<OnSharedPreferenceChangeListener, Object> mListeners =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private volatile boolean isDeleted = false;
|
||||
|
||||
@@ -114,7 +113,8 @@ public final class RemotePreferences implements SharedPreferences {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
public void unregisterOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,9 @@ public final class RemotePreferences implements SharedPreferences {
|
||||
changes.addAll(mDelete);
|
||||
changes.addAll(mMap.keySet());
|
||||
for (String key : changes) {
|
||||
mListeners.keySet().forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
|
||||
mListeners
|
||||
.keySet()
|
||||
.forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -22,7 +21,12 @@ public final class XposedProvider extends ContentProvider {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
||||
public Cursor query(
|
||||
@NonNull Uri uri,
|
||||
@Nullable String[] projection,
|
||||
@Nullable String selection,
|
||||
@Nullable String[] selectionArgs,
|
||||
@Nullable String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -39,12 +43,17 @@ public final class XposedProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||
public int delete(
|
||||
@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||
public int update(
|
||||
@NonNull Uri uri,
|
||||
@Nullable ContentValues values,
|
||||
@Nullable String selection,
|
||||
@Nullable String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ package io.github.libxposed.service;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -16,7 +14,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
@SuppressWarnings("unused")
|
||||
public final class XposedService {
|
||||
|
||||
public final static class ServiceException extends RuntimeException {
|
||||
public static final class ServiceException extends RuntimeException {
|
||||
ServiceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
@@ -26,43 +24,38 @@ public final class XposedService {
|
||||
}
|
||||
}
|
||||
|
||||
private final static Map<OnScopeEventListener, IXposedScopeCallback> scopeCallbacks = new WeakHashMap<>();
|
||||
private static final Map<OnScopeEventListener, IXposedScopeCallback> scopeCallbacks =
|
||||
new WeakHashMap<>();
|
||||
|
||||
/**
|
||||
* Callback interface for module scope request.
|
||||
*/
|
||||
/** Callback interface for module scope request. */
|
||||
public interface OnScopeEventListener {
|
||||
/**
|
||||
* Callback when the request notification / window prompted.
|
||||
*
|
||||
* @param packageName Package name of requested app
|
||||
*/
|
||||
default void onScopeRequestPrompted(String packageName) {
|
||||
}
|
||||
default void onScopeRequestPrompted(String packageName) {}
|
||||
|
||||
/**
|
||||
* Callback when the request is approved.
|
||||
*
|
||||
* @param packageName Package name of requested app
|
||||
*/
|
||||
default void onScopeRequestApproved(String packageName) {
|
||||
}
|
||||
default void onScopeRequestApproved(String packageName) {}
|
||||
|
||||
/**
|
||||
* Callback when the request is denied.
|
||||
*
|
||||
* @param packageName Package name of requested app
|
||||
*/
|
||||
default void onScopeRequestDenied(String packageName) {
|
||||
}
|
||||
default void onScopeRequestDenied(String packageName) {}
|
||||
|
||||
/**
|
||||
* Callback when the request is timeout or revoked.
|
||||
*
|
||||
* @param packageName Package name of requested app
|
||||
*/
|
||||
default void onScopeRequestTimeout(String packageName) {
|
||||
}
|
||||
default void onScopeRequestTimeout(String packageName) {}
|
||||
|
||||
/**
|
||||
* Callback when the request is failed.
|
||||
@@ -70,11 +63,13 @@ public final class XposedService {
|
||||
* @param packageName Package name of requested app
|
||||
* @param message Error message
|
||||
*/
|
||||
default void onScopeRequestFailed(String packageName, String message) {
|
||||
}
|
||||
default void onScopeRequestFailed(String packageName, String message) {}
|
||||
|
||||
private IXposedScopeCallback asInterface() {
|
||||
return scopeCallbacks.computeIfAbsent(this, (listener) -> new IXposedScopeCallback.Stub() {
|
||||
return scopeCallbacks.computeIfAbsent(
|
||||
this,
|
||||
(listener) ->
|
||||
new IXposedScopeCallback.Stub() {
|
||||
@Override
|
||||
public void onScopeRequestPrompted(String packageName) {
|
||||
listener.onScopeRequestPrompted(packageName);
|
||||
@@ -104,28 +99,21 @@ public final class XposedService {
|
||||
}
|
||||
|
||||
public enum Privilege {
|
||||
/**
|
||||
* Unknown privilege value.
|
||||
*/
|
||||
/** Unknown privilege value. */
|
||||
FRAMEWORK_PRIVILEGE_UNKNOWN,
|
||||
|
||||
/**
|
||||
* The framework is running as root.
|
||||
*/
|
||||
/** The framework is running as root. */
|
||||
FRAMEWORK_PRIVILEGE_ROOT,
|
||||
|
||||
/**
|
||||
* The framework is running in a container with a fake system_server.
|
||||
*/
|
||||
/** The framework is running in a container with a fake system_server. */
|
||||
FRAMEWORK_PRIVILEGE_CONTAINER,
|
||||
|
||||
/**
|
||||
* The framework is running as a different app, which may have at most shell permission.
|
||||
*/
|
||||
/** The framework is running as a different app, which may have at most shell permission. */
|
||||
FRAMEWORK_PRIVILEGE_APP,
|
||||
|
||||
/**
|
||||
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will be null and remote file is unsupported.
|
||||
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will
|
||||
* be null and remote file is unsupported.
|
||||
*/
|
||||
FRAMEWORK_PRIVILEGE_EMBEDDED
|
||||
}
|
||||
@@ -211,7 +199,9 @@ public final class XposedService {
|
||||
public Privilege getFrameworkPrivilege() {
|
||||
try {
|
||||
int value = mService.getFrameworkPrivilege();
|
||||
return (value >= 0 && value <= 3) ? Privilege.values()[value + 1] : Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
|
||||
return (value >= 0 && value <= 3)
|
||||
? Privilege.values()[value + 1]
|
||||
: Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
|
||||
} catch (RemoteException e) {
|
||||
throw new ServiceException(e);
|
||||
}
|
||||
@@ -273,7 +263,9 @@ public final class XposedService {
|
||||
*/
|
||||
@NonNull
|
||||
public SharedPreferences getRemotePreferences(@NonNull String group) {
|
||||
return mRemotePrefs.computeIfAbsent(group, k -> {
|
||||
return mRemotePrefs.computeIfAbsent(
|
||||
group,
|
||||
k -> {
|
||||
try {
|
||||
RemotePreferences instance = RemotePreferences.newInstance(this, k);
|
||||
if (instance == null) {
|
||||
@@ -300,7 +292,9 @@ public final class XposedService {
|
||||
deletionLock.writeLock().lock();
|
||||
try {
|
||||
mService.deleteRemotePreferences(group);
|
||||
mRemotePrefs.computeIfPresent(group, (k, v) -> {
|
||||
mRemotePrefs.computeIfPresent(
|
||||
group,
|
||||
(k, v) -> {
|
||||
v.setDeleted();
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -2,9 +2,7 @@ package io.github.libxposed.service;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
@@ -12,21 +10,17 @@ import java.util.Set;
|
||||
@SuppressWarnings("unused")
|
||||
public final class XposedServiceHelper {
|
||||
|
||||
/**
|
||||
* Callback interface for Xposed service.
|
||||
*/
|
||||
/** Callback interface for Xposed service. */
|
||||
public interface OnServiceListener {
|
||||
/**
|
||||
* Callback when the service is connected.<br/>
|
||||
* Callback when the service is connected.<br>
|
||||
* This method could be called multiple times if multiple Xposed frameworks exist.
|
||||
*
|
||||
* @param service Service instance
|
||||
*/
|
||||
void onServiceBind(@NonNull XposedService service);
|
||||
|
||||
/**
|
||||
* Callback when the service is dead.
|
||||
*/
|
||||
/** Callback when the service is dead. */
|
||||
void onServiceDied(@NonNull XposedService service);
|
||||
}
|
||||
|
||||
@@ -52,7 +46,7 @@ public final class XposedServiceHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a ServiceListener to receive service binders from Xposed frameworks.<br/>
|
||||
* Register a ServiceListener to receive service binders from Xposed frameworks.<br>
|
||||
* This method should only be called once.
|
||||
*
|
||||
* @param listener Listener to register
|
||||
|
||||
@@ -18,8 +18,8 @@ import io.nekohasekai.sfa.bg.UpdateProfileWork
|
||||
import io.nekohasekai.sfa.constant.Bugs
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.provider.DocumentsContract
|
||||
import android.provider.DocumentsProvider
|
||||
import android.webkit.MimeTypeMap
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class WorkingDirectoryProvider : DocumentsProvider() {
|
||||
|
||||
@@ -47,7 +46,7 @@ class WorkingDirectoryProvider : DocumentsProvider() {
|
||||
add(
|
||||
DocumentsContract.Root.COLUMN_FLAGS,
|
||||
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
|
||||
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
||||
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD,
|
||||
)
|
||||
add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
|
||||
@@ -64,11 +63,7 @@ class WorkingDirectoryProvider : DocumentsProvider() {
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryChildDocuments(
|
||||
parentDocumentId: String,
|
||||
projection: Array<out String>?,
|
||||
sortOrder: String?
|
||||
): Cursor {
|
||||
override fun queryChildDocuments(parentDocumentId: String, projection: Array<out String>?, sortOrder: String?): Cursor {
|
||||
val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||
val parent = getFileForDocId(parentDocumentId)
|
||||
parent.listFiles()?.forEach { file ->
|
||||
@@ -77,21 +72,13 @@ class WorkingDirectoryProvider : DocumentsProvider() {
|
||||
return result
|
||||
}
|
||||
|
||||
override fun openDocument(
|
||||
documentId: String,
|
||||
mode: String,
|
||||
signal: CancellationSignal?
|
||||
): ParcelFileDescriptor {
|
||||
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor {
|
||||
val file = getFileForDocId(documentId)
|
||||
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||
return ParcelFileDescriptor.open(file, accessMode)
|
||||
}
|
||||
|
||||
override fun createDocument(
|
||||
parentDocumentId: String,
|
||||
mimeType: String,
|
||||
displayName: String
|
||||
): String {
|
||||
override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String {
|
||||
val parent = getFileForDocId(parentDocumentId)
|
||||
val file = File(parent, displayName)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -21,10 +21,7 @@ class AppChangeReceiver : BroadcastReceiver() {
|
||||
private const val TAG = "AppChangeReceiver"
|
||||
}
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.d(TAG, "onReceive: ${intent.action}")
|
||||
if (!Settings.perAppProxyEnabled) {
|
||||
Log.d(TAG, "per app proxy disabled")
|
||||
|
||||
@@ -12,10 +12,7 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ import io.nekohasekai.libbox.PlatformInterface
|
||||
import io.nekohasekai.libbox.SystemProxyStatus
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.MainActivity
|
||||
import io.nekohasekai.sfa.constant.Action
|
||||
import io.nekohasekai.sfa.constant.Alert
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.database.ProfileManager
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.hasPermission
|
||||
import io.nekohasekai.sfa.compose.MainActivity
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,10 +44,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class BoxService(
|
||||
private val service: Service,
|
||||
private val platformInterface: PlatformInterface,
|
||||
) : CommandServerHandler {
|
||||
class BoxService(private val service: Service, private val platformInterface: PlatformInterface) : CommandServerHandler {
|
||||
companion object {
|
||||
private const val PROFILE_UPDATE_INTERVAL = 15L * 60 * 1000 // 15 minutes in milliseconds
|
||||
private const val TAG = "BoxService"
|
||||
@@ -81,10 +78,7 @@ class BoxService(
|
||||
private var receiverRegistered = false
|
||||
private val receiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Action.SERVICE_CLOSE -> {
|
||||
stopService()
|
||||
@@ -316,10 +310,7 @@ class BoxService(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun stopAndAlert(
|
||||
type: Alert,
|
||||
message: String? = null,
|
||||
) {
|
||||
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
|
||||
Settings.startedByUser = false
|
||||
withContext(Dispatchers.Main) {
|
||||
if (receiverRegistered) {
|
||||
@@ -368,9 +359,7 @@ class BoxService(
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
internal fun onBind(): IBinder {
|
||||
return binder
|
||||
}
|
||||
internal fun onBind(): IBinder = binder
|
||||
|
||||
internal fun onDestroy() {
|
||||
binder.close()
|
||||
|
||||
@@ -13,7 +13,6 @@ import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@@ -134,11 +133,7 @@ object DebugInfoExporter {
|
||||
return count
|
||||
}
|
||||
|
||||
private fun addLogEntries(
|
||||
zip: ZipOutputStream,
|
||||
warnings: MutableList<String>,
|
||||
context: Context,
|
||||
): Int {
|
||||
private fun addLogEntries(zip: ZipOutputStream, warnings: MutableList<String>, context: Context): Int {
|
||||
var count = 0
|
||||
if (streamCommandToZip(zip, "logs/logcat.txt", warnings, listOf("logcat", "-d", "-b", "all")) != null) count++
|
||||
if (streamCommandToZip(zip, "logs/dmesg.txt", warnings, listOf("dmesg")) != null) count++
|
||||
@@ -185,11 +180,7 @@ object DebugInfoExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSystemEntries(
|
||||
zip: ZipOutputStream,
|
||||
warnings: MutableList<String>,
|
||||
packageName: String,
|
||||
): Int {
|
||||
private fun addSystemEntries(zip: ZipOutputStream, warnings: MutableList<String>, packageName: String): Int {
|
||||
var count = 0
|
||||
if (streamCommandToZip(zip, "system/getprop.txt", warnings, listOf("getprop")) != null) count++
|
||||
if (streamCommandToZip(zip, "system/uname.txt", warnings, listOf("uname", "-a")) != null) count++
|
||||
@@ -214,23 +205,24 @@ object DebugInfoExporter {
|
||||
"system/packages_pm.txt",
|
||||
warnings,
|
||||
listOf("pm", "list", "packages", "-f"),
|
||||
) != null) count++
|
||||
) != null
|
||||
) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if (streamCommandToZip(
|
||||
zip,
|
||||
"system/dumpsys_package_${packageName}.txt",
|
||||
"system/dumpsys_package_$packageName.txt",
|
||||
warnings,
|
||||
listOf("dumpsys", "package", packageName),
|
||||
) != null) count++
|
||||
) != null
|
||||
) {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun addFileEntry(
|
||||
zip: ZipOutputStream,
|
||||
file: File,
|
||||
entryName: String,
|
||||
warnings: MutableList<String>,
|
||||
): Boolean {
|
||||
private fun addFileEntry(zip: ZipOutputStream, file: File, entryName: String, warnings: MutableList<String>): Boolean {
|
||||
if (!file.isFile) {
|
||||
warnings.add("missing file: ${file.path}")
|
||||
return false
|
||||
@@ -262,18 +254,14 @@ object DebugInfoExporter {
|
||||
zip.closeEntry()
|
||||
}
|
||||
|
||||
private data class CommandResult(
|
||||
val exitCode: Int,
|
||||
val bytes: Long,
|
||||
)
|
||||
private data class CommandResult(val exitCode: Int, val bytes: Long)
|
||||
|
||||
private fun streamCommandToZip(
|
||||
zip: ZipOutputStream,
|
||||
entryName: String,
|
||||
warnings: MutableList<String>,
|
||||
command: List<String>,
|
||||
): CommandResult? {
|
||||
return try {
|
||||
): CommandResult? = try {
|
||||
val process = ProcessBuilder(command).redirectErrorStream(true).start()
|
||||
val entry = ZipEntry(entryName)
|
||||
zip.putNextEntry(entry)
|
||||
@@ -298,15 +286,8 @@ object DebugInfoExporter {
|
||||
runCatching { zip.closeEntry() }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildError(
|
||||
stage: String,
|
||||
detail: String,
|
||||
throwable: Throwable?,
|
||||
warnings: List<String>,
|
||||
outputPath: String?,
|
||||
): String {
|
||||
private fun buildError(stage: String, detail: String, throwable: Throwable?, warnings: List<String>, outputPath: String?): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("stage=").append(stage).append('\n')
|
||||
if (!outputPath.isNullOrBlank()) {
|
||||
|
||||
@@ -60,7 +60,8 @@ object DefaultNetworkListener {
|
||||
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
||||
var network: Network? = null
|
||||
val pendingRequests = arrayListOf<NetworkMessage.Get>()
|
||||
for (message in channel) when (message) {
|
||||
for (message in channel) {
|
||||
when (message) {
|
||||
is NetworkMessage.Start -> {
|
||||
if (listeners.isEmpty()) register()
|
||||
listeners[message.key] = message.listener
|
||||
@@ -79,8 +80,10 @@ object DefaultNetworkListener {
|
||||
}
|
||||
|
||||
is NetworkMessage.Stop ->
|
||||
if (listeners.isNotEmpty() && // was not empty
|
||||
listeners.remove(message.key) != null && listeners.isEmpty()
|
||||
if (listeners.isNotEmpty() &&
|
||||
// was not empty
|
||||
listeners.remove(message.key) != null &&
|
||||
listeners.isEmpty()
|
||||
) {
|
||||
network = null
|
||||
unregister()
|
||||
@@ -109,31 +112,31 @@ object DefaultNetworkListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun start(
|
||||
key: Any,
|
||||
listener: (Network?) -> Unit,
|
||||
) = networkActor.send(
|
||||
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(
|
||||
NetworkMessage.Start(
|
||||
key,
|
||||
listener,
|
||||
),
|
||||
)
|
||||
|
||||
suspend fun get(): Network = if (fallback) @TargetApi(23) {
|
||||
suspend fun get(): Network = if (fallback) {
|
||||
@TargetApi(23)
|
||||
Application.connectivity.activeNetwork
|
||||
?: error("missing default network") // failed to listen, return current if available
|
||||
} else NetworkMessage.Get().run {
|
||||
} else {
|
||||
NetworkMessage.Get().run {
|
||||
networkActor.send(this)
|
||||
response.await()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
|
||||
|
||||
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
||||
private object Callback : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) =
|
||||
runBlocking {
|
||||
override fun onAvailable(network: Network) = runBlocking {
|
||||
networkActor.send(
|
||||
NetworkMessage.Put(
|
||||
network,
|
||||
@@ -141,16 +144,12 @@ object DefaultNetworkListener {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
// it's a good idea to refresh capabilities
|
||||
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) =
|
||||
runBlocking {
|
||||
override fun onLost(network: Network) = runBlocking {
|
||||
networkActor.send(
|
||||
NetworkMessage.Lost(
|
||||
network,
|
||||
|
||||
@@ -40,9 +40,7 @@ object DefaultNetworkMonitor {
|
||||
checkDefaultInterfaceUpdate(defaultNetwork)
|
||||
}
|
||||
|
||||
private fun checkDefaultInterfaceUpdate(
|
||||
newNetwork: Network?
|
||||
) {
|
||||
private fun checkDefaultInterfaceUpdate(newNetwork: Network?) {
|
||||
val listener = listener ?: return
|
||||
if (newNetwork != null) {
|
||||
val interfaceName =
|
||||
@@ -61,5 +59,4 @@ object DefaultNetworkMonitor {
|
||||
listener.updateDefaultInterface("", -1, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,15 +19,10 @@ import kotlin.coroutines.suspendCoroutine
|
||||
object LocalResolver : LocalDNSTransport {
|
||||
private const val RCODE_NXDOMAIN = 3
|
||||
|
||||
override fun raw(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
override fun raw(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun exchange(
|
||||
ctx: ExchangeContext,
|
||||
message: ByteArray,
|
||||
) {
|
||||
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
|
||||
return runBlocking {
|
||||
val defaultNetwork = DefaultNetworkMonitor.require()
|
||||
suspendCoroutine { continuation ->
|
||||
@@ -35,10 +30,7 @@ object LocalResolver : LocalDNSTransport {
|
||||
ctx.onCancel(signal::cancel)
|
||||
val callback =
|
||||
object : DnsResolver.Callback<ByteArray> {
|
||||
override fun onAnswer(
|
||||
answer: ByteArray,
|
||||
rcode: Int,
|
||||
) {
|
||||
override fun onAnswer(answer: ByteArray, rcode: Int) {
|
||||
if (rcode == 0) {
|
||||
ctx.rawSuccess(answer)
|
||||
} else {
|
||||
@@ -70,11 +62,7 @@ object LocalResolver : LocalDNSTransport {
|
||||
}
|
||||
}
|
||||
|
||||
override fun lookup(
|
||||
ctx: ExchangeContext,
|
||||
network: String,
|
||||
domain: String,
|
||||
) {
|
||||
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
|
||||
return runBlocking {
|
||||
val defaultNetwork = DefaultNetworkMonitor.require()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -84,10 +72,7 @@ object LocalResolver : LocalDNSTransport {
|
||||
val callback =
|
||||
object : DnsResolver.Callback<Collection<InetAddress>> {
|
||||
@Suppress("ThrowableNotThrown")
|
||||
override fun onAnswer(
|
||||
answer: Collection<InetAddress>,
|
||||
rcode: Int,
|
||||
) {
|
||||
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
|
||||
if (rcode == 0) {
|
||||
ctx.success(
|
||||
(answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.nekohasekai.sfa.bg;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -14,14 +13,16 @@ public class LogEntry implements Parcelable {
|
||||
|
||||
public final int level;
|
||||
public final long timestamp;
|
||||
@NonNull
|
||||
public final String source;
|
||||
@NonNull
|
||||
public final String message;
|
||||
@Nullable
|
||||
public final String stackTrace;
|
||||
@NonNull public final String source;
|
||||
@NonNull public final String message;
|
||||
@Nullable public final String stackTrace;
|
||||
|
||||
public LogEntry(int level, long timestamp, @NonNull String source, @NonNull String message, @Nullable String stackTrace) {
|
||||
public LogEntry(
|
||||
int level,
|
||||
long timestamp,
|
||||
@NonNull String source,
|
||||
@NonNull String message,
|
||||
@Nullable String stackTrace) {
|
||||
this.level = level;
|
||||
this.timestamp = timestamp;
|
||||
this.source = source;
|
||||
@@ -51,7 +52,8 @@ public class LogEntry implements Parcelable {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<LogEntry> CREATOR = new Creator<>() {
|
||||
public static final Creator<LogEntry> CREATOR =
|
||||
new Creator<>() {
|
||||
@Override
|
||||
public LogEntry createFromParcel(Parcel in) {
|
||||
return new LogEntry(in);
|
||||
|
||||
@@ -2,12 +2,10 @@ package io.nekohasekai.sfa.bg;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class PackageEntry implements Parcelable {
|
||||
@NonNull
|
||||
public final String packageName;
|
||||
@NonNull public final String packageName;
|
||||
|
||||
public PackageEntry(@NonNull String packageName) {
|
||||
this.packageName = packageName;
|
||||
@@ -27,7 +25,8 @@ public class PackageEntry implements Parcelable {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<PackageEntry> CREATOR = new Creator<>() {
|
||||
public static final Creator<PackageEntry> CREATOR =
|
||||
new Creator<>() {
|
||||
@Override
|
||||
public PackageEntry createFromParcel(Parcel in) {
|
||||
return new PackageEntry(in);
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -106,7 +105,8 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable {
|
||||
if (i < n) {
|
||||
dest.writeInt(0);
|
||||
final int start = i;
|
||||
Binder retriever = new Binder() {
|
||||
Binder retriever =
|
||||
new Binder() {
|
||||
@Override
|
||||
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
|
||||
throws RemoteException {
|
||||
|
||||
@@ -27,9 +27,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
|
||||
|
||||
interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
|
||||
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
}
|
||||
@@ -38,9 +36,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
error("invalid argument")
|
||||
}
|
||||
|
||||
override fun useProcFS(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
}
|
||||
override fun useProcFS(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun findConnectionOwner(
|
||||
@@ -136,13 +132,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
return InterfaceArray(interfaces.iterator())
|
||||
}
|
||||
|
||||
override fun underNetworkExtension(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun underNetworkExtension(): Boolean = false
|
||||
|
||||
override fun includeAllNetworks(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun includeAllNetworks(): Boolean = false
|
||||
|
||||
override fun clearDNSCache() {
|
||||
}
|
||||
@@ -161,9 +153,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
return WIFIState(ssid, wifiInfo.bssid)
|
||||
}
|
||||
|
||||
override fun localDNSTransport(): LocalDNSTransport? {
|
||||
return LocalResolver
|
||||
}
|
||||
override fun localDNSTransport(): LocalDNSTransport? = LocalResolver
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
override fun systemCertificates(): StringIterator {
|
||||
@@ -182,15 +172,10 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
return StringArray(certificates.iterator())
|
||||
}
|
||||
|
||||
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) :
|
||||
NetworkInterfaceIterator {
|
||||
override fun hasNext(): Boolean {
|
||||
return iterator.hasNext()
|
||||
}
|
||||
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) : NetworkInterfaceIterator {
|
||||
override fun hasNext(): Boolean = iterator.hasNext()
|
||||
|
||||
override fun next(): LibboxNetworkInterface {
|
||||
return iterator.next()
|
||||
}
|
||||
override fun next(): LibboxNetworkInterface = iterator.next()
|
||||
}
|
||||
|
||||
class StringArray(private val iterator: Iterator<String>) : StringIterator {
|
||||
@@ -199,22 +184,16 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return iterator.hasNext()
|
||||
override fun hasNext(): Boolean = iterator.hasNext()
|
||||
|
||||
override fun next(): String = iterator.next()
|
||||
}
|
||||
|
||||
override fun next(): String {
|
||||
return iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
private fun InterfaceAddress.toPrefix(): String {
|
||||
return if (address is Inet6Address) {
|
||||
private fun InterfaceAddress.toPrefix(): String = if (address is Inet6Address) {
|
||||
"${Inet6Address.getByAddress(address.address).hostAddress}/$networkPrefixLength"
|
||||
} else {
|
||||
"${address.hostAddress}/$networkPrefixLength"
|
||||
}
|
||||
}
|
||||
|
||||
private val NetworkInterface.flags: Int
|
||||
@SuppressLint("SoonBlockedPrivateApi")
|
||||
|
||||
@@ -4,14 +4,12 @@ import android.app.Service
|
||||
import android.content.Intent
|
||||
import io.nekohasekai.libbox.Notification
|
||||
|
||||
class ProxyService : Service(), PlatformInterfaceWrapper {
|
||||
class ProxyService :
|
||||
Service(),
|
||||
PlatformInterfaceWrapper {
|
||||
private val service = BoxService(this, this)
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
) = service.onStartCommand()
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand()
|
||||
|
||||
override fun onBind(intent: Intent) = service.onBind()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ object RootClient {
|
||||
Shell.setDefaultBuilder(
|
||||
Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setTimeout(10)
|
||||
.setTimeout(10),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ object RootClient {
|
||||
val svc = bindService()
|
||||
return try {
|
||||
val slice = svc.getInstalledPackages(flags, userId)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = slice.list as List<PackageInfo>
|
||||
list
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package io.nekohasekai.sfa.bg
|
||||
|
||||
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
|
||||
import java.io.IOException
|
||||
|
||||
class RootServer : RootService() {
|
||||
@@ -17,10 +16,7 @@ class RootServer : RootService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun getInstalledPackages(
|
||||
flags: Int,
|
||||
userId: Int
|
||||
): ParceledListSlice<PackageInfo> {
|
||||
override fun getInstalledPackages(flags: Int, userId: Int): ParceledListSlice<PackageInfo> {
|
||||
val allPackages = PrivilegedServiceUtils.getInstalledPackages(flags, userId)
|
||||
return ParceledListSlice(allPackages)
|
||||
}
|
||||
@@ -30,16 +26,12 @@ class RootServer : RootService() {
|
||||
PrivilegedServiceUtils.installPackage(apk, size, userId)
|
||||
}
|
||||
|
||||
override fun exportDebugInfo(outputPath: String?): String {
|
||||
return DebugInfoExporter.export(
|
||||
override fun exportDebugInfo(outputPath: String?): String = DebugInfoExporter.export(
|
||||
this@RootServer,
|
||||
outputPath!!,
|
||||
BuildConfig.APPLICATION_ID
|
||||
BuildConfig.APPLICATION_ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
override fun onBind(intent: Intent): IBinder = binder
|
||||
}
|
||||
|
||||
@@ -43,9 +43,7 @@ class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatus(): Int {
|
||||
return (status.value ?: Status.Stopped).ordinal
|
||||
}
|
||||
override fun getStatus(): Int = (status.value ?: Status.Stopped).ordinal
|
||||
|
||||
override fun registerCallback(callback: IServiceCallback) {
|
||||
callbacks.register(callback)
|
||||
|
||||
@@ -18,11 +18,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServiceConnection(
|
||||
private val context: Context,
|
||||
callback: Callback,
|
||||
private val register: Boolean = true,
|
||||
) : ServiceConnection {
|
||||
class ServiceConnection(private val context: Context, callback: Callback, private val register: Boolean = true) : ServiceConnection {
|
||||
companion object {
|
||||
private const val TAG = "ServiceConnection"
|
||||
}
|
||||
@@ -66,10 +62,7 @@ class ServiceConnection(
|
||||
Log.d(TAG, "request reconnect")
|
||||
}
|
||||
|
||||
override fun onServiceConnected(
|
||||
name: ComponentName,
|
||||
binder: IBinder,
|
||||
) {
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
val service = IService.Stub.asInterface(binder)
|
||||
this.service = service
|
||||
try {
|
||||
@@ -98,10 +91,7 @@ class ServiceConnection(
|
||||
interface Callback {
|
||||
fun onServiceStatusChanged(status: Status)
|
||||
|
||||
fun onServiceAlert(
|
||||
type: Alert,
|
||||
message: String?,
|
||||
) {
|
||||
fun onServiceAlert(type: Alert, message: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +100,7 @@ class ServiceConnection(
|
||||
callback.onServiceStatusChanged(Status.values()[status])
|
||||
}
|
||||
|
||||
override fun onServiceAlert(
|
||||
type: Int,
|
||||
message: String?,
|
||||
) {
|
||||
override fun onServiceAlert(type: Int, message: String?) {
|
||||
callback.onServiceAlert(Alert.values()[type], message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import androidx.lifecycle.MutableLiveData
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.StatusMessage
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.compose.MainActivity
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.MainActivity
|
||||
import io.nekohasekai.sfa.constant.Action
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
@@ -27,10 +27,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServiceNotification(
|
||||
private val status: MutableLiveData<Status>,
|
||||
private val service: Service,
|
||||
) : BroadcastReceiver(), CommandClient.Handler {
|
||||
class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) :
|
||||
BroadcastReceiver(),
|
||||
CommandClient.Handler {
|
||||
companion object {
|
||||
private const val notificationId = 1
|
||||
private const val notificationChannel = "service"
|
||||
@@ -82,10 +81,7 @@ class ServiceNotification(
|
||||
}
|
||||
}
|
||||
|
||||
fun show(
|
||||
lastProfileName: String,
|
||||
@StringRes contentTextId: Int,
|
||||
) {
|
||||
fun show(lastProfileName: String, @StringRes contentTextId: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Application.notification.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
@@ -132,10 +128,7 @@ class ServiceNotification(
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
commandClient.connect()
|
||||
|
||||
@@ -8,7 +8,9 @@ import androidx.annotation.RequiresApi
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
|
||||
@RequiresApi(24)
|
||||
class TileService : TileService(), ServiceConnection.Callback {
|
||||
class TileService :
|
||||
TileService(),
|
||||
ServiceConnection.Callback {
|
||||
private val connection = ServiceConnection(this, this)
|
||||
|
||||
override fun onServiceStatusChanged(status: Status) {
|
||||
|
||||
@@ -59,10 +59,7 @@ class UpdateProfileWork {
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateTask(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
class UpdateTask(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
var selectedProfileUpdated = false
|
||||
val remoteProfiles =
|
||||
|
||||
@@ -15,18 +15,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class VPNService : VpnService(), PlatformInterfaceWrapper {
|
||||
class VPNService :
|
||||
VpnService(),
|
||||
PlatformInterfaceWrapper {
|
||||
companion object {
|
||||
private const val TAG = "VPNService"
|
||||
}
|
||||
|
||||
private val service = BoxService(this, this)
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
) = service.onStartCommand()
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
val binder = super.onBind(intent)
|
||||
|
||||
@@ -16,34 +16,29 @@ import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material.icons.filled.UnfoldLess
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Job
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -53,9 +48,9 @@ import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
@@ -69,9 +64,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@@ -81,6 +79,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
@@ -91,25 +90,24 @@ import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
||||
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
|
||||
import io.nekohasekai.sfa.compose.base.UiEvent
|
||||
import io.nekohasekai.sfa.compose.component.ServiceStatusBar
|
||||
import io.nekohasekai.sfa.compose.component.UptimeText
|
||||
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
||||
import io.nekohasekai.sfa.compose.component.UptimeText
|
||||
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
|
||||
import io.nekohasekai.sfa.compose.navigation.ProfileRoutes
|
||||
import io.nekohasekai.sfa.compose.navigation.SFANavHost
|
||||
import io.nekohasekai.sfa.compose.navigation.Screen
|
||||
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
||||
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
|
||||
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
|
||||
import io.nekohasekai.sfa.compose.topbar.TopBarController
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.CardGroup
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||
import io.nekohasekai.sfa.compose.theme.SFATheme
|
||||
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
|
||||
import io.nekohasekai.sfa.compose.topbar.TopBarController
|
||||
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
|
||||
import io.nekohasekai.sfa.constant.Alert
|
||||
import io.nekohasekai.sfa.constant.ServiceMode
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
@@ -119,10 +117,13 @@ import io.nekohasekai.sfa.ktx.launchCustomTab
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
class MainActivity :
|
||||
ComponentActivity(),
|
||||
ServiceConnection.Callback {
|
||||
private val connection = ServiceConnection(this, this)
|
||||
private lateinit var dashboardViewModel: DashboardViewModel
|
||||
private var currentServiceStatus by mutableStateOf(Status.Stopped)
|
||||
@@ -253,8 +254,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepare() =
|
||||
withContext(Dispatchers.Main) {
|
||||
private suspend fun prepare() = withContext(Dispatchers.Main) {
|
||||
try {
|
||||
val intent = VpnService.prepare(this@MainActivity)
|
||||
if (intent != null) {
|
||||
@@ -388,8 +388,11 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
text = {
|
||||
MarkdownText(
|
||||
markdown = stringResource(
|
||||
if (BuildConfig.FLAVOR == "play") R.string.check_update_prompt_play
|
||||
else R.string.check_update_prompt_github
|
||||
if (BuildConfig.FLAVOR == "play") {
|
||||
R.string.check_update_prompt_play
|
||||
} else {
|
||||
R.string.check_update_prompt_github
|
||||
},
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
@@ -534,7 +537,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -909,7 +912,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
val groupsUiState by groupsViewModel.uiState.collectAsState()
|
||||
val allCollapsed = groupsUiState.expandedGroups.isEmpty()
|
||||
@@ -943,12 +946,16 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
if (groupsUiState.groups.isNotEmpty()) {
|
||||
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
||||
Icon(
|
||||
imageVector = if (allCollapsed) Icons.Default.UnfoldMore
|
||||
else Icons.Default.UnfoldLess,
|
||||
contentDescription = if (allCollapsed)
|
||||
imageVector = if (allCollapsed) {
|
||||
Icons.Default.UnfoldMore
|
||||
} else {
|
||||
Icons.Default.UnfoldLess
|
||||
},
|
||||
contentDescription = if (allCollapsed) {
|
||||
stringResource(R.string.expand_all)
|
||||
else
|
||||
stringResource(R.string.collapse_all),
|
||||
} else {
|
||||
stringResource(R.string.collapse_all)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1032,10 +1039,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
connection.reconnect()
|
||||
}
|
||||
|
||||
override fun onServiceAlert(
|
||||
type: Alert,
|
||||
message: String?,
|
||||
) {
|
||||
override fun onServiceAlert(type: Alert, message: String?) {
|
||||
when (type) {
|
||||
Alert.RequestLocationPermission -> {
|
||||
return requestLocationPermission()
|
||||
@@ -1071,11 +1075,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceAlertDialog(
|
||||
alertType: Alert,
|
||||
message: String?,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
private fun ServiceAlertDialog(alertType: Alert, message: String?, onDismiss: () -> Unit) {
|
||||
val title =
|
||||
when (alertType) {
|
||||
Alert.RequestNotificationPermission -> stringResource(R.string.notification_permission_title)
|
||||
@@ -1106,10 +1106,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocationPermissionDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
private fun LocationPermissionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.location_permission_title)) },
|
||||
@@ -1128,10 +1125,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackgroundLocationPermissionDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
private fun BackgroundLocationPermissionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.location_permission_title)) },
|
||||
|
||||
@@ -56,10 +56,7 @@ abstract class BaseViewModel<State, Event> : ViewModel() {
|
||||
sendGlobalEvent(UiEvent.ErrorMessage(message))
|
||||
}
|
||||
|
||||
protected fun launch(
|
||||
onError: ((Throwable) -> Unit)? = null,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) {
|
||||
protected fun launch(onError: ((Throwable) -> Unit)? = null, block: suspend CoroutineScope.() -> Unit) {
|
||||
val errorHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
onError?.invoke(throwable) ?: sendError(throwable)
|
||||
|
||||
@@ -29,7 +29,5 @@ object GlobalEventBus {
|
||||
* Try to emit an event without suspending.
|
||||
* Returns true if the event was emitted successfully.
|
||||
*/
|
||||
fun tryEmit(event: UiEvent): Boolean {
|
||||
return _events.tryEmit(event)
|
||||
}
|
||||
fun tryEmit(event: UiEvent): Boolean = _events.tryEmit(event)
|
||||
}
|
||||
|
||||
@@ -19,11 +19,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
|
||||
@Composable
|
||||
fun SelectableMessageDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
fun SelectableMessageDialog(title: String, message: String, onDismiss: () -> Unit) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -8,8 +8,4 @@ sealed class UiState<out T> {
|
||||
data class Error(val exception: Throwable, val message: String? = null) : UiState<Nothing>()
|
||||
}
|
||||
|
||||
data class BaseUiState<T>(
|
||||
val isLoading: Boolean = false,
|
||||
val data: T? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
data class BaseUiState<T>(val isLoading: Boolean = false, val data: T? = null, val error: String? = null)
|
||||
|
||||
@@ -164,10 +164,7 @@ fun ServiceStatusBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusItem(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun StatusItem(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -178,10 +175,7 @@ private fun StatusItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UptimeText(
|
||||
startTime: Long,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun UptimeText(startTime: Long, modifier: Modifier = Modifier) {
|
||||
var currentTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
|
||||
LaunchedEffect(startTime) {
|
||||
|
||||
@@ -27,11 +27,7 @@ import org.kodein.emoji.EmojiTemplateCatalog
|
||||
import org.kodein.emoji.all
|
||||
|
||||
@Composable
|
||||
fun UpdateAvailableDialog(
|
||||
updateInfo: UpdateInfo,
|
||||
onDismiss: () -> Unit,
|
||||
onUpdate: () -> Unit,
|
||||
) {
|
||||
fun UpdateAvailableDialog(updateInfo: UpdateInfo, onDismiss: () -> Unit, onUpdate: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val emojiCatalog = remember { EmojiTemplateCatalog(Emoji.all()) }
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
@@ -24,10 +23,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
@Composable
|
||||
fun QRCodeDialog(
|
||||
bitmap: Bitmap,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
fun QRCodeDialog(bitmap: Bitmap, onDismiss: () -> Unit) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
|
||||
@@ -33,6 +33,7 @@ class QRSBitmapGenerator(
|
||||
private val actualBufferSize = bufferSize.coerceAtMost(frames.size)
|
||||
private val bitmapBuffer = arrayOfNulls<Bitmap>(actualBufferSize)
|
||||
private var generationJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
private var currentFrameIndex = 0
|
||||
private var generatedUpTo = -1
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.nekohasekai.sfa.compose.component.qr
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -58,11 +58,7 @@ import io.nekohasekai.sfa.qrs.QRSEncoder
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun QRSDialog(
|
||||
profileData: ByteArray,
|
||||
profileName: String,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
fun QRSDialog(profileData: ByteArray, profileName: String, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val isTablet = configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
|
||||
@@ -40,8 +40,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -54,18 +54,14 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
||||
import kotlin.math.max
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun QRScanSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onScanResult: (QRScanResult) -> Unit,
|
||||
viewModel: QRScanViewModel = viewModel(),
|
||||
) {
|
||||
fun QRScanSheet(onDismiss: () -> Unit, onScanResult: (QRScanResult) -> Unit, viewModel: QRScanViewModel = viewModel()) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
@@ -74,12 +70,12 @@ fun QRScanSheet(
|
||||
var hasPermission by remember {
|
||||
mutableStateOf(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
PackageManager.PERMISSION_GRANTED,
|
||||
)
|
||||
}
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
contract = ActivityResultContracts.RequestPermission(),
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
hasPermission = true
|
||||
@@ -113,7 +109,7 @@ fun QRScanSheet(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.9f)
|
||||
.fillMaxHeight(0.9f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -138,44 +134,44 @@ fun QRScanSheet(
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
(if (uiState.useFrontCamera) "✓ " else " ") +
|
||||
stringResource(R.string.profile_add_scan_use_front_camera)
|
||||
stringResource(R.string.profile_add_scan_use_front_camera),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.toggleFrontCamera(lifecycleOwner)
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
(if (uiState.torchEnabled) "✓ " else " ") +
|
||||
stringResource(R.string.profile_add_scan_enable_torch)
|
||||
stringResource(R.string.profile_add_scan_enable_torch),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.toggleTorch()
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
)
|
||||
if (uiState.vendorAnalyzerAvailable) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
(if (uiState.useVendorAnalyzer) "✓ " else " ") +
|
||||
stringResource(R.string.profile_add_scan_use_vendor_analyzer)
|
||||
stringResource(R.string.profile_add_scan_use_vendor_analyzer),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.toggleVendorAnalyzer()
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -185,7 +181,7 @@ fun QRScanSheet(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.weight(1f),
|
||||
) {
|
||||
if (hasPermission) {
|
||||
CameraPreview(
|
||||
@@ -201,7 +197,7 @@ fun QRScanSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
contentAlignment = Alignment.Center
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
@@ -214,7 +210,7 @@ fun QRScanSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.5f)),
|
||||
contentAlignment = Alignment.Center
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
@@ -228,18 +224,18 @@ fun QRScanSheet(
|
||||
Text(
|
||||
text = "${minOf(99, (progress * 100).toInt())}%",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
color = Color.White
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "QRS",
|
||||
style = MaterialTheme.typography.headlineLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
color = Color.White,
|
||||
modifier = Modifier.offset(y = (-88).dp)
|
||||
modifier = Modifier.offset(y = (-88).dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +253,7 @@ fun QRScanSheet(
|
||||
TextButton(onClick = { viewModel.dismissError() }) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -294,7 +290,7 @@ private fun CameraPreview(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
@@ -309,11 +305,7 @@ private fun CameraPreview(
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapCropAreaToPreview(
|
||||
area: QRCodeCropArea,
|
||||
viewWidth: Float,
|
||||
viewHeight: Float,
|
||||
): Rect? {
|
||||
private fun mapCropAreaToPreview(area: QRCodeCropArea, viewWidth: Float, viewHeight: Float): Rect? {
|
||||
if (viewWidth <= 0f || viewHeight <= 0f) return null
|
||||
|
||||
val rotation = ((area.rotationDegrees % 360) + 360) % 360
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
package io.nekohasekai.sfa.compose.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.nekohasekai.sfa.ktx.toList
|
||||
import io.nekohasekai.libbox.Connection as LibboxConnection
|
||||
import io.nekohasekai.libbox.ProcessInfo as LibboxProcessInfo
|
||||
import io.nekohasekai.sfa.ktx.toList
|
||||
|
||||
@Immutable
|
||||
data class ProcessInfo(
|
||||
val processId: Long,
|
||||
val userId: Int,
|
||||
val userName: String,
|
||||
val processPath: String,
|
||||
val packageName: String,
|
||||
) {
|
||||
data class ProcessInfo(val processId: Long, val userId: Int, val userName: String, val processPath: String, val packageName: String) {
|
||||
companion object {
|
||||
fun from(processInfo: LibboxProcessInfo?): ProcessInfo? {
|
||||
if (processInfo == null) return null
|
||||
@@ -68,16 +62,13 @@ data class Connection(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performSearchPlain(content: String): Boolean {
|
||||
return destination.contains(content, ignoreCase = true) ||
|
||||
private fun performSearchPlain(content: String): Boolean = destination.contains(content, ignoreCase = true) ||
|
||||
domain.contains(content, ignoreCase = true) ||
|
||||
outbound.contains(content, ignoreCase = true) ||
|
||||
rule.contains(content, ignoreCase = true) ||
|
||||
processInfo?.packageName?.contains(content, ignoreCase = true) == true
|
||||
}
|
||||
|
||||
private fun performSearchType(type: String, value: String): Boolean {
|
||||
return when (type) {
|
||||
private fun performSearchType(type: String, value: String): Boolean = when (type) {
|
||||
"network" -> network.equals(value, ignoreCase = true)
|
||||
"inbound" -> inbound.contains(value, ignoreCase = true)
|
||||
"inbound.type" -> inboundType.equals(value, ignoreCase = true)
|
||||
@@ -92,11 +83,9 @@ data class Connection(
|
||||
"chain" -> chain.any { it.contains(value, ignoreCase = true) }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(connection: LibboxConnection): Connection {
|
||||
return Connection(
|
||||
fun from(connection: LibboxConnection): Connection = Connection(
|
||||
id = connection.id,
|
||||
inbound = connection.inbound,
|
||||
inboundType = connection.inboundType,
|
||||
@@ -123,4 +112,3 @@ data class Connection(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,7 @@ data class Group(
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GroupItem(
|
||||
val tag: String,
|
||||
val type: String,
|
||||
val urlTestTime: Long,
|
||||
val urlTestDelay: Int,
|
||||
) {
|
||||
data class GroupItem(val tag: String, val type: String, val urlTestTime: Long, val urlTestDelay: Int) {
|
||||
constructor(item: OutboundGroupItem) : this(
|
||||
item.tag,
|
||||
item.type,
|
||||
|
||||
@@ -10,11 +10,7 @@ import androidx.compose.material.icons.filled.SwapVert
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.nekohasekai.sfa.R
|
||||
|
||||
sealed class Screen(
|
||||
val route: String,
|
||||
@StringRes val titleRes: Int,
|
||||
val icon: ImageVector,
|
||||
) {
|
||||
sealed class Screen(val route: String, @StringRes val titleRes: Int, val icon: ImageVector) {
|
||||
object Dashboard : Screen(
|
||||
route = "dashboard",
|
||||
titleRes = R.string.title_dashboard,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package io.nekohasekai.sfa.compose.navigation
|
||||
|
||||
data class NewProfileArgs(
|
||||
val importName: String? = null,
|
||||
val importUrl: String? = null,
|
||||
val qrsData: ByteArray? = null,
|
||||
)
|
||||
data class NewProfileArgs(val importName: String? = null, val importUrl: String? = null, val qrsData: ByteArray? = null)
|
||||
|
||||
object ProfileRoutes {
|
||||
const val NewProfile = "profile/new"
|
||||
|
||||
@@ -12,18 +12,20 @@ import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsRoute
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsRoute
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||
import io.nekohasekai.sfa.compose.screen.log.HookLogScreen
|
||||
import io.nekohasekai.sfa.compose.screen.log.LogScreen
|
||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
|
||||
import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
|
||||
import io.nekohasekai.sfa.compose.screen.profile.EditProfileRoute
|
||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen
|
||||
@@ -31,8 +33,6 @@ import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.SettingsScreen
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
|
||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
|
||||
|
||||
private val slideInFromRight: AnimatedContentTransitionScope<*>.() -> androidx.compose.animation.EnterTransition = {
|
||||
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300))
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
@@ -60,6 +59,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -124,10 +124,7 @@ class NewProfileViewModel(application: Application) : AndroidViewModel(applicati
|
||||
_uiState.update { it.copy(autoUpdateInterval = intValue.coerceAtLeast(15)) }
|
||||
}
|
||||
|
||||
fun setImportUri(
|
||||
uri: Uri,
|
||||
fileName: String?,
|
||||
) {
|
||||
fun setImportUri(uri: Uri, fileName: String?) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
importUri = uri,
|
||||
|
||||
@@ -23,8 +23,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
|
||||
sealed class QRCodeParseResult {
|
||||
data class RemoteProfile(val name: String, val host: String, val url: String) :
|
||||
QRCodeParseResult()
|
||||
data class RemoteProfile(val name: String, val host: String, val url: String) : QRCodeParseResult()
|
||||
|
||||
data class LocalProfile(val name: String) : QRCodeParseResult()
|
||||
|
||||
@@ -43,8 +42,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
data class Error(val message: String) : UriParseResult()
|
||||
}
|
||||
|
||||
suspend fun importFromUri(uri: Uri): ImportResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun importFromUri(uri: Uri): ImportResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val data =
|
||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
@@ -80,8 +78,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun parseUri(uri: Uri): UriParseResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun parseUri(uri: Uri): UriParseResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val data =
|
||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
@@ -112,8 +109,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun parseQRCode(data: String): QRCodeParseResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun parseQRCode(data: String): QRCodeParseResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Check if it's a sing-box remote profile import link
|
||||
if (data.startsWith("sing-box://import-remote-profile")) {
|
||||
@@ -157,8 +153,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun importFromQRCode(data: String): ImportResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun importFromQRCode(data: String): ImportResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Check if it's a sing-box remote profile import link
|
||||
if (data.startsWith("sing-box://import-remote-profile")) {
|
||||
@@ -194,8 +189,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun parseQRSData(data: ByteArray): QRSParseResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun parseQRSData(data: ByteArray): QRSParseResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val content = try {
|
||||
Libbox.decodeProfileContent(data)
|
||||
@@ -210,8 +204,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun importFromQRSData(data: ByteArray): ImportResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun importFromQRSData(data: ByteArray): ImportResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val content = try {
|
||||
Libbox.decodeProfileContent(data)
|
||||
@@ -259,10 +252,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
return ImportResult.Success(profile)
|
||||
}
|
||||
|
||||
private suspend fun importRemoteProfile(
|
||||
name: String,
|
||||
url: String,
|
||||
): ImportResult {
|
||||
private suspend fun importRemoteProfile(name: String, url: String): ImportResult {
|
||||
val typedProfile =
|
||||
TypedProfile().apply {
|
||||
type = TypedProfile.Type.Remote
|
||||
@@ -297,14 +287,12 @@ class ProfileImportHandler(private val context: Context) {
|
||||
?: "Remote Profile"
|
||||
}
|
||||
|
||||
private fun extractHostFromUrl(url: String): String {
|
||||
return try {
|
||||
private fun extractHostFromUrl(url: String): String = try {
|
||||
val uri = Uri.parse(url)
|
||||
uri.host ?: url
|
||||
} catch (e: Exception) {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileNameFromUri(uri: Uri): String {
|
||||
var filename = "Imported Profile"
|
||||
@@ -354,10 +342,7 @@ class ProfileImportHandler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importJsonConfiguration(
|
||||
jsonContent: String,
|
||||
profileName: String,
|
||||
): ImportResult {
|
||||
private suspend fun importJsonConfiguration(jsonContent: String, profileName: String): ImportResult {
|
||||
return try {
|
||||
// Validate the JSON configuration using sing-box
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.nekohasekai.sfa.compose.screen.connections
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -10,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -32,14 +32,14 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.R
|
||||
@@ -286,10 +286,7 @@ fun ConnectionDetailsScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailSection(
|
||||
title: String,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
private fun DetailSection(title: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -317,12 +314,7 @@ private fun DetailSection(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(
|
||||
label: String,
|
||||
value: String,
|
||||
monospace: Boolean = false,
|
||||
valueColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
private fun DetailRow(label: String, value: String, monospace: Boolean = false, valueColor: Color = MaterialTheme.colorScheme.onSurface) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -346,20 +338,10 @@ private fun DetailRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBounceBlockingNestedScrollConnection(
|
||||
scrollState: ScrollState
|
||||
): NestedScrollConnection = remember(scrollState) {
|
||||
private fun rememberBounceBlockingNestedScrollConnection(scrollState: ScrollState): NestedScrollConnection = remember(scrollState) {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
return if (available.y < 0) available else Offset.Zero
|
||||
}
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
return if (available.y < 0) available else Velocity.Zero
|
||||
}
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +45,13 @@ import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.model.Connection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
private fun Drawable.toBitmap(): Bitmap {
|
||||
if (this is BitmapDrawable) return bitmap
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
intrinsicWidth.coerceAtLeast(1),
|
||||
intrinsicHeight.coerceAtLeast(1),
|
||||
Bitmap.Config.ARGB_8888
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
setBounds(0, 0, canvas.width, canvas.height)
|
||||
@@ -62,10 +59,7 @@ private fun Drawable.toBitmap(): Bitmap {
|
||||
return bitmap
|
||||
}
|
||||
|
||||
data class AppInfo(
|
||||
val icon: ImageBitmap,
|
||||
val label: String,
|
||||
)
|
||||
data class AppInfo(val icon: ImageBitmap, val label: String)
|
||||
|
||||
@Composable
|
||||
private fun rememberAppInfo(packageName: String): AppInfo? {
|
||||
@@ -86,12 +80,7 @@ private fun rememberAppInfo(packageName: String): AppInfo? {
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ConnectionItem(
|
||||
connection: Connection,
|
||||
onClick: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun ConnectionItem(connection: Connection, onClick: () -> Unit, onClose: () -> Unit, modifier: Modifier = Modifier) {
|
||||
var showContextMenu by remember { mutableStateOf(false) }
|
||||
val packageName = connection.processInfo?.packageName?.takeIf { it.isNotEmpty() }
|
||||
val appInfo = packageName?.let { rememberAppInfo(it) }
|
||||
|
||||
@@ -18,11 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
@@ -54,15 +49,20 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.model.Connection
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.compose.model.Connection
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -118,7 +118,7 @@ fun ConnectionsPage(
|
||||
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
||||
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
||||
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -230,7 +230,7 @@ fun ConnectionsPage(
|
||||
stringResource(R.string.close_search)
|
||||
} else {
|
||||
stringResource(R.string.search)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -433,20 +433,10 @@ fun ConnectionsScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBounceBlockingNestedScrollConnection(
|
||||
lazyListState: LazyListState
|
||||
): NestedScrollConnection = remember(lazyListState) {
|
||||
private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
return if (available.y < 0) available else Offset.Zero
|
||||
}
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
return if (available.y < 0) available else Velocity.Zero
|
||||
}
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ import io.nekohasekai.libbox.Connections
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
||||
import io.nekohasekai.sfa.compose.base.ScreenEvent
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.ktx.toList
|
||||
import io.nekohasekai.sfa.compose.model.Connection
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.ktx.toList
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -22,6 +21,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
data class ConnectionsUiState(
|
||||
val connections: List<Connection> = emptyList(),
|
||||
@@ -38,7 +38,9 @@ sealed class ConnectionsEvent : ScreenEvent {
|
||||
data object AllConnectionsClosed : ConnectionsEvent()
|
||||
}
|
||||
|
||||
class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler {
|
||||
class ConnectionsViewModel :
|
||||
BaseViewModel<ConnectionsUiState, ConnectionsEvent>(),
|
||||
CommandClient.Handler {
|
||||
private val commandClient = CommandClient(
|
||||
viewModelScope,
|
||||
CommandClient.ConnectionType.Connections,
|
||||
@@ -62,7 +64,7 @@ class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>
|
||||
combine(
|
||||
AppLifecycleObserver.isForeground,
|
||||
_isVisible,
|
||||
_serviceStatus
|
||||
_serviceStatus,
|
||||
) { foreground, visible, status ->
|
||||
Triple(foreground, visible, status)
|
||||
}.collect { (foreground, visible, status) ->
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -14,8 +13,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -44,12 +43,7 @@ import io.nekohasekai.sfa.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ClashModeCard(
|
||||
modes: List<String>,
|
||||
selectedMode: String,
|
||||
onModeSelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun ClashModeCard(modes: List<String>, selectedMode: String, onModeSelected: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -127,11 +121,7 @@ fun ClashModeCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModeDropdown(
|
||||
modes: List<String>,
|
||||
selectedMode: String,
|
||||
onModeSelected: (String) -> Unit,
|
||||
) {
|
||||
private fun ModeDropdown(modes: List<String>, selectedMode: String, onModeSelected: (String) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
@@ -24,11 +24,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
|
||||
@Composable
|
||||
fun ConnectionsCard(
|
||||
connectionsIn: String,
|
||||
connectionsOut: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun ConnectionsCard(connectionsIn: String, connectionsOut: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -37,10 +36,7 @@ import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class CardRenderItem(
|
||||
val cards: List<CardGroup>,
|
||||
val isRow: Boolean,
|
||||
)
|
||||
data class CardRenderItem(val cards: List<CardGroup>, val isRow: Boolean)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -307,11 +303,7 @@ fun processCardsForRendering(
|
||||
* This function is only relevant when the service is running.
|
||||
* Note: Profiles card is always available and should not use this function.
|
||||
*/
|
||||
fun isCardAvailableWhenServiceRunning(
|
||||
cardGroup: CardGroup,
|
||||
uiState: DashboardUiState,
|
||||
): Boolean {
|
||||
return when (cardGroup) {
|
||||
fun isCardAvailableWhenServiceRunning(cardGroup: CardGroup, uiState: DashboardUiState): Boolean = when (cardGroup) {
|
||||
CardGroup.ClashMode -> uiState.clashModeVisible
|
||||
CardGroup.UploadTraffic -> uiState.trafficVisible
|
||||
CardGroup.DownloadTraffic -> uiState.trafficVisible
|
||||
@@ -320,4 +312,3 @@ fun isCardAvailableWhenServiceRunning(
|
||||
CardGroup.SystemProxy -> uiState.systemProxyVisible
|
||||
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import io.nekohasekai.sfa.compat.animateItemCompat
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Cable
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Folder
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.Route
|
||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||
@@ -65,6 +63,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compat.animateItemCompat
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -96,12 +95,12 @@ fun DashboardSettingsBottomSheet(
|
||||
var dragOffset by remember { mutableStateOf(0f) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
fun onMove(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
) {
|
||||
if (fromIndex != toIndex && fromIndex >= 0 && toIndex >= 0 &&
|
||||
fromIndex < reorderedList.size && toIndex < reorderedList.size
|
||||
fun onMove(fromIndex: Int, toIndex: Int) {
|
||||
if (fromIndex != toIndex &&
|
||||
fromIndex >= 0 &&
|
||||
toIndex >= 0 &&
|
||||
fromIndex < reorderedList.size &&
|
||||
toIndex < reorderedList.size
|
||||
) {
|
||||
val newList = reorderedList.toMutableList()
|
||||
val item = newList.removeAt(fromIndex)
|
||||
|
||||
@@ -114,16 +114,15 @@ data class DashboardUiState(
|
||||
),
|
||||
val showCardSettingsDialog: Boolean = false,
|
||||
) {
|
||||
data class DeprecatedNote(
|
||||
val message: String,
|
||||
val migrationLink: String?,
|
||||
)
|
||||
data class DeprecatedNote(val message: String, val migrationLink: String?)
|
||||
}
|
||||
|
||||
// DashboardViewModel now only uses UiEvent for all events
|
||||
// No need for DashboardEvent anymore as all events are handled globally
|
||||
|
||||
class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandClient.Handler {
|
||||
class DashboardViewModel :
|
||||
BaseViewModel<DashboardUiState, UiEvent>(),
|
||||
CommandClient.Handler {
|
||||
private val _serviceStatus = MutableStateFlow(Status.Stopped)
|
||||
val serviceStatus: StateFlow<Status> = _serviceStatus.asStateFlow()
|
||||
|
||||
@@ -395,10 +394,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
}
|
||||
}
|
||||
|
||||
fun moveProfile(
|
||||
from: Int,
|
||||
to: Int,
|
||||
) {
|
||||
fun moveProfile(from: Int, to: Int) {
|
||||
val currentProfiles = currentState.profiles.toMutableList()
|
||||
|
||||
if (from < to) {
|
||||
@@ -614,10 +610,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
}
|
||||
}
|
||||
|
||||
override fun initializeClashMode(
|
||||
modeList: List<String>,
|
||||
currentMode: String,
|
||||
) {
|
||||
override fun initializeClashMode(modeList: List<String>, currentMode: String) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
updateState {
|
||||
copy(
|
||||
@@ -702,8 +695,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
}
|
||||
|
||||
// Helper functions for serialization
|
||||
private fun getDefaultItemOrder() =
|
||||
listOf(
|
||||
private fun getDefaultItemOrder() = listOf(
|
||||
CardGroup.UploadTraffic,
|
||||
CardGroup.DownloadTraffic,
|
||||
CardGroup.Debug,
|
||||
@@ -766,11 +758,9 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
|
||||
private fun cardGroupToString(card: CardGroup): String = card.name
|
||||
|
||||
private fun stringToCardGroup(name: String): CardGroup? {
|
||||
return try {
|
||||
private fun stringToCardGroup(name: String): CardGroup? = try {
|
||||
CardGroup.valueOf(name)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
|
||||
@Composable
|
||||
fun DebugCard(
|
||||
memory: String,
|
||||
goroutines: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun DebugCard(memory: String, goroutines: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
@@ -24,12 +24,7 @@ import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.LineChart
|
||||
|
||||
@Composable
|
||||
fun DownloadTrafficCard(
|
||||
downlink: String,
|
||||
downlinkTotal: String,
|
||||
downlinkHistory: List<Float>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun DownloadTrafficCard(downlink: String, downlinkTotal: String, downlinkHistory: List<Float>, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
@@ -26,10 +26,10 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.UnfoldLess
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material.icons.filled.UnfoldLess
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -58,19 +58,19 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.compose.model.Group
|
||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -401,12 +401,7 @@ private fun ProxyGroupItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProxyItemsList(
|
||||
items: List<GroupItem>,
|
||||
selectedTag: String,
|
||||
isSelectable: Boolean,
|
||||
onItemSelected: (String) -> Unit,
|
||||
) {
|
||||
private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
|
||||
val itemsPerRow = 2
|
||||
val chunkedItems =
|
||||
remember(items) {
|
||||
@@ -450,13 +445,7 @@ private fun ProxyItemsList(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ProxyChip(
|
||||
item: GroupItem,
|
||||
isSelected: Boolean,
|
||||
isSelectable: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
// Use simpler, faster animations
|
||||
val animatedElevation by animateFloatAsState(
|
||||
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
|
||||
@@ -566,11 +555,7 @@ private fun ProxyChip(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProxyLatencyBadge(
|
||||
delay: Int,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
|
||||
// Direct color calculation without animation for better performance
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val latencyColor =
|
||||
@@ -624,15 +609,9 @@ private fun ProxyLatencyBadge(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBounceBlockingNestedScrollConnection(
|
||||
lazyListState: LazyListState
|
||||
): NestedScrollConnection = remember(lazyListState) {
|
||||
private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
// Only block upward scroll (y < 0) at bottom to prevent sheet expansion
|
||||
// Allow downward scroll (y > 0) at top to let sheet collapse
|
||||
return if (available.y < 0) available else Offset.Zero
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -42,9 +43,7 @@ import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -52,6 +51,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -16,7 +17,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -29,11 +29,7 @@ import io.nekohasekai.sfa.compose.util.ProfileIcons
|
||||
import io.nekohasekai.sfa.database.Profile
|
||||
|
||||
@Composable
|
||||
fun ProfileSelectorButton(
|
||||
selectedProfile: Profile?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun ProfileSelectorButton(selectedProfile: Profile?, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = modifier.fillMaxWidth().height(48.dp),
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -21,33 +22,30 @@ import androidx.compose.material.icons.filled.AccessTime
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.DataObject
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.IosShare
|
||||
import androidx.compose.material.icons.filled.QrCode2
|
||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.DataObject
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material.icons.outlined.CreateNewFolder
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.FileUpload
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -55,6 +53,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -64,11 +63,11 @@ import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.ProfileContent
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.component.qr.QRCodeDialog
|
||||
import io.nekohasekai.sfa.compose.component.qr.QRScanSheet
|
||||
import io.nekohasekai.sfa.compose.component.qr.QRSDialog
|
||||
import io.nekohasekai.sfa.compose.component.qr.QRScanSheet
|
||||
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
|
||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
||||
import io.nekohasekai.sfa.compose.screen.configuration.ProfileImportHandler
|
||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
||||
import io.nekohasekai.sfa.compose.util.QRCodeGenerator
|
||||
import io.nekohasekai.sfa.compose.util.RelativeTimeFormatter
|
||||
import io.nekohasekai.sfa.database.Profile
|
||||
|
||||
@@ -23,12 +23,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
|
||||
@Composable
|
||||
fun SystemProxyCard(
|
||||
enabled: Boolean,
|
||||
isSwitching: Boolean,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun SystemProxyCard(enabled: Boolean, isSwitching: Boolean, onToggle: (Boolean) -> Unit, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
@@ -24,12 +24,7 @@ import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.LineChart
|
||||
|
||||
@Composable
|
||||
fun UploadTrafficCard(
|
||||
uplink: String,
|
||||
uplinkTotal: String,
|
||||
uplinkHistory: List<Float>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun UploadTrafficCard(uplink: String, uplinkTotal: String, uplinkHistory: List<Float>, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
@@ -54,9 +54,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.compose.model.Group
|
||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
|
||||
@Composable
|
||||
fun GroupsScreen(
|
||||
@@ -294,12 +294,7 @@ private fun ProxyGroupCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProxyItemsList(
|
||||
items: List<GroupItem>,
|
||||
selectedTag: String,
|
||||
isSelectable: Boolean,
|
||||
onItemSelected: (String) -> Unit,
|
||||
) {
|
||||
private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
|
||||
// Cache the chunked items to avoid re-chunking on every recomposition
|
||||
val itemsPerRow = 2
|
||||
val chunkedItems =
|
||||
@@ -344,13 +339,7 @@ private fun ProxyItemsList(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ProxyChip(
|
||||
item: GroupItem,
|
||||
isSelected: Boolean,
|
||||
isSelectable: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
// Use simpler, faster animations
|
||||
val animatedElevation by animateFloatAsState(
|
||||
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
|
||||
@@ -460,11 +449,7 @@ private fun ProxyChip(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProxyLatencyBadge(
|
||||
delay: Int,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
|
||||
// Direct color calculation without animation for better performance
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val latencyColor =
|
||||
|
||||
@@ -5,10 +5,10 @@ import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.OutboundGroup
|
||||
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
||||
import io.nekohasekai.sfa.compose.base.ScreenEvent
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.compose.model.Group
|
||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||
import io.nekohasekai.sfa.compose.model.toList
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -28,9 +28,9 @@ sealed class GroupsEvent : ScreenEvent {
|
||||
data class GroupSelected(val groupTag: String, val itemTag: String) : GroupsEvent()
|
||||
}
|
||||
|
||||
class GroupsViewModel(
|
||||
private val sharedCommandClient: CommandClient? = null,
|
||||
) : BaseViewModel<GroupsUiState, GroupsEvent>(), CommandClient.Handler {
|
||||
class GroupsViewModel(private val sharedCommandClient: CommandClient? = null) :
|
||||
BaseViewModel<GroupsUiState, GroupsEvent>(),
|
||||
CommandClient.Handler {
|
||||
private val commandClient: CommandClient
|
||||
private val isUsingSharedClient: Boolean
|
||||
|
||||
@@ -154,10 +154,7 @@ class GroupsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun selectGroupItem(
|
||||
groupTag: String,
|
||||
itemTag: String,
|
||||
) {
|
||||
fun selectGroupItem(groupTag: String, itemTag: String) {
|
||||
// Check if this is actually a different selection
|
||||
val currentGroup = uiState.value.groups.find { it.tag == groupTag }
|
||||
if (currentGroup?.selected == itemTag) {
|
||||
|
||||
@@ -15,7 +15,9 @@ import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
abstract class BaseLogViewModel : ViewModel(), LogViewerViewModel {
|
||||
abstract class BaseLogViewModel :
|
||||
ViewModel(),
|
||||
LogViewerViewModel {
|
||||
protected val _uiState = MutableStateFlow(LogUiState())
|
||||
override val uiState: StateFlow<LogUiState> = _uiState.asStateFlow()
|
||||
|
||||
@@ -119,9 +121,7 @@ abstract class BaseLogViewModel : ViewModel(), LogViewerViewModel {
|
||||
.joinToString("\n")
|
||||
}
|
||||
|
||||
override fun getAllLogsText(): String {
|
||||
return _uiState.value.logs.joinToString("\n") { AnsiColorUtils.stripAnsi(it.entry.message) }
|
||||
}
|
||||
override fun getAllLogsText(): String = _uiState.value.logs.joinToString("\n") { AnsiColorUtils.stripAnsi(it.entry.message) }
|
||||
|
||||
protected fun updateDisplayedLogs() {
|
||||
val currentState = _uiState.value
|
||||
|
||||
@@ -3,16 +3,9 @@ package io.nekohasekai.sfa.compose.screen.log
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
|
||||
data class LogEntryData(
|
||||
val level: LogLevel,
|
||||
val message: String,
|
||||
)
|
||||
data class LogEntryData(val level: LogLevel, val message: String)
|
||||
|
||||
data class ProcessedLogEntry(
|
||||
val id: Long,
|
||||
val entry: LogEntryData,
|
||||
val annotatedString: AnnotatedString,
|
||||
)
|
||||
data class ProcessedLogEntry(val id: Long, val entry: LogEntryData, val annotatedString: AnnotatedString)
|
||||
|
||||
enum class LogLevel(val label: String, val priority: Int) {
|
||||
Default("Default", 7),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.nekohasekai.sfa.compose.screen.log
|
||||
|
||||
import android.content.ClipData
|
||||
import android.os.Build
|
||||
import android.content.res.Configuration
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
||||
@@ -13,7 +13,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.LinkedList
|
||||
|
||||
class LogViewModel : BaseLogViewModel(), CommandClient.Handler {
|
||||
class LogViewModel :
|
||||
BaseLogViewModel(),
|
||||
CommandClient.Handler {
|
||||
companion object {
|
||||
private val maxLines = 3000
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -53,13 +53,13 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
|
||||
import io.nekohasekai.sfa.compose.shared.PackageCache
|
||||
import io.nekohasekai.sfa.compose.shared.SortMode
|
||||
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
||||
@@ -68,11 +68,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
private data class LoadResult(
|
||||
val packages: List<PackageCache>,
|
||||
val selectedUids: Set<Int>,
|
||||
)
|
||||
private data class LoadResult(val packages: List<PackageCache>, val selectedUids: Set<Int>)
|
||||
|
||||
private const val VPN_SERVICE_PERMISSION = "android.permission.BIND_VPN_SERVICE"
|
||||
|
||||
@@ -126,7 +122,8 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
||||
val hasManagement = permissions.any { it in managementPermissions }
|
||||
val isSelf = packageCache.packageName == context.packageName
|
||||
val hasVpnService =
|
||||
!isSelf && (
|
||||
!isSelf &&
|
||||
(
|
||||
permissions.any { it == VPN_SERVICE_PERMISSION } ||
|
||||
packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true
|
||||
)
|
||||
@@ -138,11 +135,9 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun buildPackageList(newUids: Set<Int>): Set<String> {
|
||||
return newUids.mapNotNull { uid ->
|
||||
fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
|
||||
packages.find { it.uid == uid }?.packageName
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun updateCurrentPackages(filterQuery: String) {
|
||||
currentPackages =
|
||||
|
||||
@@ -239,9 +239,13 @@ fun EditProfileContentScreen(
|
||||
}
|
||||
// Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y - Redo
|
||||
(
|
||||
modifierPressed && event.isShiftPressed && event.key == Key.Z ||
|
||||
modifierPressed && event.key == Key.Y
|
||||
) && !uiState.isReadOnly -> {
|
||||
modifierPressed &&
|
||||
event.isShiftPressed &&
|
||||
event.key == Key.Z ||
|
||||
modifierPressed &&
|
||||
event.key == Key.Y
|
||||
) &&
|
||||
!uiState.isReadOnly -> {
|
||||
viewModel.redo()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -38,11 +38,7 @@ data class EditProfileContentUiState(
|
||||
val profileName: String = "", // Add profile name
|
||||
)
|
||||
|
||||
class EditProfileContentViewModel(
|
||||
private val profileId: Long,
|
||||
initialProfileName: String = "",
|
||||
initialIsReadOnly: Boolean = false,
|
||||
) : ViewModel() {
|
||||
class EditProfileContentViewModel(private val profileId: Long, initialProfileName: String = "", initialIsReadOnly: Boolean = false) : ViewModel() {
|
||||
private val _uiState =
|
||||
MutableStateFlow(
|
||||
EditProfileContentUiState(
|
||||
@@ -56,10 +52,7 @@ class EditProfileContentViewModel(
|
||||
private var editor: ManualScrollTextProcessor? = null
|
||||
private var configCheckJob: Job? = null
|
||||
|
||||
fun setEditor(
|
||||
textProcessor: ManualScrollTextProcessor,
|
||||
isReadOnly: Boolean = false,
|
||||
) {
|
||||
fun setEditor(textProcessor: ManualScrollTextProcessor, isReadOnly: Boolean = false) {
|
||||
val isNewEditor = editor != textProcessor
|
||||
editor = textProcessor
|
||||
textProcessor.resumeAutoScroll()
|
||||
@@ -89,18 +82,12 @@ class EditProfileContentViewModel(
|
||||
// Customize text selection to remove Cut and Paste options
|
||||
textProcessor.customSelectionActionModeCallback =
|
||||
object : android.view.ActionMode.Callback {
|
||||
override fun onCreateActionMode(
|
||||
mode: android.view.ActionMode?,
|
||||
menu: android.view.Menu?,
|
||||
): Boolean {
|
||||
override fun onCreateActionMode(mode: android.view.ActionMode?, menu: android.view.Menu?): Boolean {
|
||||
// Allow the action mode to be created
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
mode: android.view.ActionMode?,
|
||||
menu: android.view.Menu?,
|
||||
): Boolean {
|
||||
override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: android.view.Menu?): Boolean {
|
||||
// Remove editing-related menu items, keep only Copy and Select All
|
||||
menu?.let { m ->
|
||||
// Remove all editing-related items
|
||||
@@ -116,10 +103,7 @@ class EditProfileContentViewModel(
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
mode: android.view.ActionMode?,
|
||||
item: android.view.MenuItem?,
|
||||
): Boolean {
|
||||
override fun onActionItemClicked(mode: android.view.ActionMode?, item: android.view.MenuItem?): Boolean {
|
||||
// Let the default implementation handle allowed actions (copy, select all)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
|
||||
@Composable
|
||||
fun EditProfileRoute(
|
||||
profileId: Long,
|
||||
onNavigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun EditProfileRoute(profileId: Long, onNavigateBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
if (profileId == -1L) {
|
||||
LaunchedEffect(Unit) {
|
||||
onNavigateBack()
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
@@ -61,6 +60,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
||||
@@ -183,13 +183,11 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkHasChanges(state: EditProfileUiState): Boolean {
|
||||
return state.name != state.originalName ||
|
||||
private fun checkHasChanges(state: EditProfileUiState): Boolean = state.name != state.originalName ||
|
||||
state.icon != state.originalIcon ||
|
||||
state.remoteUrl != state.originalRemoteUrl ||
|
||||
state.autoUpdate != state.originalAutoUpdate ||
|
||||
state.autoUpdateInterval != state.originalAutoUpdateInterval
|
||||
}
|
||||
|
||||
fun saveChanges() {
|
||||
val state = _uiState.value
|
||||
@@ -343,10 +341,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
fun saveExportToUri(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
) {
|
||||
fun saveExportToUri(context: Context, uri: Uri) {
|
||||
val content = pendingExportContent ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -37,11 +37,7 @@ import io.nekohasekai.sfa.compose.util.ProfileIcon
|
||||
import io.nekohasekai.sfa.compose.util.ProfileIcons
|
||||
|
||||
@Composable
|
||||
fun IconSelectionDialog(
|
||||
currentIconId: String?,
|
||||
onIconSelected: (String?) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
fun IconSelectionDialog(currentIconId: String?, onIconSelected: (String?) -> Unit, onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier =
|
||||
@@ -110,12 +106,7 @@ fun IconSelectionDialog(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun IconOption(
|
||||
icon: ProfileIcon?,
|
||||
label: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
private fun IconOption(icon: ProfileIcon?, label: String, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -60,6 +59,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -74,11 +74,7 @@ import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun IconSelectionScreen(
|
||||
currentIconId: String?,
|
||||
onIconSelected: (String?) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
fun IconSelectionScreen(currentIconId: String?, onIconSelected: (String?) -> Unit, onNavigateBack: () -> Unit) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var selectedCategory by remember { mutableStateOf<String?>(null) }
|
||||
var viewMode by remember { mutableStateOf(IconViewMode.CATEGORIES) }
|
||||
@@ -436,11 +432,7 @@ fun IconSelectionScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryList(
|
||||
categories: List<IconCategory>,
|
||||
currentIconId: String?,
|
||||
onCategoryClick: (IconCategory) -> Unit,
|
||||
) {
|
||||
private fun CategoryList(categories: List<IconCategory>, currentIconId: String?, onCategoryClick: (IconCategory) -> Unit) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
@@ -456,11 +448,7 @@ private fun CategoryList(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CategoryCard(
|
||||
category: IconCategory,
|
||||
hasSelectedIcon: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
private fun CategoryCard(category: IconCategory, hasSelectedIcon: Boolean, onClick: () -> Unit) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -517,11 +505,7 @@ private fun CategoryCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconGrid(
|
||||
icons: List<ProfileIcon>,
|
||||
currentIconId: String?,
|
||||
onIconClick: (ProfileIcon) -> Unit,
|
||||
) {
|
||||
private fun IconGrid(icons: List<ProfileIcon>, currentIconId: String?, onIconClick: (ProfileIcon) -> Unit) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 72.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -539,11 +523,7 @@ private fun IconGrid(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun IconGridItem(
|
||||
icon: ProfileIcon,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
private fun IconGridItem(icon: ProfileIcon, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier =
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.blacksquircle.ui.editorkit.widget.TextProcessor;
|
||||
|
||||
public class ManualScrollTextProcessor extends TextProcessor {
|
||||
@@ -117,7 +116,8 @@ public class ManualScrollTextProcessor extends TextProcessor {
|
||||
}
|
||||
|
||||
if (userDragging) {
|
||||
if (downSelectionStart >= 0 && (selStart != downSelectionStart || selEnd != downSelectionEnd)) {
|
||||
if (downSelectionStart >= 0
|
||||
&& (selStart != downSelectionStart || selEnd != downSelectionEnd)) {
|
||||
restoringSelection = true;
|
||||
int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart;
|
||||
setSelection(downSelectionStart, targetEnd);
|
||||
|
||||
@@ -6,9 +6,9 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -54,7 +54,6 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -66,9 +65,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -79,13 +76,13 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
|
||||
import io.nekohasekai.sfa.compose.shared.PackageCache
|
||||
import io.nekohasekai.sfa.compose.shared.SortMode
|
||||
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -97,16 +94,9 @@ import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
private data class LoadResult(
|
||||
val proxyMode: Int,
|
||||
val packages: List<PackageCache>,
|
||||
val selectedUids: Set<Int>,
|
||||
)
|
||||
private data class LoadResult(val proxyMode: Int, val packages: List<PackageCache>, val selectedUids: Set<Int>)
|
||||
|
||||
private data class ScanProgress(
|
||||
val current: Int,
|
||||
val max: Int,
|
||||
)
|
||||
private data class ScanProgress(val current: Int, val max: Int)
|
||||
|
||||
private sealed class ScanResult {
|
||||
data object Empty : ScanResult()
|
||||
@@ -139,11 +129,9 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
||||
var scanProgress by remember { mutableStateOf<ScanProgress?>(null) }
|
||||
var scanResult by remember { mutableStateOf<ScanResult?>(null) }
|
||||
|
||||
fun buildPackageList(newUids: Set<Int>): Set<String> {
|
||||
return newUids.mapNotNull { uid ->
|
||||
fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
|
||||
packages.find { it.uid == uid }?.packageName
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun updateCurrentPackages(filterQuery: String) {
|
||||
currentPackages =
|
||||
|
||||
@@ -14,12 +14,7 @@ data class QRCodeCropArea(
|
||||
)
|
||||
|
||||
object QRCodeSmartCrop {
|
||||
fun findCropArea(
|
||||
yData: ByteArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
rotationDegrees: Int,
|
||||
): QRCodeCropArea? {
|
||||
fun findCropArea(yData: ByteArray, width: Int, height: Int, rotationDegrees: Int): QRCodeCropArea? {
|
||||
val minDim = min(width, height)
|
||||
if (minDim <= 0) return null
|
||||
|
||||
@@ -94,14 +89,7 @@ object QRCodeSmartCrop {
|
||||
return bestArea
|
||||
}
|
||||
|
||||
private data class CropComponent(
|
||||
val minX: Int,
|
||||
val minY: Int,
|
||||
val maxX: Int,
|
||||
val maxY: Int,
|
||||
val count: Int,
|
||||
val score: Float,
|
||||
)
|
||||
private data class CropComponent(val minX: Int, val minY: Int, val maxX: Int, val maxY: Int, val count: Int, val score: Float)
|
||||
|
||||
private fun findBestComponent(
|
||||
yData: ByteArray,
|
||||
@@ -233,13 +221,7 @@ object QRCodeSmartCrop {
|
||||
return best
|
||||
}
|
||||
|
||||
private fun buildCropArea(
|
||||
component: CropComponent,
|
||||
step: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
rotationDegrees: Int,
|
||||
): QRCodeCropArea? {
|
||||
private fun buildCropArea(component: CropComponent, step: Int, width: Int, height: Int, rotationDegrees: Int): QRCodeCropArea? {
|
||||
val left = component.minX * step
|
||||
val top = component.minY * step
|
||||
val right = min(width, (component.maxX + 1) * step)
|
||||
|
||||
@@ -83,7 +83,7 @@ class QRScanViewModel(application: Application) : AndroidViewModel(application)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
vendorAnalyzerAvailable = vendorAnalyzer != null,
|
||||
useVendorAnalyzer = vendorAnalyzer != null
|
||||
useVendorAnalyzer = vendorAnalyzer != null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ class QRScanViewModel(application: Application) : AndroidViewModel(application)
|
||||
lifecycleOwner,
|
||||
cameraSelector,
|
||||
preview,
|
||||
analysis
|
||||
analysis,
|
||||
)
|
||||
val maxZoom = camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f
|
||||
_uiState.update { it.copy(maxZoomRatio = maxZoom, zoomRatio = 1f) }
|
||||
|
||||
@@ -109,12 +109,10 @@ class ZxingQRCodeAnalyzer(
|
||||
return yData
|
||||
}
|
||||
|
||||
private fun tryDecode(bitmap: BinaryBitmap): Result? {
|
||||
return try {
|
||||
private fun tryDecode(bitmap: BinaryBitmap): Result? = try {
|
||||
qrCodeReader.decode(bitmap)
|
||||
} catch (_: NotFoundException) {
|
||||
qrCodeReader.reset()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.provider.Settings as AndroidSettings
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -29,7 +28,6 @@ import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SystemUpdateAlt
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Card
|
||||
@@ -42,6 +40,7 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -53,8 +52,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -62,22 +59,25 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import io.nekohasekai.sfa.xposed.XposedActivation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import android.provider.Settings as AndroidSettings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -136,7 +136,8 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
isMethodAvailable = success
|
||||
silentInstallError = if (success) {
|
||||
null
|
||||
} else when (silentInstallMethod) {
|
||||
} else {
|
||||
when (silentInstallMethod) {
|
||||
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
||||
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
||||
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
|
||||
@@ -144,6 +145,7 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showTrackDialog) {
|
||||
UpdateTrackDialog(
|
||||
@@ -224,12 +226,14 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
isMethodAvailable = success
|
||||
silentInstallError = if (success) {
|
||||
null
|
||||
} else when (method) {
|
||||
} else {
|
||||
when (method) {
|
||||
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
||||
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
||||
else -> context.getString(R.string.silent_install_verify_failed, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showInstallMethodMenu = false },
|
||||
)
|
||||
@@ -478,12 +482,14 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
isMethodAvailable = success
|
||||
silentInstallError = if (success) {
|
||||
null
|
||||
} else when (silentInstallMethod) {
|
||||
} else {
|
||||
when (silentInstallMethod) {
|
||||
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
||||
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
||||
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
silentInstallError = null
|
||||
}
|
||||
@@ -510,11 +516,13 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
Text(
|
||||
if (xposedActivated) {
|
||||
stringResource(R.string.install_method_root)
|
||||
} else when (silentInstallMethod) {
|
||||
} else {
|
||||
when (silentInstallMethod) {
|
||||
"PACKAGE_INSTALLER" -> stringResource(R.string.install_method_package_installer)
|
||||
"SHIZUKU" -> stringResource(R.string.install_method_shizuku)
|
||||
"ROOT" -> stringResource(R.string.install_method_root)
|
||||
else -> silentInstallMethod
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
@@ -597,7 +605,7 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
.clickable {
|
||||
val intent = Intent(
|
||||
AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
Uri.parse("package:${context.packageName}"),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package io.nekohasekai.sfa.compose.screen.settings
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -11,11 +16,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.DeleteForever
|
||||
@@ -343,7 +343,7 @@ private fun openInFileManager(context: Context) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_file_manager),
|
||||
Toast.LENGTH_SHORT
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.nekohasekai.sfa.compose.screen.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
@@ -61,18 +60,18 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
||||
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
|
||||
import io.nekohasekai.sfa.compose.base.UiEvent
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.utils.DetectionResult
|
||||
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||
import io.nekohasekai.sfa.utils.VpnDetectionTest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -122,7 +121,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
var messageDialogMessage by remember { mutableStateOf("") }
|
||||
|
||||
val saveFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/zip")
|
||||
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
||||
) { uri ->
|
||||
val file = exportedFile
|
||||
if (uri != null && file != null) {
|
||||
@@ -231,8 +230,11 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
if (exportError != null) exportError!!
|
||||
else stringResource(R.string.exporting)
|
||||
if (exportError != null) {
|
||||
exportError!!
|
||||
} else {
|
||||
stringResource(R.string.exporting)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -273,7 +275,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.cache",
|
||||
file
|
||||
file,
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/zip"
|
||||
@@ -283,7 +285,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
context.startActivity(Intent.createChooser(intent, null))
|
||||
showExportSuccessDialog = false
|
||||
exportedFile = null
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.menu_share))
|
||||
}
|
||||
@@ -293,7 +295,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
onClick = {
|
||||
val file = exportedFile ?: return@TextButton
|
||||
saveFileLauncher.launch(file.name)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
@@ -448,7 +450,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
}
|
||||
val timestamp =
|
||||
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val outZip = File(exportBase, "sing-box-lsposed-debug-${timestamp}.zip")
|
||||
val outZip = File(exportBase, "sing-box-lsposed-debug-$timestamp.zip")
|
||||
exportCancelled = false
|
||||
exportError = null
|
||||
showExportProgressDialog = true
|
||||
@@ -469,7 +471,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
messageDialogTitle = context.getString(R.string.error_title)
|
||||
messageDialogMessage = context.getString(
|
||||
R.string.privilege_settings_export_debug_failed,
|
||||
failure
|
||||
failure,
|
||||
)
|
||||
showMessageDialog = true
|
||||
}
|
||||
@@ -621,7 +623,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||
} else {
|
||||
RoundedCornerShape(12.dp)
|
||||
}
|
||||
},
|
||||
),
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
@@ -662,7 +664,6 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,7 +731,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||
} else {
|
||||
RoundedCornerShape(12.dp)
|
||||
}
|
||||
},
|
||||
),
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
@@ -847,11 +848,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelfTestDialog(
|
||||
isRunning: Boolean,
|
||||
result: DetectionResult?,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
private fun SelfTestDialog(isRunning: Boolean, result: DetectionResult?, onDismiss: () -> Unit) {
|
||||
val notDetectedText = stringResource(R.string.privilege_settings_hide_test_not_detected)
|
||||
|
||||
AlertDialog(
|
||||
|
||||
@@ -54,14 +54,14 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.bg.RootClient
|
||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -272,32 +272,44 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
Text(
|
||||
stringResource(R.string.per_app_proxy_package_query_mode),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (modeEnabled) Color.Unspecified
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
||||
color = if (modeEnabled) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
|
||||
},
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (useRootMode) "ROOT" else "Shizuku",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
|
||||
color = if (modeEnabled) {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha)
|
||||
},
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Tune,
|
||||
contentDescription = null,
|
||||
tint = if (modeEnabled) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
||||
tint = if (modeEnabled) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
|
||||
},
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
||||
tint = if (modeEnabled) {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
@@ -601,7 +613,7 @@ fun ProfileOverrideScreen(navController: NavController) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.root_access_denied,
|
||||
Toast.LENGTH_LONG
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +63,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ServiceSettingsScreen(
|
||||
navController: NavController,
|
||||
serviceConnection: ServiceConnection? = null,
|
||||
) {
|
||||
fun ServiceSettingsScreen(navController: NavController, serviceConnection: ServiceConnection? = null) {
|
||||
OverrideTopBar {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.service)) },
|
||||
|
||||
@@ -15,15 +15,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SwapHoriz
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -49,15 +48,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -7,20 +7,12 @@ import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
|
||||
internal data class TopBarEntry(
|
||||
val key: Any,
|
||||
val content: @Composable () -> Unit,
|
||||
)
|
||||
internal data class TopBarEntry(val key: Any, val content: @Composable () -> Unit)
|
||||
|
||||
class TopBarController internal constructor(
|
||||
private val state: MutableState<List<TopBarEntry>>,
|
||||
) {
|
||||
class TopBarController internal constructor(private val state: MutableState<List<TopBarEntry>>) {
|
||||
val current: (@Composable () -> Unit)? get() = state.value.lastOrNull()?.content
|
||||
|
||||
fun set(
|
||||
key: Any,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
fun set(key: Any, content: @Composable () -> Unit) {
|
||||
state.value = state.value.filterNot { it.key == key } + TopBarEntry(key, content)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@ import androidx.compose.material.icons.sharp.*
|
||||
import androidx.compose.material.icons.twotone.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
data class IconCategory(
|
||||
val name: String,
|
||||
val icons: List<ProfileIcon>,
|
||||
)
|
||||
data class IconCategory(val name: String, val icons: List<ProfileIcon>)
|
||||
|
||||
object MaterialIconsLibrary {
|
||||
val categories =
|
||||
@@ -416,20 +413,16 @@ object MaterialIconsLibrary {
|
||||
),
|
||||
)
|
||||
|
||||
fun getAllIcons(): List<ProfileIcon> {
|
||||
return categories.flatMap { it.icons }
|
||||
}
|
||||
fun getAllIcons(): List<ProfileIcon> = categories.flatMap { it.icons }
|
||||
|
||||
fun getIconById(id: String?): ImageVector? {
|
||||
if (id == null) return null
|
||||
return getAllIcons().find { it.id == id }?.icon
|
||||
}
|
||||
|
||||
fun getCategoryForIcon(iconId: String): String? {
|
||||
return categories.find { category ->
|
||||
fun getCategoryForIcon(iconId: String): String? = categories.find { category ->
|
||||
category.icons.any { it.id == iconId }
|
||||
}?.name
|
||||
}
|
||||
|
||||
fun searchIcons(query: String): List<ProfileIcon> {
|
||||
val lowercaseQuery = query.lowercase()
|
||||
|
||||
@@ -5,11 +5,7 @@ import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
|
||||
|
||||
data class ProfileIcon(
|
||||
val id: String,
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
)
|
||||
data class ProfileIcon(val id: String, val icon: ImageVector, val label: String)
|
||||
|
||||
object ProfileIcons {
|
||||
// Use the complete Material Icons library with all available icons
|
||||
@@ -26,13 +22,9 @@ object ProfileIcons {
|
||||
return Icons.AutoMirrored.Default.InsertDriveFile
|
||||
}
|
||||
|
||||
fun getCategoryForIcon(iconId: String): String? {
|
||||
return MaterialIconsLibrary.getCategoryForIcon(iconId)
|
||||
}
|
||||
fun getCategoryForIcon(iconId: String): String? = MaterialIconsLibrary.getCategoryForIcon(iconId)
|
||||
|
||||
fun searchIcons(query: String): List<ProfileIcon> {
|
||||
return MaterialIconsLibrary.searchIcons(query)
|
||||
}
|
||||
fun searchIcons(query: String): List<ProfileIcon> = MaterialIconsLibrary.searchIcons(query)
|
||||
|
||||
fun getCategories() = MaterialIconsLibrary.categories
|
||||
}
|
||||
|
||||
@@ -87,12 +87,7 @@ object QRCodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
fun generate(
|
||||
content: String,
|
||||
size: Int = 512,
|
||||
foregroundColor: Int = Color.BLACK,
|
||||
backgroundColor: Int = Color.WHITE,
|
||||
): Bitmap {
|
||||
fun generate(content: String, size: Int = 512, foregroundColor: Int = Color.BLACK, backgroundColor: Int = Color.WHITE): Bitmap {
|
||||
val writer = QRCodeWriter()
|
||||
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size)
|
||||
|
||||
|
||||
@@ -11,10 +11,7 @@ object RelativeTimeFormatter {
|
||||
* Formats a date as relative time for recent dates (within 7 days)
|
||||
* or as full date/time for older dates.
|
||||
*/
|
||||
fun format(
|
||||
context: Context,
|
||||
date: Date?,
|
||||
): String {
|
||||
fun format(context: Context, date: Date?): String {
|
||||
if (date == null) return ""
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
@@ -59,10 +56,7 @@ object RelativeTimeFormatter {
|
||||
* Formats a date as short relative time for compact displays.
|
||||
* Uses shorter format like "2h" instead of "2 hours ago".
|
||||
*/
|
||||
fun formatShort(
|
||||
context: Context,
|
||||
date: Date?,
|
||||
): String {
|
||||
fun formatShort(context: Context, date: Date?): String {
|
||||
if (date == null) return ""
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
@@ -5,9 +5,6 @@ import io.nekohasekai.sfa.compose.util.ProfileIcon
|
||||
/**
|
||||
* Represents a category of Material Icons following Google's official taxonomy
|
||||
*/
|
||||
data class IconCategory(
|
||||
val name: String,
|
||||
val icons: List<ProfileIcon>,
|
||||
) {
|
||||
data class IconCategory(val name: String, val icons: List<ProfileIcon>) {
|
||||
val size: Int get() = icons.size
|
||||
}
|
||||
|
||||
@@ -52,16 +52,12 @@ object MaterialIconsLibrary {
|
||||
/**
|
||||
* Get all icons from all categories
|
||||
*/
|
||||
fun getAllIcons(): List<ProfileIcon> {
|
||||
return categories.flatMap { it.icons }
|
||||
}
|
||||
fun getAllIcons(): List<ProfileIcon> = categories.flatMap { it.icons }
|
||||
|
||||
/**
|
||||
* Get an icon by its ID
|
||||
*/
|
||||
fun getIconById(id: String): ImageVector? {
|
||||
return getAllIcons().find { it.id == id }?.icon
|
||||
}
|
||||
fun getIconById(id: String): ImageVector? = getAllIcons().find { it.id == id }?.icon
|
||||
|
||||
/**
|
||||
* Get the category name for a given icon ID
|
||||
@@ -91,22 +87,16 @@ object MaterialIconsLibrary {
|
||||
/**
|
||||
* Get icons by category name
|
||||
*/
|
||||
fun getIconsByCategory(categoryName: String): List<ProfileIcon> {
|
||||
return categories.find { it.name.equals(categoryName, ignoreCase = true) }?.icons
|
||||
fun getIconsByCategory(categoryName: String): List<ProfileIcon> = categories.find { it.name.equals(categoryName, ignoreCase = true) }?.icons
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of icons in the library
|
||||
*/
|
||||
fun getTotalIconCount(): Int {
|
||||
return categories.sumOf { it.icons.size }
|
||||
}
|
||||
fun getTotalIconCount(): Int = categories.sumOf { it.icons.size }
|
||||
|
||||
/**
|
||||
* Get category names
|
||||
*/
|
||||
fun getCategoryNames(): List<String> {
|
||||
return categories.map { it.name }
|
||||
}
|
||||
fun getCategoryNames(): List<String> = categories.map { it.name }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user