diff --git a/app/src/github/java/io/nekohasekai/sfa/vendor/RootInstaller.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/RootInstaller.kt index 64cf0a0..482ca04 100644 --- a/app/src/github/java/io/nekohasekai/sfa/vendor/RootInstaller.kt +++ b/app/src/github/java/io/nekohasekai/sfa/vendor/RootInstaller.kt @@ -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) diff --git a/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt index fe632b6..149c4f7 100644 --- a/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt +++ b/app/src/github/java/io/nekohasekai/sfa/vendor/SystemPackageInstaller.kt @@ -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) diff --git a/app/src/github/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt b/app/src/github/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt index 68f11d1..7b14573 100644 --- a/app/src/github/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt +++ b/app/src/github/java/io/nekohasekai/sfa/vendor/UpdateWorker.kt @@ -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( - 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") } diff --git a/app/src/main/java/android/content/IIntentReceiver.java b/app/src/main/java/android/content/IIntentReceiver.java index 028a42a..b046168 100644 --- a/app/src/main/java/android/content/IIntentReceiver.java +++ b/app/src/main/java/android/content/IIntentReceiver.java @@ -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); } diff --git a/app/src/main/java/android/content/IIntentSender.java b/app/src/main/java/android/content/IIntentSender.java index 6d008f4..51d7057 100644 --- a/app/src/main/java/android/content/IIntentSender.java +++ b/app/src/main/java/android/content/IIntentSender.java @@ -7,17 +7,23 @@ 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) { - throw new UnsupportedOperationException(); - } - - @Override - public IBinder asBinder() { - return this; - } + abstract class Stub extends Binder implements IIntentSender { + public static IIntentSender asInterface(IBinder binder) { + throw new UnsupportedOperationException(); } + + @Override + public IBinder asBinder() { + return this; + } + } } diff --git a/app/src/main/java/android/content/pm/IPackageInstaller.java b/app/src/main/java/android/content/pm/IPackageInstaller.java index 5805e37..b04ba0d 100644 --- a/app/src/main/java/android/content/pm/IPackageInstaller.java +++ b/app/src/main/java/android/content/pm/IPackageInstaller.java @@ -7,15 +7,20 @@ 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; + IPackageInstallerSession openSession(int sessionId) throws RemoteException; - void abandonSession(int sessionId) throws RemoteException; + void abandonSession(int sessionId) throws RemoteException; - abstract class Stub extends Binder implements IPackageInstaller { - public static IPackageInstaller asInterface(IBinder binder) { - throw new UnsupportedOperationException(); - } + abstract class Stub extends Binder implements IPackageInstaller { + public static IPackageInstaller asInterface(IBinder binder) { + throw new UnsupportedOperationException(); } + } } diff --git a/app/src/main/java/android/content/pm/IPackageInstallerSession.java b/app/src/main/java/android/content/pm/IPackageInstallerSession.java index 9a726a1..6f200af 100644 --- a/app/src/main/java/android/content/pm/IPackageInstallerSession.java +++ b/app/src/main/java/android/content/pm/IPackageInstallerSession.java @@ -6,9 +6,9 @@ import android.os.IInterface; public interface IPackageInstallerSession extends IInterface { - abstract class Stub extends Binder implements IPackageInstallerSession { - public static IPackageInstallerSession asInterface(IBinder binder) { - throw new UnsupportedOperationException(); - } + abstract class Stub extends Binder implements IPackageInstallerSession { + public static IPackageInstallerSession asInterface(IBinder binder) { + throw new UnsupportedOperationException(); } + } } diff --git a/app/src/main/java/io/github/libxposed/service/RemotePreferences.java b/app/src/main/java/io/github/libxposed/service/RemotePreferences.java index dbff984..c822080 100644 --- a/app/src/main/java/io/github/libxposed/service/RemotePreferences.java +++ b/app/src/main/java/io/github/libxposed/service/RemotePreferences.java @@ -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; @@ -26,211 +24,215 @@ import java.util.concurrent.locks.ReentrantLock; @SuppressWarnings("unchecked") public final class RemotePreferences implements SharedPreferences { - private static final String TAG = "RemotePreferences"; - private static final Object CONTENT = new Object(); - private static final Handler HANDLER = new Handler(Looper.getMainLooper()); + private static final String TAG = "RemotePreferences"; + private static final Object CONTENT = new Object(); + private static final Handler HANDLER = new Handler(Looper.getMainLooper()); - private final XposedService mService; - private final String mGroup; - private final Lock mLock = new ReentrantLock(); - private final Map mMap = new ConcurrentHashMap<>(); - private final Map mListeners = Collections.synchronizedMap(new WeakHashMap<>()); + private final XposedService mService; + private final String mGroup; + private final Lock mLock = new ReentrantLock(); + private final Map mMap = new ConcurrentHashMap<>(); + private final Map mListeners = + Collections.synchronizedMap(new WeakHashMap<>()); - private volatile boolean isDeleted = false; + private volatile boolean isDeleted = false; - private RemotePreferences(XposedService service, String group) { - this.mService = service; - this.mGroup = group; + private RemotePreferences(XposedService service, String group) { + this.mService = service; + this.mGroup = group; + } + + @Nullable + static RemotePreferences newInstance(XposedService service, String group) throws RemoteException { + Bundle output = service.getRaw().requestRemotePreferences(group); + if (output == null) return null; + RemotePreferences prefs = new RemotePreferences(service, group); + if (output.containsKey("map")) { + prefs.mMap.putAll((Map) output.getSerializable("map")); } + return prefs; + } - @Nullable - static RemotePreferences newInstance(XposedService service, String group) throws RemoteException { - Bundle output = service.getRaw().requestRemotePreferences(group); - if (output == null) return null; - RemotePreferences prefs = new RemotePreferences(service, group); - if (output.containsKey("map")) { - prefs.mMap.putAll((Map) output.getSerializable("map")); - } - return prefs; - } + void setDeleted() { + this.isDeleted = true; + } - void setDeleted() { - this.isDeleted = true; + @Override + public Map getAll() { + return new TreeMap<>(mMap); + } + + @Nullable + @Override + public String getString(String key, @Nullable String defValue) { + return (String) mMap.getOrDefault(key, defValue); + } + + @Nullable + @Override + public Set getStringSet(String key, @Nullable Set defValues) { + return (Set) mMap.getOrDefault(key, defValues); + } + + @Override + public int getInt(String key, int defValue) { + Integer v = (Integer) mMap.getOrDefault(key, defValue); + assert v != null; + return v; + } + + @Override + public long getLong(String key, long defValue) { + Long v = (Long) mMap.getOrDefault(key, defValue); + assert v != null; + return v; + } + + @Override + public float getFloat(String key, float defValue) { + Float v = (Float) mMap.getOrDefault(key, defValue); + assert v != null; + return v; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + Boolean v = (Boolean) mMap.getOrDefault(key, defValue); + assert v != null; + return v; + } + + @Override + public boolean contains(String key) { + return mMap.containsKey(key); + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + mListeners.put(listener, CONTENT); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + mListeners.remove(listener); + } + + @Override + public Editor edit() { + return new Editor(); + } + + public class Editor implements SharedPreferences.Editor { + + private final HashSet mDelete = new HashSet<>(); + private final HashMap mPut = new HashMap<>(); + + private void put(String key, @NonNull Object value) { + mDelete.remove(key); + mPut.put(key, value); } @Override - public Map getAll() { - return new TreeMap<>(mMap); - } - - @Nullable - @Override - public String getString(String key, @Nullable String defValue) { - return (String) mMap.getOrDefault(key, defValue); - } - - @Nullable - @Override - public Set getStringSet(String key, @Nullable Set defValues) { - return (Set) mMap.getOrDefault(key, defValues); + public SharedPreferences.Editor putString(String key, @Nullable String value) { + if (value == null) remove(key); + else put(key, value); + return this; } @Override - public int getInt(String key, int defValue) { - Integer v = (Integer) mMap.getOrDefault(key, defValue); - assert v != null; - return v; + public SharedPreferences.Editor putStringSet(String key, @Nullable Set values) { + if (values == null) remove(key); + else put(key, values); + return this; } @Override - public long getLong(String key, long defValue) { - Long v = (Long) mMap.getOrDefault(key, defValue); - assert v != null; - return v; + public SharedPreferences.Editor putInt(String key, int value) { + put(key, value); + return this; } @Override - public float getFloat(String key, float defValue) { - Float v = (Float) mMap.getOrDefault(key, defValue); - assert v != null; - return v; + public SharedPreferences.Editor putLong(String key, long value) { + put(key, value); + return this; } @Override - public boolean getBoolean(String key, boolean defValue) { - Boolean v = (Boolean) mMap.getOrDefault(key, defValue); - assert v != null; - return v; + public SharedPreferences.Editor putFloat(String key, float value) { + put(key, value); + return this; } @Override - public boolean contains(String key) { - return mMap.containsKey(key); + public SharedPreferences.Editor putBoolean(String key, boolean value) { + put(key, value); + return this; } @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - mListeners.put(listener, CONTENT); + public SharedPreferences.Editor remove(String key) { + mDelete.add(key); + mPut.remove(key); + return this; } @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - mListeners.remove(listener); + public SharedPreferences.Editor clear() { + mDelete.clear(); + mPut.clear(); + return this; + } + + private void doUpdate(boolean throwing) { + mService.deletionLock.readLock().lock(); + try { + if (isDeleted) { + throw new IllegalStateException("This preferences group has been deleted"); + } + mDelete.forEach(mMap::remove); + mMap.putAll(mPut); + List changes = new ArrayList<>(mDelete.size() + mMap.size()); + changes.addAll(mDelete); + changes.addAll(mMap.keySet()); + for (String key : changes) { + mListeners + .keySet() + .forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key)); + } + + Bundle bundle = new Bundle(); + bundle.putSerializable("delete", mDelete); + bundle.putSerializable("put", mPut); + try { + mService.getRaw().updateRemotePreferences(mGroup, bundle); + } catch (RemoteException e) { + if (throwing) { + throw new RuntimeException(e); + } else { + Log.e(TAG, "Failed to update remote preferences", e); + } + } + } finally { + mService.deletionLock.readLock().unlock(); + } } @Override - public Editor edit() { - return new Editor(); + public boolean commit() { + if (!mLock.tryLock()) return false; + try { + doUpdate(true); + return true; + } finally { + mLock.unlock(); + } } - public class Editor implements SharedPreferences.Editor { - - private final HashSet mDelete = new HashSet<>(); - private final HashMap mPut = new HashMap<>(); - - private void put(String key, @NonNull Object value) { - mDelete.remove(key); - mPut.put(key, value); - } - - @Override - public SharedPreferences.Editor putString(String key, @Nullable String value) { - if (value == null) remove(key); - else put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putStringSet(String key, @Nullable Set values) { - if (values == null) remove(key); - else put(key, values); - return this; - } - - @Override - public SharedPreferences.Editor putInt(String key, int value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putLong(String key, long value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putFloat(String key, float value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor putBoolean(String key, boolean value) { - put(key, value); - return this; - } - - @Override - public SharedPreferences.Editor remove(String key) { - mDelete.add(key); - mPut.remove(key); - return this; - } - - @Override - public SharedPreferences.Editor clear() { - mDelete.clear(); - mPut.clear(); - return this; - } - - private void doUpdate(boolean throwing) { - mService.deletionLock.readLock().lock(); - try { - if (isDeleted) { - throw new IllegalStateException("This preferences group has been deleted"); - } - mDelete.forEach(mMap::remove); - mMap.putAll(mPut); - List changes = new ArrayList<>(mDelete.size() + mMap.size()); - changes.addAll(mDelete); - changes.addAll(mMap.keySet()); - for (String key : changes) { - mListeners.keySet().forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key)); - } - - Bundle bundle = new Bundle(); - bundle.putSerializable("delete", mDelete); - bundle.putSerializable("put", mPut); - try { - mService.getRaw().updateRemotePreferences(mGroup, bundle); - } catch (RemoteException e) { - if (throwing) { - throw new RuntimeException(e); - } else { - Log.e(TAG, "Failed to update remote preferences", e); - } - } - } finally { - mService.deletionLock.readLock().unlock(); - } - } - - @Override - public boolean commit() { - if (!mLock.tryLock()) return false; - try { - doUpdate(true); - return true; - } finally { - mLock.unlock(); - } - } - - @Override - public void apply() { - HANDLER.post(() -> doUpdate(false)); - } + @Override + public void apply() { + HANDLER.post(() -> doUpdate(false)); } + } } diff --git a/app/src/main/java/io/github/libxposed/service/XposedProvider.java b/app/src/main/java/io/github/libxposed/service/XposedProvider.java index 98f56d5..f63e929 100644 --- a/app/src/main/java/io/github/libxposed/service/XposedProvider.java +++ b/app/src/main/java/io/github/libxposed/service/XposedProvider.java @@ -7,58 +7,67 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; public final class XposedProvider extends ContentProvider { - private static final String TAG = "XposedProvider"; + private static final String TAG = "XposedProvider"; - @Override - public boolean onCreate() { - return false; - } + @Override + public boolean onCreate() { + return false; + } - @Nullable - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { - return null; - } + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + return null; + } - @Nullable - @Override - public String getType(@NonNull Uri uri) { - return null; - } + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return null; + } - @Nullable - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - return null; - } + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } + @Override + 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) { - return 0; - } + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } - @Nullable - @Override - public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { - if (method.equals(IXposedService.SEND_BINDER) && extras != null) { - IBinder binder = extras.getBinder("binder"); - if (binder != null) { - Log.d(TAG, "binder received: " + binder); - XposedServiceHelper.onBinderReceived(binder); - } - return new Bundle(); - } - return null; + @Nullable + @Override + public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { + if (method.equals(IXposedService.SEND_BINDER) && extras != null) { + IBinder binder = extras.getBinder("binder"); + if (binder != null) { + Log.d(TAG, "binder received: " + binder); + XposedServiceHelper.onBinderReceived(binder); + } + return new Bundle(); } + return null; + } } diff --git a/app/src/main/java/io/github/libxposed/service/XposedService.java b/app/src/main/java/io/github/libxposed/service/XposedService.java index 4f15994..12cc98b 100644 --- a/app/src/main/java/io/github/libxposed/service/XposedService.java +++ b/app/src/main/java/io/github/libxposed/service/XposedService.java @@ -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,363 +14,359 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; @SuppressWarnings("unused") public final class XposedService { - public final static class ServiceException extends RuntimeException { - ServiceException(String message) { - super(message); - } - - ServiceException(RemoteException e) { - super("Xposed service error", e); - } + public static final class ServiceException extends RuntimeException { + ServiceException(String message) { + super(message); } - private final static Map scopeCallbacks = new WeakHashMap<>(); + ServiceException(RemoteException e) { + super("Xposed service error", e); + } + } + + private static final Map scopeCallbacks = + new WeakHashMap<>(); + + /** 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) {} /** - * Callback interface for module scope request. + * Callback when the request is approved. + * + * @param packageName Package name of requested app */ - public interface OnScopeEventListener { - /** - * Callback when the request notification / window prompted. - * - * @param packageName Package name of requested app - */ - default void onScopeRequestPrompted(String packageName) { - } + default void onScopeRequestApproved(String packageName) {} - /** - * Callback when the request is approved. - * - * @param packageName Package name of requested app - */ - default void onScopeRequestApproved(String packageName) { - } + /** + * Callback when the request is denied. + * + * @param packageName Package name of requested app + */ + default void onScopeRequestDenied(String packageName) {} - /** - * Callback when the request is denied. - * - * @param packageName Package name of requested app - */ - 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) {} - /** - * Callback when the request is timeout or revoked. - * - * @param packageName Package name of requested app - */ - default void onScopeRequestTimeout(String packageName) { - } + /** + * Callback when the request is failed. + * + * @param packageName Package name of requested app + * @param message Error message + */ + default void onScopeRequestFailed(String packageName, String message) {} - /** - * Callback when the request is failed. - * - * @param packageName Package name of requested app - * @param message Error message - */ - default void onScopeRequestFailed(String packageName, String message) { - } - - private IXposedScopeCallback asInterface() { - return scopeCallbacks.computeIfAbsent(this, (listener) -> new IXposedScopeCallback.Stub() { + private IXposedScopeCallback asInterface() { + return scopeCallbacks.computeIfAbsent( + this, + (listener) -> + new IXposedScopeCallback.Stub() { @Override public void onScopeRequestPrompted(String packageName) { - listener.onScopeRequestPrompted(packageName); + listener.onScopeRequestPrompted(packageName); } @Override public void onScopeRequestApproved(String packageName) { - listener.onScopeRequestApproved(packageName); + listener.onScopeRequestApproved(packageName); } @Override public void onScopeRequestDenied(String packageName) { - listener.onScopeRequestDenied(packageName); + listener.onScopeRequestDenied(packageName); } @Override public void onScopeRequestTimeout(String packageName) { - listener.onScopeRequestTimeout(packageName); + listener.onScopeRequestTimeout(packageName); } @Override public void onScopeRequestFailed(String packageName, String message) { - listener.onScopeRequestFailed(packageName, message); + listener.onScopeRequestFailed(packageName, message); } - }); - } + }); } + } - public enum Privilege { - /** - * Unknown privilege value. - */ - FRAMEWORK_PRIVILEGE_UNKNOWN, + public enum Privilege { + /** Unknown privilege value. */ + FRAMEWORK_PRIVILEGE_UNKNOWN, - /** - * The framework is running as root. - */ - FRAMEWORK_PRIVILEGE_ROOT, + /** The framework is running as root. */ + FRAMEWORK_PRIVILEGE_ROOT, - /** - * The framework is running in a container with a fake system_server. - */ - FRAMEWORK_PRIVILEGE_CONTAINER, + /** 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. - */ - FRAMEWORK_PRIVILEGE_APP, - - /** - * The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will be null and remote file is unsupported. - */ - FRAMEWORK_PRIVILEGE_EMBEDDED - } - - private final IXposedService mService; - private final Map mRemotePrefs = new HashMap<>(); - - final ReentrantReadWriteLock deletionLock = new ReentrantReadWriteLock(); - - XposedService(IXposedService service) { - mService = service; - } - - IXposedService getRaw() { - return mService; - } + /** The framework is running as a different app, which may have at most shell permission. */ + FRAMEWORK_PRIVILEGE_APP, /** - * Get the Xposed API version of current implementation. - * - * @return API version - * @throws ServiceException If the service is dead or an error occurred + * The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will + * be null and remote file is unsupported. */ - public int getAPIVersion() { - try { - return mService.getAPIVersion(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } + FRAMEWORK_PRIVILEGE_EMBEDDED + } - /** - * Get the Xposed framework name of current implementation. - * - * @return Framework name - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public String getFrameworkName() { - try { - return mService.getFrameworkName(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } + private final IXposedService mService; + private final Map mRemotePrefs = new HashMap<>(); - /** - * Get the Xposed framework version of current implementation. - * - * @return Framework version - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public String getFrameworkVersion() { - try { - return mService.getFrameworkVersion(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } + final ReentrantReadWriteLock deletionLock = new ReentrantReadWriteLock(); - /** - * Get the Xposed framework version code of current implementation. - * - * @return Framework version code - * @throws ServiceException If the service is dead or an error occurred - */ - public long getFrameworkVersionCode() { - try { - return mService.getFrameworkVersionCode(); - } catch (RemoteException e) { - throw new ServiceException(e); - } - } + XposedService(IXposedService service) { + mService = service; + } - /** - * Get the Xposed framework privilege of current implementation. - * - * @return Framework privilege - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public Privilege getFrameworkPrivilege() { - try { - int value = mService.getFrameworkPrivilege(); - return (value >= 0 && value <= 3) ? Privilege.values()[value + 1] : Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN; - } catch (RemoteException e) { - throw new ServiceException(e); - } - } + IXposedService getRaw() { + return mService; + } - /** - * Get the application scope of current module. - * - * @return Module scope - * @throws ServiceException If the service is dead or an error occurred - */ - @NonNull - public List getScope() { - try { - return mService.getScope(); - } catch (RemoteException e) { - throw new ServiceException(e); - } + /** + * Get the Xposed API version of current implementation. + * + * @return API version + * @throws ServiceException If the service is dead or an error occurred + */ + public int getAPIVersion() { + try { + return mService.getAPIVersion(); + } catch (RemoteException e) { + throw new ServiceException(e); } + } - /** - * Request to add a new app to the module scope. - * - * @param packageName Package name of the app to be added - * @param callback Callback to be invoked when the request is completed or error occurred - * @throws ServiceException If the service is dead or an error occurred - */ - public void requestScope(@NonNull String packageName, @NonNull OnScopeEventListener callback) { - try { - mService.requestScope(packageName, callback.asInterface()); - } catch (RemoteException e) { - throw new ServiceException(e); - } + /** + * Get the Xposed framework name of current implementation. + * + * @return Framework name + * @throws ServiceException If the service is dead or an error occurred + */ + @NonNull + public String getFrameworkName() { + try { + return mService.getFrameworkName(); + } catch (RemoteException e) { + throw new ServiceException(e); } + } - /** - * Remove an app from the module scope. - * - * @param packageName Package name of the app to be added - * @return null if successful, or non-null with error message - * @throws ServiceException If the service is dead or an error occurred - */ - @Nullable - public String removeScope(@NonNull String packageName) { - try { - return mService.removeScope(packageName); - } catch (RemoteException e) { - throw new ServiceException(e); - } + /** + * Get the Xposed framework version of current implementation. + * + * @return Framework version + * @throws ServiceException If the service is dead or an error occurred + */ + @NonNull + public String getFrameworkVersion() { + try { + return mService.getFrameworkVersion(); + } catch (RemoteException e) { + throw new ServiceException(e); } + } - /** - * Get remote preferences from Xposed framework. If the group does not exist, it will be created. - * - * @param group Group name - * @return The preferences - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework is embedded - */ - @NonNull - public SharedPreferences getRemotePreferences(@NonNull String group) { - return mRemotePrefs.computeIfAbsent(group, k -> { - try { - RemotePreferences instance = RemotePreferences.newInstance(this, k); - if (instance == null) { - throw new ServiceException("Framework returns null"); - } - return instance; - } catch (RemoteException e) { - if (e.getCause() instanceof UnsupportedOperationException cause) { - throw cause; - } - throw new ServiceException(e); + /** + * Get the Xposed framework version code of current implementation. + * + * @return Framework version code + * @throws ServiceException If the service is dead or an error occurred + */ + public long getFrameworkVersionCode() { + try { + return mService.getFrameworkVersionCode(); + } catch (RemoteException e) { + throw new ServiceException(e); + } + } + + /** + * Get the Xposed framework privilege of current implementation. + * + * @return Framework privilege + * @throws ServiceException If the service is dead or an error occurred + */ + @NonNull + public Privilege getFrameworkPrivilege() { + try { + int value = mService.getFrameworkPrivilege(); + return (value >= 0 && value <= 3) + ? Privilege.values()[value + 1] + : Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN; + } catch (RemoteException e) { + throw new ServiceException(e); + } + } + + /** + * Get the application scope of current module. + * + * @return Module scope + * @throws ServiceException If the service is dead or an error occurred + */ + @NonNull + public List getScope() { + try { + return mService.getScope(); + } catch (RemoteException e) { + throw new ServiceException(e); + } + } + + /** + * Request to add a new app to the module scope. + * + * @param packageName Package name of the app to be added + * @param callback Callback to be invoked when the request is completed or error occurred + * @throws ServiceException If the service is dead or an error occurred + */ + public void requestScope(@NonNull String packageName, @NonNull OnScopeEventListener callback) { + try { + mService.requestScope(packageName, callback.asInterface()); + } catch (RemoteException e) { + throw new ServiceException(e); + } + } + + /** + * Remove an app from the module scope. + * + * @param packageName Package name of the app to be added + * @return null if successful, or non-null with error message + * @throws ServiceException If the service is dead or an error occurred + */ + @Nullable + public String removeScope(@NonNull String packageName) { + try { + return mService.removeScope(packageName); + } catch (RemoteException e) { + throw new ServiceException(e); + } + } + + /** + * Get remote preferences from Xposed framework. If the group does not exist, it will be created. + * + * @param group Group name + * @return The preferences + * @throws ServiceException If the service is dead or an error occurred + * @throws UnsupportedOperationException If the framework is embedded + */ + @NonNull + public SharedPreferences getRemotePreferences(@NonNull String group) { + return mRemotePrefs.computeIfAbsent( + group, + k -> { + try { + RemotePreferences instance = RemotePreferences.newInstance(this, k); + if (instance == null) { + throw new ServiceException("Framework returns null"); } + return instance; + } catch (RemoteException e) { + if (e.getCause() instanceof UnsupportedOperationException cause) { + throw cause; + } + throw new ServiceException(e); + } }); - } + } - /** - * Delete a group of remote preferences. - * - * @param group Group name - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework is embedded - */ - public void deleteRemotePreferences(@NonNull String group) { - deletionLock.writeLock().lock(); - try { - mService.deleteRemotePreferences(group); - mRemotePrefs.computeIfPresent(group, (k, v) -> { - v.setDeleted(); - return null; - }); - } catch (RemoteException e) { - if (e.getCause() instanceof UnsupportedOperationException cause) { - throw cause; - } - throw new ServiceException(e); - } finally { - deletionLock.writeLock().unlock(); - } + /** + * Delete a group of remote preferences. + * + * @param group Group name + * @throws ServiceException If the service is dead or an error occurred + * @throws UnsupportedOperationException If the framework is embedded + */ + public void deleteRemotePreferences(@NonNull String group) { + deletionLock.writeLock().lock(); + try { + mService.deleteRemotePreferences(group); + mRemotePrefs.computeIfPresent( + group, + (k, v) -> { + v.setDeleted(); + return null; + }); + } catch (RemoteException e) { + if (e.getCause() instanceof UnsupportedOperationException cause) { + throw cause; + } + throw new ServiceException(e); + } finally { + deletionLock.writeLock().unlock(); } + } - /** - * List all files in the module's shared data directory. - * - * @return The file list - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework is embedded - */ - @NonNull - public String[] listRemoteFiles() { - try { - String[] files = mService.listRemoteFiles(); - if (files == null) throw new ServiceException("Framework returns null"); - return files; - } catch (RemoteException e) { - if (e.getCause() instanceof UnsupportedOperationException cause) { - throw cause; - } - throw new ServiceException(e); - } + /** + * List all files in the module's shared data directory. + * + * @return The file list + * @throws ServiceException If the service is dead or an error occurred + * @throws UnsupportedOperationException If the framework is embedded + */ + @NonNull + public String[] listRemoteFiles() { + try { + String[] files = mService.listRemoteFiles(); + if (files == null) throw new ServiceException("Framework returns null"); + return files; + } catch (RemoteException e) { + if (e.getCause() instanceof UnsupportedOperationException cause) { + throw cause; + } + throw new ServiceException(e); } + } - /** - * Open a file in the module's shared data directory. The file will be created if not exists. - * - * @param name File name, must not contain path separators and . or .. - * @return The file descriptor - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework is embedded - */ - @NonNull - public ParcelFileDescriptor openRemoteFile(@NonNull String name) { - try { - ParcelFileDescriptor file = mService.openRemoteFile(name); - if (file == null) throw new ServiceException("Framework returns null"); - return file; - } catch (RemoteException e) { - if (e.getCause() instanceof UnsupportedOperationException cause) { - throw cause; - } - throw new ServiceException(e); - } + /** + * Open a file in the module's shared data directory. The file will be created if not exists. + * + * @param name File name, must not contain path separators and . or .. + * @return The file descriptor + * @throws ServiceException If the service is dead or an error occurred + * @throws UnsupportedOperationException If the framework is embedded + */ + @NonNull + public ParcelFileDescriptor openRemoteFile(@NonNull String name) { + try { + ParcelFileDescriptor file = mService.openRemoteFile(name); + if (file == null) throw new ServiceException("Framework returns null"); + return file; + } catch (RemoteException e) { + if (e.getCause() instanceof UnsupportedOperationException cause) { + throw cause; + } + throw new ServiceException(e); } + } - /** - * Delete a file in the module's shared data directory. - * - * @param name File name, must not contain path separators and . or .. - * @return true if successful, false if the file does not exist - * @throws ServiceException If the service is dead or an error occurred - * @throws UnsupportedOperationException If the framework is embedded - */ - public boolean deleteRemoteFile(@NonNull String name) { - try { - return mService.deleteRemoteFile(name); - } catch (RemoteException e) { - if (e.getCause() instanceof UnsupportedOperationException cause) { - throw cause; - } - throw new ServiceException(e); - } + /** + * Delete a file in the module's shared data directory. + * + * @param name File name, must not contain path separators and . or .. + * @return true if successful, false if the file does not exist + * @throws ServiceException If the service is dead or an error occurred + * @throws UnsupportedOperationException If the framework is embedded + */ + public boolean deleteRemoteFile(@NonNull String name) { + try { + return mService.deleteRemoteFile(name); + } catch (RemoteException e) { + if (e.getCause() instanceof UnsupportedOperationException cause) { + throw cause; + } + throw new ServiceException(e); } + } } diff --git a/app/src/main/java/io/github/libxposed/service/XposedServiceHelper.java b/app/src/main/java/io/github/libxposed/service/XposedServiceHelper.java index 8b8c883..0936ad1 100644 --- a/app/src/main/java/io/github/libxposed/service/XposedServiceHelper.java +++ b/app/src/main/java/io/github/libxposed/service/XposedServiceHelper.java @@ -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,67 +10,63 @@ import java.util.Set; @SuppressWarnings("unused") public final class XposedServiceHelper { + /** Callback interface for Xposed service. */ + public interface OnServiceListener { /** - * Callback interface for Xposed service. - */ - public interface OnServiceListener { - /** - * Callback when the service is connected.
- * 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. - */ - void onServiceDied(@NonNull XposedService service); - } - - private static final String TAG = "XposedServiceHelper"; - private static final Set mCache = new HashSet<>(); - private static OnServiceListener mListener = null; - - static void onBinderReceived(IBinder binder) { - if (binder == null) return; - synchronized (mCache) { - try { - XposedService service = new XposedService(IXposedService.Stub.asInterface(binder)); - if (mListener == null) { - mCache.add(service); - } else { - binder.linkToDeath(() -> mListener.onServiceDied(service), 0); - mListener.onServiceBind(service); - } - } catch (Throwable t) { - Log.e(TAG, "onBinderReceived", t); - } - } - } - - /** - * Register a ServiceListener to receive service binders from Xposed frameworks.
- * This method should only be called once. + * Callback when the service is connected.
+ * This method could be called multiple times if multiple Xposed frameworks exist. * - * @param listener Listener to register + * @param service Service instance */ - public static void registerListener(OnServiceListener listener) { - synchronized (mCache) { - mListener = listener; - if (!mCache.isEmpty()) { - for (Iterator it = mCache.iterator(); it.hasNext(); ) { - try { - XposedService service = it.next(); - service.getRaw().asBinder().linkToDeath(() -> mListener.onServiceDied(service), 0); - mListener.onServiceBind(service); - } catch (Throwable t) { - Log.e(TAG, "registerListener", t); - it.remove(); - } - } - mCache.clear(); - } + void onServiceBind(@NonNull XposedService service); + + /** Callback when the service is dead. */ + void onServiceDied(@NonNull XposedService service); + } + + private static final String TAG = "XposedServiceHelper"; + private static final Set mCache = new HashSet<>(); + private static OnServiceListener mListener = null; + + static void onBinderReceived(IBinder binder) { + if (binder == null) return; + synchronized (mCache) { + try { + XposedService service = new XposedService(IXposedService.Stub.asInterface(binder)); + if (mListener == null) { + mCache.add(service); + } else { + binder.linkToDeath(() -> mListener.onServiceDied(service), 0); + mListener.onServiceBind(service); } + } catch (Throwable t) { + Log.e(TAG, "onBinderReceived", t); + } } + } + + /** + * Register a ServiceListener to receive service binders from Xposed frameworks.
+ * This method should only be called once. + * + * @param listener Listener to register + */ + public static void registerListener(OnServiceListener listener) { + synchronized (mCache) { + mListener = listener; + if (!mCache.isEmpty()) { + for (Iterator it = mCache.iterator(); it.hasNext(); ) { + try { + XposedService service = it.next(); + service.getRaw().asBinder().linkToDeath(() -> mListener.onServiceDied(service), 0); + mListener.onServiceBind(service); + } catch (Throwable t) { + Log.e(TAG, "registerListener", t); + it.remove(); + } + } + mCache.clear(); + } + } + } } diff --git a/app/src/main/java/io/nekohasekai/sfa/Application.kt b/app/src/main/java/io/nekohasekai/sfa/Application.kt index 2e196b3..bb5ba70 100644 --- a/app/src/main/java/io/nekohasekai/sfa/Application.kt +++ b/app/src/main/java/io/nekohasekai/sfa/Application.kt @@ -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 diff --git a/app/src/main/java/io/nekohasekai/sfa/WorkingDirectoryProvider.kt b/app/src/main/java/io/nekohasekai/sfa/WorkingDirectoryProvider.kt index dde9466..b293dec 100644 --- a/app/src/main/java/io/nekohasekai/sfa/WorkingDirectoryProvider.kt +++ b/app/src/main/java/io/nekohasekai/sfa/WorkingDirectoryProvider.kt @@ -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?, - sortOrder: String? - ): Cursor { + override fun queryChildDocuments(parentDocumentId: String, projection: Array?, 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) diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt b/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt index d76140d..0aaeb84 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt @@ -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") diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/BootReceiver.kt b/app/src/main/java/io/nekohasekai/sfa/bg/BootReceiver.kt index 7e30618..013406c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/BootReceiver.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/BootReceiver.kt @@ -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 -> { } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt index cff5b70..2a5af61 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt @@ -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() diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/DebugInfoExporter.kt b/app/src/main/java/io/nekohasekai/sfa/bg/DebugInfoExporter.kt index 2904ec0..a45c8d2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/DebugInfoExporter.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/DebugInfoExporter.kt @@ -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, - context: Context, - ): Int { + private fun addLogEntries(zip: ZipOutputStream, warnings: MutableList, 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, - packageName: String, - ): Int { + private fun addSystemEntries(zip: ZipOutputStream, warnings: MutableList, 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++ @@ -210,27 +201,28 @@ object DebugInfoExporter { if (cmdPackages != null) count++ if ((cmdPackages == null || cmdPackages.bytes == 0L) && (cmdPackages?.exitCode ?: 1) != 0) { if (streamCommandToZip( - zip, - "system/packages_pm.txt", - warnings, - listOf("pm", "list", "packages", "-f"), - ) != null) count++ + zip, + "system/packages_pm.txt", + warnings, + listOf("pm", "list", "packages", "-f"), + ) != null + ) { + count++ + } } if (streamCommandToZip( - zip, - "system/dumpsys_package_${packageName}.txt", - warnings, - listOf("dumpsys", "package", packageName), - ) != null) count++ + zip, + "system/dumpsys_package_$packageName.txt", + warnings, + listOf("dumpsys", "package", packageName), + ) != null + ) { + count++ + } return count } - private fun addFileEntry( - zip: ZipOutputStream, - file: File, - entryName: String, - warnings: MutableList, - ): Boolean { + private fun addFileEntry(zip: ZipOutputStream, file: File, entryName: String, warnings: MutableList): Boolean { if (!file.isFile) { warnings.add("missing file: ${file.path}") return false @@ -262,51 +254,40 @@ 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, command: List, - ): CommandResult? { - return try { - val process = ProcessBuilder(command).redirectErrorStream(true).start() - val entry = ZipEntry(entryName) - zip.putNextEntry(entry) - var bytes = 0L - process.inputStream.use { input -> - val buffer = ByteArray(16 * 1024) - while (true) { - val read = input.read(buffer) - if (read <= 0) break - zip.write(buffer, 0, read) - bytes += read - } + ): CommandResult? = try { + val process = ProcessBuilder(command).redirectErrorStream(true).start() + val entry = ZipEntry(entryName) + zip.putNextEntry(entry) + var bytes = 0L + process.inputStream.use { input -> + val buffer = ByteArray(16 * 1024) + while (true) { + val read = input.read(buffer) + if (read <= 0) break + zip.write(buffer, 0, read) + bytes += read } - zip.closeEntry() - val code = process.waitFor() - if (code != 0) { - warnings.add("command failed (${command.joinToString(" ")}): exit=$code") - } - CommandResult(code, bytes) - } catch (e: Throwable) { - warnings.add("command failed (${command.joinToString(" ")}): ${e.message}") - runCatching { zip.closeEntry() } - null } + zip.closeEntry() + val code = process.waitFor() + if (code != 0) { + warnings.add("command failed (${command.joinToString(" ")}): exit=$code") + } + CommandResult(code, bytes) + } catch (e: Throwable) { + warnings.add("command failed (${command.joinToString(" ")}): ${e.message}") + runCatching { zip.closeEntry() } + null } - private fun buildError( - stage: String, - detail: String, - throwable: Throwable?, - warnings: List, - outputPath: String?, - ): String { + private fun buildError(stage: String, detail: String, throwable: Throwable?, warnings: List, outputPath: String?): String { val sb = StringBuilder() sb.append("stage=").append(stage).append('\n') if (!outputPath.isNullOrBlank()) { diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkListener.kt b/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkListener.kt index 60c0a8a..d7dfc2b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkListener.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkListener.kt @@ -60,103 +60,102 @@ object DefaultNetworkListener { val listeners = mutableMapOf Unit>() var network: Network? = null val pendingRequests = arrayListOf() - for (message in channel) when (message) { - is NetworkMessage.Start -> { - if (listeners.isEmpty()) register() - listeners[message.key] = message.listener - if (network != null) message.listener(network) - } - - is NetworkMessage.Get -> { - check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" } - if (network == null) { - pendingRequests += message - } else { - message.response.complete( - network, - ) - } - } - - is NetworkMessage.Stop -> - if (listeners.isNotEmpty() && // was not empty - listeners.remove(message.key) != null && listeners.isEmpty() - ) { - network = null - unregister() + for (message in channel) { + when (message) { + is NetworkMessage.Start -> { + if (listeners.isEmpty()) register() + listeners[message.key] = message.listener + if (network != null) message.listener(network) } - is NetworkMessage.Put -> { - network = message.network - pendingRequests.forEach { it.response.complete(message.network) } - pendingRequests.clear() - listeners.values.forEach { it(network) } - } - - is NetworkMessage.Update -> - if (network == message.network) { - listeners.values.forEach { - it( + is NetworkMessage.Get -> { + check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" } + if (network == null) { + pendingRequests += message + } else { + message.response.complete( network, ) } } - is NetworkMessage.Lost -> - if (network == message.network) { - network = null - listeners.values.forEach { it(null) } + is NetworkMessage.Stop -> + if (listeners.isNotEmpty() && + // was not empty + listeners.remove(message.key) != null && + listeners.isEmpty() + ) { + network = null + unregister() + } + + is NetworkMessage.Put -> { + network = message.network + pendingRequests.forEach { it.response.complete(message.network) } + pendingRequests.clear() + listeners.values.forEach { it(network) } } + + is NetworkMessage.Update -> + if (network == message.network) { + listeners.values.forEach { + it( + network, + ) + } + } + + is NetworkMessage.Lost -> + if (network == message.network) { + network = null + listeners.values.forEach { it(null) } + } + } } } - 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 { - networkActor.send(this) - response.await() + } 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 { - networkActor.send( - NetworkMessage.Put( - network, - ), - ) - } + override fun onAvailable(network: Network) = runBlocking { + networkActor.send( + NetworkMessage.Put( + network, + ), + ) + } - 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 { - networkActor.send( - NetworkMessage.Lost( - network, - ), - ) - } + override fun onLost(network: Network) = runBlocking { + networkActor.send( + NetworkMessage.Lost( + network, + ), + ) + } } private var fallback = false diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkMonitor.kt b/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkMonitor.kt index 9c44237..3c02e04 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkMonitor.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/DefaultNetworkMonitor.kt @@ -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) } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/LocalResolver.kt b/app/src/main/java/io/nekohasekai/sfa/bg/LocalResolver.kt index 42d14ad..26f0254 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/LocalResolver.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/LocalResolver.kt @@ -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 { - 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> { @Suppress("ThrowableNotThrown") - override fun onAnswer( - answer: Collection, - rcode: Int, - ) { + override fun onAnswer(answer: Collection, rcode: Int) { if (rcode == 0) { ctx.success( (answer as Collection).mapNotNull { it?.hostAddress } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/LogEntry.java b/app/src/main/java/io/nekohasekai/sfa/bg/LogEntry.java index d2840a3..f8b2ed3 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/LogEntry.java +++ b/app/src/main/java/io/nekohasekai/sfa/bg/LogEntry.java @@ -2,64 +2,66 @@ package io.nekohasekai.sfa.bg; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class LogEntry implements Parcelable { - public static final int LEVEL_DEBUG = 0; - public static final int LEVEL_INFO = 1; - public static final int LEVEL_WARN = 2; - public static final int LEVEL_ERROR = 3; + public static final int LEVEL_DEBUG = 0; + public static final int LEVEL_INFO = 1; + public static final int LEVEL_WARN = 2; + public static final int LEVEL_ERROR = 3; - public final int level; - public final long timestamp; - @NonNull - public final String source; - @NonNull - public final String message; - @Nullable - public final String stackTrace; + public final int level; + public final long timestamp; + @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) { - this.level = level; - this.timestamp = timestamp; - this.source = source; - this.message = message; - this.stackTrace = 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; + this.message = message; + this.stackTrace = stackTrace; + } - protected LogEntry(Parcel in) { - level = in.readInt(); - timestamp = in.readLong(); - source = in.readString(); - message = in.readString(); - stackTrace = in.readString(); - } + protected LogEntry(Parcel in) { + level = in.readInt(); + timestamp = in.readLong(); + source = in.readString(); + message = in.readString(); + stackTrace = in.readString(); + } - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeInt(level); - dest.writeLong(timestamp); - dest.writeString(source); - dest.writeString(message); - dest.writeString(stackTrace); - } + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(level); + dest.writeLong(timestamp); + dest.writeString(source); + dest.writeString(message); + dest.writeString(stackTrace); + } - @Override - public int describeContents() { - return 0; - } + @Override + public int describeContents() { + return 0; + } - public static final Creator CREATOR = new Creator<>() { + public static final Creator CREATOR = + new Creator<>() { @Override public LogEntry createFromParcel(Parcel in) { - return new LogEntry(in); + return new LogEntry(in); } @Override public LogEntry[] newArray(int size) { - return new LogEntry[size]; + return new LogEntry[size]; } - }; + }; } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/PackageEntry.java b/app/src/main/java/io/nekohasekai/sfa/bg/PackageEntry.java index 42735b9..c447193 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/PackageEntry.java +++ b/app/src/main/java/io/nekohasekai/sfa/bg/PackageEntry.java @@ -2,40 +2,39 @@ 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; - } + public PackageEntry(@NonNull String packageName) { + this.packageName = packageName; + } - protected PackageEntry(Parcel in) { - packageName = in.readString(); - } + protected PackageEntry(Parcel in) { + packageName = in.readString(); + } - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString(packageName); - } + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(packageName); + } - @Override - public int describeContents() { - return 0; - } + @Override + public int describeContents() { + return 0; + } - public static final Creator CREATOR = new Creator<>() { + public static final Creator CREATOR = + new Creator<>() { @Override public PackageEntry createFromParcel(Parcel in) { - return new PackageEntry(in); + return new PackageEntry(in); } @Override public PackageEntry[] newArray(int size) { - return new PackageEntry[size]; + return new PackageEntry[size]; } - }; + }; } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ParceledListSlice.java b/app/src/main/java/io/nekohasekai/sfa/bg/ParceledListSlice.java index d72419d..9840067 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ParceledListSlice.java +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ParceledListSlice.java @@ -21,132 +21,132 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; - import java.util.ArrayList; import java.util.List; public class ParceledListSlice implements Parcelable { - private static final int MAX_IPC_SIZE = 64 * 1024; + private static final int MAX_IPC_SIZE = 64 * 1024; - private final List mList; + private final List mList; - public ParceledListSlice(List list) { - mList = list; + public ParceledListSlice(List list) { + mList = list; + } + + private ParceledListSlice(Parcel in, ClassLoader loader) { + final int n = in.readInt(); + mList = new ArrayList<>(n); + if (n <= 0) { + return; } - private ParceledListSlice(Parcel in, ClassLoader loader) { - final int n = in.readInt(); - mList = new ArrayList<>(n); - if (n <= 0) { - return; - } + int i = 0; + while (i < n) { + if (in.readInt() == 0) { + break; + } + @SuppressWarnings("unchecked") + T item = (T) in.readParcelable(loader); + mList.add(item); + i++; + } + if (i >= n) { + return; + } + final IBinder retriever = in.readStrongBinder(); + while (i < n) { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInt(i); + try { + retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + reply.recycle(); + data.recycle(); + return; + } + while (i < n && reply.readInt() != 0) { + @SuppressWarnings("unchecked") + T item = (T) reply.readParcelable(loader); + mList.add(item); + i++; + } + reply.recycle(); + data.recycle(); + } + } - int i = 0; - while (i < n) { - if (in.readInt() == 0) { - break; - } - @SuppressWarnings("unchecked") - T item = (T) in.readParcelable(loader); - mList.add(item); - i++; - } - if (i >= n) { - return; - } - final IBinder retriever = in.readStrongBinder(); - while (i < n) { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - data.writeInt(i); - try { - retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0); - } catch (RemoteException e) { - reply.recycle(); - data.recycle(); - return; - } - while (i < n && reply.readInt() != 0) { - @SuppressWarnings("unchecked") - T item = (T) reply.readParcelable(loader); - mList.add(item); + public List getList() { + return mList; + } + + @Override + public int describeContents() { + int contents = 0; + for (int i = 0; i < mList.size(); i++) { + contents |= mList.get(i).describeContents(); + } + return contents; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + final int n = mList.size(); + dest.writeInt(n); + if (n <= 0) { + return; + } + int i = 0; + while (i < n && dest.dataSize() < MAX_IPC_SIZE) { + dest.writeInt(1); + dest.writeParcelable(mList.get(i), flags); + i++; + } + if (i < n) { + dest.writeInt(0); + final int start = i; + Binder retriever = + new Binder() { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + if (code != FIRST_CALL_TRANSACTION) { + return super.onTransact(code, data, reply, flags); + } + int i = data.readInt(); + if (i < start || i > n) { + return false; + } + while (i < n && reply.dataSize() < MAX_IPC_SIZE) { + reply.writeInt(1); + reply.writeParcelable(mList.get(i), flags); i++; + } + if (i < n) { + reply.writeInt(0); + } + return true; } - reply.recycle(); - data.recycle(); - } + }; + dest.writeStrongBinder(retriever); } + } - public List getList() { - return mList; - } - - @Override - public int describeContents() { - int contents = 0; - for (int i = 0; i < mList.size(); i++) { - contents |= mList.get(i).describeContents(); + public static final Parcelable.ClassLoaderCreator CREATOR = + new Parcelable.ClassLoaderCreator() { + @Override + public ParceledListSlice createFromParcel(Parcel in) { + return new ParceledListSlice(in, null); } - return contents; - } - @Override - public void writeToParcel(Parcel dest, int flags) { - final int n = mList.size(); - dest.writeInt(n); - if (n <= 0) { - return; + @Override + public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { + return new ParceledListSlice(in, loader); } - int i = 0; - while (i < n && dest.dataSize() < MAX_IPC_SIZE) { - dest.writeInt(1); - dest.writeParcelable(mList.get(i), flags); - i++; + + @Override + public ParceledListSlice[] newArray(int size) { + return new ParceledListSlice[size]; } - if (i < n) { - dest.writeInt(0); - final int start = i; - Binder retriever = new Binder() { - @Override - protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) - throws RemoteException { - if (code != FIRST_CALL_TRANSACTION) { - return super.onTransact(code, data, reply, flags); - } - int i = data.readInt(); - if (i < start || i > n) { - return false; - } - while (i < n && reply.dataSize() < MAX_IPC_SIZE) { - reply.writeInt(1); - reply.writeParcelable(mList.get(i), flags); - i++; - } - if (i < n) { - reply.writeInt(0); - } - return true; - } - }; - dest.writeStrongBinder(retriever); - } - } - - public static final Parcelable.ClassLoaderCreator CREATOR = - new Parcelable.ClassLoaderCreator() { - @Override - public ParceledListSlice createFromParcel(Parcel in) { - return new ParceledListSlice(in, null); - } - - @Override - public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { - return new ParceledListSlice(in, loader); - } - - @Override - public ParceledListSlice[] newArray(int size) { - return new ParceledListSlice[size]; - } - }; + }; } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt b/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt index 0fa2a14..fa7cea5 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt @@ -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) : - NetworkInterfaceIterator { - override fun hasNext(): Boolean { - return iterator.hasNext() - } + private class InterfaceArray(private val iterator: Iterator) : 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) : StringIterator { @@ -199,21 +184,15 @@ interface PlatformInterfaceWrapper : PlatformInterface { return 0 } - override fun hasNext(): Boolean { - return iterator.hasNext() - } + override fun hasNext(): Boolean = iterator.hasNext() - override fun next(): String { - return iterator.next() - } + override fun next(): String = iterator.next() } - private fun InterfaceAddress.toPrefix(): String { - return if (address is Inet6Address) { - "${Inet6Address.getByAddress(address.address).hostAddress}/$networkPrefixLength" - } else { - "${address.hostAddress}/$networkPrefixLength" - } + private fun InterfaceAddress.toPrefix(): String = if (address is Inet6Address) { + "${Inet6Address.getByAddress(address.address).hostAddress}/$networkPrefixLength" + } else { + "${address.hostAddress}/$networkPrefixLength" } private val NetworkInterface.flags: Int diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt index a7dc553..74c0412 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt @@ -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() diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt b/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt index 53e099d..ab33003 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt @@ -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 list diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt b/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt index 1d95894..352d159 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt @@ -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 { + override fun getInstalledPackages(flags: Int, userId: Int): ParceledListSlice { 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( - this@RootServer, - outputPath!!, - BuildConfig.APPLICATION_ID - ) - } + override fun exportDebugInfo(outputPath: String?): String = DebugInfoExporter.export( + this@RootServer, + outputPath!!, + BuildConfig.APPLICATION_ID, + ) } - override fun onBind(intent: Intent): IBinder { - return binder - } + override fun onBind(intent: Intent): IBinder = binder } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceBinder.kt b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceBinder.kt index 7e133a3..c430114 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceBinder.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceBinder.kt @@ -43,9 +43,7 @@ class ServiceBinder(private val status: MutableLiveData) : 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) diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceConnection.kt b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceConnection.kt index fbd17b8..d6c76c9 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceConnection.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceConnection.kt @@ -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) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt index db10ede..bd1d7fb 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt @@ -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, - private val service: Service, -) : BroadcastReceiver(), CommandClient.Handler { +class ServiceNotification(private val status: MutableLiveData, 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() diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/TileService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/TileService.kt index 4174a53..3b96002 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/TileService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/TileService.kt @@ -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) { diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt b/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt index 797a776..8f49694 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt @@ -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 = diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt index 8827062..d3374ea 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt @@ -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) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/LineChart.kt b/app/src/main/java/io/nekohasekai/sfa/compose/LineChart.kt index 6df820a..867de45 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/LineChart.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/LineChart.kt @@ -41,9 +41,9 @@ fun LineChart( Canvas( modifier = - modifier - .fillMaxWidth() - .height(80.dp), + modifier + .fillMaxWidth() + .height(80.dp), ) { val width = size.width val height = size.height @@ -96,11 +96,11 @@ fun LineChart( path = path, color = lineColor, style = - Stroke( - width = 2.dp.toPx(), - cap = StrokeCap.Round, - join = StrokeJoin.Round, - ), + Stroke( + width = 2.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round, + ), ) // Draw gradient fill under the line diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt index 522ebfd..36e2311 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/MainActivity.kt @@ -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,21 +254,20 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback { } } - private suspend fun prepare() = - withContext(Dispatchers.Main) { - try { - val intent = VpnService.prepare(this@MainActivity) - if (intent != null) { - prepareLauncher.launch(intent) - true - } else { - false - } - } catch (e: Exception) { - onServiceAlert(Alert.RequestVPNPermission, e.message) + private suspend fun prepare() = withContext(Dispatchers.Main) { + try { + val intent = VpnService.prepare(this@MainActivity) + if (intent != null) { + prepareLauncher.launch(intent) true + } else { + false } + } catch (e: Exception) { + onServiceAlert(Alert.RequestVPNPermission, e.message) + true } + } @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -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 @@ -729,17 +732,17 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback { icon = { Icon( imageVector = - if (isRunning || isStopping) { - Icons.Default.Stop - } else { - Icons.Default.PlayArrow - }, + if (isRunning || isStopping) { + Icons.Default.Stop + } else { + Icons.Default.PlayArrow + }, contentDescription = - if (isRunning || isStopping) { - stringResource(R.string.stop) - } else { - stringResource(R.string.action_start) - }, + if (isRunning || isStopping) { + stringResource(R.string.stop) + } else { + stringResource(R.string.action_start) + }, ) }, text = { @@ -873,9 +876,9 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback { }, label = { Text(stringResource(screen.titleRes)) }, selected = - currentDestination?.hierarchy?.any { - it.route == screen.route - } == true, + currentDestination?.hierarchy?.any { + it.route == screen.route + } == true, onClick = { navController.navigate(screen.route) { // Pop up to the start destination of the graph to @@ -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)) }, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/base/BaseViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/base/BaseViewModel.kt index c896377..b78d059 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/base/BaseViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/base/BaseViewModel.kt @@ -56,10 +56,7 @@ abstract class BaseViewModel : 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) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/base/GlobalEventBus.kt b/app/src/main/java/io/nekohasekai/sfa/compose/base/GlobalEventBus.kt index 92747f9..b31d900 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/base/GlobalEventBus.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/base/GlobalEventBus.kt @@ -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) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/base/SelectableMessageDialog.kt b/app/src/main/java/io/nekohasekai/sfa/compose/base/SelectableMessageDialog.kt index 1a6ae37..c5742a6 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/base/SelectableMessageDialog.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/base/SelectableMessageDialog.kt @@ -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() diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/base/UiState.kt b/app/src/main/java/io/nekohasekai/sfa/compose/base/UiState.kt index 55e36dc..ed9c357 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/base/UiState.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/base/UiState.kt @@ -8,8 +8,4 @@ sealed class UiState { data class Error(val exception: Throwable, val message: String? = null) : UiState() } -data class BaseUiState( - val isLoading: Boolean = false, - val data: T? = null, - val error: String? = null, -) +data class BaseUiState(val isLoading: Boolean = false, val data: T? = null, val error: String? = null) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/component/ServiceStatusBar.kt b/app/src/main/java/io/nekohasekai/sfa/compose/component/ServiceStatusBar.kt index 1c65745..225942c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/component/ServiceStatusBar.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/component/ServiceStatusBar.kt @@ -65,9 +65,9 @@ fun ServiceStatusBar( ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -85,11 +85,11 @@ fun ServiceStatusBar( // Connections button Row( modifier = - Modifier - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer) - .clickable(onClick = onConnectionsClick) - .padding(horizontal = 12.dp, vertical = 8.dp), + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.secondaryContainer) + .clickable(onClick = onConnectionsClick) + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { @@ -112,11 +112,11 @@ fun ServiceStatusBar( if (hasGroups) { Row( modifier = - Modifier - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer) - .clickable(onClick = onGroupsClick) - .padding(horizontal = 12.dp, vertical = 8.dp), + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.secondaryContainer) + .clickable(onClick = onGroupsClick) + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { @@ -139,11 +139,11 @@ fun ServiceStatusBar( // Stop button Row( modifier = - Modifier - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) - .clickable(onClick = onStopClick) - .padding(horizontal = 12.dp, vertical = 8.dp), + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primaryContainer) + .clickable(onClick = onStopClick) + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { @@ -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) { diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/component/UpdateDialog.kt b/app/src/main/java/io/nekohasekai/sfa/compose/component/UpdateDialog.kt index deacda2..791c78c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/component/UpdateDialog.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/component/UpdateDialog.kt @@ -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()) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRCodeDialog.kt b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRCodeDialog.kt index 430949f..535b87c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRCodeDialog.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRCodeDialog.kt @@ -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,24 +23,21 @@ 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), ) { Card( modifier = - Modifier - .fillMaxWidth(0.9f) - .wrapContentHeight(), + Modifier + .fillMaxWidth(0.9f) + .wrapContentHeight(), shape = RoundedCornerShape(16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) { Surface( modifier = Modifier @@ -52,9 +48,9 @@ fun QRCodeDialog( ) { Box( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface), + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), contentAlignment = Alignment.Center, ) { Image( diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSBitmapState.kt b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSBitmapState.kt index 2b691ed..3a36f52 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSBitmapState.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSBitmapState.kt @@ -33,6 +33,7 @@ class QRSBitmapGenerator( private val actualBufferSize = bufferSize.coerceAtMost(frames.size) private val bitmapBuffer = arrayOfNulls(actualBufferSize) private var generationJob: Job? = null + @Volatile private var currentFrameIndex = 0 private var generatedUpTo = -1 diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSDialog.kt b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSDialog.kt index e580bda..2e4cdf3 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSDialog.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRSDialog.kt @@ -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) @@ -126,16 +122,16 @@ fun QRSDialog( ) { Card( modifier = - if (isTablet) { - Modifier - .fillMaxWidth(0.85f) - .sizeIn(maxWidth = 960.dp) - .wrapContentHeight() - } else { - Modifier - .fillMaxWidth(0.9f) - .wrapContentHeight() - }, + if (isTablet) { + Modifier + .fillMaxWidth(0.85f) + .sizeIn(maxWidth = 960.dp) + .wrapContentHeight() + } else { + Modifier + .fillMaxWidth(0.9f) + .wrapContentHeight() + }, shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRScanSheet.kt b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRScanSheet.kt index 281f461..ea0677e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRScanSheet.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/component/qr/QRScanSheet.kt @@ -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 diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/model/Connection.kt b/app/src/main/java/io/nekohasekai/sfa/compose/model/Connection.kt index d2d5227..c84759e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/model/Connection.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/model/Connection.kt @@ -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,59 +62,53 @@ data class Connection( return true } - private fun performSearchPlain(content: String): Boolean { - return 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 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) { - "network" -> network.equals(value, ignoreCase = true) - "inbound" -> inbound.contains(value, ignoreCase = true) - "inbound.type" -> inboundType.equals(value, ignoreCase = true) - "source" -> source.contains(value, ignoreCase = true) - "destination" -> destination.contains(value, ignoreCase = true) - "outbound" -> outbound.contains(value, ignoreCase = true) - "outbound.type" -> outboundType.equals(value, ignoreCase = true) - "rule" -> rule.contains(value, ignoreCase = true) - "protocol" -> protocolName.equals(value, ignoreCase = true) - "user" -> user.contains(value, ignoreCase = true) - "package" -> processInfo?.packageName?.contains(value, ignoreCase = true) == true - "chain" -> chain.any { it.contains(value, ignoreCase = true) } - else -> false - } + 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) + "source" -> source.contains(value, ignoreCase = true) + "destination" -> destination.contains(value, ignoreCase = true) + "outbound" -> outbound.contains(value, ignoreCase = true) + "outbound.type" -> outboundType.equals(value, ignoreCase = true) + "rule" -> rule.contains(value, ignoreCase = true) + "protocol" -> protocolName.equals(value, ignoreCase = true) + "user" -> user.contains(value, ignoreCase = true) + "package" -> processInfo?.packageName?.contains(value, ignoreCase = true) == true + "chain" -> chain.any { it.contains(value, ignoreCase = true) } + else -> false } companion object { - fun from(connection: LibboxConnection): Connection { - return Connection( - id = connection.id, - inbound = connection.inbound, - inboundType = connection.inboundType, - ipVersion = connection.ipVersion, - network = connection.network, - source = connection.source, - destination = connection.destination, - domain = connection.domain, - displayDestination = connection.displayDestination(), - protocolName = connection.protocol, - user = connection.user, - fromOutbound = connection.fromOutbound, - createdAt = connection.createdAt, - closedAt = if (connection.closedAt > 0) connection.closedAt else null, - upload = connection.uplink, - download = connection.downlink, - uploadTotal = connection.uplinkTotal, - downloadTotal = connection.downlinkTotal, - rule = connection.rule, - outbound = connection.outbound, - outboundType = connection.outboundType, - chain = connection.chain().toList(), - processInfo = ProcessInfo.from(connection.processInfo), - ) - } + fun from(connection: LibboxConnection): Connection = Connection( + id = connection.id, + inbound = connection.inbound, + inboundType = connection.inboundType, + ipVersion = connection.ipVersion, + network = connection.network, + source = connection.source, + destination = connection.destination, + domain = connection.domain, + displayDestination = connection.displayDestination(), + protocolName = connection.protocol, + user = connection.user, + fromOutbound = connection.fromOutbound, + createdAt = connection.createdAt, + closedAt = if (connection.closedAt > 0) connection.closedAt else null, + upload = connection.uplink, + download = connection.downlink, + uploadTotal = connection.uplinkTotal, + downloadTotal = connection.downlinkTotal, + rule = connection.rule, + outbound = connection.outbound, + outboundType = connection.outboundType, + chain = connection.chain().toList(), + processInfo = ProcessInfo.from(connection.processInfo), + ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/model/Groups.kt b/app/src/main/java/io/nekohasekai/sfa/compose/model/Groups.kt index 426caee..5c92160 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/model/Groups.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/model/Groups.kt @@ -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, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/navigation/NavigationDestinations.kt b/app/src/main/java/io/nekohasekai/sfa/compose/navigation/NavigationDestinations.kt index 0dc6437..27456b9 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/navigation/NavigationDestinations.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/navigation/NavigationDestinations.kt @@ -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, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/navigation/ProfileRoutes.kt b/app/src/main/java/io/nekohasekai/sfa/compose/navigation/ProfileRoutes.kt index 91ea094..54d2dcd 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/navigation/ProfileRoutes.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/navigation/ProfileRoutes.kt @@ -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" diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/navigation/SFANavigation.kt b/app/src/main/java/io/nekohasekai/sfa/compose/navigation/SFANavigation.kt index d9b8ef5..2f46d17 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/navigation/SFANavigation.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/navigation/SFANavigation.kt @@ -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)) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileScreen.kt index a32b239..72559fd 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileScreen.kt @@ -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 @@ -149,9 +149,9 @@ fun NewProfileScreen( } }, colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) } @@ -164,20 +164,20 @@ fun NewProfileScreen( Box(modifier = Modifier.fillMaxSize()) { Column( modifier = - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp) - .padding(bottom = bottomBarPadding), + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + .padding(bottom = bottomBarPadding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { // Profile Name Card( modifier = Modifier.fillMaxWidth(), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + ), ) { Column( modifier = Modifier.padding(16.dp), @@ -211,9 +211,9 @@ fun NewProfileScreen( Card( modifier = Modifier.fillMaxWidth(), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + ), ) { Column( modifier = Modifier.padding(16.dp), @@ -233,30 +233,30 @@ fun NewProfileScreen( onClick = { viewModel.updateProfileType(ProfileType.Local) }, modifier = Modifier.weight(1f), shape = - RoundedCornerShape( - topStart = 12.dp, - bottomStart = 12.dp, - topEnd = 0.dp, - bottomEnd = 0.dp, - ), + RoundedCornerShape( + topStart = 12.dp, + bottomStart = 12.dp, + topEnd = 0.dp, + bottomEnd = 0.dp, + ), colors = - if (uiState.profileType == ProfileType.Local) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } else { - ButtonDefaults.outlinedButtonColors() - }, + if (uiState.profileType == ProfileType.Local) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } else { + ButtonDefaults.outlinedButtonColors() + }, border = - BorderStroke( - 1.dp, - if (uiState.profileType == ProfileType.Local) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.outline - }, - ), + BorderStroke( + 1.dp, + if (uiState.profileType == ProfileType.Local) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outline + }, + ), ) { Text(stringResource(R.string.profile_type_local)) } @@ -264,30 +264,30 @@ fun NewProfileScreen( onClick = { viewModel.updateProfileType(ProfileType.Remote) }, modifier = Modifier.weight(1f), shape = - RoundedCornerShape( - topStart = 0.dp, - bottomStart = 0.dp, - topEnd = 12.dp, - bottomEnd = 12.dp, - ), + RoundedCornerShape( + topStart = 0.dp, + bottomStart = 0.dp, + topEnd = 12.dp, + bottomEnd = 12.dp, + ), colors = - if (uiState.profileType == ProfileType.Remote) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } else { - ButtonDefaults.outlinedButtonColors() - }, + if (uiState.profileType == ProfileType.Remote) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } else { + ButtonDefaults.outlinedButtonColors() + }, border = - BorderStroke( - 1.dp, - if (uiState.profileType == ProfileType.Remote) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.outline - }, - ), + BorderStroke( + 1.dp, + if (uiState.profileType == ProfileType.Remote) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outline + }, + ), ) { Text(stringResource(R.string.profile_type_remote)) } @@ -304,9 +304,9 @@ fun NewProfileScreen( Card( modifier = Modifier.fillMaxWidth(), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f), + ), ) { Column( modifier = Modifier.padding(16.dp), @@ -326,30 +326,30 @@ fun NewProfileScreen( onClick = { viewModel.updateProfileSource(ProfileSource.CreateNew) }, modifier = Modifier.weight(1f), shape = - RoundedCornerShape( - topStart = 12.dp, - bottomStart = 12.dp, - topEnd = 0.dp, - bottomEnd = 0.dp, - ), + RoundedCornerShape( + topStart = 12.dp, + bottomStart = 12.dp, + topEnd = 0.dp, + bottomEnd = 0.dp, + ), colors = - if (uiState.profileSource == ProfileSource.CreateNew) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } else { - ButtonDefaults.outlinedButtonColors() - }, + if (uiState.profileSource == ProfileSource.CreateNew) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } else { + ButtonDefaults.outlinedButtonColors() + }, border = - BorderStroke( - 1.dp, - if (uiState.profileSource == ProfileSource.CreateNew) { - MaterialTheme.colorScheme.secondary - } else { - MaterialTheme.colorScheme.outline - }, - ), + BorderStroke( + 1.dp, + if (uiState.profileSource == ProfileSource.CreateNew) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.outline + }, + ), ) { Icon( Icons.Default.CreateNewFolder, @@ -363,30 +363,30 @@ fun NewProfileScreen( onClick = { viewModel.updateProfileSource(ProfileSource.Import) }, modifier = Modifier.weight(1f), shape = - RoundedCornerShape( - topStart = 0.dp, - bottomStart = 0.dp, - topEnd = 12.dp, - bottomEnd = 12.dp, - ), + RoundedCornerShape( + topStart = 0.dp, + bottomStart = 0.dp, + topEnd = 12.dp, + bottomEnd = 12.dp, + ), colors = - if (uiState.profileSource == ProfileSource.Import) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } else { - ButtonDefaults.outlinedButtonColors() - }, + if (uiState.profileSource == ProfileSource.Import) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } else { + ButtonDefaults.outlinedButtonColors() + }, border = - BorderStroke( - 1.dp, - if (uiState.profileSource == ProfileSource.Import) { - MaterialTheme.colorScheme.secondary - } else { - MaterialTheme.colorScheme.outline - }, - ), + BorderStroke( + 1.dp, + if (uiState.profileSource == ProfileSource.Import) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.outline + }, + ), ) { Icon( Icons.Default.FileUpload, @@ -408,20 +408,20 @@ fun NewProfileScreen( onClick = { filePickerLauncher.launch("*/*") }, modifier = Modifier.fillMaxWidth(), border = - BorderStroke( - 1.dp, - if (uiState.importError != null) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.outline - }, - ), + BorderStroke( + 1.dp, + if (uiState.importError != null) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.outline + }, + ), ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -429,11 +429,11 @@ fun NewProfileScreen( Icons.Default.FileUpload, contentDescription = null, tint = - if (uiState.importError != null) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - }, + if (uiState.importError != null) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + }, ) Column(modifier = Modifier.weight(1f)) { Text( @@ -473,9 +473,9 @@ fun NewProfileScreen( Card( modifier = Modifier.fillMaxWidth(), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f), + ), ) { Column( modifier = Modifier.padding(16.dp), @@ -550,18 +550,18 @@ fun NewProfileScreen( Surface( modifier = - Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), color = MaterialTheme.colorScheme.surface, tonalElevation = 3.dp, ) { Box( modifier = - Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(16.dp), + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(16.dp), ) { Button( onClick = { viewModel.validateAndCreateProfile() }, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileViewModel.kt index 151cdad..11c35bb 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/NewProfileViewModel.kt @@ -124,21 +124,18 @@ 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, importFileName = fileName, importError = null, // Clear error when file is selected name = - if (it.name.isEmpty()) { - fileName?.substringBeforeLast(".") ?: "Imported Profile" - } else { - it.name - }, + if (it.name.isEmpty()) { + fileName?.substringBeforeLast(".") ?: "Imported Profile" + } else { + it.name + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/ProfileImportHandler.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/ProfileImportHandler.kt index 3f04bee..0a8e9a5 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/ProfileImportHandler.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/configuration/ProfileImportHandler.kt @@ -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,188 +42,182 @@ class ProfileImportHandler(private val context: Context) { data class Error(val message: String) : UriParseResult() } - suspend fun importFromUri(uri: Uri): ImportResult = - withContext(Dispatchers.IO) { - try { - val data = - context.contentResolver.openInputStream(uri)?.use { it.readBytes() } - ?: return@withContext ImportResult.Error(context.getString(R.string.error_empty_file)) + suspend fun importFromUri(uri: Uri): ImportResult = withContext(Dispatchers.IO) { + try { + val data = + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + ?: return@withContext ImportResult.Error(context.getString(R.string.error_empty_file)) - // Get the filename from the URI - val filename = getFileNameFromUri(uri) + // Get the filename from the URI + val filename = getFileNameFromUri(uri) - // Try to detect if it's a JSON configuration file - val dataString = String(data) - if (isJsonConfiguration(dataString)) { - // It's a JSON configuration, import it directly as a local profile - return@withContext importJsonConfiguration(dataString, filename) - } - - // Try to decode as ProfileContent (the old way) - val content = - try { - Libbox.decodeProfileContent(data) - } catch (e: Exception) { - // If it fails, try one more time as JSON - if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) { - return@withContext importJsonConfiguration(dataString, filename) - } - return@withContext ImportResult.Error( - context.getString(R.string.error_decode_profile, e.message), - ) - } - - importProfile(content) - } catch (e: Exception) { - ImportResult.Error(e.message ?: "Unknown error") + // Try to detect if it's a JSON configuration file + val dataString = String(data) + if (isJsonConfiguration(dataString)) { + // It's a JSON configuration, import it directly as a local profile + return@withContext importJsonConfiguration(dataString, filename) } - } - suspend fun parseUri(uri: Uri): UriParseResult = - withContext(Dispatchers.IO) { - try { - val data = - context.contentResolver.openInputStream(uri)?.use { it.readBytes() } - ?: return@withContext UriParseResult.Error(context.getString(R.string.error_empty_file)) - - val filename = getFileNameFromUri(uri) - val dataString = String(data) - - if (isJsonConfiguration(dataString)) { - return@withContext UriParseResult.Success(name = filename) - } - - val content = - try { - Libbox.decodeProfileContent(data) - } catch (e: Exception) { - if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) { - return@withContext UriParseResult.Success(name = filename) - } - return@withContext UriParseResult.Error( - context.getString(R.string.error_decode_profile, e.message), - ) - } - - UriParseResult.Success(name = content.name) - } catch (e: Exception) { - UriParseResult.Error(e.message ?: "Unknown error") - } - } - - 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")) { - try { - val profileInfo = Libbox.parseRemoteProfileImportLink(data) - return@withContext QRCodeParseResult.RemoteProfile( - name = profileInfo.name, - host = profileInfo.host, - url = profileInfo.url, - ) - } catch (e: Exception) { - return@withContext QRCodeParseResult.Error( - context.getString(R.string.error_decode_profile, e.message), - ) - } - } - - // Check if it's a direct URL - if (data.startsWith("http://") || data.startsWith("https://")) { - val profileName = extractProfileNameFromUrl(data) - return@withContext QRCodeParseResult.RemoteProfile( - name = profileName, - host = extractHostFromUrl(data), - url = data, - ) - } - - // Try to decode as profile content - val content = - try { - Libbox.decodeProfileContent(data.toByteArray()) - } catch (e: Exception) { - return@withContext QRCodeParseResult.Error( - context.getString(R.string.error_decode_profile, e.message), - ) - } - - return@withContext QRCodeParseResult.LocalProfile(name = content.name) - } catch (e: Exception) { - QRCodeParseResult.Error(e.message ?: "Unknown error") - } - } - - 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")) { - try { - val profileInfo = Libbox.parseRemoteProfileImportLink(data) - return@withContext importRemoteProfile(profileInfo.name, profileInfo.url) - } catch (e: Exception) { - return@withContext ImportResult.Error( - context.getString(R.string.error_decode_profile, e.message), - ) - } - } - - // Check if it's a URL or direct profile content - if (data.startsWith("http://") || data.startsWith("https://")) { - // Handle remote profile URL - val profileName = extractProfileNameFromUrl(data) - importRemoteProfile(profileName, data) - } else { - // Try to decode as profile content - val content = - try { - Libbox.decodeProfileContent(data.toByteArray()) - } catch (e: Exception) { - return@withContext ImportResult.Error( - context.getString(R.string.error_decode_profile, e.message), - ) - } - importProfile(content) - } - } catch (e: Exception) { - ImportResult.Error(e.message ?: "Unknown error") - } - } - - suspend fun parseQRSData(data: ByteArray): QRSParseResult = - withContext(Dispatchers.IO) { - try { - val content = try { + // Try to decode as ProfileContent (the old way) + val content = + try { Libbox.decodeProfileContent(data) } catch (e: Exception) { - return@withContext QRSParseResult.Error( + // If it fails, try one more time as JSON + if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) { + return@withContext importJsonConfiguration(dataString, filename) + } + return@withContext ImportResult.Error( context.getString(R.string.error_decode_profile, e.message), ) } - QRSParseResult.Success(name = content.name) - } catch (e: Exception) { - QRSParseResult.Error(e.message ?: "Unknown error") - } - } - suspend fun importFromQRSData(data: ByteArray): ImportResult = - withContext(Dispatchers.IO) { - try { - val content = try { + importProfile(content) + } catch (e: Exception) { + ImportResult.Error(e.message ?: "Unknown error") + } + } + + suspend fun parseUri(uri: Uri): UriParseResult = withContext(Dispatchers.IO) { + try { + val data = + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + ?: return@withContext UriParseResult.Error(context.getString(R.string.error_empty_file)) + + val filename = getFileNameFromUri(uri) + val dataString = String(data) + + if (isJsonConfiguration(dataString)) { + return@withContext UriParseResult.Success(name = filename) + } + + val content = + try { Libbox.decodeProfileContent(data) + } catch (e: Exception) { + if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) { + return@withContext UriParseResult.Success(name = filename) + } + return@withContext UriParseResult.Error( + context.getString(R.string.error_decode_profile, e.message), + ) + } + + UriParseResult.Success(name = content.name) + } catch (e: Exception) { + UriParseResult.Error(e.message ?: "Unknown error") + } + } + + 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")) { + try { + val profileInfo = Libbox.parseRemoteProfileImportLink(data) + return@withContext QRCodeParseResult.RemoteProfile( + name = profileInfo.name, + host = profileInfo.host, + url = profileInfo.url, + ) + } catch (e: Exception) { + return@withContext QRCodeParseResult.Error( + context.getString(R.string.error_decode_profile, e.message), + ) + } + } + + // Check if it's a direct URL + if (data.startsWith("http://") || data.startsWith("https://")) { + val profileName = extractProfileNameFromUrl(data) + return@withContext QRCodeParseResult.RemoteProfile( + name = profileName, + host = extractHostFromUrl(data), + url = data, + ) + } + + // Try to decode as profile content + val content = + try { + Libbox.decodeProfileContent(data.toByteArray()) + } catch (e: Exception) { + return@withContext QRCodeParseResult.Error( + context.getString(R.string.error_decode_profile, e.message), + ) + } + + return@withContext QRCodeParseResult.LocalProfile(name = content.name) + } catch (e: Exception) { + QRCodeParseResult.Error(e.message ?: "Unknown error") + } + } + + 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")) { + try { + val profileInfo = Libbox.parseRemoteProfileImportLink(data) + return@withContext importRemoteProfile(profileInfo.name, profileInfo.url) } catch (e: Exception) { return@withContext ImportResult.Error( context.getString(R.string.error_decode_profile, e.message), ) } - importProfile(content) - } catch (e: Exception) { - ImportResult.Error(e.message ?: "Unknown error") } + + // Check if it's a URL or direct profile content + if (data.startsWith("http://") || data.startsWith("https://")) { + // Handle remote profile URL + val profileName = extractProfileNameFromUrl(data) + importRemoteProfile(profileName, data) + } else { + // Try to decode as profile content + val content = + try { + Libbox.decodeProfileContent(data.toByteArray()) + } catch (e: Exception) { + return@withContext ImportResult.Error( + context.getString(R.string.error_decode_profile, e.message), + ) + } + importProfile(content) + } + } catch (e: Exception) { + ImportResult.Error(e.message ?: "Unknown error") } + } + + suspend fun parseQRSData(data: ByteArray): QRSParseResult = withContext(Dispatchers.IO) { + try { + val content = try { + Libbox.decodeProfileContent(data) + } catch (e: Exception) { + return@withContext QRSParseResult.Error( + context.getString(R.string.error_decode_profile, e.message), + ) + } + QRSParseResult.Success(name = content.name) + } catch (e: Exception) { + QRSParseResult.Error(e.message ?: "Unknown error") + } + } + + suspend fun importFromQRSData(data: ByteArray): ImportResult = withContext(Dispatchers.IO) { + try { + val content = try { + Libbox.decodeProfileContent(data) + } catch (e: Exception) { + return@withContext ImportResult.Error( + context.getString(R.string.error_decode_profile, e.message), + ) + } + importProfile(content) + } catch (e: Exception) { + ImportResult.Error(e.message ?: "Unknown error") + } + } private suspend fun importProfile(content: ProfileContent): ImportResult { val typedProfile = TypedProfile() @@ -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,13 +287,11 @@ class ProfileImportHandler(private val context: Context) { ?: "Remote Profile" } - private fun extractHostFromUrl(url: String): String { - return try { - val uri = Uri.parse(url) - uri.host ?: url - } catch (e: Exception) { - url - } + 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 { @@ -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 { diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionDetailsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionDetailsScreen.kt index eb997e4..d7551a2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionDetailsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionDetailsScreen.kt @@ -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 } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionItem.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionItem.kt index f7865a1..49dc7a0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionItem.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionItem.kt @@ -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) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsScreen.kt index b85aaef..bdd0fd9 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsScreen.kt @@ -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 } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsViewModel.kt index 9cd0c78..8668f0b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/connections/ConnectionsViewModel.kt @@ -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 = emptyList(), @@ -38,7 +38,9 @@ sealed class ConnectionsEvent : ScreenEvent { data object AllConnectionsClosed : ConnectionsEvent() } -class ConnectionsViewModel : BaseViewModel(), CommandClient.Handler { +class ConnectionsViewModel : + BaseViewModel(), + CommandClient.Handler { private val commandClient = CommandClient( viewModelScope, CommandClient.ConnectionType.Connections, @@ -62,7 +64,7 @@ class ConnectionsViewModel : BaseViewModel combine( AppLifecycleObserver.isForeground, _isVisible, - _serviceStatus + _serviceStatus, ) { foreground, visible, status -> Triple(foreground, visible, status) }.collect { (foreground, visible, status) -> diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ClashModeCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ClashModeCard.kt index 4c3e634..80d0072 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ClashModeCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ClashModeCard.kt @@ -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,20 +43,15 @@ import io.nekohasekai.sfa.R @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ClashModeCard( - modes: List, - selectedMode: String, - onModeSelected: (String) -> Unit, - modifier: Modifier = Modifier, -) { +fun ClashModeCard(modes: List, selectedMode: String, onModeSelected: (String) -> Unit, modifier: Modifier = Modifier) { Card( modifier = modifier.fillMaxWidth(), ) { Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -109,10 +103,10 @@ fun ClashModeCard( modes.forEachIndexed { index, mode -> SegmentedButton( shape = - SegmentedButtonDefaults.itemShape( - index = index, - count = modes.size, - ), + SegmentedButtonDefaults.itemShape( + index = index, + count = modes.size, + ), onClick = { onModeSelected(mode) }, selected = mode == selectedMode, ) { @@ -127,11 +121,7 @@ fun ClashModeCard( } @Composable -private fun ModeDropdown( - modes: List, - selectedMode: String, - onModeSelected: (String) -> Unit, -) { +private fun ModeDropdown(modes: List, selectedMode: String, onModeSelected: (String) -> Unit) { var expanded by remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxWidth()) { @@ -147,9 +137,9 @@ private fun ModeDropdown( ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ConnectionsCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ConnectionsCard.kt index fab887d..572338b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ConnectionsCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ConnectionsCard.kt @@ -24,19 +24,15 @@ 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(), ) { Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardScreen.kt index b61066c..b13ea38 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardScreen.kt @@ -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, - val isRow: Boolean, -) +data class CardRenderItem(val cards: List, val isRow: Boolean) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -87,18 +83,18 @@ fun DashboardScreen( } }, dismissButton = - if (!note.migrationLink.isNullOrBlank()) { - { - TextButton(onClick = { - viewModel.sendGlobalEvent(UiEvent.OpenUrl(note.migrationLink)) - viewModel.dismissDeprecatedNote() - }) { - Text(stringResource(R.string.error_deprecated_documentation)) - } + if (!note.migrationLink.isNullOrBlank()) { + { + TextButton(onClick = { + viewModel.sendGlobalEvent(UiEvent.OpenUrl(note.migrationLink)) + viewModel.dismissDeprecatedNote() + }) { + Text(stringResource(R.string.error_deprecated_documentation)) } - } else { - null - }, + } + } else { + null + }, ) } @@ -134,9 +130,9 @@ fun DashboardScreen( } LazyColumn( modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), + Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(bottom = bottomPadding), ) { @@ -172,8 +168,8 @@ fun DashboardScreen( DashboardCardRenderer( cardGroup = cardGroup, cardWidth = - uiState.cardWidths[cardGroup] - ?: CardWidth.Full, + uiState.cardWidths[cardGroup] + ?: CardWidth.Full, uiState = uiState, onClashModeSelected = viewModel::selectClashMode, onSystemProxyToggle = viewModel::toggleSystemProxy, @@ -199,9 +195,9 @@ fun DashboardScreen( onOpenNewProfile = onOpenNewProfile, commandClient = viewModel.commandClient, modifier = - Modifier - .weight(1f) - .fillMaxWidth(), + Modifier + .weight(1f) + .fillMaxWidth(), ) } } @@ -211,8 +207,8 @@ fun DashboardScreen( DashboardCardRenderer( cardGroup = cardGroup, cardWidth = - uiState.cardWidths[cardGroup] - ?: CardWidth.Full, + uiState.cardWidths[cardGroup] + ?: CardWidth.Full, uiState = uiState, serviceStatus = serviceStatus, onClashModeSelected = viewModel::selectClashMode, @@ -307,17 +303,12 @@ 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) { - CardGroup.ClashMode -> uiState.clashModeVisible - CardGroup.UploadTraffic -> uiState.trafficVisible - CardGroup.DownloadTraffic -> uiState.trafficVisible - CardGroup.Debug -> true // Debug info is always available when service is running - CardGroup.Connections -> uiState.trafficVisible - CardGroup.SystemProxy -> uiState.systemProxyVisible - CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety - } +fun isCardAvailableWhenServiceRunning(cardGroup: CardGroup, uiState: DashboardUiState): Boolean = when (cardGroup) { + CardGroup.ClashMode -> uiState.clashModeVisible + CardGroup.UploadTraffic -> uiState.trafficVisible + CardGroup.DownloadTraffic -> uiState.trafficVisible + CardGroup.Debug -> true // Debug info is always available when service is running + CardGroup.Connections -> uiState.trafficVisible + CardGroup.SystemProxy -> uiState.systemProxyVisible + CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt index ff8b937..12ec111 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardSettingsBottomSheet.kt @@ -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) @@ -135,17 +134,17 @@ fun DashboardSettingsBottomSheet( ) { Column( modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight(0.8f), + Modifier + .fillMaxWidth() + .fillMaxHeight(0.8f), ) { // Header with reset button Row( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -199,18 +198,18 @@ fun DashboardSettingsBottomSheet( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = - Modifier - .padding(horizontal = 24.dp) - .padding(bottom = 12.dp), + Modifier + .padding(horizontal = 24.dp) + .padding(bottom = 12.dp), ) // Reorderable list LazyColumn( state = listState, modifier = - Modifier - .fillMaxWidth() - .weight(1f), + Modifier + .fillMaxWidth() + .weight(1f), contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -275,13 +274,13 @@ fun DashboardSettingsBottomSheet( dragOffset = 0f }, modifier = - animateItemCompat( - placementSpec = - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), + animateItemCompat( + placementSpec = + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, ), + ), ) } } @@ -315,40 +314,40 @@ fun DashboardItemCard( Card( modifier = - modifier - .fillMaxWidth() - .offset(y = with(LocalDensity.current) { offsetY.value.toDp() }) - .zIndex(if (isDragging) 1f else 0f) - .clip(RoundedCornerShape(12.dp)), + modifier + .fillMaxWidth() + .offset(y = with(LocalDensity.current) { offsetY.value.toDp() }) + .zIndex(if (isDragging) 1f else 0f) + .clip(RoundedCornerShape(12.dp)), elevation = - CardDefaults.cardElevation( - defaultElevation = cardElevation, - ), + CardDefaults.cardElevation( + defaultElevation = cardElevation, + ), colors = - CardDefaults.cardColors( - containerColor = - if (isDragging) { - MaterialTheme.colorScheme.surface.copy(alpha = 0.95f) - } else { - MaterialTheme.colorScheme.surface - }, - ), + CardDefaults.cardColors( + containerColor = + if (isDragging) { + MaterialTheme.colorScheme.surface.copy(alpha = 0.95f) + } else { + MaterialTheme.colorScheme.surface + }, + ), border = - BorderStroke( - width = 1.dp, - color = - if (isVisible) { - MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) - } else { - MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) - }, - ), + BorderStroke( + width = 1.dp, + color = + if (isVisible) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) + } else { + MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) + }, + ), ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), + Modifier + .fillMaxWidth() + .padding(12.dp), verticalAlignment = Alignment.CenterVertically, ) { // Drag handle @@ -361,66 +360,66 @@ fun DashboardItemCard( imageVector = Icons.Default.DragHandle, contentDescription = stringResource(R.string.drag_to_reorder), modifier = - Modifier - .size(24.dp) - .draggable( - state = draggableState, - orientation = Orientation.Vertical, - onDragStarted = { onDragStart() }, - onDragStopped = { onDragEnd() }, - ) - .padding(4.dp), + Modifier + .size(24.dp) + .draggable( + state = draggableState, + orientation = Orientation.Vertical, + onDragStarted = { onDragStart() }, + onDragStopped = { onDragEnd() }, + ) + .padding(4.dp), tint = - if (isDragging) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isDragging) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) // Card icon Icon( imageVector = - when (cardGroup) { - CardGroup.Debug -> Icons.Outlined.BugReport - CardGroup.Connections -> Icons.Outlined.Cable - CardGroup.UploadTraffic -> Icons.Outlined.Upload - CardGroup.DownloadTraffic -> Icons.Outlined.Download - CardGroup.ClashMode -> Icons.Outlined.Route - CardGroup.SystemProxy -> Icons.Outlined.SettingsEthernet - CardGroup.Profiles -> Icons.Outlined.Person - }, + when (cardGroup) { + CardGroup.Debug -> Icons.Outlined.BugReport + CardGroup.Connections -> Icons.Outlined.Cable + CardGroup.UploadTraffic -> Icons.Outlined.Upload + CardGroup.DownloadTraffic -> Icons.Outlined.Download + CardGroup.ClashMode -> Icons.Outlined.Route + CardGroup.SystemProxy -> Icons.Outlined.SettingsEthernet + CardGroup.Profiles -> Icons.Outlined.Person + }, contentDescription = null, modifier = - Modifier - .size(24.dp) - .padding(horizontal = 4.dp), + Modifier + .size(24.dp) + .padding(horizontal = 4.dp), tint = - if (isVisible) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isVisible) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) // Card info Column( modifier = - Modifier - .weight(1f) - .padding(horizontal = 8.dp), + Modifier + .weight(1f) + .padding(horizontal = 8.dp), ) { Text( text = - when (cardGroup) { - CardGroup.Debug -> stringResource(R.string.title_debug) - CardGroup.Connections -> stringResource(R.string.title_connections) - CardGroup.UploadTraffic -> stringResource(R.string.upload) - CardGroup.DownloadTraffic -> stringResource(R.string.download) - CardGroup.ClashMode -> stringResource(R.string.clash_mode) - CardGroup.SystemProxy -> stringResource(R.string.system_proxy) - CardGroup.Profiles -> stringResource(R.string.title_configuration) - }, + when (cardGroup) { + CardGroup.Debug -> stringResource(R.string.title_debug) + CardGroup.Connections -> stringResource(R.string.title_connections) + CardGroup.UploadTraffic -> stringResource(R.string.upload) + CardGroup.DownloadTraffic -> stringResource(R.string.download) + CardGroup.ClashMode -> stringResource(R.string.clash_mode) + CardGroup.SystemProxy -> stringResource(R.string.system_proxy) + CardGroup.Profiles -> stringResource(R.string.title_configuration) + }, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardViewModel.kt index 2b58b55..943eca0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DashboardViewModel.kt @@ -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(), CommandClient.Handler { +class DashboardViewModel : + BaseViewModel(), + CommandClient.Handler { private val _serviceStatus = MutableStateFlow(Status.Stopped) val serviceStatus: StateFlow = _serviceStatus.asStateFlow() @@ -395,10 +394,7 @@ class DashboardViewModel : BaseViewModel(), 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(), CommandCl } } - override fun initializeClashMode( - modeList: List, - currentMode: String, - ) { + override fun initializeClashMode(modeList: List, currentMode: String) { viewModelScope.launch(Dispatchers.Main) { updateState { copy( @@ -702,16 +695,15 @@ class DashboardViewModel : BaseViewModel(), CommandCl } // Helper functions for serialization - private fun getDefaultItemOrder() = - listOf( - CardGroup.UploadTraffic, - CardGroup.DownloadTraffic, - CardGroup.Debug, - CardGroup.Connections, - CardGroup.SystemProxy, - CardGroup.ClashMode, - CardGroup.Profiles, - ) + private fun getDefaultItemOrder() = listOf( + CardGroup.UploadTraffic, + CardGroup.DownloadTraffic, + CardGroup.Debug, + CardGroup.Connections, + CardGroup.SystemProxy, + CardGroup.ClashMode, + CardGroup.Profiles, + ) private fun loadItemOrder(): List { val savedOrder = Settings.dashboardItemOrder @@ -766,11 +758,9 @@ class DashboardViewModel : BaseViewModel(), CommandCl private fun cardGroupToString(card: CardGroup): String = card.name - private fun stringToCardGroup(name: String): CardGroup? { - return try { - CardGroup.valueOf(name) - } catch (e: IllegalArgumentException) { - null - } + private fun stringToCardGroup(name: String): CardGroup? = try { + CardGroup.valueOf(name) + } catch (e: IllegalArgumentException) { + null } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DebugCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DebugCard.kt index 3361370..817bd97 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DebugCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DebugCard.kt @@ -24,19 +24,15 @@ 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(), ) { Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DownloadTrafficCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DownloadTrafficCard.kt index 9cff4cc..5f86056 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DownloadTrafficCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/DownloadTrafficCard.kt @@ -24,20 +24,15 @@ import io.nekohasekai.sfa.R import io.nekohasekai.sfa.compose.LineChart @Composable -fun DownloadTrafficCard( - downlink: String, - downlinkTotal: String, - downlinkHistory: List, - modifier: Modifier = Modifier, -) { +fun DownloadTrafficCard(downlink: String, downlinkTotal: String, downlinkHistory: List, modifier: Modifier = Modifier) { Card( modifier = modifier.fillMaxWidth(), ) { Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/GroupsCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/GroupsCard.kt index 015e85d..c7a3ae7 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/GroupsCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/GroupsCard.kt @@ -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) @@ -84,12 +84,12 @@ fun GroupsCard( ) { val actualViewModel: GroupsViewModel = viewModel ?: viewModel( factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return GroupsViewModel(commandClient) as T - } - }, + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return GroupsViewModel(commandClient) as T + } + }, ) val snackbarHostState = remember { SnackbarHostState() } val uiState by actualViewModel.uiState.collectAsState() @@ -104,17 +104,17 @@ fun GroupsCard( IconButton(onClick = { actualViewModel.toggleAllGroups() }) { Icon( imageVector = - if (allCollapsed) { - Icons.Default.UnfoldMore - } else { - Icons.Default.UnfoldLess - }, + if (allCollapsed) { + Icons.Default.UnfoldMore + } else { + Icons.Default.UnfoldLess + }, contentDescription = - if (allCollapsed) { - stringResource(R.string.expand_all) - } else { - stringResource(R.string.collapse_all) - }, + if (allCollapsed) { + stringResource(R.string.expand_all) + } else { + stringResource(R.string.collapse_all) + }, ) } } @@ -186,9 +186,9 @@ private fun GroupsCardContent( if (uiState.isLoading) { Box( modifier = - Modifier - .fillMaxWidth() - .height(200.dp), + Modifier + .fillMaxWidth() + .height(200.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() @@ -196,9 +196,9 @@ private fun GroupsCardContent( } else if (uiState.groups.isEmpty()) { Box( modifier = - Modifier - .fillMaxWidth() - .height(100.dp), + Modifier + .fillMaxWidth() + .height(100.dp), contentAlignment = Alignment.Center, ) { Text( @@ -216,12 +216,12 @@ private fun GroupsCardContent( .nestedScroll(bounceBlockingConnection), state = lazyListState, contentPadding = - PaddingValues( - start = 16.dp, - end = 16.dp, - top = 8.dp, - bottom = 16.dp, - ), + PaddingValues( + start = 16.dp, + end = 16.dp, + top = 8.dp, + bottom = 16.dp, + ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { items( @@ -347,17 +347,17 @@ private fun ProxyGroupItem( imageVector = Icons.Default.ExpandMore, contentDescription = if (isExpanded) "Collapse" else "Expand", modifier = - Modifier - .size(24.dp) - .graphicsLayer { rotationZ = rotationAngle }, + Modifier + .size(24.dp) + .graphicsLayer { rotationZ = rotationAngle }, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -365,21 +365,21 @@ private fun ProxyGroupItem( AnimatedVisibility( visible = isExpanded && group.items.isNotEmpty(), enter = - expandVertically(animationSpec = tween(300)) + - fadeIn( - animationSpec = - tween( - 300, - ), + expandVertically(animationSpec = tween(300)) + + fadeIn( + animationSpec = + tween( + 300, ), + ), exit = - shrinkVertically(animationSpec = tween(300)) + - fadeOut( - animationSpec = - tween( - 300, - ), + shrinkVertically(animationSpec = tween(300)) + + fadeOut( + animationSpec = + tween( + 300, ), + ), ) { Column { HorizontalDivider( @@ -401,12 +401,7 @@ private fun ProxyGroupItem( } @Composable -private fun ProxyItemsList( - items: List, - selectedTag: String, - isSelectable: Boolean, - onItemSelected: (String) -> Unit, -) { +private fun ProxyItemsList(items: List, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) { val itemsPerRow = 2 val chunkedItems = remember(items) { @@ -415,9 +410,9 @@ private fun ProxyItemsList( Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { chunkedItems.forEach { rowItems -> @@ -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, @@ -475,18 +464,18 @@ private fun ProxyChip( androidx.compose.foundation.BorderStroke( width = if (isSelected) 2.dp else 1.dp, color = - when { - isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) - }, + when { + isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + }, ) val content: @Composable () -> Unit = { Row( modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), + Modifier + .fillMaxWidth() + .padding(12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -500,11 +489,11 @@ private fun ProxyChip( style = MaterialTheme.typography.bodyMedium, fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -520,11 +509,11 @@ private fun ProxyChip( text = Libbox.proxyDisplayType(item.type), style = MaterialTheme.typography.labelSmall, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + }, ) // Latency @@ -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 diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilePickerSheet.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilePickerSheet.kt index 8797021..d245e92 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilePickerSheet.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilePickerSheet.kt @@ -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 diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfileSelectorButton.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfileSelectorButton.kt index 6ebc94d..c7a8fa2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfileSelectorButton.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfileSelectorButton.kt @@ -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), diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilesCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilesCard.kt index 3cba0ef..30975c9 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilesCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/ProfilesCard.kt @@ -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 diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/SystemProxyCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/SystemProxyCard.kt index d6b1df3..479ddb0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/SystemProxyCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/SystemProxyCard.kt @@ -23,20 +23,15 @@ 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(), ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/UploadTrafficCard.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/UploadTrafficCard.kt index 75f07e2..23ed1fb 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/UploadTrafficCard.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/UploadTrafficCard.kt @@ -24,20 +24,15 @@ import io.nekohasekai.sfa.R import io.nekohasekai.sfa.compose.LineChart @Composable -fun UploadTrafficCard( - uplink: String, - uplinkTotal: String, - uplinkHistory: List, - modifier: Modifier = Modifier, -) { +fun UploadTrafficCard(uplink: String, uplinkTotal: String, uplinkHistory: List, modifier: Modifier = Modifier) { Card( modifier = modifier.fillMaxWidth(), ) { Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsScreen.kt index 5710000..d18be3f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsScreen.kt @@ -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( @@ -121,12 +121,12 @@ fun GroupsScreen( LazyColumn( modifier = modifier.fillMaxSize(), contentPadding = - PaddingValues( - start = 16.dp, - end = 16.dp, - top = 8.dp, - bottom = 16.dp, - ), + PaddingValues( + start = 16.dp, + end = 16.dp, + top = 8.dp, + bottom = 16.dp, + ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { items( @@ -254,17 +254,17 @@ private fun ProxyGroupCard( imageVector = Icons.Default.ExpandMore, contentDescription = if (isExpanded) collapseContentDescription else expandContentDescription, modifier = - Modifier - .size(24.dp) - .graphicsLayer { rotationZ = rotationAngle }, + Modifier + .size(24.dp) + .graphicsLayer { rotationZ = rotationAngle }, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -294,12 +294,7 @@ private fun ProxyGroupCard( } @Composable -private fun ProxyItemsList( - items: List, - selectedTag: String, - isSelectable: Boolean, - onItemSelected: (String) -> Unit, -) { +private fun ProxyItemsList(items: List, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) { // Cache the chunked items to avoid re-chunking on every recomposition val itemsPerRow = 2 val chunkedItems = @@ -310,9 +305,9 @@ private fun ProxyItemsList( // Use Column with Rows for better control over item sizing Column( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { chunkedItems.forEach { rowItems -> @@ -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, @@ -369,18 +358,18 @@ private fun ProxyChip( androidx.compose.foundation.BorderStroke( width = if (isSelected) 2.dp else 1.dp, color = - when { - isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) - }, + when { + isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + }, ) val content: @Composable () -> Unit = { Row( modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), + Modifier + .fillMaxWidth() + .padding(12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -394,11 +383,11 @@ private fun ProxyChip( style = MaterialTheme.typography.bodyMedium, fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -414,11 +403,11 @@ private fun ProxyChip( text = Libbox.proxyDisplayType(item.type), style = MaterialTheme.typography.labelSmall, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + }, ) // Latency @@ -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 = diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsViewModel.kt index 1151252..5d1c0f8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/dashboard/groups/GroupsViewModel.kt @@ -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(), CommandClient.Handler { +class GroupsViewModel(private val sharedCommandClient: CommandClient? = null) : + BaseViewModel(), + 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) { @@ -175,13 +172,13 @@ class GroupsViewModel( updateState { copy( groups = - groups.map { group -> - if (group.tag == groupTag) { - group.copy(selected = itemTag) - } else { - group - } - }, + groups.map { group -> + if (group.tag == groupTag) { + group.copy(selected = itemTag) + } else { + group + } + }, showCloseConnectionsSnackbar = true, ) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/BaseLogViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/BaseLogViewModel.kt index cb15553..9fe1a46 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/BaseLogViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/BaseLogViewModel.kt @@ -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 = _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 diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogModels.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogModels.kt index 2fe584d..aad3eaf 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogModels.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogModels.kt @@ -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), diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogScreen.kt index 0f2e978..85770dd 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogScreen.kt @@ -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 @@ -144,17 +144,17 @@ fun LogScreen( IconButton(onClick = { resolvedViewModel.togglePause() }) { Icon( imageVector = - if (uiState.isPaused) { - Icons.Default.PlayArrow - } else { - Icons.Default.Pause - }, + if (uiState.isPaused) { + Icons.Default.PlayArrow + } else { + Icons.Default.Pause + }, contentDescription = - if (uiState.isPaused) { - stringResource(R.string.content_description_resume_logs) - } else { - stringResource(R.string.content_description_pause_logs) - }, + if (uiState.isPaused) { + stringResource(R.string.content_description_resume_logs) + } else { + stringResource(R.string.content_description_pause_logs) + }, ) } } @@ -162,23 +162,23 @@ fun LogScreen( IconButton(onClick = { resolvedViewModel.toggleSearch() }) { Icon( imageVector = - if (uiState.isSearchActive) { - Icons.Default.ExpandLess - } else { - Icons.Default.Search - }, + if (uiState.isSearchActive) { + Icons.Default.ExpandLess + } else { + Icons.Default.Search + }, contentDescription = - if (uiState.isSearchActive) { - stringResource(R.string.content_description_collapse_search) - } else { - stringResource(R.string.content_description_search_logs) - }, + if (uiState.isSearchActive) { + stringResource(R.string.content_description_collapse_search) + } else { + stringResource(R.string.content_description_search_logs) + }, tint = - if (uiState.isSearchActive) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, + if (uiState.isSearchActive) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, ) } @@ -281,9 +281,9 @@ fun LogScreen( ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -298,10 +298,10 @@ fun LogScreen( } Text( text = - stringResource( - R.string.selected_count, - uiState.selectedLogIndices.size, - ), + stringResource( + R.string.selected_count, + uiState.selectedLogIndices.size, + ), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(start = 8.dp), ) @@ -343,18 +343,18 @@ fun LogScreen( ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( text = - stringResource( - R.string.filter_label, - uiState.filterLogLevel.label, - ), + stringResource( + R.string.filter_label, + uiState.filterLogLevel.label, + ), style = MaterialTheme.typography.bodySmall, ) TextButton( @@ -375,19 +375,19 @@ fun LogScreen( AnimatedVisibility( visible = uiState.isSearchActive, enter = - expandVertically( + expandVertically( + animationSpec = tween(300), + ) + + fadeIn( animationSpec = tween(300), - ) + - fadeIn( - animationSpec = tween(300), - ), + ), exit = - shrinkVertically( + shrinkVertically( + animationSpec = tween(300), + ) + + fadeOut( animationSpec = tween(300), - ) + - fadeOut( - animationSpec = tween(300), - ), + ), ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -405,10 +405,10 @@ fun LogScreen( value = uiState.searchQuery, onValueChange = { resolvedViewModel.updateSearchQuery(it) }, modifier = - Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) - .focusRequester(focusRequester), + Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .focusRequester(focusRequester), placeholder = { Text(stringResource(R.string.search_logs_placeholder)) }, leadingIcon = { Icon( @@ -429,11 +429,11 @@ fun LogScreen( singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = - KeyboardActions( - onSearch = { - focusManager.clearFocus() - }, - ), + KeyboardActions( + onSearch = { + focusManager.clearFocus() + }, + ), ) } } @@ -495,12 +495,12 @@ fun LogScreen( state = listState, modifier = Modifier.fillMaxSize(), contentPadding = - PaddingValues( - start = 8.dp, - end = 8.dp, - top = 8.dp, - bottom = bottomPadding, - ), + PaddingValues( + start = 8.dp, + end = 8.dp, + top = 8.dp, + bottom = bottomPadding, + ), verticalArrangement = Arrangement.spacedBy(2.dp), ) { itemsIndexed( @@ -532,9 +532,9 @@ fun LogScreen( // Options Menu - Material 3 style Box( modifier = - Modifier - .align(Alignment.TopEnd) - .padding(end = 8.dp), + Modifier + .align(Alignment.TopEnd) + .padding(end = 8.dp), ) { var expandedLogLevel by remember { mutableStateOf(false) } var expandedSave by remember { mutableStateOf(false) } @@ -595,11 +595,11 @@ fun LogScreen( trailingIcon = { Icon( imageVector = - if (expandedLogLevel) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (expandedLogLevel) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -620,23 +620,23 @@ fun LogScreen( leadingIcon = { Icon( imageVector = - if (uiState.filterLogLevel == level) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (uiState.filterLogLevel == level) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = - if (uiState.filterLogLevel == level) { - stringResource(R.string.group_selected_title) - } else { - null - }, + if (uiState.filterLogLevel == level) { + stringResource(R.string.group_selected_title) + } else { + null + }, tint = - if (uiState.filterLogLevel == level) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (uiState.filterLogLevel == level) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -665,11 +665,11 @@ fun LogScreen( trailingIcon = { Icon( imageVector = - if (expandedSave) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (expandedSave) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -841,9 +841,9 @@ fun LogScreen( val fabEndPadding = if (isTablet) 20.dp else 16.dp Column( modifier = - Modifier - .align(Alignment.BottomEnd) - .padding(bottom = fabBottomPadding, end = fabEndPadding, top = 16.dp), + Modifier + .align(Alignment.BottomEnd) + .padding(bottom = fabBottomPadding, end = fabEndPadding, top = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { // Scroll to bottom FAB @@ -880,34 +880,34 @@ fun LogItem( ) { Card( modifier = - Modifier - .fillMaxWidth() - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ), + Modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ), shape = RoundedCornerShape(4.dp), colors = - CardDefaults.cardColors( - containerColor = - if (isSelected) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - }, - ), - border = + CardDefaults.cardColors( + containerColor = if (isSelected) { - CardDefaults.outlinedCardBorder().copy( - width = 2.dp, - brush = - androidx.compose.ui.graphics.SolidColor( - MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), - ), - ) + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) } else { - null + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) }, + ), + border = + if (isSelected) { + CardDefaults.outlinedCardBorder().copy( + width = 2.dp, + brush = + androidx.compose.ui.graphics.SolidColor( + MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), + ), + ) + } else { + null + }, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -917,13 +917,13 @@ fun LogItem( Icon( imageVector = if (isSelected) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank, contentDescription = - if (isSelected) { - stringResource(R.string.group_selected_title) - } else { - stringResource( - R.string.not_selected, - ) - }, + if (isSelected) { + stringResource(R.string.group_selected_title) + } else { + stringResource( + R.string.not_selected, + ) + }, modifier = Modifier.padding(start = 12.dp, end = 4.dp), tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -931,14 +931,14 @@ fun LogItem( Text( text = annotatedString, modifier = - Modifier - .weight(1f) - .padding( - start = if (isSelectionMode) 4.dp else 12.dp, - end = 12.dp, - top = 8.dp, - bottom = 8.dp, - ), + Modifier + .weight(1f) + .padding( + start = if (isSelectionMode) 4.dp else 12.dp, + end = 12.dp, + top = 8.dp, + bottom = 8.dp, + ), fontSize = 13.sp, fontFamily = FontFamily.Monospace, lineHeight = 18.sp, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogViewModel.kt index 4e885ba..601a51d 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/log/LogViewModel.kt @@ -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 } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/privilegesettings/PrivilegeSettingsManageScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/privilegesettings/PrivilegeSettingsManageScreen.kt index b702a50..8734b43 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/privilegesettings/PrivilegeSettingsManageScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/privilegesettings/PrivilegeSettingsManageScreen.kt @@ -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, - val selectedUids: Set, -) +private data class LoadResult(val packages: List, val selectedUids: Set) private const val VPN_SERVICE_PERMISSION = "android.permission.BIND_VPN_SERVICE" @@ -126,10 +122,11 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) { val hasManagement = permissions.any { it in managementPermissions } val isSelf = packageCache.packageName == context.packageName val hasVpnService = - !isSelf && ( - permissions.any { it == VPN_SERVICE_PERMISSION } || - packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true - ) + !isSelf && + ( + permissions.any { it == VPN_SERVICE_PERMISSION } || + packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true + ) return when { hasManagement && hasVpnService -> RiskCategory.BOTH hasManagement -> RiskCategory.MANAGEMENT_APP @@ -138,11 +135,9 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) { } } - fun buildPackageList(newUids: Set): Set { - return newUids.mapNotNull { uid -> - packages.find { it.uid == uid }?.packageName - }.toSet() - } + fun buildPackageList(newUids: Set): Set = newUids.mapNotNull { uid -> + packages.find { it.uid == uid }?.packageName + }.toSet() fun updateCurrentPackages(filterQuery: String) { currentPackages = @@ -443,10 +438,10 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) { ) }, colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface, - ), + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + ), ) } @@ -492,10 +487,10 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) { updateCurrentPackages(it) }, modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .focusRequester(focusRequester), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .focusRequester(focusRequester), placeholder = { Text(stringResource(R.string.search)) }, leadingIcon = { Icon( @@ -524,10 +519,10 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = - androidx.compose.foundation.layout.PaddingValues( - horizontal = 16.dp, - vertical = 12.dp, - ), + androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 12.dp, + ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(currentPackages, key = { it.packageName }) { packageCache -> diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentScreen.kt index dbada86..1c883d0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentScreen.kt @@ -182,17 +182,17 @@ fun EditProfileContentScreen( Icon( imageVector = if (uiState.showSearchBar) Icons.Default.ExpandLess else Icons.Default.Search, contentDescription = - if (uiState.showSearchBar) { - stringResource(R.string.content_description_collapse_search) - } else { - stringResource(R.string.search) - }, + if (uiState.showSearchBar) { + stringResource(R.string.content_description_collapse_search) + } else { + stringResource(R.string.search) + }, tint = - if (uiState.showSearchBar) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, + if (uiState.showSearchBar) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, ) } @@ -206,626 +206,630 @@ fun EditProfileContentScreen( imageVector = Icons.Default.Save, contentDescription = stringResource(R.string.save), tint = - if (uiState.hasUnsavedChanges) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, + if (uiState.hasUnsavedChanges) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, ) } } }, colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) } Column( modifier = - modifier - .fillMaxSize() - .onPreviewKeyEvent { event -> - if (event.type == KeyEventType.KeyDown) { - // Support both Ctrl (Windows/Linux) and Cmd (macOS) - val modifierPressed = event.isCtrlPressed || event.isMetaPressed + modifier + .fillMaxSize() + .onPreviewKeyEvent { event -> + if (event.type == KeyEventType.KeyDown) { + // Support both Ctrl (Windows/Linux) and Cmd (macOS) + val modifierPressed = event.isCtrlPressed || event.isMetaPressed - when { - // Ctrl/Cmd+Z - Undo - modifierPressed && event.key == Key.Z && !event.isShiftPressed && !uiState.isReadOnly -> { - viewModel.undo() - true - } - // Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y - Redo - ( - modifierPressed && event.isShiftPressed && event.key == Key.Z || - modifierPressed && event.key == Key.Y - ) && !uiState.isReadOnly -> { - viewModel.redo() - true - } - // Ctrl/Cmd+S - Save - modifierPressed && event.key == Key.S && !uiState.isReadOnly -> { - if (uiState.hasUnsavedChanges && !uiState.isLoading) { - viewModel.saveConfiguration() - } - true - } - // Ctrl/Cmd+F - Search - modifierPressed && event.key == Key.F -> { - viewModel.toggleSearchBar() - true - } - // Ctrl/Cmd+A - Select All - modifierPressed && event.key == Key.A -> { - viewModel.selectAll() - true - } - // Ctrl/Cmd+X - Cut (only in edit mode) - modifierPressed && event.key == Key.X && !uiState.isReadOnly -> { - viewModel.cut() - true - } - // Ctrl/Cmd+C - Copy - modifierPressed && event.key == Key.C -> { - viewModel.copy() - true - } - // Ctrl/Cmd+V - Paste (only in edit mode) - modifierPressed && event.key == Key.V && !uiState.isReadOnly -> { - viewModel.paste() - true - } - // Escape - Close search bar if open - event.key == Key.Escape && uiState.showSearchBar -> { - viewModel.toggleSearchBar() - true - } - // F3 or Ctrl/Cmd+G - Find next (when search is active) - (event.key == Key.F3 || (modifierPressed && event.key == Key.G && !event.isShiftPressed)) && - uiState.searchQuery.isNotEmpty() -> { - viewModel.findNext() - viewModel.focusEditor() - true - } - // Shift+F3 or Ctrl/Cmd+Shift+G - Find previous (when search is active) - ( - (event.isShiftPressed && event.key == Key.F3) || - (modifierPressed && event.isShiftPressed && event.key == Key.G) + when { + // Ctrl/Cmd+Z - Undo + modifierPressed && event.key == Key.Z && !event.isShiftPressed && !uiState.isReadOnly -> { + viewModel.undo() + true + } + // Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y - Redo + ( + modifierPressed && + event.isShiftPressed && + event.key == Key.Z || + modifierPressed && + event.key == Key.Y ) && - uiState.searchQuery.isNotEmpty() -> { - viewModel.findPrevious() - viewModel.focusEditor() - true - } - - else -> false + !uiState.isReadOnly -> { + viewModel.redo() + true } - } else { - false + // Ctrl/Cmd+S - Save + modifierPressed && event.key == Key.S && !uiState.isReadOnly -> { + if (uiState.hasUnsavedChanges && !uiState.isLoading) { + viewModel.saveConfiguration() + } + true + } + // Ctrl/Cmd+F - Search + modifierPressed && event.key == Key.F -> { + viewModel.toggleSearchBar() + true + } + // Ctrl/Cmd+A - Select All + modifierPressed && event.key == Key.A -> { + viewModel.selectAll() + true + } + // Ctrl/Cmd+X - Cut (only in edit mode) + modifierPressed && event.key == Key.X && !uiState.isReadOnly -> { + viewModel.cut() + true + } + // Ctrl/Cmd+C - Copy + modifierPressed && event.key == Key.C -> { + viewModel.copy() + true + } + // Ctrl/Cmd+V - Paste (only in edit mode) + modifierPressed && event.key == Key.V && !uiState.isReadOnly -> { + viewModel.paste() + true + } + // Escape - Close search bar if open + event.key == Key.Escape && uiState.showSearchBar -> { + viewModel.toggleSearchBar() + true + } + // F3 or Ctrl/Cmd+G - Find next (when search is active) + (event.key == Key.F3 || (modifierPressed && event.key == Key.G && !event.isShiftPressed)) && + uiState.searchQuery.isNotEmpty() -> { + viewModel.findNext() + viewModel.focusEditor() + true + } + // Shift+F3 or Ctrl/Cmd+Shift+G - Find previous (when search is active) + ( + (event.isShiftPressed && event.key == Key.F3) || + (modifierPressed && event.isShiftPressed && event.key == Key.G) + ) && + uiState.searchQuery.isNotEmpty() -> { + viewModel.findPrevious() + viewModel.focusEditor() + true + } + + else -> false } - }, + } else { + false + } + }, ) { - // Search bar (appears at top when activated) - AnimatedVisibility( - visible = uiState.showSearchBar, - enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), - exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + // Search bar (appears at top when activated) + AnimatedVisibility( + visible = uiState.showSearchBar, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 2.dp, ) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceContainer, - tonalElevation = 2.dp, + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Row( + OutlinedTextField( + value = uiState.searchQuery, + onValueChange = { viewModel.updateSearchQuery(it) }, modifier = - Modifier - .fillMaxWidth() - .padding(start = 12.dp, end = 12.dp, bottom = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - value = uiState.searchQuery, - onValueChange = { viewModel.updateSearchQuery(it) }, - modifier = - Modifier - .weight(1f) - .focusRequester(searchFocusRequester) - .onPreviewKeyEvent { event -> - if (event.key == Key.Enter && event.type == KeyEventType.KeyDown) { - coroutineScope.launch { - // Clear focus from search field first - focusManager.clearFocus() - // Small delay to let UI update - delay(100) - // Then focus editor with current search result selection - viewModel.focusEditorWithCurrentSearchResult() - } - true - } else { - false - } - }, - label = { Text(stringResource(R.string.search)) }, - placeholder = { Text(stringResource(R.string.search_placeholder)) }, - singleLine = true, - leadingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - trailingIcon = { - if (uiState.searchQuery.isNotEmpty()) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = - if (uiState.searchResultCount > 0) { - "${uiState.currentSearchIndex}/${uiState.searchResultCount}" - } else { - "0/0" - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(end = 4.dp), - ) - IconButton( - onClick = { - // Focus editor with current selection before clearing search - viewModel.focusEditorWithCurrentSearchResult() - viewModel.updateSearchQuery("") - focusManager.clearFocus() - }, - modifier = Modifier.size(24.dp), - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.clear), - modifier = Modifier.size(18.dp), - ) - } + Modifier + .weight(1f) + .focusRequester(searchFocusRequester) + .onPreviewKeyEvent { event -> + if (event.key == Key.Enter && event.type == KeyEventType.KeyDown) { + coroutineScope.launch { + // Clear focus from search field first + focusManager.clearFocus() + // Small delay to let UI update + delay(100) + // Then focus editor with current search result selection + viewModel.focusEditorWithCurrentSearchResult() } + true + } else { + false } }, - ) - - // Only show navigation buttons when there are search results - if (uiState.searchQuery.isNotEmpty() && uiState.searchResultCount > 0) { - Spacer(modifier = Modifier.width(8.dp)) - - IconButton( - onClick = { - viewModel.findPrevious() - viewModel.focusEditor() - }, - ) { - Icon( - imageVector = Icons.Default.ArrowUpward, - contentDescription = stringResource(R.string.previous), - tint = MaterialTheme.colorScheme.primary, - ) - } - - IconButton( - onClick = { - viewModel.findNext() - viewModel.focusEditor() - }, - ) { - Icon( - imageVector = Icons.Default.ArrowDownward, - contentDescription = stringResource(R.string.next), - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } - } - } - - // Editor in a Box with floating elements - Box( - modifier = - Modifier - .fillMaxSize() - .clipToBounds() - .weight(1f), - ) { - // Editor - AndroidView( - factory = { context -> - ManualScrollTextProcessor(context).apply { - language = JsonLanguage() - setTextSize(14f) - setPadding(16, 16, 16, if (uiState.isReadOnly) 16 else 120) // Less padding for read-only - typeface = android.graphics.Typeface.MONOSPACE - setBackgroundColor( - androidx.core.content.ContextCompat.getColor(context, android.R.color.transparent), + label = { Text(stringResource(R.string.search)) }, + placeholder = { Text(stringResource(R.string.search_placeholder)) }, + singleLine = true, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - // Set up the editor with read-only state - this handles all configuration - viewModel.setEditor(this, uiState.isReadOnly) - } - }, - update = { textProcessor -> - // Re-apply configuration when read-only state changes - viewModel.setEditor(textProcessor, uiState.isReadOnly) - }, - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - ) - - // Simple loading indicator at the top - if (uiState.isLoading) { - LinearProgressIndicator( - modifier = - Modifier - .fillMaxWidth() - .align(Alignment.TopCenter), - ) - } - - // Floating bottom editor bar with error banner (only show if not read-only) - if (!uiState.isReadOnly) { - Column( - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .imePadding(), - ) { - // Configuration error banner (appears above the symbol bar) - AnimatedVisibility( - visible = uiState.configurationError != null, - enter = slideInVertically { it } + fadeIn(), - exit = slideOutVertically { it } + fadeOut(), - ) { - Surface( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .padding(bottom = 2.dp), - shape = RoundedCornerShape(12.dp), - tonalElevation = 6.dp, - shadowElevation = 4.dp, - color = MaterialTheme.colorScheme.errorContainer, - ) { + }, + trailingIcon = { + if (uiState.searchQuery.isNotEmpty()) { Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp), - // Match symbol bar padding verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.size(20.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) Text( - text = uiState.configurationError ?: "", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.weight(1f), - maxLines = 2, - overflow = TextOverflow.Ellipsis, + text = + if (uiState.searchResultCount > 0) { + "${uiState.currentSearchIndex}/${uiState.searchResultCount}" + } else { + "0/0" + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(end = 4.dp), ) IconButton( - onClick = { viewModel.dismissConfigurationError() }, + onClick = { + // Focus editor with current selection before clearing search + viewModel.focusEditorWithCurrentSearchResult() + viewModel.updateSearchQuery("") + focusManager.clearFocus() + }, + modifier = Modifier.size(24.dp), ) { Icon( imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.dismiss), - tint = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.size(20.dp), + contentDescription = stringResource(R.string.clear), + modifier = Modifier.size(18.dp), ) } } } + }, + ) + + // Only show navigation buttons when there are search results + if (uiState.searchQuery.isNotEmpty() && uiState.searchResultCount > 0) { + Spacer(modifier = Modifier.width(8.dp)) + + IconButton( + onClick = { + viewModel.findPrevious() + viewModel.focusEditor() + }, + ) { + Icon( + imageVector = Icons.Default.ArrowUpward, + contentDescription = stringResource(R.string.previous), + tint = MaterialTheme.colorScheme.primary, + ) } - // Symbol input bar - Surface( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 8.dp), - shape = RoundedCornerShape(12.dp), - tonalElevation = 6.dp, - shadowElevation = 4.dp, - color = MaterialTheme.colorScheme.surface, + IconButton( + onClick = { + viewModel.findNext() + viewModel.focusEditor() + }, ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - // Undo button with text - TextButton( - onClick = { viewModel.undo() }, - enabled = uiState.canUndo, - modifier = Modifier.padding(end = 4.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.Undo, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = - if (uiState.canUndo) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(R.string.menu_undo), - style = MaterialTheme.typography.labelLarge, - color = - if (uiState.canUndo) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, - ) - } - - // Redo button with text - TextButton( - onClick = { viewModel.redo() }, - enabled = uiState.canRedo, - modifier = Modifier.padding(end = 4.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.Redo, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = - if (uiState.canRedo) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(R.string.menu_redo), - style = MaterialTheme.typography.labelLarge, - color = - if (uiState.canRedo) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - }, - ) - } - - // Format button with text - TextButton( - onClick = { viewModel.formatConfiguration() }, - modifier = Modifier.padding(end = 8.dp), - ) { - Icon( - imageVector = Icons.Default.Code, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(R.string.menu_format), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - } - - VerticalDivider( - modifier = - Modifier - .height(24.dp) - .padding(horizontal = 8.dp), - ) - - // Symbols ranked by frequency of use in JSON - - // Most common - quotes and colon (used for every key-value pair) - TextButton( - onClick = { viewModel.insertSymbol("\"") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = "\"", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - TextButton( - onClick = { viewModel.insertSymbol(":") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = ":", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - TextButton( - onClick = { viewModel.insertSymbol(",") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = ",", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.width(4.dp)) - - // Object brackets (very common) - TextButton( - onClick = { viewModel.insertSymbol("{") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = "{", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - TextButton( - onClick = { viewModel.insertSymbol("}") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = "}", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.width(4.dp)) - - // Array brackets (common) - TextButton( - onClick = { viewModel.insertSymbol("[") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = "[", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - TextButton( - onClick = { viewModel.insertSymbol("]") }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = "]", - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.width(4.dp)) - - // Common values - using same TextButton style for keywords - listOf("true", "false").forEach { text -> - TextButton( - onClick = { viewModel.insertSymbol(text) }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), - ) { - Text( - text = text, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - } - } - - Spacer(modifier = Modifier.width(4.dp)) - - // Less common symbols - same TextButton style - listOf("-", "_", "/", "\\", "(", ")", "@", "#", "$", "%", "&", "*").forEach { symbol -> - TextButton( - onClick = { viewModel.insertSymbol(symbol) }, - modifier = - Modifier - .padding(0.dp) - .height(36.dp) - .width(36.dp), - shape = RoundedCornerShape(4.dp), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = symbol, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - - // End padding for scroll - Spacer(modifier = Modifier.width(8.dp)) - } + Icon( + imageVector = Icons.Default.ArrowDownward, + contentDescription = stringResource(R.string.next), + tint = MaterialTheme.colorScheme.primary, + ) } } } } } + + // Editor in a Box with floating elements + Box( + modifier = + Modifier + .fillMaxSize() + .clipToBounds() + .weight(1f), + ) { + // Editor + AndroidView( + factory = { context -> + ManualScrollTextProcessor(context).apply { + language = JsonLanguage() + setTextSize(14f) + setPadding(16, 16, 16, if (uiState.isReadOnly) 16 else 120) // Less padding for read-only + typeface = android.graphics.Typeface.MONOSPACE + setBackgroundColor( + androidx.core.content.ContextCompat.getColor(context, android.R.color.transparent), + ) + // Set up the editor with read-only state - this handles all configuration + viewModel.setEditor(this, uiState.isReadOnly) + } + }, + update = { textProcessor -> + // Re-apply configuration when read-only state changes + viewModel.setEditor(textProcessor, uiState.isReadOnly) + }, + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) + + // Simple loading indicator at the top + if (uiState.isLoading) { + LinearProgressIndicator( + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.TopCenter), + ) + } + + // Floating bottom editor bar with error banner (only show if not read-only) + if (!uiState.isReadOnly) { + Column( + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .imePadding(), + ) { + // Configuration error banner (appears above the symbol bar) + AnimatedVisibility( + visible = uiState.configurationError != null, + enter = slideInVertically { it } + fadeIn(), + exit = slideOutVertically { it } + fadeOut(), + ) { + Surface( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .padding(bottom = 2.dp), + shape = RoundedCornerShape(12.dp), + tonalElevation = 6.dp, + shadowElevation = 4.dp, + color = MaterialTheme.colorScheme.errorContainer, + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + // Match symbol bar padding + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = uiState.configurationError ?: "", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + IconButton( + onClick = { viewModel.dismissConfigurationError() }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.dismiss), + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(20.dp), + ) + } + } + } + } + + // Symbol input bar + Surface( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + tonalElevation = 6.dp, + shadowElevation = 4.dp, + color = MaterialTheme.colorScheme.surface, + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Undo button with text + TextButton( + onClick = { viewModel.undo() }, + enabled = uiState.canUndo, + modifier = Modifier.padding(end = 4.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.Undo, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = + if (uiState.canUndo) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(R.string.menu_undo), + style = MaterialTheme.typography.labelLarge, + color = + if (uiState.canUndo) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + } + + // Redo button with text + TextButton( + onClick = { viewModel.redo() }, + enabled = uiState.canRedo, + modifier = Modifier.padding(end = 4.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.Redo, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = + if (uiState.canRedo) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(R.string.menu_redo), + style = MaterialTheme.typography.labelLarge, + color = + if (uiState.canRedo) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + } + + // Format button with text + TextButton( + onClick = { viewModel.formatConfiguration() }, + modifier = Modifier.padding(end = 8.dp), + ) { + Icon( + imageVector = Icons.Default.Code, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(R.string.menu_format), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + + VerticalDivider( + modifier = + Modifier + .height(24.dp) + .padding(horizontal = 8.dp), + ) + + // Symbols ranked by frequency of use in JSON + + // Most common - quotes and colon (used for every key-value pair) + TextButton( + onClick = { viewModel.insertSymbol("\"") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "\"", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + TextButton( + onClick = { viewModel.insertSymbol(":") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = ":", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + TextButton( + onClick = { viewModel.insertSymbol(",") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = ",", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + Spacer(modifier = Modifier.width(4.dp)) + + // Object brackets (very common) + TextButton( + onClick = { viewModel.insertSymbol("{") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "{", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + TextButton( + onClick = { viewModel.insertSymbol("}") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "}", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + Spacer(modifier = Modifier.width(4.dp)) + + // Array brackets (common) + TextButton( + onClick = { viewModel.insertSymbol("[") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "[", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + TextButton( + onClick = { viewModel.insertSymbol("]") }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "]", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + + Spacer(modifier = Modifier.width(4.dp)) + + // Common values - using same TextButton style for keywords + listOf("true", "false").forEach { text -> + TextButton( + onClick = { viewModel.insertSymbol(text) }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), + ) { + Text( + text = text, + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + } + + Spacer(modifier = Modifier.width(4.dp)) + + // Less common symbols - same TextButton style + listOf("-", "_", "/", "\\", "(", ")", "@", "#", "$", "%", "&", "*").forEach { symbol -> + TextButton( + onClick = { viewModel.insertSymbol(symbol) }, + modifier = + Modifier + .padding(0.dp) + .height(36.dp) + .width(36.dp), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = symbol, + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + // End padding for scroll + Spacer(modifier = Modifier.width(8.dp)) + } + } + } + } + } + } // Unsaved changes dialog if (showUnsavedChangesDialog) { AlertDialog( diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentViewModel.kt index 4a0155c..1c19a9b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileContentViewModel.kt @@ -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 } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileRoute.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileRoute.kt index 2245c69..b617b43 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileRoute.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileRoute.kt @@ -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() @@ -84,12 +80,12 @@ fun EditProfileRoute( composable( route = "icon_selection/{currentIconId}", arguments = - listOf( - navArgument("currentIconId") { - type = NavType.StringType - nullable = true - }, - ), + listOf( + navArgument("currentIconId") { + type = NavType.StringType + nullable = true + }, + ), enterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Left, @@ -134,16 +130,16 @@ fun EditProfileRoute( composable( route = "edit_content/{profileName}/{isReadOnly}", arguments = - listOf( - navArgument("profileName") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("isReadOnly") { - type = NavType.BoolType - defaultValue = false - }, - ), + listOf( + navArgument("profileName") { + type = NavType.StringType + defaultValue = "" + }, + navArgument("isReadOnly") { + type = NavType.BoolType + defaultValue = false + }, + ), enterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Left, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileScreen.kt index 567e4a5..84af0e0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileScreen.kt @@ -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 @@ -179,9 +179,9 @@ fun EditProfileScreen( } }, colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) } @@ -199,324 +199,324 @@ fun EditProfileScreen( Box( modifier = Modifier.fillMaxSize(), ) { - // Progress indicator at top (only for initial loading) - if (uiState.isLoading) { - LinearProgressIndicator( + // Progress indicator at top (only for initial loading) + if (uiState.isLoading) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + ) + } + + if (!uiState.isLoading) { + Column( + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + .padding(bottom = bottomBarPadding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Basic Information Card + Card( modifier = Modifier.fillMaxWidth(), - ) - } - - if (!uiState.isLoading) { - Column( - modifier = - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp) - .padding(bottom = bottomBarPadding), - verticalArrangement = Arrangement.spacedBy(16.dp), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + ), ) { - // Basic Information Card - Card( - modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), - ), + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Text( - text = stringResource(R.string.basic_information), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary, - ) + Text( + text = stringResource(R.string.basic_information), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + ) - OutlinedTextField( - value = uiState.name, - onValueChange = viewModel::updateName, - label = { Text(stringResource(R.string.profile_name)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), - ) - - // Icon selection with Material You style - Text( - text = stringResource(R.string.icon), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 4.dp), - ) - - Surface( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable { viewModel.showIconDialog() }, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - shape = RoundedCornerShape(12.dp), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - // Display current icon - val currentIcon = - ProfileIcons.getIconById(uiState.icon) - ?: Icons.AutoMirrored.Filled.InsertDriveFile - - Icon( - imageVector = currentIcon, - contentDescription = stringResource(R.string.profile_icon), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - - Text( - text = - uiState.icon?.let { iconId -> - MaterialIconsLibrary.getAllIcons() - .find { it.id == iconId }?.label - } ?: stringResource(R.string.default_text), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f), - ) - - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = stringResource(R.string.select_icon), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - } - - // Remote Profile Options - if (uiState.profileType == TypedProfile.Type.Remote) { - Card( + OutlinedTextField( + value = uiState.name, + onValueChange = viewModel::updateName, + label = { Text(stringResource(R.string.profile_name)) }, modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f), - ), - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - Icons.Default.CloudDownload, - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(20.dp), - ) - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = stringResource(R.string.remote_configuration), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.tertiary, - ) - uiState.lastUpdated?.let { lastUpdated -> - Text( - text = - stringResource( - R.string.last_updated_format, - RelativeTimeFormatter.format( - context, - lastUpdated, - ), - ), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - // Update button in top-right corner - IconButton( - onClick = { viewModel.updateRemoteProfile() }, - enabled = !uiState.isUpdating && !uiState.showUpdateSuccess, - ) { - when { - uiState.isUpdating -> { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeWidth = 2.dp, - ) - } - uiState.showUpdateSuccess -> { - Icon( - Icons.Default.Check, - contentDescription = stringResource(R.string.success), - tint = MaterialTheme.colorScheme.primary, - ) - } - else -> { - Icon( - Icons.Default.Update, - contentDescription = stringResource(R.string.profile_update), - tint = MaterialTheme.colorScheme.tertiary, - ) - } - } - } - } + singleLine = true, + ) - OutlinedTextField( - value = uiState.remoteUrl, - onValueChange = viewModel::updateRemoteUrl, - label = { Text(stringResource(R.string.profile_url)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, + HorizontalDivider( + modifier = Modifier.padding(vertical = 4.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ) + + // Icon selection with Material You style + Text( + text = stringResource(R.string.icon), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 4.dp), + ) + + Surface( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { viewModel.showIconDialog() }, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + shape = RoundedCornerShape(12.dp), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + // Display current icon + val currentIcon = + ProfileIcons.getIconById(uiState.icon) + ?: Icons.AutoMirrored.Filled.InsertDriveFile + + Icon( + imageVector = currentIcon, + contentDescription = stringResource(R.string.profile_icon), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, ) - HorizontalDivider() + Text( + text = + uiState.icon?.let { iconId -> + MaterialIconsLibrary.getAllIcons() + .find { it.id == iconId }?.label + } ?: stringResource(R.string.default_text), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) - // Auto Update Toggle - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.profile_auto_update), - style = MaterialTheme.typography.bodyLarge, - ) - Switch( - checked = uiState.autoUpdate, - onCheckedChange = viewModel::updateAutoUpdate, - ) - } - - AnimatedVisibility(visible = uiState.autoUpdate) { - OutlinedTextField( - value = uiState.autoUpdateInterval.toString(), - onValueChange = viewModel::updateAutoUpdateInterval, - label = { Text(stringResource(R.string.profile_auto_update_interval)) }, - supportingText = { - uiState.autoUpdateIntervalError?.let { error -> - Text( - text = error, - color = MaterialTheme.colorScheme.error, - ) - } ?: Text(stringResource(R.string.profile_auto_update_interval_minimum_hint)) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - isError = uiState.autoUpdateIntervalError != null, - ) - } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = stringResource(R.string.select_icon), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } + } - // Content Card (for both Local and Remote profiles) - placed at the end + // Remote Profile Options + if (uiState.profileType == TypedProfile.Type.Remote) { Card( modifier = Modifier.fillMaxWidth(), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f), + ), ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Default.CloudDownload, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(20.dp), + ) + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResource(R.string.remote_configuration), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.tertiary, + ) + uiState.lastUpdated?.let { lastUpdated -> + Text( + text = + stringResource( + R.string.last_updated_format, + RelativeTimeFormatter.format( + context, + lastUpdated, + ), + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + // Update button in top-right corner + IconButton( + onClick = { viewModel.updateRemoteProfile() }, + enabled = !uiState.isUpdating && !uiState.showUpdateSuccess, + ) { + when { + uiState.isUpdating -> { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + ) + } + uiState.showUpdateSuccess -> { + Icon( + Icons.Default.Check, + contentDescription = stringResource(R.string.success), + tint = MaterialTheme.colorScheme.primary, + ) + } + else -> { + Icon( + Icons.Default.Update, + contentDescription = stringResource(R.string.profile_update), + tint = MaterialTheme.colorScheme.tertiary, + ) + } + } + } + } + + OutlinedTextField( + value = uiState.remoteUrl, + onValueChange = viewModel::updateRemoteUrl, + label = { Text(stringResource(R.string.profile_url)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + HorizontalDivider() + + // Auto Update Toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - Icons.AutoMirrored.Filled.InsertDriveFile, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(20.dp), - ) Text( - text = stringResource(R.string.content), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, + text = stringResource(R.string.profile_auto_update), + style = MaterialTheme.typography.bodyLarge, + ) + Switch( + checked = uiState.autoUpdate, + onCheckedChange = viewModel::updateAutoUpdate, ) } - // JSON Editor/Viewer option - Surface( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable { - onNavigateToEditContent( - uiState.name, - uiState.profileType == TypedProfile.Type.Remote, + AnimatedVisibility(visible = uiState.autoUpdate) { + OutlinedTextField( + value = uiState.autoUpdateInterval.toString(), + onValueChange = viewModel::updateAutoUpdateInterval, + label = { Text(stringResource(R.string.profile_auto_update_interval)) }, + supportingText = { + uiState.autoUpdateIntervalError?.let { error -> + Text( + text = error, + color = MaterialTheme.colorScheme.error, ) - }, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - shape = RoundedCornerShape(12.dp), + } ?: Text(stringResource(R.string.profile_auto_update_interval_minimum_hint)) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + isError = uiState.autoUpdateIntervalError != null, + ) + } + } + } + } + + // Content Card (for both Local and Remote profiles) - placed at the end + Card( + modifier = Modifier.fillMaxWidth(), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f), + ), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.AutoMirrored.Filled.InsertDriveFile, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.content), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + ) + } + + // JSON Editor/Viewer option + Surface( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { + onNavigateToEditContent( + uiState.name, + uiState.profileType == TypedProfile.Type.Remote, + ) + }, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + shape = RoundedCornerShape(12.dp), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = Icons.Default.Code, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Text( - text = - if (uiState.profileType == TypedProfile.Type.Remote) { - stringResource(R.string.json_viewer) - } else { - stringResource(R.string.json_editor) - }, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f), - ) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + Icon( + imageVector = Icons.Default.Code, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = + if (uiState.profileType == TypedProfile.Type.Remote) { + stringResource(R.string.json_viewer) + } else { + stringResource(R.string.json_editor) + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } } } + } AnimatedVisibility( visible = uiState.hasChanges, enter = fadeIn() + expandVertically(), @@ -530,10 +530,10 @@ fun EditProfileScreen( ) { Box( modifier = - Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(16.dp), + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(16.dp), ) { Button( onClick = { viewModel.saveChanges() }, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileViewModel.kt index 7457fcf..57d8531 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/EditProfileViewModel.kt @@ -109,9 +109,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat state.copy( name = name, hasChanges = - checkHasChanges( - state.copy(name = name), - ), + checkHasChanges( + state.copy(name = name), + ), ) } } @@ -121,9 +121,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat state.copy( icon = icon, hasChanges = - checkHasChanges( - state.copy(icon = icon), - ), + checkHasChanges( + state.copy(icon = icon), + ), ) } } @@ -141,9 +141,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat state.copy( remoteUrl = url, hasChanges = - checkHasChanges( - state.copy(remoteUrl = url), - ), + checkHasChanges( + state.copy(remoteUrl = url), + ), ) } } @@ -153,9 +153,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat state.copy( autoUpdate = enabled, hasChanges = - checkHasChanges( - state.copy(autoUpdate = enabled), - ), + checkHasChanges( + state.copy(autoUpdate = enabled), + ), ) } } @@ -174,22 +174,20 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat autoUpdateInterval = intValue, autoUpdateIntervalError = error, hasChanges = - if (error == null) { - checkHasChanges(state.copy(autoUpdateInterval = intValue)) - } else { - state.hasChanges - }, + if (error == null) { + checkHasChanges(state.copy(autoUpdateInterval = intValue)) + } else { + state.hasChanges + }, ) } } - private fun checkHasChanges(state: EditProfileUiState): Boolean { - return state.name != state.originalName || - state.icon != state.originalIcon || - state.remoteUrl != state.originalRemoteUrl || - state.autoUpdate != state.originalAutoUpdate || - state.autoUpdateInterval != state.originalAutoUpdateInterval - } + 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) { diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionDialog.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionDialog.kt index e96e287..cf507ae 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionDialog.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionDialog.kt @@ -37,17 +37,13 @@ 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 = - Modifier - .fillMaxWidth() - .heightIn(max = 500.dp), + Modifier + .fillMaxWidth() + .heightIn(max = 500.dp), shape = RoundedCornerShape(16.dp), ) { Column( @@ -65,9 +61,9 @@ fun IconSelectionDialog( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = - Modifier - .fillMaxWidth() - .weight(1f), + Modifier + .fillMaxWidth() + .weight(1f), ) { // Add option to remove custom icon (use default) item { @@ -110,40 +106,35 @@ 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 - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(8.dp)) - .clickable { onClick() }, + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .clickable { onClick() }, colors = - CardDefaults.cardColors( - containerColor = - if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - }, - ), - border = + CardDefaults.cardColors( + containerColor = if (isSelected) { - CardDefaults.outlinedCardBorder() + MaterialTheme.colorScheme.primaryContainer } else { - null + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) }, + ), + border = + if (isSelected) { + CardDefaults.outlinedCardBorder() + } else { + null + }, ) { Column( modifier = - Modifier - .fillMaxSize() - .padding(8.dp), + Modifier + .fillMaxSize() + .padding(8.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -153,11 +144,11 @@ private fun IconOption( contentDescription = label, modifier = Modifier.size(28.dp), tint = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) } else { // Default icon indicator @@ -165,11 +156,11 @@ private fun IconOption( text = stringResource(R.string.auto), style = MaterialTheme.typography.bodyMedium, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) } @@ -179,11 +170,11 @@ private fun IconOption( text = label, style = MaterialTheme.typography.labelSmall, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionScreen.kt index 061a5fa..9e37361 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/IconSelectionScreen.kt @@ -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(null) } var viewMode by remember { mutableStateOf(IconViewMode.CATEGORIES) } @@ -126,26 +122,26 @@ fun IconSelectionScreen( Icon( imageVector = Icons.Default.Search, contentDescription = - if (isSearchActive) { - stringResource(R.string.close_search) - } else { - stringResource( - R.string.search_icons, - ) - }, + if (isSearchActive) { + stringResource(R.string.close_search) + } else { + stringResource( + R.string.search_icons, + ) + }, tint = - if (isSearchActive) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, + if (isSearchActive) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, ) } }, colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) } @@ -167,27 +163,27 @@ fun IconSelectionScreen( Box(modifier = Modifier.fillMaxSize()) { Column( modifier = - Modifier - .fillMaxSize() - .padding(bottom = bottomBarPadding), + Modifier + .fillMaxSize() + .padding(bottom = bottomBarPadding), ) { // Show search bar with animation AnimatedVisibility( visible = isSearchActive, enter = - expandVertically( + expandVertically( + animationSpec = tween(300), + ) + + fadeIn( animationSpec = tween(300), - ) + - fadeIn( - animationSpec = tween(300), - ), + ), exit = - shrinkVertically( + shrinkVertically( + animationSpec = tween(300), + ) + + fadeOut( animationSpec = tween(300), - ) + - fadeOut( - animationSpec = tween(300), - ), + ), ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -212,10 +208,10 @@ fun IconSelectionScreen( } }, modifier = - Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) - .focusRequester(focusRequester), + Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .focusRequester(focusRequester), placeholder = { Text(stringResource(R.string.search_icons_placeholder)) }, leadingIcon = { Icon( @@ -240,20 +236,20 @@ fun IconSelectionScreen( singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = - KeyboardActions( - onSearch = { - focusManager.clearFocus() - }, - ), + KeyboardActions( + onSearch = { + focusManager.clearFocus() + }, + ), ) } } Column( modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), + Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), ) { // View mode tabs (only show when not searching) AnimatedVisibility(visible = searchQuery.isEmpty()) { @@ -269,11 +265,11 @@ fun IconSelectionScreen( }, label = { Text(stringResource(R.string.categories)) }, leadingIcon = - if (viewMode == IconViewMode.CATEGORIES && selectedCategory == null) { - { Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) } - } else { - null - }, + if (viewMode == IconViewMode.CATEGORIES && selectedCategory == null) { + { Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) } + } else { + null + }, ) FilterChip( @@ -284,11 +280,11 @@ fun IconSelectionScreen( }, label = { Text(stringResource(R.string.all_icons)) }, leadingIcon = - if (viewMode == IconViewMode.ALL) { - { Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) } - } else { - null - }, + if (viewMode == IconViewMode.ALL) { + { Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) } + } else { + null + }, ) FilterChip( @@ -329,9 +325,9 @@ fun IconSelectionScreen( // Main content area Box( modifier = - Modifier - .fillMaxWidth() - .weight(1f), + Modifier + .fillMaxWidth() + .weight(1f), ) { when { // Search results @@ -387,21 +383,21 @@ fun IconSelectionScreen( currentIcon?.let { (id, icon) -> Card( modifier = - Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(horizontal = 16.dp, vertical = 8.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), + ), ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), + Modifier + .fillMaxWidth() + .padding(12.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -415,10 +411,10 @@ fun IconSelectionScreen( val iconInfo = MaterialIconsLibrary.getAllIcons().find { it.id == id } Text( text = - stringResource( - R.string.current_icon_format, - iconInfo?.label ?: id, - ), + stringResource( + R.string.current_icon_format, + iconInfo?.label ?: id, + ), style = MaterialTheme.typography.bodyMedium, ) MaterialIconsLibrary.getCategoryForIcon(id)?.let { category -> @@ -436,11 +432,7 @@ fun IconSelectionScreen( } @Composable -private fun CategoryList( - categories: List, - currentIconId: String?, - onCategoryClick: (IconCategory) -> Unit, -) { +private fun CategoryList(categories: List, currentIconId: String?, onCategoryClick: (IconCategory) -> Unit) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -456,29 +448,25 @@ 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(), colors = - CardDefaults.cardColors( - containerColor = - if (hasSelectedIcon) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - }, - ), + CardDefaults.cardColors( + containerColor = + if (hasSelectedIcon) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + }, + ), ) { Row( modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + Modifier + .fillMaxWidth() + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { @@ -517,11 +505,7 @@ private fun CategoryCard( } @Composable -private fun IconGrid( - icons: List, - currentIconId: String?, - onIconClick: (ProfileIcon) -> Unit, -) { +private fun IconGrid(icons: List, currentIconId: String?, onIconClick: (ProfileIcon) -> Unit) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 72.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -539,38 +523,34 @@ 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 = - Modifier - .fillMaxWidth() - .aspectRatio(1f), + Modifier + .fillMaxWidth() + .aspectRatio(1f), colors = - CardDefaults.cardColors( - containerColor = - if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - }, - ), - border = + CardDefaults.cardColors( + containerColor = if (isSelected) { - CardDefaults.outlinedCardBorder() + MaterialTheme.colorScheme.primaryContainer } else { - null + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) }, + ), + border = + if (isSelected) { + CardDefaults.outlinedCardBorder() + } else { + null + }, ) { Column( modifier = - Modifier - .fillMaxSize() - .padding(8.dp), + Modifier + .fillMaxSize() + .padding(8.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -579,11 +559,11 @@ private fun IconGridItem( contentDescription = icon.label, modifier = Modifier.size(28.dp), tint = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, ) Spacer(modifier = Modifier.height(4.dp)) @@ -592,11 +572,11 @@ private fun IconGridItem( text = icon.label, style = MaterialTheme.typography.labelSmall, color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, maxLines = 2, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, @@ -610,9 +590,9 @@ private fun IconGridItem( private fun EmptySearchResult(query: String) { Column( modifier = - Modifier - .fillMaxSize() - .padding(32.dp), + Modifier + .fillMaxSize() + .padding(32.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/ManualScrollTextProcessor.java b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/ManualScrollTextProcessor.java index 4ed1dc0..ad1ead9 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/ManualScrollTextProcessor.java +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profile/ManualScrollTextProcessor.java @@ -4,129 +4,129 @@ 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 { - private final int touchSlop; - private boolean allowCursorAutoScroll = true; - private float downX; - private float downY; - private boolean userDragging; - private int downSelectionStart = -1; - private int downSelectionEnd = -1; - private boolean restoringSelection; + private final int touchSlop; + private boolean allowCursorAutoScroll = true; + private float downX; + private float downY; + private boolean userDragging; + private int downSelectionStart = -1; + private int downSelectionEnd = -1; + private boolean restoringSelection; - public ManualScrollTextProcessor(Context context) { - this(context, null); + public ManualScrollTextProcessor(Context context) { + this(context, null); + } + + public ManualScrollTextProcessor(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.autoCompleteTextViewStyle); + } + + public ManualScrollTextProcessor(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + } + + public void resumeAutoScroll() { + allowCursorAutoScroll = true; + userDragging = false; + } + + @Override + public boolean bringPointIntoView(int offset) { + if (allowCursorAutoScroll) { + return super.bringPointIntoView(offset); } + return false; + } - public ManualScrollTextProcessor(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.autoCompleteTextViewStyle); - } - - public ManualScrollTextProcessor(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); - } - - public void resumeAutoScroll() { - allowCursorAutoScroll = true; + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + downX = event.getX(); + downY = event.getY(); userDragging = false; + restoringSelection = false; + downSelectionStart = getSelectionStart(); + downSelectionEnd = getSelectionEnd(); + break; + case MotionEvent.ACTION_MOVE: + if (!userDragging) { + float dx = Math.abs(event.getX() - downX); + float dy = Math.abs(event.getY() - downY); + if (dx > touchSlop || dy > touchSlop) { + userDragging = true; + allowCursorAutoScroll = false; + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + break; + default: + break; } - @Override - public boolean bringPointIntoView(int offset) { - if (allowCursorAutoScroll) { - return super.bringPointIntoView(offset); - } - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - downX = event.getX(); - downY = event.getY(); - userDragging = false; - restoringSelection = false; - downSelectionStart = getSelectionStart(); - downSelectionEnd = getSelectionEnd(); - break; - case MotionEvent.ACTION_MOVE: - if (!userDragging) { - float dx = Math.abs(event.getX() - downX); - float dy = Math.abs(event.getY() - downY); - if (dx > touchSlop || dy > touchSlop) { - userDragging = true; - allowCursorAutoScroll = false; - } - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - break; - default: - break; - } - - boolean handled = super.onTouchEvent(event); - - switch (action) { - case MotionEvent.ACTION_MOVE: - if (userDragging) { - maybeRestoreSelection(); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (userDragging) { - maybeRestoreSelection(); - } else { - resumeAutoScroll(); - } - break; - default: - break; - } - - return handled; - } - - private void maybeRestoreSelection() { - if (userDragging && !restoringSelection) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); - if (selStart != downSelectionStart || selEnd != downSelectionEnd) { - restoringSelection = true; - int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart; - setSelection(downSelectionStart, targetEnd); - } - } - } - - @Override - protected void onSelectionChanged(int selStart, int selEnd) { - if (restoringSelection) { - restoringSelection = false; - super.onSelectionChanged(selStart, selEnd); - return; - } + boolean handled = super.onTouchEvent(event); + switch (action) { + case MotionEvent.ACTION_MOVE: if (userDragging) { - if (downSelectionStart >= 0 && (selStart != downSelectionStart || selEnd != downSelectionEnd)) { - restoringSelection = true; - int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart; - setSelection(downSelectionStart, targetEnd); - return; - } + maybeRestoreSelection(); } - - downSelectionStart = selStart; - downSelectionEnd = selEnd; - super.onSelectionChanged(selStart, selEnd); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (userDragging) { + maybeRestoreSelection(); + } else { + resumeAutoScroll(); + } + break; + default: + break; } + + return handled; + } + + private void maybeRestoreSelection() { + if (userDragging && !restoringSelection) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + if (selStart != downSelectionStart || selEnd != downSelectionEnd) { + restoringSelection = true; + int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart; + setSelection(downSelectionStart, targetEnd); + } + } + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + if (restoringSelection) { + restoringSelection = false; + super.onSelectionChanged(selStart, selEnd); + return; + } + + if (userDragging) { + if (downSelectionStart >= 0 + && (selStart != downSelectionStart || selEnd != downSelectionEnd)) { + restoringSelection = true; + int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart; + setSelection(downSelectionStart, targetEnd); + return; + } + } + + downSelectionStart = selStart; + downSelectionEnd = selEnd; + super.onSelectionChanged(selStart, selEnd); + } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profileoverride/PerAppProxyScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profileoverride/PerAppProxyScreen.kt index c26a698..84fd713 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/profileoverride/PerAppProxyScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/profileoverride/PerAppProxyScreen.kt @@ -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, - val selectedUids: Set, -) +private data class LoadResult(val proxyMode: Int, val packages: List, val selectedUids: Set) -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(null) } var scanResult by remember { mutableStateOf(null) } - fun buildPackageList(newUids: Set): Set { - return newUids.mapNotNull { uid -> - packages.find { it.uid == uid }?.packageName - }.toSet() - } + fun buildPackageList(newUids: Set): Set = newUids.mapNotNull { uid -> + packages.find { it.uid == uid }?.packageName + }.toSet() fun updateCurrentPackages(filterQuery: String) { currentPackages = @@ -411,10 +399,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) { ) }, colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - titleContentColor = MaterialTheme.colorScheme.onSurface, - ), + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + ), ) } @@ -435,11 +423,11 @@ fun PerAppProxyScreen(onBack: () -> Unit) { ) { Text( text = - if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) { - stringResource(R.string.per_app_proxy_mode_include_description) - } else { - stringResource(R.string.per_app_proxy_mode_exclude_description) - }, + if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) { + stringResource(R.string.per_app_proxy_mode_include_description) + } else { + stringResource(R.string.per_app_proxy_mode_exclude_description) + }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), style = MaterialTheme.typography.bodyMedium, ) @@ -465,10 +453,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) { updateCurrentPackages(it) }, modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .focusRequester(focusRequester), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .focusRequester(focusRequester), placeholder = { Text(stringResource(R.string.search)) }, leadingIcon = { Icon( @@ -497,10 +485,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = - androidx.compose.foundation.layout.PaddingValues( - horizontal = 16.dp, - vertical = 12.dp, - ), + androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 12.dp, + ), verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(currentPackages, key = { it.packageName }) { packageCache -> @@ -609,10 +597,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) { Spacer(modifier = Modifier.height(16.dp)) Box( modifier = - Modifier - .fillMaxWidth() - .heightIn(max = 360.dp) - .verticalScroll(rememberScrollState()), + Modifier + .fillMaxWidth() + .heightIn(max = 360.dp) + .verticalScroll(rememberScrollState()), ) { Text( text = dialogContent, @@ -722,11 +710,11 @@ private fun PerAppProxyMenus( trailingIcon = { Icon( imageVector = - if (showModeMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showModeMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -742,18 +730,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -768,18 +756,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -799,11 +787,11 @@ private fun PerAppProxyMenus( trailingIcon = { Icon( imageVector = - if (showSortMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showSortMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -819,18 +807,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (sortMode == SortMode.NAME) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (sortMode == SortMode.NAME) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (sortMode == SortMode.NAME) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (sortMode == SortMode.NAME) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -845,18 +833,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (sortMode == SortMode.PACKAGE_NAME) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (sortMode == SortMode.PACKAGE_NAME) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (sortMode == SortMode.PACKAGE_NAME) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (sortMode == SortMode.PACKAGE_NAME) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -871,18 +859,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (sortMode == SortMode.UID) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (sortMode == SortMode.UID) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (sortMode == SortMode.UID) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (sortMode == SortMode.UID) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -897,18 +885,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (sortMode == SortMode.INSTALL_TIME) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (sortMode == SortMode.INSTALL_TIME) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (sortMode == SortMode.INSTALL_TIME) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (sortMode == SortMode.INSTALL_TIME) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -923,18 +911,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (sortMode == SortMode.UPDATE_TIME) { - Icons.Default.RadioButtonChecked - } else { - Icons.Default.RadioButtonUnchecked - }, + if (sortMode == SortMode.UPDATE_TIME) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (sortMode == SortMode.UPDATE_TIME) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (sortMode == SortMode.UPDATE_TIME) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -949,18 +937,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (sortReverse) { - Icons.Default.Check - } else { - Icons.Default.RadioButtonUnchecked - }, + if (sortReverse) { + Icons.Default.Check + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (sortReverse) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (sortReverse) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -980,11 +968,11 @@ private fun PerAppProxyMenus( trailingIcon = { Icon( imageVector = - if (showFilterMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showFilterMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -1000,18 +988,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (hideSystemApps) { - Icons.Default.Check - } else { - Icons.Default.RadioButtonUnchecked - }, + if (hideSystemApps) { + Icons.Default.Check + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (hideSystemApps) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (hideSystemApps) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -1026,18 +1014,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (hideOfflineApps) { - Icons.Default.Check - } else { - Icons.Default.RadioButtonUnchecked - }, + if (hideOfflineApps) { + Icons.Default.Check + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (hideOfflineApps) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (hideOfflineApps) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -1052,18 +1040,18 @@ private fun PerAppProxyMenus( leadingIcon = { Icon( imageVector = - if (hideDisabledApps) { - Icons.Default.Check - } else { - Icons.Default.RadioButtonUnchecked - }, + if (hideDisabledApps) { + Icons.Default.Check + } else { + Icons.Default.RadioButtonUnchecked + }, contentDescription = null, tint = - if (hideDisabledApps) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + if (hideDisabledApps) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, modifier = Modifier.padding(start = 24.dp), ) }, @@ -1083,11 +1071,11 @@ private fun PerAppProxyMenus( trailingIcon = { Icon( imageVector = - if (showSelectMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showSelectMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -1140,11 +1128,11 @@ private fun PerAppProxyMenus( trailingIcon = { Icon( imageVector = - if (showBackupMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showBackupMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -1197,11 +1185,11 @@ private fun PerAppProxyMenus( trailingIcon = { Icon( imageVector = - if (showScanMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showScanMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, @@ -1331,7 +1319,7 @@ object PerAppProxyScanner { if (!( packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(".dex") - ) + ) ) { continue } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRCodeSmartCrop.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRCodeSmartCrop.kt index 45add0c..d9a5de0 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRCodeSmartCrop.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRCodeSmartCrop.kt @@ -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) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRScanViewModel.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRScanViewModel.kt index 9537934..7c1d1ec 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRScanViewModel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/QRScanViewModel.kt @@ -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) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/ZxingQRCodeAnalyzer.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/ZxingQRCodeAnalyzer.kt index f7bb989..f79eb9c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/ZxingQRCodeAnalyzer.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/qrscan/ZxingQRCodeAnalyzer.kt @@ -109,12 +109,10 @@ class ZxingQRCodeAnalyzer( return yData } - private fun tryDecode(bitmap: BinaryBitmap): Result? { - return try { - qrCodeReader.decode(bitmap) - } catch (_: NotFoundException) { - qrCodeReader.reset() - null - } + private fun tryDecode(bitmap: BinaryBitmap): Result? = try { + qrCodeReader.decode(bitmap) + } catch (_: NotFoundException) { + qrCodeReader.reset() + null } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/AppSettingsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/AppSettingsScreen.kt index e2ae02b..3785c43 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/AppSettingsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/AppSettingsScreen.kt @@ -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,10 +136,12 @@ fun AppSettingsScreen(navController: NavController) { isMethodAvailable = success silentInstallError = if (success) { null - } 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 { + 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) + } } } } @@ -224,10 +226,12 @@ fun AppSettingsScreen(navController: NavController) { isMethodAvailable = success silentInstallError = if (success) { null - } 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) + } 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) + } } } }, @@ -259,22 +263,22 @@ fun AppSettingsScreen(navController: NavController) { Column( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp), + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + .padding(vertical = 8.dp), ) { // Info Card Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { ListItem( @@ -303,12 +307,12 @@ fun AppSettingsScreen(navController: NavController) { } }, modifier = - Modifier - .clip(RoundedCornerShape(12.dp)), + Modifier + .clip(RoundedCornerShape(12.dp)), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -324,13 +328,13 @@ fun AppSettingsScreen(navController: NavController) { Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { val updateItemCount = @@ -393,12 +397,12 @@ fun AppSettingsScreen(navController: NavController) { ) }, modifier = - updateItemModifier() - .clickable { showTrackDialog = true }, + updateItemModifier() + .clickable { showTrackDialog = true }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -429,9 +433,9 @@ fun AppSettingsScreen(navController: NavController) { }, modifier = updateItemModifier(), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) if (Vendor.supportsSilentInstall()) { @@ -478,10 +482,12 @@ fun AppSettingsScreen(navController: NavController) { isMethodAvailable = success silentInstallError = if (success) { null - } 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 { + 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 { @@ -493,9 +499,9 @@ fun AppSettingsScreen(navController: NavController) { }, modifier = updateItemModifier(), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) if (silentInstallEnabled) { @@ -510,11 +516,13 @@ fun AppSettingsScreen(navController: NavController) { Text( if (xposedActivated) { stringResource(R.string.install_method_root) - } 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 + } 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, ) @@ -527,12 +535,12 @@ fun AppSettingsScreen(navController: NavController) { ) }, modifier = - updateItemModifier() - .let { if (!xposedActivated) it.clickable { showInstallMethodMenu = true } else it }, + updateItemModifier() + .let { if (!xposedActivated) it.clickable { showInstallMethodMenu = true } else it }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) if (silentInstallMethod == "SHIZUKU" && !isMethodAvailable) { @@ -558,15 +566,15 @@ fun AppSettingsScreen(navController: NavController) { ) }, modifier = - updateItemModifier() - .clickable { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/")) - context.startActivity(intent) - }, + updateItemModifier() + .clickable { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/")) + context.startActivity(intent) + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -593,18 +601,18 @@ fun AppSettingsScreen(navController: NavController) { ) }, modifier = - updateItemModifier() - .clickable { - val intent = Intent( - AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - Uri.parse("package:${context.packageName}") - ) - context.startActivity(intent) - }, + updateItemModifier() + .clickable { + val intent = Intent( + AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:${context.packageName}"), + ) + context.startActivity(intent) + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -646,9 +654,9 @@ fun AppSettingsScreen(navController: NavController) { }, modifier = updateItemModifier(), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -666,13 +674,13 @@ fun AppSettingsScreen(navController: NavController) { Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { ListItem( @@ -698,40 +706,40 @@ fun AppSettingsScreen(navController: NavController) { } }, modifier = - Modifier - .clip( - if (hasUpdate) { - RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) - } else { - RoundedCornerShape(12.dp) - }, - ) - .clickable(enabled = !isChecking) { - if (hasUpdate && updateInfo != null) { - showUpdateAvailableDialog = true - } else { - scope.launch { - UpdateState.isChecking.value = true - withContext(Dispatchers.IO) { - try { - val result = Vendor.checkUpdateAsync() - UpdateState.setUpdate(result) - if (result == null) { - showErrorDialog = R.string.no_updates_available - } - } catch (_: UpdateCheckException.TrackNotSupported) { - showErrorDialog = R.string.update_track_not_supported - } catch (_: Exception) { - } - } - UpdateState.isChecking.value = false - } - } + Modifier + .clip( + if (hasUpdate) { + RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) + } else { + RoundedCornerShape(12.dp) }, + ) + .clickable(enabled = !isChecking) { + if (hasUpdate && updateInfo != null) { + showUpdateAvailableDialog = true + } else { + scope.launch { + UpdateState.isChecking.value = true + withContext(Dispatchers.IO) { + try { + val result = Vendor.checkUpdateAsync() + UpdateState.setUpdate(result) + if (result == null) { + showErrorDialog = R.string.no_updates_available + } + } catch (_: UpdateCheckException.TrackNotSupported) { + showErrorDialog = R.string.update_track_not_supported + } catch (_: Exception) { + } + } + UpdateState.isChecking.value = false + } + } + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) if (hasUpdate && updateInfo != null) { @@ -756,15 +764,15 @@ fun AppSettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .clickable { - showUpdateAvailableDialog = true - }, + Modifier + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) + .clickable { + showUpdateAvailableDialog = true + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -791,11 +799,11 @@ private fun UpdateTrackDialog( tracks.forEach { (value, label) -> Row( modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .clickable { onTrackSelected(value) } - .padding(vertical = 8.dp), + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onTrackSelected(value) } + .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton( @@ -841,11 +849,11 @@ private fun InstallMethodDialog( methods.forEach { (value, label) -> Row( modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .clickable { onMethodSelected(value) } - .padding(vertical = 8.dp), + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onMethodSelected(value) } + .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton( diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/CoreSettingsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/CoreSettingsScreen.kt index d0e4229..c23400c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/CoreSettingsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/CoreSettingsScreen.kt @@ -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 @@ -95,22 +95,22 @@ fun CoreSettingsScreen(navController: NavController) { Column( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp), + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + .padding(vertical = 8.dp), ) { // Core Information Card Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { // Version Info @@ -138,9 +138,9 @@ fun CoreSettingsScreen(navController: NavController) { }, modifier = Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) // Data Size @@ -167,16 +167,16 @@ fun CoreSettingsScreen(navController: NavController) { ) }, modifier = - Modifier.clip( - RoundedCornerShape( - bottomStart = 12.dp, - bottomEnd = 12.dp, - ), + Modifier.clip( + RoundedCornerShape( + bottomStart = 12.dp, + bottomEnd = 12.dp, ), + ), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -193,13 +193,13 @@ fun CoreSettingsScreen(navController: NavController) { Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { ListItem( headlineContent = { @@ -228,9 +228,9 @@ fun CoreSettingsScreen(navController: NavController) { }, modifier = Modifier.clip(RoundedCornerShape(12.dp)), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -246,13 +246,13 @@ fun CoreSettingsScreen(navController: NavController) { // Working Directory Card Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { // Browse ListItem( @@ -270,15 +270,15 @@ fun CoreSettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - .clickable { - openInFileManager(context) - }, + Modifier + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .clickable { + openInFileManager(context) + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) // Destroy @@ -298,28 +298,28 @@ fun CoreSettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .clickable { - scope.launch(Dispatchers.IO) { - val filesDir = context.getExternalFilesDir(null) ?: context.filesDir - filesDir.deleteRecursively() - filesDir.mkdirs() + Modifier + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) + .clickable { + scope.launch(Dispatchers.IO) { + val filesDir = context.getExternalFilesDir(null) ?: context.filesDir + filesDir.deleteRecursively() + filesDir.mkdirs() - // Recalculate data size - val newSize = - filesDir.walkTopDown() - .filter { it.isFile } - .map { it.length() } - .sum() - val formattedSize = Libbox.formatBytes(newSize) - dataSize = formattedSize - } - }, + // Recalculate data size + val newSize = + filesDir.walkTopDown() + .filter { it.isFile } + .map { it.length() } + .sum() + val formattedSize = Libbox.formatBytes(newSize) + dataSize = formattedSize + } + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -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() } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/PrivilegeSettingsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/PrivilegeSettingsScreen.kt index cdb091f..dbcf6bc 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/PrivilegeSettingsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/PrivilegeSettingsScreen.kt @@ -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) { @@ -146,9 +145,9 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status HookStatusClient.refresh() } - val hasPendingDowngrade = HookModuleUpdateNotifier.isDowngrade(systemHookStatus) - val hasPendingUpdate = HookModuleUpdateNotifier.isUpgrade(systemHookStatus) - val hasPendingChange = hasPendingDowngrade || hasPendingUpdate + val hasPendingDowngrade = HookModuleUpdateNotifier.isDowngrade(systemHookStatus) + val hasPendingUpdate = HookModuleUpdateNotifier.isUpgrade(systemHookStatus) + val hasPendingChange = hasPendingDowngrade || hasPendingUpdate androidx.compose.runtime.LaunchedEffect(systemHookStatus) { HookModuleUpdateNotifier.maybeNotify(context, systemHookStatus) } @@ -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)) } @@ -413,11 +415,11 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status ) }, modifier = - Modifier - .clip(logItemShape) - .clickable { - navController.navigate("settings/privilege/logs") - }, + Modifier + .clip(logItemShape) + .clickable { + navController.navigate("settings/privilege/logs") + }, colors = ListItemDefaults.colors( containerColor = Color.Transparent, ), @@ -439,42 +441,42 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status ) }, modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .clickable { - val exportBase = File(context.cacheDir, "debug") - if (!exportBase.exists()) { - exportBase.mkdirs() + Modifier + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) + .clickable { + val exportBase = File(context.cacheDir, "debug") + if (!exportBase.exists()) { + exportBase.mkdirs() + } + val timestamp = + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val outZip = File(exportBase, "sing-box-lsposed-debug-$timestamp.zip") + exportCancelled = false + exportError = null + showExportProgressDialog = true + scope.launch { + val result = withContext(Dispatchers.IO) { + PrivilegeSettingsClient.exportDebugInfo(outZip.absolutePath) } - val timestamp = - SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) - val outZip = File(exportBase, "sing-box-lsposed-debug-${timestamp}.zip") - exportCancelled = false - exportError = null - showExportProgressDialog = true - scope.launch { - val result = withContext(Dispatchers.IO) { - PrivilegeSettingsClient.exportDebugInfo(outZip.absolutePath) - } - if (exportCancelled) { - outZip.delete() - return@launch - } - showExportProgressDialog = false - val failure = result.error - if (failure == null) { - exportedFile = outZip - showExportSuccessDialog = true - } else { - messageDialogTitle = context.getString(R.string.error_title) - messageDialogMessage = context.getString( - R.string.privilege_settings_export_debug_failed, - failure - ) - showMessageDialog = true - } + if (exportCancelled) { + outZip.delete() + return@launch } - }, + showExportProgressDialog = false + val failure = result.error + if (failure == null) { + exportedFile = outZip + showExportSuccessDialog = true + } else { + messageDialogTitle = context.getString(R.string.error_title) + messageDialogMessage = context.getString( + R.string.privilege_settings_export_debug_failed, + failure, + ) + showMessageDialog = true + } + } + }, colors = ListItemDefaults.colors( containerColor = Color.Transparent, ), @@ -496,44 +498,44 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status ) }, modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .clickable { - scope.launch { - val failure = withContext(Dispatchers.IO) { - runCatching { - val process = Runtime.getRuntime().exec( - arrayOf( - "su", - "-c", - "/system/bin/svc power reboot || /system/bin/reboot", - ), - ) - val error = process.errorStream.bufferedReader().use { it.readText().trim() } - process.inputStream.close() - process.outputStream.close() - process.errorStream.close() - val code = process.waitFor() - if (code == 0) { - null - } else { - error.ifBlank { "exit=$code" } - } - }.getOrElse { it.message ?: "unknown" } - } - if (failure != null) { - val message = - if (failure == "unknown" || failure.startsWith("exit=")) { - context.getString(R.string.root_access_required) - } else { - context.getString(R.string.privilege_module_restart_failed, failure) - } - messageDialogTitle = context.getString(R.string.error_title) - messageDialogMessage = message - showMessageDialog = true - } + Modifier + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) + .clickable { + scope.launch { + val failure = withContext(Dispatchers.IO) { + runCatching { + val process = Runtime.getRuntime().exec( + arrayOf( + "su", + "-c", + "/system/bin/svc power reboot || /system/bin/reboot", + ), + ) + val error = process.errorStream.bufferedReader().use { it.readText().trim() } + process.inputStream.close() + process.outputStream.close() + process.errorStream.close() + val code = process.waitFor() + if (code == 0) { + null + } else { + error.ifBlank { "exit=$code" } + } + }.getOrElse { it.message ?: "unknown" } } - }, + if (failure != null) { + val message = + if (failure == "unknown" || failure.startsWith("exit=")) { + context.getString(R.string.root_access_required) + } else { + context.getString(R.string.privilege_module_restart_failed, failure) + } + messageDialogTitle = context.getString(R.string.error_title) + messageDialogMessage = message + showMessageDialog = true + } + } + }, colors = ListItemDefaults.colors( containerColor = Color.Transparent, ), @@ -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( diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ProfileOverrideScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ProfileOverrideScreen.kt index 59d7740..9f1f76c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ProfileOverrideScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ProfileOverrideScreen.kt @@ -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 @@ -162,22 +162,22 @@ fun ProfileOverrideScreen(navController: NavController) { Column( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp), + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + .padding(vertical = 8.dp), ) { // Card 1: Auto Redirect Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { ListItem( headlineContent = { @@ -232,9 +232,9 @@ fun ProfileOverrideScreen(navController: NavController) { }, modifier = Modifier.clip(RoundedCornerShape(12.dp)), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } @@ -254,13 +254,13 @@ fun ProfileOverrideScreen(navController: NavController) { Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { // Mode selector (only when privileged query is needed) @@ -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 @@ -355,19 +367,19 @@ fun ProfileOverrideScreen(navController: NavController) { ) }, modifier = - Modifier.clip( - if (showModeSelector) { - RoundedCornerShape(0.dp) - } else if (perAppProxyEnabled && canUsePerAppProxy) { - RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) - } else { - RoundedCornerShape(12.dp) - }, - ), + Modifier.clip( + if (showModeSelector) { + RoundedCornerShape(0.dp) + } else if (perAppProxyEnabled && canUsePerAppProxy) { + RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) + } else { + RoundedCornerShape(12.dp) + }, + ), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) if (perAppProxyEnabled && canUsePerAppProxy) { @@ -409,13 +421,13 @@ fun ProfileOverrideScreen(navController: NavController) { ) }, modifier = - Modifier.clickable(enabled = manageEnabled) { - navController.navigate("settings/profile_override/manage") - }, + Modifier.clickable(enabled = manageEnabled) { + navController.navigate("settings/profile_override/manage") + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) // Managed Mode toggle @@ -477,9 +489,9 @@ fun ProfileOverrideScreen(navController: NavController) { }, modifier = Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -601,7 +613,7 @@ fun ProfileOverrideScreen(navController: NavController) { Toast.makeText( context, R.string.root_access_denied, - Toast.LENGTH_LONG + Toast.LENGTH_LONG, ).show() } } @@ -706,7 +718,7 @@ private suspend fun scanAllChinaApps(): Set = withContext(Dispatchers.De val chinaApps = mutableSetOf() installedPackages.map { packageInfo -> async { - if (PerAppProxyScanner.scanChinaPackage(packageInfo)) { + if (PerAppProxyScanner.scanChinaPackage(packageInfo)) { synchronized(chinaApps) { chinaApps.add(packageInfo.packageName) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ServiceSettingsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ServiceSettingsScreen.kt index a2254f5..6ed9dd4 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ServiceSettingsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/ServiceSettingsScreen.kt @@ -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)) }, @@ -113,23 +110,23 @@ fun ServiceSettingsScreen( Column( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp), + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + .padding(vertical = 8.dp), ) { // Background Permission Card (only show if battery optimization is not ignored) if (!isBatteryOptimizationIgnored && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f), - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f), + ), ) { Column( modifier = Modifier.padding(16.dp), @@ -193,13 +190,13 @@ fun ServiceSettingsScreen( // Options Section Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { ListItem( headlineContent = { @@ -234,9 +231,9 @@ fun ServiceSettingsScreen( }, modifier = Modifier.clip(RoundedCornerShape(12.dp)), colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/SettingsScreen.kt b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/SettingsScreen.kt index 570066d..f468f51 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/screen/settings/SettingsScreen.kt @@ -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 @@ -87,22 +83,22 @@ fun SettingsScreen(navController: NavController) { Column( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp), + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + .padding(vertical = 8.dp), ) { // General Settings Group Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { ListItem( @@ -125,13 +121,13 @@ fun SettingsScreen(navController: NavController) { } }, modifier = - Modifier - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - .clickable { navController.navigate("settings/app") }, + Modifier + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .clickable { navController.navigate("settings/app") }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) ListItem( @@ -149,12 +145,12 @@ fun SettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clickable { navController.navigate("settings/core") }, + Modifier + .clickable { navController.navigate("settings/core") }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) ListItem( @@ -178,9 +174,9 @@ fun SettingsScreen(navController: NavController) { }, modifier = Modifier.clickable { navController.navigate("settings/service") }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) ListItem( @@ -198,12 +194,12 @@ fun SettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clickable { navController.navigate("settings/profile_override") }, + Modifier + .clickable { navController.navigate("settings/profile_override") }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) ListItem( @@ -228,13 +224,13 @@ fun SettingsScreen(navController: NavController) { } }, modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .clickable { navController.navigate("settings/privilege") }, + Modifier + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) + .clickable { navController.navigate("settings/privilege") }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } @@ -249,13 +245,13 @@ fun SettingsScreen(navController: NavController) { Card( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), ) { Column { ListItem( @@ -280,17 +276,17 @@ fun SettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - .clickable { - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW) - intent.data = android.net.Uri.parse("https://sing-box.sagernet.org/") - context.startActivity(intent) - }, + Modifier + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .clickable { + val intent = android.content.Intent(android.content.Intent.ACTION_VIEW) + intent.data = android.net.Uri.parse("https://sing-box.sagernet.org/") + context.startActivity(intent) + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) ListItem( @@ -315,17 +311,17 @@ fun SettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clickable { - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW) - intent.data = - android.net.Uri.parse("https://github.com/SagerNet/sing-box-for-android") - context.startActivity(intent) - }, + Modifier + .clickable { + val intent = android.content.Intent(android.content.Intent.ACTION_VIEW) + intent.data = + android.net.Uri.parse("https://github.com/SagerNet/sing-box-for-android") + context.startActivity(intent) + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) ListItem( @@ -350,17 +346,17 @@ fun SettingsScreen(navController: NavController) { ) }, modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .clickable { - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW) - intent.data = android.net.Uri.parse("https://sekai.icu/sponsors/") - context.startActivity(intent) - }, + Modifier + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) + .clickable { + val intent = android.content.Intent(android.content.Intent.ACTION_VIEW) + intent.data = android.net.Uri.parse("https://sekai.icu/sponsors/") + context.startActivity(intent) + }, colors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - ), + ListItemDefaults.colors( + containerColor = Color.Transparent, + ), ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/shared/AppSelectionComponents.kt b/app/src/main/java/io/nekohasekai/sfa/compose/shared/AppSelectionComponents.kt index 21d1be3..2272ebe 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/shared/AppSelectionComponents.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/shared/AppSelectionComponents.kt @@ -178,9 +178,9 @@ fun AppSelectionCard( modifier = cardModifier, shape = cardShape, colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - ), + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ), ) { Row( modifier = Modifier.padding(12.dp), @@ -236,11 +236,11 @@ fun AppSelectionCard( trailingIcon = { Icon( imageVector = - if (showCopyMenu) { - Icons.Default.ExpandLess - } else { - Icons.Default.ExpandMore - }, + if (showCopyMenu) { + Icons.Default.ExpandLess + } else { + Icons.Default.ExpandMore + }, contentDescription = null, ) }, diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/theme/Type.kt b/app/src/main/java/io/nekohasekai/sfa/compose/theme/Type.kt index d66330f..48cccc3 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/theme/Type.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/theme/Type.kt @@ -11,127 +11,127 @@ val Typography = Typography( // Display styles displayLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), displayMedium = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), displaySmall = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), // Headline styles headlineLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), headlineMedium = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), headlineSmall = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), // Title styles titleLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), titleMedium = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), titleSmall = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), // Body styles bodyLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), bodyMedium = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), bodySmall = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), // Label styles labelLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), labelMedium = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), labelSmall = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), ) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/topbar/TopBarController.kt b/app/src/main/java/io/nekohasekai/sfa/compose/topbar/TopBarController.kt index a027c92..6c80f2e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/topbar/TopBarController.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/topbar/TopBarController.kt @@ -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>, -) { +class TopBarController internal constructor(private val state: MutableState>) { 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) } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/util/MaterialIconsLibrary.kt b/app/src/main/java/io/nekohasekai/sfa/compose/util/MaterialIconsLibrary.kt index 2d37d47..b6bb873 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/util/MaterialIconsLibrary.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/util/MaterialIconsLibrary.kt @@ -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, -) +data class IconCategory(val name: String, val icons: List) object MaterialIconsLibrary { val categories = @@ -416,20 +413,16 @@ object MaterialIconsLibrary { ), ) - fun getAllIcons(): List { - return categories.flatMap { it.icons } - } + fun getAllIcons(): List = 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 -> - category.icons.any { it.id == iconId } - }?.name - } + fun getCategoryForIcon(iconId: String): String? = categories.find { category -> + category.icons.any { it.id == iconId } + }?.name fun searchIcons(query: String): List { val lowercaseQuery = query.lowercase() diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/util/ProfileIcons.kt b/app/src/main/java/io/nekohasekai/sfa/compose/util/ProfileIcons.kt index 19c0367..6ec38ac 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/util/ProfileIcons.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/util/ProfileIcons.kt @@ -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 { - return MaterialIconsLibrary.searchIcons(query) - } + fun searchIcons(query: String): List = MaterialIconsLibrary.searchIcons(query) fun getCategories() = MaterialIconsLibrary.categories } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/util/QRCodeGenerator.kt b/app/src/main/java/io/nekohasekai/sfa/compose/util/QRCodeGenerator.kt index c324186..e1f717e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/util/QRCodeGenerator.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/util/QRCodeGenerator.kt @@ -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) diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/util/RelativeTimeFormatter.kt b/app/src/main/java/io/nekohasekai/sfa/compose/util/RelativeTimeFormatter.kt index a12f2c9..3513a6c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/util/RelativeTimeFormatter.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/util/RelativeTimeFormatter.kt @@ -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() diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/IconCategory.kt b/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/IconCategory.kt index a271eac..040e630 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/IconCategory.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/IconCategory.kt @@ -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, -) { +data class IconCategory(val name: String, val icons: List) { val size: Int get() = icons.size } diff --git a/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/MaterialIconsLibrary.kt b/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/MaterialIconsLibrary.kt index 3921dfe..9d2c351 100644 --- a/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/MaterialIconsLibrary.kt +++ b/app/src/main/java/io/nekohasekai/sfa/compose/util/icons/MaterialIconsLibrary.kt @@ -52,16 +52,12 @@ object MaterialIconsLibrary { /** * Get all icons from all categories */ - fun getAllIcons(): List { - return categories.flatMap { it.icons } - } + fun getAllIcons(): List = 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 { - return categories.find { it.name.equals(categoryName, ignoreCase = true) }?.icons - ?: emptyList() - } + fun getIconsByCategory(categoryName: String): List = 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 { - return categories.map { it.name } - } + fun getCategoryNames(): List = categories.map { it.name } } diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/Bugs.kt b/app/src/main/java/io/nekohasekai/sfa/constant/Bugs.kt index d83575c..680c0fc 100644 --- a/app/src/main/java/io/nekohasekai/sfa/constant/Bugs.kt +++ b/app/src/main/java/io/nekohasekai/sfa/constant/Bugs.kt @@ -8,6 +8,7 @@ object Bugs { // https://github.com/golang/go/issues/68760 val fixAndroidStack = BuildConfig.DEBUG || - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1 || + Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && + Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P } diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/EnabledType.kt b/app/src/main/java/io/nekohasekai/sfa/constant/EnabledType.kt index 46be667..fb70bf5 100644 --- a/app/src/main/java/io/nekohasekai/sfa/constant/EnabledType.kt +++ b/app/src/main/java/io/nekohasekai/sfa/constant/EnabledType.kt @@ -8,27 +8,18 @@ enum class EnabledType(val boolValue: Boolean) { Disabled(false), ; - fun getString(context: Context): String { - return when (this) { - Enabled -> context.getString(R.string.enabled) - Disabled -> context.getString(R.string.disabled) - } + fun getString(context: Context): String = when (this) { + Enabled -> context.getString(R.string.enabled) + Disabled -> context.getString(R.string.disabled) } companion object { - fun from(value: Boolean): EnabledType { - return if (value) Enabled else Disabled - } + fun from(value: Boolean): EnabledType = if (value) Enabled else Disabled - fun valueOf( - context: Context, - value: String, - ): EnabledType { - return when (value) { - context.getString(R.string.enabled) -> Enabled - context.getString(R.string.disabled) -> Disabled - else -> Disabled - } + fun valueOf(context: Context, value: String): EnabledType = when (value) { + context.getString(R.string.enabled) -> Enabled + context.getString(R.string.disabled) -> Disabled + else -> Disabled } } } diff --git a/app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt b/app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt index a3b64eb..b0e1643 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt @@ -35,17 +35,11 @@ object ProfileManager { .build() } - suspend fun nextOrder(): Long { - return instance.profileDao().nextOrder() ?: 0 - } + suspend fun nextOrder(): Long = instance.profileDao().nextOrder() ?: 0 - suspend fun nextFileID(): Long { - return instance.profileDao().nextFileID() ?: 1 - } + suspend fun nextFileID(): Long = instance.profileDao().nextFileID() ?: 1 - suspend fun get(id: Long): Profile? { - return instance.profileDao().get(id) - } + suspend fun get(id: Long): Profile? = instance.profileDao().get(id) suspend fun create(profile: Profile, andSelect: Boolean = false): Profile { profile.id = instance.profileDao().insert(profile) @@ -98,7 +92,5 @@ object ProfileManager { } } - suspend fun list(): List { - return instance.profileDao().list() - } + suspend fun list(): List = instance.profileDao().list() } diff --git a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt index efded71..b79ab58 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt @@ -82,12 +82,10 @@ object Settings { const val PACKAGE_QUERY_MODE_ROOT = "ROOT" var perAppProxyPackageQueryMode by dataStore.string(SettingsKey.PER_APP_PROXY_PACKAGE_QUERY_MODE) { PACKAGE_QUERY_MODE_SHIZUKU } - fun getEffectivePerAppProxyList(): Set { - return if (perAppProxyManagedMode) { - perAppProxyList union perAppProxyManagedList - } else { - perAppProxyList - } + fun getEffectivePerAppProxyList(): Set = if (perAppProxyManagedMode) { + perAppProxyList union perAppProxyManagedList + } else { + perAppProxyList } var systemProxyEnabled by dataStore.boolean(SettingsKey.SYSTEM_PROXY_ENABLED) { true } @@ -95,7 +93,7 @@ object Settings { var privilegeSettingsEnabled by dataStore.boolean(SettingsKey.PRIVILEGE_SETTINGS_ENABLED) { false } var privilegeSettingsList by dataStore.stringSet(SettingsKey.PRIVILEGE_SETTINGS_LIST) { emptySet() } var privilegeSettingsInterfaceRenameEnabled by dataStore.boolean( - SettingsKey.PRIVILEGE_SETTINGS_INTERFACE_RENAME_ENABLED + SettingsKey.PRIVILEGE_SETTINGS_INTERFACE_RENAME_ENABLED, ) { false } var privilegeSettingsInterfacePrefix by dataStore.string(SettingsKey.PRIVILEGE_SETTINGS_INTERFACE_PREFIX) { "wlan" } @@ -106,11 +104,9 @@ object Settings { var cachedApkPath by dataStore.string(SettingsKey.CACHED_APK_PATH) { "" } var lastShownUpdateVersion by dataStore.int(SettingsKey.LAST_SHOWN_UPDATE_VERSION) { 0 } - fun serviceClass(): Class<*> { - return when (serviceMode) { - ServiceMode.VPN -> VPNService::class.java - else -> ProxyService::class.java - } + fun serviceClass(): Class<*> = when (serviceMode) { + ServiceMode.VPN -> VPNService::class.java + else -> ProxyService::class.java } suspend fun rebuildServiceMode(): Boolean { diff --git a/app/src/main/java/io/nekohasekai/sfa/database/TypedProfile.kt b/app/src/main/java/io/nekohasekai/sfa/database/TypedProfile.kt index 8c26f30..77c826f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/TypedProfile.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/TypedProfile.kt @@ -16,11 +16,9 @@ class TypedProfile() : Parcelable { Remote, ; - fun getString(context: Context): String { - return when (this) { - Local -> context.getString(R.string.profile_type_local) - Remote -> context.getString(R.string.profile_type_remote) - } + fun getString(context: Context): String = when (this) { + Local -> context.getString(R.string.profile_type_local) + Remote -> context.getString(R.string.profile_type_remote) } companion object { @@ -54,10 +52,7 @@ class TypedProfile() : Parcelable { } } - override fun writeToParcel( - writer: Parcel, - flags: Int, - ) { + override fun writeToParcel(writer: Parcel, flags: Int) { writer.writeInt(1) writer.writeString(path) writer.writeInt(type.ordinal) @@ -67,18 +62,12 @@ class TypedProfile() : Parcelable { writer.writeInt(autoUpdateInterval) } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): TypedProfile { - return TypedProfile(parcel) - } + override fun createFromParcel(parcel: Parcel): TypedProfile = TypedProfile(parcel) - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array = arrayOfNulls(size) } class Convertor { diff --git a/app/src/main/java/io/nekohasekai/sfa/database/preference/KeyValueEntity.kt b/app/src/main/java/io/nekohasekai/sfa/database/preference/KeyValueEntity.kt index 9d6dbab..29c0500 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/preference/KeyValueEntity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/preference/KeyValueEntity.kt @@ -24,13 +24,9 @@ class KeyValueEntity() : Parcelable { @JvmField val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): KeyValueEntity { - return KeyValueEntity(parcel) - } + override fun createFromParcel(parcel: Parcel): KeyValueEntity = KeyValueEntity(parcel) - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array = arrayOfNulls(size) } } @@ -129,16 +125,14 @@ class KeyValueEntity() : Parcelable { } @Suppress("IMPLICIT_CAST_TO_ANY") - override fun toString(): String { - return when (valueType) { - TYPE_BOOLEAN -> boolean - TYPE_FLOAT -> float - TYPE_LONG -> long - TYPE_STRING -> string - TYPE_STRING_SET -> stringSet - else -> null - }?.toString() ?: "null" - } + override fun toString(): String = when (valueType) { + TYPE_BOOLEAN -> boolean + TYPE_FLOAT -> float + TYPE_LONG -> long + TYPE_STRING -> string + TYPE_STRING_SET -> stringSet + else -> null + }?.toString() ?: "null" constructor(parcel: Parcel) : this() { key = parcel.readString()!! @@ -146,16 +140,11 @@ class KeyValueEntity() : Parcelable { value = parcel.createByteArray()!! } - override fun writeToParcel( - parcel: Parcel, - flags: Int, - ) { + override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(key) parcel.writeInt(valueType) parcel.writeByteArray(value) } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 } diff --git a/app/src/main/java/io/nekohasekai/sfa/database/preference/OnPreferenceDataStoreChangeListener.kt b/app/src/main/java/io/nekohasekai/sfa/database/preference/OnPreferenceDataStoreChangeListener.kt index 418c0ef..ac5c7b8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/preference/OnPreferenceDataStoreChangeListener.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/preference/OnPreferenceDataStoreChangeListener.kt @@ -3,8 +3,5 @@ package io.nekohasekai.sfa.database.preference import androidx.preference.PreferenceDataStore interface OnPreferenceDataStoreChangeListener { - fun onPreferenceDataStoreChanged( - store: PreferenceDataStore, - key: String, - ) + fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) } diff --git a/app/src/main/java/io/nekohasekai/sfa/database/preference/RoomPreferenceDataStore.kt b/app/src/main/java/io/nekohasekai/sfa/database/preference/RoomPreferenceDataStore.kt index 5868d54..c868e45 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/preference/RoomPreferenceDataStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/preference/RoomPreferenceDataStore.kt @@ -3,8 +3,7 @@ package io.nekohasekai.sfa.database.preference import androidx.preference.PreferenceDataStore @Suppress("MemberVisibilityCanBePrivate", "unused") -open class RoomPreferenceDataStore(private val kvPairDao: KeyValueEntity.Dao) : - PreferenceDataStore() { +open class RoomPreferenceDataStore(private val kvPairDao: KeyValueEntity.Dao) : PreferenceDataStore() { fun getBoolean(key: String) = kvPairDao[key]?.boolean fun getFloat(key: String) = kvPairDao[key]?.float @@ -19,102 +18,54 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValueEntity.Dao) : fun reset() = kvPairDao.reset() - override fun getBoolean( - key: String, - defValue: Boolean, - ) = getBoolean(key) ?: defValue + override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue - override fun getFloat( - key: String, - defValue: Float, - ) = getFloat(key) ?: defValue + override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue - override fun getInt( - key: String, - defValue: Int, - ) = getInt(key) ?: defValue + override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue - override fun getLong( - key: String, - defValue: Long, - ) = getLong(key) ?: defValue + override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue - override fun getString( - key: String, - defValue: String?, - ) = getString(key) ?: defValue + override fun getString(key: String, defValue: String?) = getString(key) ?: defValue - override fun getStringSet( - key: String, - defValue: MutableSet?, - ) = getStringSet(key) ?: defValue + override fun getStringSet(key: String, defValue: MutableSet?) = getStringSet(key) ?: defValue - fun putBoolean( - key: String, - value: Boolean?, - ) = if (value == null) remove(key) else putBoolean(key, value) + fun putBoolean(key: String, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value) - fun putFloat( - key: String, - value: Float?, - ) = if (value == null) remove(key) else putFloat(key, value) + fun putFloat(key: String, value: Float?) = if (value == null) remove(key) else putFloat(key, value) - fun putInt( - key: String, - value: Int?, - ) = if (value == null) remove(key) else putLong(key, value.toLong()) + fun putInt(key: String, value: Int?) = if (value == null) remove(key) else putLong(key, value.toLong()) - fun putLong( - key: String, - value: Long?, - ) = if (value == null) remove(key) else putLong(key, value) + fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value) - override fun putBoolean( - key: String, - value: Boolean, - ) { + override fun putBoolean(key: String, value: Boolean) { kvPairDao.put(KeyValueEntity(key).put(value)) fireChangeListener(key) } - override fun putFloat( - key: String, - value: Float, - ) { + override fun putFloat(key: String, value: Float) { kvPairDao.put(KeyValueEntity(key).put(value)) fireChangeListener(key) } - override fun putInt( - key: String, - value: Int, - ) { + override fun putInt(key: String, value: Int) { kvPairDao.put(KeyValueEntity(key).put(value.toLong())) fireChangeListener(key) } - override fun putLong( - key: String, - value: Long, - ) { + override fun putLong(key: String, value: Long) { kvPairDao.put(KeyValueEntity(key).put(value)) fireChangeListener(key) } - override fun putString( - key: String, - value: String?, - ) = if (value == null) { + override fun putString(key: String, value: String?) = if (value == null) { remove(key) } else { kvPairDao.put(KeyValueEntity(key).put(value)) fireChangeListener(key) } - override fun putStringSet( - key: String, - values: MutableSet?, - ) = if (values == null) { + override fun putStringSet(key: String, values: MutableSet?) = if (values == null) { remove(key) } else { kvPairDao.put(KeyValueEntity(key).put(values)) diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Colors.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Colors.kt index 16c16d7..44fc467 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Colors.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Colors.kt @@ -10,20 +10,13 @@ import androidx.core.content.ContextCompat import com.google.android.material.color.MaterialColors @ColorInt -fun Context.getAttrColor( - @AttrRes attrColor: Int, - typedValue: TypedValue = TypedValue(), - resolveRefs: Boolean = true, -): Int { +fun Context.getAttrColor(@AttrRes attrColor: Int, typedValue: TypedValue = TypedValue(), resolveRefs: Boolean = true): Int { theme.resolveAttribute(attrColor, typedValue, resolveRefs) return typedValue.data } @ColorInt -fun colorForURLTestDelay( - context: Context, - urlTestDelay: Int, -): Int { +fun colorForURLTestDelay(context: Context, urlTestDelay: Int): Int { if (urlTestDelay <= 0) { return Color.GRAY } diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Context.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Context.kt index ac31b70..76a8b64 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Context.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Context.kt @@ -4,6 +4,4 @@ import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat -fun Context.hasPermission(permission: String): Boolean { - return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -} +fun Context.hasPermission(permission: String): Boolean = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt index 78c7c69..591e1cc 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt @@ -3,18 +3,14 @@ package io.nekohasekai.sfa.ktx import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import androidx.annotation.StringRes import android.widget.ScrollView import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sfa.R -fun Context.errorDialogBuilder( - @StringRes messageId: Int, -): MaterialAlertDialogBuilder { - return errorDialogBuilder(getString(messageId)) -} +fun Context.errorDialogBuilder(@StringRes messageId: Int): MaterialAlertDialogBuilder = errorDialogBuilder(getString(messageId)) fun Context.errorDialogBuilder(message: String): MaterialAlertDialogBuilder { val contentView = buildSelectableMessageView(message) @@ -27,9 +23,7 @@ fun Context.errorDialogBuilder(message: String): MaterialAlertDialogBuilder { .setPositiveButton(android.R.string.ok, null) } -fun Context.errorDialogBuilder(exception: Throwable): MaterialAlertDialogBuilder { - return errorDialogBuilder(exception.localizedMessage ?: exception.toString()) -} +fun Context.errorDialogBuilder(exception: Throwable): MaterialAlertDialogBuilder = errorDialogBuilder(exception.localizedMessage ?: exception.toString()) private fun Context.buildSelectableMessageView(message: String): ScrollView { val density = resources.displayMetrics.density diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Dimens.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Dimens.kt index c5921d7..a5a4d46 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Dimens.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Dimens.kt @@ -5,10 +5,6 @@ import kotlin.math.ceil private val density = Resources.getSystem().displayMetrics.density -fun dp2pxf(dpValue: Int): Float { - return density * dpValue -} +fun dp2pxf(dpValue: Int): Float = density * dpValue -fun dp2px(dpValue: Int): Int { - return ceil(dp2pxf(dpValue)).toInt() -} +fun dp2px(dpValue: Int): Int = ceil(dp2pxf(dpValue)).toInt() diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Inputs.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Inputs.kt index 166d860..d7e0e2e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Inputs.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Inputs.kt @@ -18,9 +18,7 @@ var TextInputLayout.error: String editText?.error = value } -fun TextInputLayout.setSimpleItems( - @ArrayRes redId: Int, -) { +fun TextInputLayout.setSimpleItems(@ArrayRes redId: Int) { (editText as? MaterialAutoCompleteTextView)?.setSimpleItems(redId) } diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt index 3ab1043..224df75 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt @@ -6,10 +6,7 @@ import androidx.activity.result.ActivityResultLauncher import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sfa.R -fun Activity.startFilesForResult( - launcher: ActivityResultLauncher, - input: String, -) { +fun Activity.startFilesForResult(launcher: ActivityResultLauncher, input: String) { try { return launcher.launch(input) } catch (_: ActivityNotFoundException) { diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Preferences.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Preferences.kt index dda95d0..bd0fcbd 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Preferences.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Preferences.kt @@ -3,60 +3,33 @@ package io.nekohasekai.sfa.ktx import androidx.preference.PreferenceDataStore import kotlin.reflect.KProperty -fun PreferenceDataStore.string( - name: String, - defaultValue: () -> String = { "" }, -) = PreferenceProxy(name, defaultValue, ::getString, ::putString) +fun PreferenceDataStore.string(name: String, defaultValue: () -> String = { "" }) = PreferenceProxy(name, defaultValue, ::getString, ::putString) -fun PreferenceDataStore.stringNotBlack( - name: String, - defaultValue: () -> String = { "" }, -) = PreferenceProxy(name, defaultValue, { key, default -> +fun PreferenceDataStore.stringNotBlack(name: String, defaultValue: () -> String = { "" }) = PreferenceProxy(name, defaultValue, { key, default -> getString(key, default)?.takeIf { it.isNotBlank() } ?: default }, { key, value -> putString(key, value.takeIf { it.isNotBlank() } ?: defaultValue()) }) -fun PreferenceDataStore.boolean( - name: String, - defaultValue: () -> Boolean = { false }, -) = PreferenceProxy(name, defaultValue, ::getBoolean, ::putBoolean) +fun PreferenceDataStore.boolean(name: String, defaultValue: () -> Boolean = { false }) = PreferenceProxy(name, defaultValue, ::getBoolean, ::putBoolean) -fun PreferenceDataStore.int( - name: String, - defaultValue: () -> Int = { 0 }, -) = PreferenceProxy(name, defaultValue, ::getInt, ::putInt) +fun PreferenceDataStore.int(name: String, defaultValue: () -> Int = { 0 }) = PreferenceProxy(name, defaultValue, ::getInt, ::putInt) -fun PreferenceDataStore.stringToInt( - name: String, - defaultValue: () -> Int = { 0 }, -) = PreferenceProxy(name, defaultValue, { key, default -> +fun PreferenceDataStore.stringToInt(name: String, defaultValue: () -> Int = { 0 }) = PreferenceProxy(name, defaultValue, { key, default -> getString(key, "$default")?.toIntOrNull() ?: default }, { key, value -> putString(key, "$value") }) -fun PreferenceDataStore.stringToIntIfExists( - name: String, - defaultValue: () -> Int = { 0 }, -) = PreferenceProxy(name, defaultValue, { key, default -> +fun PreferenceDataStore.stringToIntIfExists(name: String, defaultValue: () -> Int = { 0 }) = PreferenceProxy(name, defaultValue, { key, default -> getString(key, "$default")?.toIntOrNull() ?: default }, { key, value -> putString(key, value.takeIf { it > 0 }?.toString() ?: "") }) -fun PreferenceDataStore.long( - name: String, - defaultValue: () -> Long = { 0L }, -) = PreferenceProxy(name, defaultValue, ::getLong, ::putLong) +fun PreferenceDataStore.long(name: String, defaultValue: () -> Long = { 0L }) = PreferenceProxy(name, defaultValue, ::getLong, ::putLong) -fun PreferenceDataStore.stringToLong( - name: String, - defaultValue: () -> Long = { 0L }, -) = PreferenceProxy(name, defaultValue, { key, default -> +fun PreferenceDataStore.stringToLong(name: String, defaultValue: () -> Long = { 0L }) = PreferenceProxy(name, defaultValue, { key, default -> getString(key, "$default")?.toLongOrNull() ?: default }, { key, value -> putString(key, "$value") }) -fun PreferenceDataStore.stringSet( - name: String, - defaultValue: () -> Set = { emptySet() }, -) = PreferenceProxy(name, defaultValue, ::getStringSet, ::putStringSet) +fun PreferenceDataStore.stringSet(name: String, defaultValue: () -> Set = { emptySet() }) = PreferenceProxy(name, defaultValue, ::getStringSet, ::putStringSet) class PreferenceProxy( val name: String, @@ -64,14 +37,7 @@ class PreferenceProxy( val getter: (String, T) -> T?, val setter: (String, value: T) -> Unit, ) { - operator fun setValue( - thisObj: Any?, - property: KProperty<*>, - value: T, - ) = setter(name, value) + operator fun setValue(thisObj: Any?, property: KProperty<*>, value: T) = setter(name, value) - operator fun getValue( - thisObj: Any?, - property: KProperty<*>, - ) = getter(name, defaultValue())!! + operator fun getValue(thisObj: Any?, property: KProperty<*>) = getter(name, defaultValue())!! } diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt index 85ab5f2..5e50613 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt @@ -3,13 +3,13 @@ package io.nekohasekai.sfa.ktx import android.content.Context import android.content.Intent import androidx.core.content.FileProvider -import androidx.appcompat.R as AppCompatR import io.nekohasekai.libbox.ProfileContent import io.nekohasekai.sfa.database.Profile import io.nekohasekai.sfa.database.TypedProfile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File +import androidx.appcompat.R as AppCompatR suspend fun Context.shareProfile(profile: Profile) { val content = ProfileContent() diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Wrappers.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Wrappers.kt index 532754e..1dbf3b5 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ktx/Wrappers.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Wrappers.kt @@ -9,8 +9,8 @@ import io.nekohasekai.libbox.LogIterator import io.nekohasekai.libbox.RoutePrefix import io.nekohasekai.libbox.StringBox import io.nekohasekai.libbox.StringIterator -import io.nekohasekai.libbox.Connection as LibboxConnection import java.net.InetAddress +import io.nekohasekai.libbox.Connection as LibboxConnection val StringBox?.unwrap: String get() { @@ -27,39 +27,29 @@ fun Iterable.toStringIterator(): StringIterator { return 0 } - override fun hasNext(): Boolean { - return iterator.hasNext() - } + override fun hasNext(): Boolean = iterator.hasNext() - override fun next(): String { - return iterator.next() - } + override fun next(): String = iterator.next() } } -fun StringIterator.toList(): List { - return mutableListOf().apply { - while (hasNext()) { - add(next()) - } +fun StringIterator.toList(): List = mutableListOf().apply { + while (hasNext()) { + add(next()) } } -fun LogIterator.toList(): List { - return mutableListOf().apply { - while (hasNext()) { - add(next()) - } +fun LogIterator.toList(): List = mutableListOf().apply { + while (hasNext()) { + add(next()) } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun RoutePrefix.toIpPrefix() = IpPrefix(InetAddress.getByName(address()), prefix()) -fun ConnectionIterator.toList(): List { - return mutableListOf().apply { - while (hasNext()) { - add(next()) - } +fun ConnectionIterator.toList(): List = mutableListOf().apply { + while (hasNext()) { + add(next()) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/qrs/ByteArrayExtensions.kt b/app/src/main/java/io/nekohasekai/sfa/qrs/ByteArrayExtensions.kt index 83477c9..d222c87 100644 --- a/app/src/main/java/io/nekohasekai/sfa/qrs/ByteArrayExtensions.kt +++ b/app/src/main/java/io/nekohasekai/sfa/qrs/ByteArrayExtensions.kt @@ -1,11 +1,9 @@ package io.nekohasekai.sfa.qrs -fun ByteArray.readIntLE(offset: Int): Int { - return (this[offset].toInt() and 0xFF) or - ((this[offset + 1].toInt() and 0xFF) shl 8) or - ((this[offset + 2].toInt() and 0xFF) shl 16) or - ((this[offset + 3].toInt() and 0xFF) shl 24) -} +fun ByteArray.readIntLE(offset: Int): Int = (this[offset].toInt() and 0xFF) or + ((this[offset + 1].toInt() and 0xFF) shl 8) or + ((this[offset + 2].toInt() and 0xFF) shl 16) or + ((this[offset + 3].toInt() and 0xFF) shl 24) fun ByteArray.writeIntLE(offset: Int, value: Int) { this[offset] = value.toByte() diff --git a/app/src/main/java/io/nekohasekai/sfa/qrs/LubyCodec.kt b/app/src/main/java/io/nekohasekai/sfa/qrs/LubyCodec.kt index 8dd0d74..4eed13b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/qrs/LubyCodec.kt +++ b/app/src/main/java/io/nekohasekai/sfa/qrs/LubyCodec.kt @@ -3,9 +3,7 @@ package io.nekohasekai.sfa.qrs import java.util.zip.CRC32 import kotlin.random.Random -class LubyCodec( - private val sliceSize: Int = QRSConstants.DEFAULT_SLICE_SIZE, -) { +class LubyCodec(private val sliceSize: Int = QRSConstants.DEFAULT_SLICE_SIZE) { internal class IntArrayKey(val indices: IntArray) { private val hash = indices.contentHashCode() @@ -49,11 +47,7 @@ class LubyCodec( } } - class DecodingState( - val totalBlocks: Int, - val compressedSize: Int, - val checksum: Long, - ) { + class DecodingState(val totalBlocks: Int, val compressedSize: Int, val checksum: Long) { val decodedBlocks: Array = arrayOfNulls(totalBlocks) var decodedCount: Int = 0 @@ -62,10 +56,7 @@ class LubyCodec( val blockIndexMap: MutableMap> = mutableMapOf() val blockDisposeMap: MutableMap Unit>> = mutableMapOf() - class PendingBlock( - var indices: MutableList, - var data: ByteArray, - ) + class PendingBlock(var indices: MutableList, var data: ByteArray) } fun encode(originalData: ByteArray, compressedData: ByteArray, compressedSize: Int): Sequence = sequence { @@ -99,18 +90,16 @@ class LubyCodec( compressedSize = compressedSize, checksum = checksum, data = blockData, - ) + ), ) } } - fun createDecodingState(firstBlock: EncodedBlock): DecodingState { - return DecodingState( - totalBlocks = firstBlock.totalBlocks, - compressedSize = firstBlock.compressedSize, - checksum = firstBlock.checksum, - ) - } + fun createDecodingState(firstBlock: EncodedBlock): DecodingState = DecodingState( + totalBlocks = firstBlock.totalBlocks, + compressedSize = firstBlock.compressedSize, + checksum = firstBlock.checksum, + ) fun processBlock(state: DecodingState, block: EncodedBlock): Boolean { val queue = ArrayDeque() @@ -127,7 +116,7 @@ class LubyCodec( private fun processPendingBlock( state: DecodingState, pending: DecodingState.PendingBlock, - queue: ArrayDeque + queue: ArrayDeque, ) { var indices = pending.indices val data = pending.data @@ -212,9 +201,7 @@ class LubyCodec( } } - private fun indicesToKey(indices: List): IntArrayKey { - return IntArrayKey(indices.sorted().toIntArray()) - } + private fun indicesToKey(indices: List): IntArrayKey = IntArrayKey(indices.sorted().toIntArray()) private fun propagateDecoding(state: DecodingState, decodedIdx: Int, queue: ArrayDeque) { val toProcess = ArrayDeque() diff --git a/app/src/main/java/io/nekohasekai/sfa/qrs/QRSDecoder.kt b/app/src/main/java/io/nekohasekai/sfa/qrs/QRSDecoder.kt index f51cd6a..c33c8bb 100644 --- a/app/src/main/java/io/nekohasekai/sfa/qrs/QRSDecoder.kt +++ b/app/src/main/java/io/nekohasekai/sfa/qrs/QRSDecoder.kt @@ -88,17 +88,24 @@ class QRSDecoder { if (decompressedData != null) { val checksumValid = codec!!.verifyChecksum( - decompressedData, currentState.checksum, currentState.totalBlocks + decompressedData, + currentState.checksum, + currentState.totalBlocks, ) if (checksumValid) { return DecodeProgress( - currentState.decodedCount, currentState.totalBlocks, true, decompressedData + currentState.decodedCount, + currentState.totalBlocks, + true, + decompressedData, ) } } val rawChecksumValid = codec!!.verifyChecksum( - compressedData, currentState.checksum, currentState.totalBlocks + compressedData, + currentState.checksum, + currentState.totalBlocks, ) if (rawChecksumValid) { DecodeProgress(currentState.decodedCount, currentState.totalBlocks, true, compressedData) @@ -171,5 +178,4 @@ class QRSDecoder { return outputBuffer.toByteArray() } - } diff --git a/app/src/main/java/io/nekohasekai/sfa/qrs/QRSEncoder.kt b/app/src/main/java/io/nekohasekai/sfa/qrs/QRSEncoder.kt index 175b29b..bd19bca 100644 --- a/app/src/main/java/io/nekohasekai/sfa/qrs/QRSEncoder.kt +++ b/app/src/main/java/io/nekohasekai/sfa/qrs/QRSEncoder.kt @@ -4,17 +4,11 @@ import java.io.ByteArrayOutputStream import java.util.Base64 import java.util.zip.Deflater -class QRSEncoder( - private val sliceSize: Int = QRSConstants.DEFAULT_SLICE_SIZE, -) { +class QRSEncoder(private val sliceSize: Int = QRSConstants.DEFAULT_SLICE_SIZE) { private val codec = LubyCodec(sliceSize) companion object { - fun appendFileHeaderMeta( - data: ByteArray, - filename: String? = null, - contentType: String? = null, - ): ByteArray { + fun appendFileHeaderMeta(data: ByteArray, filename: String? = null, contentType: String? = null): ByteArray { val meta = buildString { append("{") var hasContent = false @@ -49,20 +43,14 @@ class QRSEncoder( return result } - private fun escapeJson(s: String): String { - return s.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") - } + private fun escapeJson(s: String): String = s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") } - data class QRSFrame( - val content: String, - val frameIndex: Int, - val totalBlocks: Int, - ) + data class QRSFrame(val content: String, val frameIndex: Int, val totalBlocks: Int) fun encode(data: ByteArray, urlPrefix: String = ""): Sequence { val compressed = compress(data) @@ -117,5 +105,4 @@ class QRSEncoder( return payload } - } diff --git a/app/src/main/java/io/nekohasekai/sfa/update/UpdateTrack.kt b/app/src/main/java/io/nekohasekai/sfa/update/UpdateTrack.kt index 23c75d7..d3e1c51 100644 --- a/app/src/main/java/io/nekohasekai/sfa/update/UpdateTrack.kt +++ b/app/src/main/java/io/nekohasekai/sfa/update/UpdateTrack.kt @@ -2,14 +2,13 @@ package io.nekohasekai.sfa.update enum class UpdateTrack { STABLE, - BETA; + BETA, + ; companion object { - fun fromString(value: String): UpdateTrack { - return when (value.lowercase()) { - "beta" -> BETA - else -> STABLE - } + fun fromString(value: String): UpdateTrack = when (value.lowercase()) { + "beta" -> BETA + else -> STABLE } } } diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/ColorUtils.kt b/app/src/main/java/io/nekohasekai/sfa/utils/ColorUtils.kt index 868047e..a9808d1 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/ColorUtils.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/ColorUtils.kt @@ -16,10 +16,7 @@ import java.util.Stack object ColorUtils { private val ansiRegex by lazy { Regex("\u001B\\[[;\\d]*m") } - fun ansiEscapeToSpannable( - context: Context, - text: String, - ): Spannable { + fun ansiEscapeToSpannable(context: Context, text: String): Spannable { val spannable = SpannableString(text.replace(ansiRegex, "")) val stack = Stack() val spans = mutableListOf() @@ -59,11 +56,7 @@ object ColorUtils { return spannable } - private data class AnsiSpan( - val instruction: AnsiInstruction, - val start: Int, - val end: Int, - ) + private data class AnsiSpan(val instruction: AnsiInstruction, val start: Int, val end: Int) private class AnsiInstruction(context: Context, code: String) { val spans: List by lazy { @@ -98,33 +91,29 @@ object ColorUtils { } } - private fun getSpan( - code: String?, - context: Context, - ): ParcelableSpan? = - when (code) { - "0", null -> null - "1" -> StyleSpan(Typeface.NORMAL) - "3" -> StyleSpan(Typeface.ITALIC) - "4" -> UnderlineSpan() - "30" -> ForegroundColorSpan(Color.BLACK) - "31" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_red)) - "32" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_green)) - "33" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_yellow)) - "34" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_blue)) - "35" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_purple)) - "36" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_blue_light)) - "37" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_white)) - else -> { - var codeInt = code.toIntOrNull() - if (codeInt != null) { - codeInt %= 125 - val row = codeInt / 36 - val column = codeInt % 36 - ForegroundColorSpan(Color.rgb(row * 51, column / 6 * 51, column % 6 * 51)) - } else { - null - } + private fun getSpan(code: String?, context: Context): ParcelableSpan? = when (code) { + "0", null -> null + "1" -> StyleSpan(Typeface.NORMAL) + "3" -> StyleSpan(Typeface.ITALIC) + "4" -> UnderlineSpan() + "30" -> ForegroundColorSpan(Color.BLACK) + "31" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_red)) + "32" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_green)) + "33" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_yellow)) + "34" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_blue)) + "35" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_purple)) + "36" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_blue_light)) + "37" -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.log_white)) + else -> { + var codeInt = code.toIntOrNull() + if (codeInt != null) { + codeInt %= 125 + val row = codeInt / 36 + val column = codeInt % 36 + ForegroundColorSpan(Color.rgb(row * 51, column / 6 * 51, column % 6 * 51)) + } else { + null } } + } } diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt b/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt index 606cffa..637692a 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt @@ -47,10 +47,8 @@ open class CommandClient( } } - private fun getAllHandlers(): List { - return synchronized(additionalHandlers) { - listOf(handler) + additionalHandlers - } + private fun getAllHandlers(): List = synchronized(additionalHandlers) { + listOf(handler) + additionalHandlers } enum class ConnectionType { @@ -76,10 +74,7 @@ open class CommandClient( fun updateGroups(newGroups: MutableList) {} - fun initializeClashMode( - modeList: List, - currentMode: String, - ) {} + fun initializeClashMode(modeList: List, currentMode: String) {} fun updateClashMode(newMode: String) {} @@ -162,10 +157,7 @@ open class CommandClient( getAllHandlers().forEach { it.updateStatus(message) } } - override fun initializeClashMode( - modeList: StringIterator, - currentMode: String, - ) { + override fun initializeClashMode(modeList: StringIterator, currentMode: String) { val modes = modeList.toList() getAllHandlers().forEach { it.initializeClashMode(modes, currentMode) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/HookErrorClient.kt b/app/src/main/java/io/nekohasekai/sfa/utils/HookErrorClient.kt index d58161f..0d1176e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/HookErrorClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/HookErrorClient.kt @@ -14,12 +14,7 @@ object HookErrorClient { PROTOCOL_ERROR, } - data class Result( - val logs: List, - val hasWarnings: Boolean, - val failure: Failure? = null, - val detail: String? = null, - ) + data class Result(val logs: List, val hasWarnings: Boolean, val failure: Failure? = null, val detail: String? = null) private fun failureResult(failure: Failure, detail: String? = null) = Result( logs = emptyList(), diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/HookModuleUpdateNotifier.kt b/app/src/main/java/io/nekohasekai/sfa/utils/HookModuleUpdateNotifier.kt index 90f0239..45e88be 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/HookModuleUpdateNotifier.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/HookModuleUpdateNotifier.kt @@ -17,17 +17,11 @@ object HookModuleUpdateNotifier { private const val CHANNEL_ID = "lsposed_module_update" private const val NOTIFICATION_ID = 0x5F10 - fun needsRestart(status: HookStatusClient.Status?): Boolean { - return isDowngrade(status) || isUpgrade(status) - } + fun needsRestart(status: HookStatusClient.Status?): Boolean = isDowngrade(status) || isUpgrade(status) - fun isDowngrade(status: HookStatusClient.Status?): Boolean { - return status != null && status.version > HookModuleVersion.CURRENT - } + fun isDowngrade(status: HookStatusClient.Status?): Boolean = status != null && status.version > HookModuleVersion.CURRENT - fun isUpgrade(status: HookStatusClient.Status?): Boolean { - return status != null && status.version < HookModuleVersion.CURRENT - } + fun isUpgrade(status: HookStatusClient.Status?): Boolean = status != null && status.version < HookModuleVersion.CURRENT fun sync(context: Context) { HookStatusClient.refresh() diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/HookStatusClient.kt b/app/src/main/java/io/nekohasekai/sfa/utils/HookStatusClient.kt index 85e98b0..e0bd730 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/HookStatusClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/HookStatusClient.kt @@ -8,12 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow object HookStatusClient { - data class Status( - val active: Boolean, - val lastPatchedAt: Long, - val version: Int, - val systemPid: Int, - ) + data class Status(val active: Boolean, val lastPatchedAt: Long, val version: Int, val systemPid: Int) private val statusFlow = MutableStateFlow(null) val status: StateFlow = statusFlow diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/PrivilegeSettingsClient.kt b/app/src/main/java/io/nekohasekai/sfa/utils/PrivilegeSettingsClient.kt index fee1744..283e714 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/PrivilegeSettingsClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/PrivilegeSettingsClient.kt @@ -16,10 +16,7 @@ object PrivilegeSettingsClient { @Volatile private var appContext: Context? = null - data class ExportResult( - val outputPath: String?, - val error: String?, - ) + data class ExportResult(val outputPath: String?, val error: String?) fun register(context: Context) { appContext = context.applicationContext @@ -55,15 +52,13 @@ object PrivilegeSettingsClient { } } - suspend fun exportDebugInfo(outputPath: String): ExportResult { - return try { - val service = RootClient.bindService() - val path = service.exportDebugInfo(outputPath) - ExportResult(path, null) - } catch (e: Throwable) { - Log.e(TAG, "Export debug info failed", e) - ExportResult(null, e.message ?: "export failed") - } + suspend fun exportDebugInfo(outputPath: String): ExportResult = try { + val service = RootClient.bindService() + val path = service.exportDebugInfo(outputPath) + ExportResult(path, null) + } catch (e: Throwable) { + Log.e(TAG, "Export debug info failed", e) + ExportResult(null, e.message ?: "export failed") } private fun isVersionMismatch(): Boolean { diff --git a/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt b/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt index 6016fd3..79a0516 100644 --- a/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt +++ b/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt @@ -1,5 +1,6 @@ package io.nekohasekai.sfa.vendor +import android.content.IIntentSender import android.content.Intent import android.content.IntentSender import android.content.pm.IPackageInstaller @@ -11,11 +12,10 @@ import android.os.Bundle import android.os.IBinder import android.os.ParcelFileDescriptor import android.system.Os -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import android.content.IIntentSender import io.nekohasekai.sfa.BuildConfig import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit object PrivilegedServiceUtils { @@ -27,14 +27,14 @@ object PrivilegedServiceUtils { iPackageManagerClass.getMethod( "getInstalledPackages", Long::class.javaPrimitiveType, - Int::class.javaPrimitiveType + Int::class.javaPrimitiveType, ) } private val getInstalledPackagesMethodInt by lazy { iPackageManagerClass.getMethod( "getInstalledPackages", Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType + Int::class.javaPrimitiveType, ) } private val getPackageInstallerMethod by lazy { iPackageManagerClass.getMethod("getPackageInstaller") } @@ -44,14 +44,14 @@ object PrivilegedServiceUtils { IPackageInstaller::class.java, String::class.java, String::class.java, - Int::class.javaPrimitiveType + Int::class.javaPrimitiveType, ) } private val packageInstallerCtorPre by lazy { PackageInstaller::class.java.getConstructor( IPackageInstaller::class.java, String::class.java, - Int::class.javaPrimitiveType + Int::class.javaPrimitiveType, ) } private val sessionCtor by lazy { @@ -149,17 +149,13 @@ object PrivilegedServiceUtils { installerPackageName: String, installerAttributionTag: String?, userId: Int, - ): PackageInstaller { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - packageInstallerCtorS.newInstance(installer, installerPackageName, installerAttributionTag, userId) - } else { - packageInstallerCtorPre.newInstance(installer, installerPackageName, userId) - } + ): PackageInstaller = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + packageInstallerCtorS.newInstance(installer, installerPackageName, installerAttributionTag, userId) + } else { + packageInstallerCtorPre.newInstance(installer, installerPackageName, userId) } - private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session { - return sessionCtor.newInstance(session) - } + private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session = sessionCtor.newInstance(session) private fun createIntentSender(onResult: (Intent) -> Unit): IntentSender { val sender = object : IIntentSender.Stub() { diff --git a/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt b/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt index 402f79f..e72e00c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt +++ b/app/src/main/java/io/nekohasekai/sfa/vendor/VendorInterface.kt @@ -6,10 +6,7 @@ import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea import io.nekohasekai.sfa.update.UpdateInfo interface VendorInterface { - fun checkUpdate( - activity: Activity, - byUser: Boolean, - ) + fun checkUpdate(activity: Activity, byUser: Boolean) fun createQRCodeAnalyzer( onSuccess: (String) -> Unit, @@ -65,6 +62,5 @@ interface VendorInterface { * @param downloadUrl The URL to download the APK from * @throws Exception if download or install fails */ - suspend fun downloadAndInstall(context: android.content.Context, downloadUrl: String): Unit = - throw UnsupportedOperationException("Not supported in this flavor") + suspend fun downloadAndInstall(context: android.content.Context, downloadUrl: String): Unit = throw UnsupportedOperationException("Not supported in this flavor") } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/HookErrorStore.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/HookErrorStore.kt index 50475c7..2603ba9 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/HookErrorStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/HookErrorStore.kt @@ -27,13 +27,7 @@ object HookErrorStore { log(LogEntry.LEVEL_DEBUG, source, message, throwable, store = false) } - private fun log( - level: Int, - source: String, - message: String, - throwable: Throwable?, - store: Boolean, - ) { + private fun log(level: Int, source: String, message: String, throwable: Throwable?, store: Boolean) { if (BuildConfig.DEBUG) { when (level) { LogEntry.LEVEL_DEBUG -> { diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/HookStatusStore.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/HookStatusStore.kt index fc0624a..17d9d60 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/HookStatusStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/HookStatusStore.kt @@ -5,6 +5,7 @@ import android.os.Process object HookStatusStore { @Volatile private var active = false + @Volatile private var lastPatchedAt = 0L @@ -16,14 +17,7 @@ object HookStatusStore { lastPatchedAt = System.currentTimeMillis() } - fun snapshot(): Status { - return Status(active, lastPatchedAt, HookModuleVersion.CURRENT, Process.myPid()) - } + fun snapshot(): Status = Status(active, lastPatchedAt, HookModuleVersion.CURRENT, Process.myPid()) - data class Status( - val active: Boolean, - val lastPatchedAt: Long, - val version: Int, - val systemPid: Int, - ) + data class Status(val active: Boolean, val lastPatchedAt: Long, val version: Int, val systemPid: Int) } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt index 7d41264..cbd7514 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt @@ -54,7 +54,7 @@ object PrivilegeChecker { pm.javaClass.getMethod( "checkUidPermission", String::class.java, - Int::class.javaPrimitiveType + Int::class.javaPrimitiveType, ).also { checkUidPermissionMethod = it } } for (permission in privilegedPermissions) { @@ -76,7 +76,7 @@ object PrivilegeChecker { private fun isSystemApp(appInfo: ApplicationInfo): Boolean { val flags = appInfo.flags return flags and ApplicationInfo.FLAG_SYSTEM != 0 || - flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0 + flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0 } private fun isExemptUid(uid: Int): Boolean { @@ -112,39 +112,35 @@ object PrivilegeChecker { } } - private fun getPackageManager(): Any? { - return try { - getPackageManagerMethod.invoke(null) + private fun getPackageManager(): Any? = try { + getPackageManagerMethod.invoke(null) + } catch (_: Throwable) { + null + } + + private fun getApplicationInfo(pm: Any, pkg: String, userId: Int): ApplicationInfo? = try { + val method = getApplicationInfoMethodInt ?: run { + pm.javaClass.getMethod( + "getApplicationInfo", + String::class.java, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ).also { getApplicationInfoMethodInt = it } + } + method.invoke(pm, pkg, 0, userId) as? ApplicationInfo + } catch (_: Throwable) { + try { + val method = getApplicationInfoMethodLong ?: run { + pm.javaClass.getMethod( + "getApplicationInfo", + String::class.java, + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ).also { getApplicationInfoMethodLong = it } + } + method.invoke(pm, pkg, 0L, userId) as? ApplicationInfo } catch (_: Throwable) { null } } - - private fun getApplicationInfo(pm: Any, pkg: String, userId: Int): ApplicationInfo? { - return try { - val method = getApplicationInfoMethodInt ?: run { - pm.javaClass.getMethod( - "getApplicationInfo", - String::class.java, - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType - ).also { getApplicationInfoMethodInt = it } - } - method.invoke(pm, pkg, 0, userId) as? ApplicationInfo - } catch (_: Throwable) { - try { - val method = getApplicationInfoMethodLong ?: run { - pm.javaClass.getMethod( - "getApplicationInfo", - String::class.java, - Long::class.javaPrimitiveType, - Int::class.javaPrimitiveType - ).also { getApplicationInfoMethodLong = it } - } - method.invoke(pm, pkg, 0L, userId) as? ApplicationInfo - } catch (_: Throwable) { - null - } - } - } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt index 0d06d4f..6ac928a 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt @@ -7,12 +7,16 @@ import java.util.concurrent.ConcurrentHashMap object PrivilegeSettingsStore { private const val SETTINGS_DIR = "/data/system/sing-box" private const val SETTINGS_FILE = "privilege_settings.conf" + @Volatile private var enabled = false + @Volatile private var packageSet: Set = emptySet() + @Volatile private var interfaceRenameEnabled = false + @Volatile private var interfacePrefix = "en" private val uidCache = ConcurrentHashMap() @@ -21,12 +25,7 @@ object PrivilegeSettingsStore { private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") } private var getPackagesForUidMethod: Method? = null - fun update( - enabled: Boolean, - packages: Set, - interfaceRenameEnabled: Boolean, - interfacePrefix: String, - ) { + fun update(enabled: Boolean, packages: Set, interfaceRenameEnabled: Boolean, interfacePrefix: String) { this.enabled = enabled packageSet = packages this.interfaceRenameEnabled = interfaceRenameEnabled @@ -41,9 +40,7 @@ object PrivilegeSettingsStore { fun isEnabled(): Boolean = enabled - fun shouldRenameInterface(): Boolean { - return interfaceRenameEnabled - } + fun shouldRenameInterface(): Boolean = interfaceRenameEnabled fun interfacePrefix(): String = interfacePrefix @@ -131,12 +128,10 @@ object PrivilegeSettingsStore { } } - private fun getPackageManager(): Any? { - return try { - getPackageManagerMethod.invoke(null) - } catch (e: Throwable) { - HookErrorStore.e("PrivilegeSettingsStore", "getPackageManager failed", e) - null - } + private fun getPackageManager(): Any? = try { + getPackageManagerMethod.invoke(null) + } catch (e: Throwable) { + HookErrorStore.e("PrivilegeSettingsStore", "getPackageManager failed", e) + null } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt index 4e4ccdf..f13ded4 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt @@ -45,9 +45,7 @@ object VpnAppStore { return result } - fun isVpnPackage(packageName: String, userId: Int): Boolean { - return getVpnPackages(userId).contains(packageName) - } + fun isVpnPackage(packageName: String, userId: Int): Boolean = getVpnPackages(userId).contains(packageName) fun isVpnUidExcludeSelf(uid: Int): Boolean { val packages = getPackagesForUid(uid) @@ -149,18 +147,14 @@ object VpnAppStore { return unwrapParceledListSlice(result) } - private fun isSystemApp(info: ApplicationInfo): Boolean { - return info.flags and ApplicationInfo.FLAG_SYSTEM != 0 || - info.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0 - } + private fun isSystemApp(info: ApplicationInfo): Boolean = info.flags and ApplicationInfo.FLAG_SYSTEM != 0 || + info.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0 - private fun getPackageManager(): Any? { - return try { - getPackageManagerMethod.invoke(null) - } catch (e: Throwable) { - HookErrorStore.e("VpnAppStore", "getPackageManager failed", e) - null - } + private fun getPackageManager(): Any? = try { + getPackageManagerMethod.invoke(null) + } catch (e: Throwable) { + HookErrorStore.e("VpnAppStore", "getPackageManager failed", e) + null } private inline fun binderLocalScope(block: () -> T): T { diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt index f854fc7..10fe4a2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt @@ -1,19 +1,16 @@ package io.nekohasekai.sfa.xposed import android.content.Context +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedModule +import io.github.libxposed.api.XposedModuleInterface import io.nekohasekai.sfa.xposed.hooks.HookIConnectivityManagerOnTransact import io.nekohasekai.sfa.xposed.hooks.hidevpn.ConnectivityServiceHookHelper import io.nekohasekai.sfa.xposed.hooks.hidevpn.HookNetworkCapabilitiesWriteToParcel import io.nekohasekai.sfa.xposed.hooks.hidevpn.HookNetworkInterfaceGetName import io.nekohasekai.sfa.xposed.hooks.hidevpnapp.HookPackageManagerGetInstalledPackages -import io.github.libxposed.api.XposedInterface -import io.github.libxposed.api.XposedModule -import io.github.libxposed.api.XposedModuleInterface -class XposedInit( - base: XposedInterface, - param: XposedModuleInterface.ModuleLoadedParam, -) : XposedModule(base, param) { +class XposedInit(base: XposedInterface, param: XposedModuleInterface.ModuleLoadedParam) : XposedModule(base, param) { private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") } private val currentActivityThreadMethod by lazy { activityThreadClass.getMethod("currentActivityThread") } @@ -47,13 +44,11 @@ class XposedInit( const val TAG = "sing-box-lsposed" } - private fun resolveSystemContext(): Context? { - return try { - val currentThread = currentActivityThreadMethod.invoke(null) - getSystemContextMethod.invoke(currentThread) as? Context - } catch (e: Throwable) { - HookErrorStore.e("XposedInit", "resolveSystemContext failed", e) - null - } + private fun resolveSystemContext(): Context? = try { + val currentThread = currentActivityThreadMethod.invoke(null) + getSystemContextMethod.invoke(currentThread) as? Context + } catch (e: Throwable) { + HookErrorStore.e("XposedInit", "resolveSystemContext failed", e) + null } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/IConnectivityManager+onTransact.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/IConnectivityManager+onTransact.kt index 9f6632d..707e7f7 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/IConnectivityManager+onTransact.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/IConnectivityManager+onTransact.kt @@ -13,10 +13,7 @@ import io.nekohasekai.sfa.xposed.HookStatusKeys import io.nekohasekai.sfa.xposed.HookStatusStore import io.nekohasekai.sfa.xposed.PrivilegeSettingsStore -class HookIConnectivityManagerOnTransact( - private val classLoader: ClassLoader, - private val context: Context?, -) : XHook { +class HookIConnectivityManagerOnTransact(private val classLoader: ClassLoader, private val context: Context?) : XHook { private companion object { private const val SOURCE = "HookIConnectivityManagerOnTransact" } @@ -37,7 +34,8 @@ class HookIConnectivityManagerOnTransact( if (code != HookStatusKeys.TRANSACTION_STATUS && code != HookStatusKeys.TRANSACTION_UPDATE_PRIVILEGE_SETTINGS && code != HookStatusKeys.TRANSACTION_GET_ERRORS && - code != HookStatusKeys.TRANSACTION_GET_INSTALLED_PACKAGES) { + code != HookStatusKeys.TRANSACTION_GET_INSTALLED_PACKAGES + ) { return } val data = param.args[1] as Parcel @@ -145,15 +143,13 @@ class HookIConnectivityManagerOnTransact( } } - private fun getPackageManager(): Any? { - return try { - val appGlobals = Class.forName("android.app.AppGlobals") - val method = appGlobals.getMethod("getPackageManager") - method.invoke(null) - } catch (e: Throwable) { - HookErrorStore.e(SOURCE, "getPackageManager failed", e) - null - } + private fun getPackageManager(): Any? = try { + val appGlobals = Class.forName("android.app.AppGlobals") + val method = appGlobals.getMethod("getPackageManager") + method.invoke(null) + } catch (e: Throwable) { + HookErrorStore.e(SOURCE, "getPackageManager failed", e) + null } private fun getInstalledPackagesCompat(pm: Any, flags: Long, userId: Int): List { diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+CONNECTIVITY_ACTION.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+CONNECTIVITY_ACTION.kt index 38e7cd2..ebc825e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+CONNECTIVITY_ACTION.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+CONNECTIVITY_ACTION.kt @@ -30,7 +30,7 @@ class HookConnectivityManagerConnectivityAction(private val helper: Connectivity ?: return param.args[0] = VpnSanitizer.cloneNetworkInfo(replacement) } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+PROXY_CHANGE_ACTION.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+PROXY_CHANGE_ACTION.kt index d43422b..46339d2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+PROXY_CHANGE_ACTION.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+PROXY_CHANGE_ACTION.kt @@ -62,7 +62,7 @@ class HookConnectivityManagerProxyChangeAction(private val helper: ConnectivityS } param.result = null } - } + }, ) } @@ -75,11 +75,9 @@ class HookConnectivityManagerProxyChangeAction(private val helper: ConnectivityS override fun beforeHook(param: MethodHookParam) { param.args[0] = emptyProxyInfo() } - } + }, ) } - private fun emptyProxyInfo(): ProxyInfo { - return ProxyInfo.buildDirectProxy("", 0) - } + private fun emptyProxyInfo(): ProxyInfo = ProxyInfo.buildDirectProxy("", 0) } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetwork.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetwork.kt index c29851b..9ce9809 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetwork.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetwork.kt @@ -20,7 +20,7 @@ class HookConnectivityManagerGetActiveNetwork(private val helper: ConnectivitySe val replacement = helper.getUnderlyingNetwork(param.thisObject, uid) ?: return param.result = replacement } - } + }, ) XposedHelpers.findAndHookMethod( @@ -35,7 +35,7 @@ class HookConnectivityManagerGetActiveNetwork(private val helper: ConnectivitySe val replacement = helper.getUnderlyingNetwork(param.thisObject, uid) ?: return param.result = replacement } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetworkInfo.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetworkInfo.kt index cf478a3..165337b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetworkInfo.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getActiveNetworkInfo.kt @@ -26,7 +26,7 @@ class HookConnectivityManagerGetActiveNetworkInfo(private val helper: Connectivi param.result = replacement } } - } + }, ) XposedHelpers.findAndHookMethod( @@ -45,7 +45,7 @@ class HookConnectivityManagerGetActiveNetworkInfo(private val helper: Connectivi param.result = replacement } } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworkInfo.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworkInfo.kt index 58c2f5f..8417399 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworkInfo.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworkInfo.kt @@ -24,7 +24,7 @@ class HookConnectivityManagerGetAllNetworkInfo(private val helper: ConnectivityS val filtered = infos.filter { it.type != ConnectivityManager.TYPE_VPN } param.result = filtered.toTypedArray() } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworks.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworks.kt index 309906b..5d3af0d 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworks.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getAllNetworks.kt @@ -23,7 +23,7 @@ class HookConnectivityManagerGetAllNetworks(private val helper: ConnectivityServ val filtered = networks.filter { !helper.isVpnNetwork(param.thisObject, it) } param.result = filtered.toTypedArray() } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getDefaultProxy.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getDefaultProxy.kt index 1025381..acade8f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getDefaultProxy.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getDefaultProxy.kt @@ -24,7 +24,7 @@ class HookConnectivityManagerGetDefaultProxy(private val helper: ConnectivitySer param.result as? ProxyInfo ?: return param.result = null } - } + }, ) XposedHelpers.findAndHookMethod( @@ -37,7 +37,7 @@ class HookConnectivityManagerGetDefaultProxy(private val helper: ConnectivitySer param.result as? ProxyInfo ?: return param.result = null } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getLinkProperties.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getLinkProperties.kt index 5e4f8ad..a2e9d41 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getLinkProperties.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getLinkProperties.kt @@ -52,7 +52,7 @@ class HookConnectivityManagerGetLinkProperties(private val helper: ConnectivityS val underlying = helper.getUnderlyingLinkProperties(param.thisObject, callerUid) param.result = underlying ?: VpnSanitizer.sanitizeLinkProperties(lp) } - } + }, ) } @@ -70,7 +70,7 @@ class HookConnectivityManagerGetLinkProperties(private val helper: ConnectivityS val underlying = helper.getUnderlyingLinkProperties(param.thisObject, uid) param.result = underlying ?: VpnSanitizer.sanitizeLinkProperties(lp) } - } + }, ) } @@ -88,7 +88,7 @@ class HookConnectivityManagerGetLinkProperties(private val helper: ConnectivityS param.result = null } } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkCapabilities.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkCapabilities.kt index 66cdf1c..9e7a539 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkCapabilities.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkCapabilities.kt @@ -83,7 +83,7 @@ class HookConnectivityManagerGetNetworkCapabilities(private val helper: Connecti if (!VpnSanitizer.shouldHide(callerUid)) return param.result = VpnSanitizer.sanitizeNetworkCapabilities(nc) } - } + }, ) } @@ -96,7 +96,7 @@ class HookConnectivityManagerGetNetworkCapabilities(private val helper: Connecti override fun afterHook(param: MethodHookParam) { sanitizeNetworkCapabilitiesResult(param) } - } + }, ) } @@ -110,7 +110,7 @@ class HookConnectivityManagerGetNetworkCapabilities(private val helper: Connecti override fun afterHook(param: MethodHookParam) { sanitizeNetworkCapabilitiesResult(param) } - } + }, ) } @@ -125,7 +125,7 @@ class HookConnectivityManagerGetNetworkCapabilities(private val helper: Connecti override fun afterHook(param: MethodHookParam) { sanitizeNetworkCapabilitiesResult(param) } - } + }, ) } @@ -155,7 +155,7 @@ class HookConnectivityManagerGetNetworkCapabilities(private val helper: Connecti if (!nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) return param.result = VpnSanitizer.sanitizeNetworkCapabilities(nc) } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkForType.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkForType.kt index f519a98..51d2151 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkForType.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkForType.kt @@ -23,7 +23,7 @@ class HookConnectivityManagerGetNetworkForType(private val helper: ConnectivityS if (!helper.shouldHide(param.thisObject, uid)) return param.result = null } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkInfo.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkInfo.kt index 9bc9234..c3b2aad 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkInfo.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+getNetworkInfo.kt @@ -26,7 +26,7 @@ class HookConnectivityManagerGetNetworkInfo(private val helper: ConnectivityServ if (!helper.shouldHide(param.thisObject, uid)) return param.result = null } - } + }, ) XposedHelpers.findAndHookMethod( @@ -48,7 +48,7 @@ class HookConnectivityManagerGetNetworkInfo(private val helper: ConnectivityServ null } } - } + }, ) } } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+requestNetwork.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+requestNetwork.kt index 03d50b4..f800171 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+requestNetwork.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityManager+requestNetwork.kt @@ -144,7 +144,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -165,7 +165,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -190,7 +190,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[1] as? NetworkCapabilities ?: return param.args[1] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -216,7 +216,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[1] as? NetworkCapabilities ?: return param.args[1] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -238,7 +238,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -257,7 +257,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -278,7 +278,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -300,7 +300,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -321,7 +321,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -339,7 +339,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -358,7 +358,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -379,7 +379,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -397,7 +397,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -416,7 +416,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.args[0] as? NetworkCapabilities ?: return param.args[0] = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -436,7 +436,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.result as? NetworkCapabilities ?: return param.result = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -455,7 +455,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ val nc = param.result as? NetworkCapabilities ?: return param.result = VpnSanitizer.sanitizeRequestCapabilities(nc) } - } + }, ) } @@ -512,7 +512,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ override fun afterHook(param: MethodHookParam) { VpnHideContext.clear() } - } + }, ) } @@ -536,7 +536,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ override fun afterHook(param: MethodHookParam) { VpnHideContext.clear() } - } + }, ) } @@ -566,7 +566,7 @@ class HookConnectivityManagerRequestNetwork(private val helper: ConnectivityServ override fun afterHook(param: MethodHookParam) { VpnHideContext.clear() } - } + }, ) } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt index fda798b..39b0b3b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt @@ -103,7 +103,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo "getFilteredNetworkInfo", naiClass, intType, - booleanType + booleanType, ) if (getFilteredNetworkInfoMethod == null) { HookErrorStore.w(SOURCE, "getFilteredNetworkInfo not found; network info sanitization disabled") @@ -423,9 +423,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo return isVpnNai(nai) } - fun isVpnNai(nai: Any): Boolean { - return isVPNMethod.invoke(nai) as Boolean - } + fun isVpnNai(nai: Any): Boolean = isVPNMethod.invoke(nai) as Boolean fun getUnderlyingNetwork(connectivityService: Any, uid: Int): Network? { val nai = getUnderlyingNai(connectivityService, uid) ?: return null @@ -465,11 +463,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo return null } - private fun findDeclaredMethod( - target: Class<*>, - name: String, - vararg parameterTypes: Class<*>, - ): Method? { + private fun findDeclaredMethod(target: Class<*>, name: String, vararg parameterTypes: Class<*>): Method? { var current: Class<*>? = target while (current != null) { try { @@ -481,14 +475,8 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo return null } - private fun requireDeclaredMethod( - target: Class<*>, - name: String, - vararg parameterTypes: Class<*>, - ): Method { - return findDeclaredMethod(target, name, *parameterTypes) - ?: throw NoSuchMethodException("${target.name}#$name") - } + private fun requireDeclaredMethod(target: Class<*>, name: String, vararg parameterTypes: Class<*>): Method = findDeclaredMethod(target, name, *parameterTypes) + ?: throw NoSuchMethodException("${target.name}#$name") /** * Resolves a class from the Connectivity module, handling APEX package rewriting. diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt index 339c193..2f78008 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt @@ -21,11 +21,11 @@ class HookNetworkCapabilitiesWriteToParcel : XHook { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { NetworkCapabilities::class.java.getDeclaredConstructor( NetworkCapabilities::class.java, - Long::class.javaPrimitiveType + Long::class.javaPrimitiveType, ).apply { isAccessible = true } } else { NetworkCapabilities::class.java.getDeclaredConstructor( - NetworkCapabilities::class.java + NetworkCapabilities::class.java, ).apply { isAccessible = true } } } @@ -75,12 +75,10 @@ class HookNetworkCapabilitiesWriteToParcel : XHook { HookErrorStore.i(SOURCE, "Hooked NetworkCapabilities.writeToParcel (sender)") } - private fun copyNetworkCapabilities(caps: NetworkCapabilities): NetworkCapabilities { - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - copyCtor.newInstance(caps, 0L) as NetworkCapabilities - } else { - copyCtor.newInstance(caps) as NetworkCapabilities - } + private fun copyNetworkCapabilities(caps: NetworkCapabilities): NetworkCapabilities = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + copyCtor.newInstance(caps, 0L) as NetworkCapabilities + } else { + copyCtor.newInstance(caps) as NetworkCapabilities } private fun sanitizeNetworkCapabilities(caps: NetworkCapabilities) { diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt index 77cbfdb..f9b7a20 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt @@ -1,6 +1,5 @@ package io.nekohasekai.sfa.xposed.hooks.hidevpn -import android.system.ErrnoException import android.system.Os import android.system.OsConstants import android.system.StructTimeval @@ -93,13 +92,9 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook param.result = renamed } - private fun findVpnClass(): Class<*> { - return XposedHelpers.findClass("com.android.server.connectivity.Vpn", classLoader) - } + private fun findVpnClass(): Class<*> = XposedHelpers.findClass("com.android.server.connectivity.Vpn", classLoader) - private fun isTunInterface(name: String): Boolean { - return name.startsWith("tun") - } + private fun isTunInterface(name: String): Boolean = name.startsWith("tun") private fun renameInterface(oldName: String, prefix: String): String? { val oldIndex = getInterfaceIndex(oldName) @@ -131,9 +126,7 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook return newName } - private fun getInterfaceIndex(name: String): Int { - return Os.if_nametoindex(name) - } + private fun getInterfaceIndex(name: String): Int = Os.if_nametoindex(name) private fun findAvailableName(prefix: String): String? { val base = prefix.trim() @@ -227,17 +220,9 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook return fd } - private fun buildNetlinkAddress(): SocketAddress { - return netlinkSocketAddressCtor.newInstance(0, 0) as SocketAddress - } + private fun buildNetlinkAddress(): SocketAddress = netlinkSocketAddressCtor.newInstance(0, 0) as SocketAddress - private fun buildLinkMessage( - index: Int, - ifName: String?, - flags: Int, - change: Int, - seq: Int, - ): ByteArray { + private fun buildLinkMessage(index: Int, ifName: String?, flags: Int, change: Int, seq: Int): ByteArray { val nameBytes = ifName?.let { (it + "\u0000").toByteArray(Charsets.US_ASCII) } val attrLen = if (nameBytes != null) NLA_HEADER_LEN + nameBytes.size else 0 val attrAligned = align(attrLen) @@ -266,15 +251,9 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook return buffer.array() } - private fun align(length: Int): Int { - return (length + 3) and -4 - } + private fun align(length: Int): Int = (length + 3) and -4 - private fun sendNetlinkMessage( - fd: FileDescriptor, - message: ByteArray, - suppressErrno: Int? = null, - ): Int? { + private fun sendNetlinkMessage(fd: FileDescriptor, message: ByteArray, suppressErrno: Int? = null): Int? { Os.write(fd, message, 0, message.size) val ack = readNetlinkAck(fd) if (ack == null) { diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt index 28ee53a..24c96c2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt @@ -29,7 +29,8 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade val hooked = ArrayList() val sdk = Build.VERSION.SDK_INT when { - sdk >= 35 /* VANILLA_ICE_CREAM */ -> { + // VANILLA_ICE_CREAM + sdk >= 35 -> { hookAppsFilter33Plus(hooked) hookArchivedPackageInternal(hooked) } @@ -57,17 +58,21 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade return } val unhooks = try { - XposedBridge.hookAllMethods(cls, "shouldFilterApplication", object : SafeMethodHook(SOURCE) { - override fun beforeHook(param: MethodHookParam) { - val callingUid = param.args[1] as Int - val callerPackages = getCallerPackages(callingUid) ?: return - val target = param.args[3]!! - val targetPackage = extractPackageName(target) ?: return - if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { - param.result = true + XposedBridge.hookAllMethods( + cls, + "shouldFilterApplication", + object : SafeMethodHook(SOURCE) { + override fun beforeHook(param: MethodHookParam) { + val callingUid = param.args[1] as Int + val callerPackages = getCallerPackages(callingUid) ?: return + val target = param.args[3]!! + val targetPackage = extractPackageName(target) ?: return + if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { + param.result = true + } } - } - }) + }, + ) } catch (e: Throwable) { HookErrorStore.w(SOURCE, "Skip AppsFilterImpl.shouldFilterApplication: ${e.message}", e) emptySet() @@ -84,17 +89,21 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade return } val unhooks = try { - XposedBridge.hookAllMethods(cls, "shouldFilterApplication", object : SafeMethodHook(SOURCE) { - override fun beforeHook(param: MethodHookParam) { - val callingUid = param.args[0] as Int - val callerPackages = getCallerPackages(callingUid) ?: return - val target = param.args[2]!! - val targetPackage = extractPackageName(target) ?: return - if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { - param.result = true + XposedBridge.hookAllMethods( + cls, + "shouldFilterApplication", + object : SafeMethodHook(SOURCE) { + override fun beforeHook(param: MethodHookParam) { + val callingUid = param.args[0] as Int + val callerPackages = getCallerPackages(callingUid) ?: return + val target = param.args[2]!! + val targetPackage = extractPackageName(target) ?: return + if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { + param.result = true + } } - } - }) + }, + ) } catch (e: Throwable) { HookErrorStore.w(SOURCE, "Skip AppsFilter.shouldFilterApplication: ${e.message}", e) emptySet() @@ -111,16 +120,20 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade return } val unhooks = try { - XposedBridge.hookAllMethods(cls, "getArchivedPackageInternal", object : SafeMethodHook(SOURCE) { - override fun beforeHook(param: MethodHookParam) { - val callingUid = Binder.getCallingUid() - val callerPackages = getCallerPackages(callingUid) ?: return - val targetPackage = param.args[0]!!.toString() - if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { - param.result = null + XposedBridge.hookAllMethods( + cls, + "getArchivedPackageInternal", + object : SafeMethodHook(SOURCE) { + override fun beforeHook(param: MethodHookParam) { + val callingUid = Binder.getCallingUid() + val callerPackages = getCallerPackages(callingUid) ?: return + val targetPackage = param.args[0]!!.toString() + if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { + param.result = null + } } - } - }) + }, + ) } catch (e: Throwable) { HookErrorStore.w(SOURCE, "Skip PackageManagerService.getArchivedPackageInternal: ${e.message}", e) emptySet() @@ -137,17 +150,21 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade return } val filterHooks = try { - XposedBridge.hookAllMethods(cls, "filterAppAccessLPr", object : SafeMethodHook(SOURCE) { - override fun beforeHook(param: MethodHookParam) { - val callingUid = param.args[1] as Int - val callerPackages = getCallerPackages(callingUid) ?: return - val target = param.args[0]!! - val targetPackage = extractPackageName(target) ?: return - if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { - param.result = true + XposedBridge.hookAllMethods( + cls, + "filterAppAccessLPr", + object : SafeMethodHook(SOURCE) { + override fun beforeHook(param: MethodHookParam) { + val callingUid = param.args[1] as Int + val callerPackages = getCallerPackages(callingUid) ?: return + val target = param.args[0]!! + val targetPackage = extractPackageName(target) ?: return + if (shouldHidePackage(callingUid, callerPackages, targetPackage)) { + param.result = true + } } - } - }) + }, + ) } catch (e: Throwable) { HookErrorStore.w(SOURCE, "Skip PackageManagerService.filterAppAccessLPr: ${e.message}", e) emptySet() @@ -157,48 +174,52 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade } val resolutionHooks = try { - XposedBridge.hookAllMethods(cls, "applyPostResolutionFilter", object : SafeMethodHook(SOURCE) { - override fun afterHook(param: MethodHookParam) { - val callingUid = param.args[3] as Int - val callerPackages = getCallerPackages(callingUid) ?: return - val rawResult = param.result ?: return - when (rawResult) { - is MutableCollection<*> -> { - @Suppress("UNCHECKED_CAST") - val result = rawResult as MutableCollection - val iterator = result.iterator() - while (iterator.hasNext()) { - val info = iterator.next() - val targetPackage = with(info) { - activityInfo?.packageName - ?: serviceInfo?.packageName - ?: providerInfo?.packageName - ?: resolvePackageName - } - if (targetPackage != null && - shouldHidePackage(callingUid, callerPackages, targetPackage) - ) { - iterator.remove() + XposedBridge.hookAllMethods( + cls, + "applyPostResolutionFilter", + object : SafeMethodHook(SOURCE) { + override fun afterHook(param: MethodHookParam) { + val callingUid = param.args[3] as Int + val callerPackages = getCallerPackages(callingUid) ?: return + val rawResult = param.result ?: return + when (rawResult) { + is MutableCollection<*> -> { + @Suppress("UNCHECKED_CAST") + val result = rawResult as MutableCollection + val iterator = result.iterator() + while (iterator.hasNext()) { + val info = iterator.next() + val targetPackage = with(info) { + activityInfo?.packageName + ?: serviceInfo?.packageName + ?: providerInfo?.packageName + ?: resolvePackageName + } + if (targetPackage != null && + shouldHidePackage(callingUid, callerPackages, targetPackage) + ) { + iterator.remove() + } } } - } - is List<*> -> { - val filtered = rawResult.filterNot { item -> - val info = item as? ResolveInfo ?: return@filterNot false - val targetPackage = with(info) { - activityInfo?.packageName - ?: serviceInfo?.packageName - ?: providerInfo?.packageName - ?: resolvePackageName + is List<*> -> { + val filtered = rawResult.filterNot { item -> + val info = item as? ResolveInfo ?: return@filterNot false + val targetPackage = with(info) { + activityInfo?.packageName + ?: serviceInfo?.packageName + ?: providerInfo?.packageName + ?: resolvePackageName + } + targetPackage != null && + shouldHidePackage(callingUid, callerPackages, targetPackage) } - targetPackage != null && - shouldHidePackage(callingUid, callerPackages, targetPackage) + param.result = filtered } - param.result = filtered } } - } - }) + }, + ) } catch (e: Throwable) { HookErrorStore.w(SOURCE, "Skip PackageManagerService.applyPostResolutionFilter: ${e.message}", e) emptySet() diff --git a/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt b/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt index cfeb2bf..3a35cfe 100644 --- a/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt +++ b/app/src/minApi21/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt @@ -6,6 +6,4 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset @Suppress("UNUSED_PARAMETER") -fun LazyItemScope.animateItemCompat( - placementSpec: FiniteAnimationSpec, -): Modifier = Modifier +fun LazyItemScope.animateItemCompat(placementSpec: FiniteAnimationSpec): Modifier = Modifier diff --git a/app/src/minApi21/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt b/app/src/minApi21/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt index 70297b2..63b471f 100644 --- a/app/src/minApi21/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt +++ b/app/src/minApi21/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt @@ -44,45 +44,39 @@ object PackageQueryManager { fun refreshShizukuState() {} - suspend fun checkRootAvailable(): Boolean { - return RootClient.checkRootAvailable() - } + suspend fun checkRootAvailable(): Boolean = RootClient.checkRootAvailable() fun setQueryMode(mode: String) { _queryMode.value = mode } - suspend fun getInstalledPackages(flags: Int, retryFlags: Int): List { - return when (val s = strategy) { - is PackageQueryStrategy.ForcedRoot -> { - val userId = android.os.Process.myUserHandle().hashCode() - HookStatusClient.getInstalledPackages(Application.application, flags.toLong(), userId) - ?: RootClient.getInstalledPackages(flags) - } - is PackageQueryStrategy.UserSelected -> RootClient.getInstalledPackages(flags) - is PackageQueryStrategy.Direct -> getPackagesViaPackageManager(flags, retryFlags) + suspend fun getInstalledPackages(flags: Int, retryFlags: Int): List = when (val s = strategy) { + is PackageQueryStrategy.ForcedRoot -> { + val userId = android.os.Process.myUserHandle().hashCode() + HookStatusClient.getInstalledPackages(Application.application, flags.toLong(), userId) + ?: RootClient.getInstalledPackages(flags) } + is PackageQueryStrategy.UserSelected -> RootClient.getInstalledPackages(flags) + is PackageQueryStrategy.Direct -> getPackagesViaPackageManager(flags, retryFlags) } - private fun getPackagesViaPackageManager(flags: Int, retryFlags: Int): List { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Application.packageManager.getInstalledPackages( - PackageManager.PackageInfoFlags.of(flags.toLong()) - ) - } else { - @Suppress("DEPRECATION") - Application.packageManager.getInstalledPackages(flags) - } - } catch (_: RuntimeException) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Application.packageManager.getInstalledPackages( - PackageManager.PackageInfoFlags.of(retryFlags.toLong()) - ) - } else { - @Suppress("DEPRECATION") - Application.packageManager.getInstalledPackages(retryFlags) - } + private fun getPackagesViaPackageManager(flags: Int, retryFlags: Int): List = try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Application.packageManager.getInstalledPackages( + PackageManager.PackageInfoFlags.of(flags.toLong()), + ) + } else { + @Suppress("DEPRECATION") + Application.packageManager.getInstalledPackages(flags) + } + } catch (_: RuntimeException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Application.packageManager.getInstalledPackages( + PackageManager.PackageInfoFlags.of(retryFlags.toLong()), + ) + } else { + @Suppress("DEPRECATION") + Application.packageManager.getInstalledPackages(retryFlags) } } } diff --git a/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt b/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt index d8b9a27..f30c74a 100644 --- a/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt +++ b/app/src/minApi23/java/io/nekohasekai/sfa/compat/LazyItemModifiers.kt @@ -5,6 +5,4 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset -fun LazyItemScope.animateItemCompat( - placementSpec: FiniteAnimationSpec, -): Modifier = Modifier.animateItem(placementSpec = placementSpec) +fun LazyItemScope.animateItemCompat(placementSpec: FiniteAnimationSpec): Modifier = Modifier.animateItem(placementSpec = placementSpec) diff --git a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt index 7e224eb..3586d8c 100644 --- a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt +++ b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/PackageQueryManager.kt @@ -32,8 +32,7 @@ object PackageQueryManager { val rootAvailable: StateFlow get() = RootClient.rootAvailable val rootServiceConnected: StateFlow get() = RootClient.serviceConnected - fun isShizukuAvailable(): Boolean = - ShizukuPackageManager.isAvailable() && ShizukuPackageManager.checkPermission() + fun isShizukuAvailable(): Boolean = ShizukuPackageManager.isAvailable() && ShizukuPackageManager.checkPermission() fun registerListeners() { ShizukuPackageManager.registerListeners() @@ -52,48 +51,42 @@ object PackageQueryManager { ShizukuPackageManager.refresh() } - suspend fun checkRootAvailable(): Boolean { - return RootClient.checkRootAvailable() - } + suspend fun checkRootAvailable(): Boolean = RootClient.checkRootAvailable() fun setQueryMode(mode: String) { _queryMode.value = mode } - suspend fun getInstalledPackages(flags: Int, retryFlags: Int): List { - return when (val s = strategy) { - is PackageQueryStrategy.ForcedRoot -> { - val userId = android.os.Process.myUserHandle().hashCode() - HookStatusClient.getInstalledPackages(Application.application, flags.toLong(), userId) - ?: RootClient.getInstalledPackages(flags) - } - is PackageQueryStrategy.UserSelected -> when (s.mode) { - Settings.PACKAGE_QUERY_MODE_ROOT -> RootClient.getInstalledPackages(flags) - else -> ShizukuPackageManager.getInstalledPackages(flags) - } - is PackageQueryStrategy.Direct -> getPackagesViaPackageManager(flags, retryFlags) + suspend fun getInstalledPackages(flags: Int, retryFlags: Int): List = when (val s = strategy) { + is PackageQueryStrategy.ForcedRoot -> { + val userId = android.os.Process.myUserHandle().hashCode() + HookStatusClient.getInstalledPackages(Application.application, flags.toLong(), userId) + ?: RootClient.getInstalledPackages(flags) } + is PackageQueryStrategy.UserSelected -> when (s.mode) { + Settings.PACKAGE_QUERY_MODE_ROOT -> RootClient.getInstalledPackages(flags) + else -> ShizukuPackageManager.getInstalledPackages(flags) + } + is PackageQueryStrategy.Direct -> getPackagesViaPackageManager(flags, retryFlags) } - private fun getPackagesViaPackageManager(flags: Int, retryFlags: Int): List { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Application.packageManager.getInstalledPackages( - PackageManager.PackageInfoFlags.of(flags.toLong()) - ) - } else { - @Suppress("DEPRECATION") - Application.packageManager.getInstalledPackages(flags) - } - } catch (_: RuntimeException) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Application.packageManager.getInstalledPackages( - PackageManager.PackageInfoFlags.of(retryFlags.toLong()) - ) - } else { - @Suppress("DEPRECATION") - Application.packageManager.getInstalledPackages(retryFlags) - } + private fun getPackagesViaPackageManager(flags: Int, retryFlags: Int): List = try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Application.packageManager.getInstalledPackages( + PackageManager.PackageInfoFlags.of(flags.toLong()), + ) + } else { + @Suppress("DEPRECATION") + Application.packageManager.getInstalledPackages(flags) + } + } catch (_: RuntimeException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Application.packageManager.getInstalledPackages( + PackageManager.PackageInfoFlags.of(retryFlags.toLong()), + ) + } else { + @Suppress("DEPRECATION") + Application.packageManager.getInstalledPackages(retryFlags) } } } diff --git a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt index 65bdeba..d007066 100644 --- a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt +++ b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuInstaller.kt @@ -12,24 +12,20 @@ object ShizukuInstaller { private const val SHIZUKU_PERMISSION_REQUEST_CODE = 1001 - fun isAvailable(): Boolean { - return try { - Shizuku.pingBinder() - } catch (e: Exception) { - false - } + fun isAvailable(): Boolean = try { + Shizuku.pingBinder() + } catch (e: Exception) { + false } - fun checkPermission(): Boolean { - return try { - if (Shizuku.isPreV11()) { - false - } else { - Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED - } - } catch (e: Exception) { + fun checkPermission(): Boolean = try { + if (Shizuku.isPreV11()) { false + } else { + Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED } + } catch (e: Exception) { + false } fun requestPermission() { @@ -38,12 +34,10 @@ object ShizukuInstaller { } } - private fun isRunningAsRoot(): Boolean { - return try { - Shizuku.getUid() == 0 - } catch (e: Exception) { - false - } + private fun isRunningAsRoot(): Boolean = try { + Shizuku.getUid() == 0 + } catch (e: Exception) { + false } suspend fun install(apkFile: File) = withContext(Dispatchers.IO) { diff --git a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPackageManager.kt b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPackageManager.kt index f2836bd..9e0de04 100644 --- a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPackageManager.kt +++ b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPackageManager.kt @@ -4,9 +4,9 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Process import io.nekohasekai.sfa.Application +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import rikka.shizuku.Shizuku @@ -46,13 +46,11 @@ object ShizukuPackageManager { refresh() } - fun isShizukuInstalled(): Boolean { - return try { - Application.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0) - true - } catch (e: PackageManager.NameNotFoundException) { - false - } + fun isShizukuInstalled(): Boolean = try { + Application.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false } fun unregisterListeners() { diff --git a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPrivilegedServiceClient.kt b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPrivilegedServiceClient.kt index 6b10f66..998c340 100644 --- a/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPrivilegedServiceClient.kt +++ b/app/src/minApi23/java/io/nekohasekai/sfa/vendor/ShizukuPrivilegedServiceClient.kt @@ -22,7 +22,7 @@ object ShizukuPrivilegedServiceClient { private var connection: ServiceConnection? = null private val args = Shizuku.UserServiceArgs( - ComponentName(Application.application, ShizukuPrivilegedService::class.java) + ComponentName(Application.application, ShizukuPrivilegedService::class.java), ) .tag("sfa-privileged") .processNameSuffix("privileged") diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt b/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt index a67ac69..f4093e3 100644 --- a/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt +++ b/app/src/other/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt @@ -54,9 +54,7 @@ object ApkInstaller { } } - fun canSystemSilentInstall(): Boolean { - return SystemPackageInstaller.canSystemSilentInstall() - } + fun canSystemSilentInstall(): Boolean = SystemPackageInstaller.canSystemSilentInstall() suspend fun canSilentInstall(): Boolean { val method = getConfiguredMethod() diff --git a/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt index 23a8887..1b0809c 100644 --- a/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt +++ b/app/src/other/java/io/nekohasekai/sfa/vendor/Vendor.kt @@ -9,8 +9,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.R import io.nekohasekai.sfa.bg.RootClient -import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea +import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.update.UpdateCheckException import io.nekohasekai.sfa.update.UpdateInfo import io.nekohasekai.sfa.update.UpdateState @@ -19,10 +19,7 @@ import io.nekohasekai.sfa.update.UpdateTrack object Vendor : VendorInterface { private const val TAG = "Vendor" - override fun checkUpdate( - activity: Activity, - byUser: Boolean, - ) { + override fun checkUpdate(activity: Activity, byUser: Boolean) { try { val updateInfo = checkUpdateAsync() if (updateInfo != null) { @@ -94,13 +91,9 @@ object Vendor : VendorInterface { onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit, onCropArea: ((QRCodeCropArea?) -> Unit)?, - ): ImageAnalysis.Analyzer? { - return null - } + ): ImageAnalysis.Analyzer? = null - override fun supportsTrackSelection(): Boolean { - return true - } + override fun supportsTrackSelection(): Boolean = true override fun checkUpdateAsync(): UpdateInfo? { val track = UpdateTrack.fromString(Settings.updateTrack) @@ -109,13 +102,9 @@ object Vendor : VendorInterface { } } - override fun supportsSilentInstall(): Boolean { - return true - } + override fun supportsSilentInstall(): Boolean = true - override fun supportsAutoUpdate(): Boolean { - return true - } + override fun supportsAutoUpdate(): Boolean = true override fun scheduleAutoUpdate() { UpdateWorker.schedule(io.nekohasekai.sfa.Application.application) diff --git a/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt index d52f905..8ef5e92 100644 --- a/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt +++ b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/ApkInstaller.kt @@ -36,9 +36,7 @@ object ApkInstaller { } } - fun canSystemSilentInstall(): Boolean { - return SystemPackageInstaller.canSystemSilentInstall() - } + fun canSystemSilentInstall(): Boolean = SystemPackageInstaller.canSystemSilentInstall() suspend fun canSilentInstall(): Boolean { val method = getConfiguredMethod() diff --git a/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt index 5b29009..d34525d 100644 --- a/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt +++ b/app/src/otherLegacy/java/io/nekohasekai/sfa/vendor/Vendor.kt @@ -9,8 +9,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.R import io.nekohasekai.sfa.bg.RootClient -import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea +import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.update.UpdateCheckException import io.nekohasekai.sfa.update.UpdateInfo import io.nekohasekai.sfa.update.UpdateState @@ -19,10 +19,7 @@ import io.nekohasekai.sfa.update.UpdateTrack object Vendor : VendorInterface { private const val TAG = "Vendor" - override fun checkUpdate( - activity: Activity, - byUser: Boolean, - ) { + override fun checkUpdate(activity: Activity, byUser: Boolean) { try { val updateInfo = checkUpdateAsync() if (updateInfo != null) { @@ -94,13 +91,9 @@ object Vendor : VendorInterface { onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit, onCropArea: ((QRCodeCropArea?) -> Unit)?, - ): ImageAnalysis.Analyzer? { - return null - } + ): ImageAnalysis.Analyzer? = null - override fun supportsTrackSelection(): Boolean { - return true - } + override fun supportsTrackSelection(): Boolean = true override fun checkUpdateAsync(): UpdateInfo? { val track = UpdateTrack.fromString(Settings.updateTrack) @@ -109,26 +102,20 @@ object Vendor : VendorInterface { } } - override fun supportsSilentInstall(): Boolean { - return true - } + override fun supportsSilentInstall(): Boolean = true - override fun supportsAutoUpdate(): Boolean { - return true - } + override fun supportsAutoUpdate(): Boolean = true override fun scheduleAutoUpdate() { UpdateWorker.schedule(io.nekohasekai.sfa.Application.application) } - override suspend fun verifySilentInstallMethod(method: String): Boolean { - return when (method) { - "PACKAGE_INSTALLER" -> { - ApkInstaller.canSystemSilentInstall() - } - "ROOT" -> RootClient.checkRootAvailable() - else -> false + override suspend fun verifySilentInstallMethod(method: String): Boolean = when (method) { + "PACKAGE_INSTALLER" -> { + ApkInstaller.canSystemSilentInstall() } + "ROOT" -> RootClient.checkRootAvailable() + else -> false } override suspend fun downloadAndInstall(context: android.content.Context, downloadUrl: String) { diff --git a/app/src/play/java/io/nekohasekai/sfa/vendor/MLKitQRCodeAnalyzer.kt b/app/src/play/java/io/nekohasekai/sfa/vendor/MLKitQRCodeAnalyzer.kt index e573f63..969ed9c 100644 --- a/app/src/play/java/io/nekohasekai/sfa/vendor/MLKitQRCodeAnalyzer.kt +++ b/app/src/play/java/io/nekohasekai/sfa/vendor/MLKitQRCodeAnalyzer.kt @@ -102,13 +102,7 @@ class MLKitQRCodeAnalyzer( } } - private fun tryInvertedScan( - yData: ByteArray, - width: Int, - height: Int, - rotationDegrees: Int, - onComplete: () -> Unit, - ) { + private fun tryInvertedScan(yData: ByteArray, width: Int, height: Int, rotationDegrees: Int, onComplete: () -> Unit) { val inverted = toLumaBitmap(yData, width, 0, 0, width, height, invert = true) barcodeScanner.process(InputImage.fromBitmap(inverted, rotationDegrees)) .addOnSuccessListener { codes -> @@ -140,15 +134,7 @@ class MLKitQRCodeAnalyzer( return yData } - private fun toLumaBitmap( - yData: ByteArray, - srcWidth: Int, - left: Int, - top: Int, - width: Int, - height: Int, - invert: Boolean, - ): Bitmap { + private fun toLumaBitmap(yData: ByteArray, srcWidth: Int, left: Int, top: Int, width: Int, height: Int, invert: Boolean): Bitmap { val size = width * height val pixels = pixelBuffer?.takeIf { it.size >= size } ?: IntArray(size).also { pixelBuffer = it } diff --git a/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt b/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt index 6b1e6ff..daf9b5f 100644 --- a/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt +++ b/app/src/play/java/io/nekohasekai/sfa/vendor/Vendor.kt @@ -19,10 +19,7 @@ import io.nekohasekai.sfa.update.UpdateState object Vendor : VendorInterface { private const val TAG = "Vendor" - override fun checkUpdate( - activity: Activity, - byUser: Boolean, - ) { + override fun checkUpdate(activity: Activity, byUser: Boolean) { val appUpdateManager = AppUpdateManagerFactory.create(activity) val appUpdateInfoTask = appUpdateManager.appUpdateInfo appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> @@ -95,11 +92,7 @@ object Vendor : VendorInterface { } } - override fun supportsTrackSelection(): Boolean { - return false - } + override fun supportsTrackSelection(): Boolean = false - override fun checkUpdateAsync(): UpdateInfo? { - return null - } + override fun checkUpdateAsync(): UpdateInfo? = null }