diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2f3ec7d..ef729a5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -95,21 +95,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/io/nekohasekai/sfa/LauncherActivity.kt b/app/src/main/java/io/nekohasekai/sfa/LauncherActivity.kt
index a844fe7..1b86ea3 100644
--- a/app/src/main/java/io/nekohasekai/sfa/LauncherActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/LauncherActivity.kt
@@ -4,25 +4,13 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import io.nekohasekai.sfa.compose.ComposeActivity
-import io.nekohasekai.sfa.database.Settings
-import io.nekohasekai.sfa.ui.MainActivity
-import kotlinx.coroutines.runBlocking
class LauncherActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val targetActivity =
- if (BuildConfig.DEBUG) {
- val useComposeUI = runBlocking { Settings.useComposeUI }
- if (useComposeUI) ComposeActivity::class.java else MainActivity::class.java
- } else {
- ComposeActivity::class.java
- }
-
val launchIntent =
- Intent(this, targetActivity).apply {
- // Transfer any intent data from launcher
+ Intent(this, ComposeActivity::class.java).apply {
intent?.let {
action = it.action
data = it.data
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 eb1b7f0..ede8b1c 100644
--- a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt
@@ -34,7 +34,7 @@ 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.ui.MainActivity
+import io.nekohasekai.sfa.compose.ComposeActivity
import io.nekohasekai.sfa.vendor.Vendor
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -397,7 +397,7 @@ class BoxService(
0,
Intent(
service,
- MainActivity::class.java,
+ ComposeActivity::class.java,
).apply {
setAction(Action.OPEN_URL).setData(Uri.parse(notification.openURL))
setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
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 ca9d733..4c20f6e 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
@@ -319,64 +319,6 @@ fun SettingsScreen(navController: NavController) {
}
}
- if (BuildConfig.DEBUG) {
- // Debug
- Spacer(modifier = Modifier.height(16.dp))
-
- Text(
- text = stringResource(R.string.title_debug),
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
- )
- Card(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- colors =
- CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainer,
- ),
- ) {
- ListItem(
- headlineContent = {
- Text(
- stringResource(R.string.switch_to_legacy_ui),
- style = MaterialTheme.typography.bodyLarge,
- )
- },
- leadingContent = {
- Icon(
- imageVector = Icons.Outlined.SwapHoriz,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary,
- )
- },
- modifier =
- Modifier
- .clip(RoundedCornerShape(12.dp))
- .clickable {
- scope.launch(Dispatchers.IO) {
- Settings.useComposeUI = false
- val intent =
- android.content.Intent(
- context,
- Class.forName("io.nekohasekai.sfa.ui.MainActivity"),
- )
- intent.flags =
- android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
- }
- },
- colors =
- ListItemDefaults.colors(
- containerColor = Color.Transparent,
- ),
- )
- }
- }
-
Spacer(modifier = Modifier.height(16.dp))
}
}
diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt b/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt
index 291d155..8193377 100644
--- a/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt
@@ -11,7 +11,6 @@ object SettingsKey {
const val AUTO_UPDATE_ENABLED = "auto_update_enabled"
const val DISABLE_MEMORY_LIMIT = "disable_memory_limit"
const val DYNAMIC_NOTIFICATION = "dynamic_notification"
- const val USE_COMPOSE_UI = "use_compose_ui"
const val DISABLE_DEPRECATED_WARNINGS = "disable_deprecated_warnings"
const val AUTO_REDIRECT = "auto_redirect"
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 ee1850c..572b0fb 100644
--- a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt
@@ -65,7 +65,6 @@ object Settings {
var autoUpdateEnabled by dataStore.boolean(SettingsKey.AUTO_UPDATE_ENABLED) { false }
var disableMemoryLimit by dataStore.boolean(SettingsKey.DISABLE_MEMORY_LIMIT)
var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true }
- var useComposeUI by dataStore.boolean(SettingsKey.USE_COMPOSE_UI) { true }
var disableDeprecatedWarnings by dataStore.boolean(SettingsKey.DISABLE_DEPRECATED_WARNINGS) { false }
const val PER_APP_PROXY_DISABLED = 0
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 f5120a8..8fd3902 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt
@@ -2,18 +2,11 @@ package io.nekohasekai.sfa.ktx
import android.content.Context
import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
import androidx.core.content.FileProvider
-import androidx.fragment.app.FragmentActivity
import androidx.appcompat.R as AppCompatR
-import com.google.zxing.BarcodeFormat
-import com.google.zxing.qrcode.QRCodeWriter
-import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.ProfileContent
import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.TypedProfile
-import io.nekohasekai.sfa.ui.shared.QRCodeDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
@@ -51,27 +44,3 @@ suspend fun Context.shareProfile(profile: Profile) {
)
}
}
-
-fun FragmentActivity.shareProfileURL(profile: Profile) {
- val link =
- Libbox.generateRemoteProfileImportLink(
- profile.name,
- profile.typed.remoteURL,
- )
- val imageSize = dp2px(256)
- val color = getAttrColor(androidx.appcompat.R.attr.colorPrimary)
- val image = QRCodeWriter().encode(link, BarcodeFormat.QR_CODE, imageSize, imageSize, null)
- val imageWidth = image.width
- val imageHeight = image.height
- val imageArray = IntArray(imageWidth * imageHeight)
- for (y in 0 until imageHeight) {
- val offset = y * imageWidth
- for (x in 0 until imageWidth) {
- imageArray[offset + x] = if (image.get(x, y)) color else Color.TRANSPARENT
- }
- }
- val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888)
- bitmap.setPixels(imageArray, 0, imageSize, 0, 0, imageWidth, imageHeight)
- val dialog = QRCodeDialog(bitmap)
- dialog.show(supportFragmentManager, "share-profile-url")
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
deleted file mode 100644
index 70a902c..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
+++ /dev/null
@@ -1,445 +0,0 @@
-package io.nekohasekai.sfa.ui
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.net.VpnService
-import android.os.Build
-import android.os.Bundle
-import android.text.Html
-import androidx.activity.result.contract.ActivityResultContract
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.RequiresApi
-import androidx.core.content.ContextCompat
-import androidx.core.view.isVisible
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.NavController
-import androidx.navigation.NavDestination
-import androidx.navigation.fragment.NavHostFragment
-import androidx.navigation.ui.AppBarConfiguration
-import androidx.navigation.ui.navigateUp
-import androidx.navigation.ui.setupActionBarWithNavController
-import androidx.navigation.ui.setupWithNavController
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.libbox.ProfileContent
-import io.nekohasekai.sfa.Application
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.bg.ServiceConnection
-import io.nekohasekai.sfa.bg.ServiceNotification
-import io.nekohasekai.sfa.constant.Action
-import io.nekohasekai.sfa.constant.Alert
-import io.nekohasekai.sfa.constant.ServiceMode
-import io.nekohasekai.sfa.constant.Status
-import io.nekohasekai.sfa.database.Profile
-import io.nekohasekai.sfa.database.ProfileManager
-import io.nekohasekai.sfa.database.Settings
-import io.nekohasekai.sfa.database.TypedProfile
-import io.nekohasekai.sfa.databinding.ActivityMainBinding
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.hasPermission
-import io.nekohasekai.sfa.ktx.launchCustomTab
-import io.nekohasekai.sfa.ui.profile.NewProfileActivity
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-import io.nekohasekai.sfa.utils.MIUIUtils
-import io.nekohasekai.sfa.vendor.Vendor
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.util.Date
-
-class MainActivity :
- AbstractActivity(),
- ServiceConnection.Callback {
- companion object {
- private const val TAG = "MainActivity"
- }
-
- private lateinit var navHostFragment: NavHostFragment
- private lateinit var navController: NavController
- private lateinit var appBarConfiguration: AppBarConfiguration
-
- private val connection = ServiceConnection(this, this)
-
- val serviceStatus = MutableLiveData(Status.Stopped)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- navHostFragment =
- supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_my) as NavHostFragment
- navController = navHostFragment.navController
- navController.setGraph(R.navigation.mobile_navigation)
- navController.addOnDestinationChangedListener(::onDestinationChanged)
- appBarConfiguration =
- AppBarConfiguration(
- setOf(
- R.id.navigation_dashboard,
- R.id.navigation_log,
- R.id.navigation_configuration,
- R.id.navigation_settings,
- ),
- )
- setupActionBarWithNavController(navController, appBarConfiguration)
- binding.navView.setupWithNavController(navController)
- reconnect()
- startIntegration()
-
- onNewIntent(intent)
- }
-
- override fun onSupportNavigateUp(): Boolean {
- return navController.navigateUp(appBarConfiguration)
- }
-
- @Suppress("UNUSED_PARAMETER")
- private fun onDestinationChanged(
- navController: NavController,
- navDestination: NavDestination,
- bundle: Bundle?,
- ) {
- val destinationId = navDestination.id
- binding.dashboardTabContainer.isVisible = destinationId == R.id.navigation_dashboard
- }
-
- public override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
- val uri = intent.data ?: return
- when (intent.action) {
- Action.OPEN_URL -> {
- launchCustomTab(uri.toString())
- return
- }
- }
- if (uri.scheme == "sing-box" && uri.host == "import-remote-profile") {
- val profile =
- try {
- Libbox.parseRemoteProfileImportLink(uri.toString())
- } catch (e: Exception) {
- errorDialogBuilder(e).show()
- return
- }
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.import_remote_profile)
- .setMessage(
- getString(
- R.string.import_remote_profile_message,
- profile.name,
- profile.host,
- ),
- )
- .setPositiveButton(R.string.ok) { _, _ ->
- startActivity(
- Intent(this, NewProfileActivity::class.java).apply {
- putExtra("importName", profile.name)
- putExtra("importURL", profile.url)
- },
- )
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- } else if (intent.action == Intent.ACTION_VIEW) {
- try {
- val data = contentResolver.openInputStream(uri)?.use { it.readBytes() } ?: return
- val content = Libbox.decodeProfileContent(data)
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.import_profile)
- .setMessage(
- getString(
- R.string.import_profile_message,
- content.name,
- ),
- )
- .setPositiveButton(R.string.ok) { _, _ ->
- lifecycleScope.launch {
- withContext(Dispatchers.IO) {
- runCatching {
- importProfile(content)
- }.onFailure {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(it).show()
- }
- }
- }
- }
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- } catch (e: Exception) {
- errorDialogBuilder(e).show()
- }
- }
- }
-
- private suspend fun importProfile(content: ProfileContent) {
- val typedProfile = TypedProfile()
- val profile = Profile(name = content.name, typed = typedProfile)
- profile.userOrder = ProfileManager.nextOrder()
- when (content.type) {
- Libbox.ProfileTypeLocal -> {
- typedProfile.type = TypedProfile.Type.Local
- }
-
- Libbox.ProfileTypeiCloud -> {
- errorDialogBuilder(R.string.icloud_profile_unsupported).show()
- return
- }
-
- Libbox.ProfileTypeRemote -> {
- typedProfile.type = TypedProfile.Type.Remote
- typedProfile.remoteURL = content.remotePath
- typedProfile.autoUpdate = content.autoUpdate
- typedProfile.autoUpdateInterval = content.autoUpdateInterval
- typedProfile.lastUpdated = Date(content.lastUpdated)
- }
- }
- val configDirectory = File(filesDir, "configs").also { it.mkdirs() }
- val configFile = File(configDirectory, "${profile.userOrder}.json")
- configFile.writeText(content.config)
- typedProfile.path = configFile.path
- ProfileManager.create(profile)
- }
-
- fun reconnect() {
- connection.reconnect()
- }
-
- private fun startIntegration() {
- lifecycleScope.launch(Dispatchers.IO) {
- if (Settings.checkUpdateEnabled) {
- Vendor.checkUpdate(this@MainActivity, false)
- }
- }
- }
-
- @SuppressLint("NewApi")
- fun startService() {
- if (!ServiceNotification.checkPermission()) {
- notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- return
- }
- startService0()
- }
-
- private fun startService0() {
- lifecycleScope.launch(Dispatchers.IO) {
- if (Settings.rebuildServiceMode()) {
- reconnect()
- }
- if (Settings.serviceMode == ServiceMode.VPN) {
- if (prepare()) {
- return@launch
- }
- }
- val intent = Intent(Application.application, Settings.serviceClass())
- withContext(Dispatchers.Main) {
- ContextCompat.startForegroundService(Application.application, intent)
- }
- }
- }
-
- private val notificationPermissionLauncher =
- registerForActivityResult(
- ActivityResultContracts.RequestPermission(),
- ) {
- if (Settings.dynamicNotification && !it) {
- onServiceAlert(Alert.RequestNotificationPermission, null)
- } else {
- startService0()
- }
- }
-
- private val locationPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) {
- if (it) {
- if (it && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- requestBackgroundLocationPermission()
- } else {
- startService()
- }
- }
- }
-
- private val backgroundLocationPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) {
- if (it) {
- startService()
- }
- }
-
- private val prepareLauncher =
- registerForActivityResult(PrepareService()) {
- if (it) {
- startService()
- } else {
- onServiceAlert(Alert.RequestVPNPermission, null)
- }
- }
-
- private class PrepareService : ActivityResultContract() {
- override fun createIntent(
- context: Context,
- input: Intent,
- ): Intent {
- return input
- }
-
- override fun parseResult(
- resultCode: Int,
- intent: Intent?,
- ): Boolean {
- return resultCode == RESULT_OK
- }
- }
-
- 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)
- false
- }
- }
-
- override fun onServiceStatusChanged(status: Status) {
- serviceStatus.postValue(status)
- }
-
- override fun onServiceAlert(
- type: Alert,
- message: String?,
- ) {
- when (type) {
- Alert.RequestLocationPermission -> {
- return requestLocationPermission()
- }
-
- else -> {}
- }
-
- val builder = MaterialAlertDialogBuilder(this)
- builder.setPositiveButton(R.string.ok, null)
- when (type) {
- Alert.RequestVPNPermission -> {
- builder.setMessage(getString(R.string.service_error_missing_permission))
- }
-
- Alert.RequestNotificationPermission -> {
- builder.setTitle(R.string.notification_permission_title)
- builder.setMessage(R.string.notification_permission_required_description)
- }
-
- Alert.EmptyConfiguration -> {
- builder.setMessage(getString(R.string.service_error_empty_configuration))
- }
-
- Alert.StartCommandServer -> {
- builder.setTitle(getString(R.string.service_error_title_start_command_server))
- builder.setMessage(message)
- }
-
- Alert.CreateService -> {
- builder.setTitle(getString(R.string.service_error_title_create_service))
- builder.setMessage(message)
- }
-
- Alert.StartService -> {
- builder.setTitle(getString(R.string.service_error_title_start_service))
- builder.setMessage(message)
- }
-
- else -> {}
- }
- builder.show()
- }
-
- private fun requestLocationPermission() {
- if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
- requestFineLocationPermission()
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- requestBackgroundLocationPermission()
- }
- }
-
- private fun requestFineLocationPermission() {
- val message =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- Html.fromHtml(
- getString(R.string.location_permission_description),
- Html.FROM_HTML_MODE_LEGACY,
- )
- } else {
- @Suppress("DEPRECATION")
- Html.fromHtml(getString(R.string.location_permission_description))
- }
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.location_permission_title)
- .setMessage(message)
- .setPositiveButton(R.string.ok) { _, _ ->
- requestFineLocationPermission0()
- }
- .setNegativeButton(R.string.no_thanks, null)
- .setCancelable(false)
- .show()
- }
-
- private fun requestFineLocationPermission0() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
- } else {
- openPermissionSettings()
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.Q)
- private fun requestBackgroundLocationPermission() {
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.location_permission_title)
- .setMessage(
- Html.fromHtml(
- getString(R.string.location_permission_background_description),
- Html.FROM_HTML_MODE_LEGACY,
- ),
- )
- .setPositiveButton(R.string.ok) { _, _ ->
- backgroundLocationPermissionLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
- }
- .setNegativeButton(R.string.no_thanks, null)
- .setCancelable(false)
- .show()
- }
-
- private fun openPermissionSettings() {
- if (MIUIUtils.isMIUI) {
- try {
- MIUIUtils.openPermissionSettings(this)
- return
- } catch (ignored: Exception) {
- }
- }
-
- try {
- val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- intent.data = Uri.parse("package:$packageName")
- startActivity(intent)
- } catch (e: Exception) {
- errorDialogBuilder(e).show()
- }
- }
-
- override fun onDestroy() {
- connection.disconnect()
- super.onDestroy()
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/ShortcutActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/ShortcutActivity.kt
deleted file mode 100644
index 42231b9..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/ShortcutActivity.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package io.nekohasekai.sfa.ui
-
-import android.app.Activity
-import android.app.KeyguardManager
-import android.content.Intent
-import android.content.pm.ShortcutManager
-import android.os.Build
-import android.os.Bundle
-import androidx.core.content.getSystemService
-import androidx.core.content.pm.ShortcutInfoCompat
-import androidx.core.content.pm.ShortcutManagerCompat
-import androidx.core.graphics.drawable.IconCompat
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.bg.BoxService
-import io.nekohasekai.sfa.bg.ServiceConnection
-import io.nekohasekai.sfa.constant.Status
-
-class ShortcutActivity : Activity(), ServiceConnection.Callback {
- private val connection = ServiceConnection(this, this, false)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
- setResult(
- RESULT_OK,
- ShortcutManagerCompat.createShortcutResultIntent(
- this,
- ShortcutInfoCompat.Builder(this, "toggle")
- .setIntent(
- Intent(
- this,
- ShortcutActivity::class.java,
- ).setAction(Intent.ACTION_MAIN),
- )
- .setIcon(
- IconCompat.createWithResource(
- this,
- R.mipmap.ic_launcher,
- ),
- )
- .setShortLabel(getString(R.string.quick_toggle))
- .build(),
- ),
- )
- finish()
- } else {
- val keyguardManager = getSystemService()
- if (keyguardManager?.isKeyguardLocked == true) {
- if (Build.VERSION.SDK_INT >= 26) {
- keyguardManager.requestDismissKeyguard(this, object : KeyguardManager.KeyguardDismissCallback() {
- override fun onDismissSucceeded() {
- super.onDismissSucceeded()
- connectAndToggle()
- }
- override fun onDismissCancelled() {
- super.onDismissCancelled()
- finish()
- }
- override fun onDismissError() {
- super.onDismissError()
- finish()
- }
- })
- } else {
- finish()
- }
- } else {
- connectAndToggle()
- }
- }
- }
-
- private fun connectAndToggle() {
- connection.connect()
- if (Build.VERSION.SDK_INT >= 25) {
- getSystemService()?.reportShortcutUsed("toggle")
- }
- }
-
- override fun onServiceStatusChanged(status: Status) {
- when (status) {
- Status.Started -> BoxService.stop()
- Status.Stopped -> BoxService.start()
- else -> {}
- }
- finish()
- }
-
- override fun onDestroy() {
- connection.disconnect()
- super.onDestroy()
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt
deleted file mode 100644
index 3838ae0..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt
+++ /dev/null
@@ -1,343 +0,0 @@
-package io.nekohasekai.sfa.ui.dashboard
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.text.TextWatcher
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isInvisible
-import androidx.core.view.isVisible
-import androidx.core.widget.addTextChangedListener
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.SimpleItemAnimator
-import com.google.android.material.textfield.MaterialAutoCompleteTextView
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.libbox.OutboundGroup
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.constant.Status
-import io.nekohasekai.sfa.databinding.FragmentDashboardGroupsBinding
-import io.nekohasekai.sfa.databinding.ViewDashboardGroupBinding
-import io.nekohasekai.sfa.databinding.ViewDashboardGroupItemBinding
-import io.nekohasekai.sfa.ktx.colorForURLTestDelay
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.text
-import io.nekohasekai.sfa.ui.MainActivity
-import io.nekohasekai.sfa.utils.CommandClient
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class GroupsFragment : Fragment(), CommandClient.Handler {
- private val activity: MainActivity? get() = super.getActivity() as MainActivity?
- private var binding: FragmentDashboardGroupsBinding? = null
- private var adapter: Adapter? = null
- private val commandClient =
- CommandClient(lifecycleScope, CommandClient.ConnectionType.Groups, this)
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val binding = FragmentDashboardGroupsBinding.inflate(inflater, container, false)
- this.binding = binding
- onCreate()
- return binding.root
- }
-
- private fun onCreate() {
- val activity = activity ?: return
- val binding = binding ?: return
- adapter = Adapter()
- binding.container.adapter = adapter
- binding.container.layoutManager = LinearLayoutManager(requireContext())
- activity.serviceStatus.observe(viewLifecycleOwner) {
- if (it == Status.Started) {
- commandClient.connect()
- }
- }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- binding = null
- commandClient.disconnect()
- }
-
- private var displayed = false
-
- private fun updateDisplayed(newValue: Boolean) {
- val binding = binding ?: return
- if (displayed != newValue) {
- displayed = newValue
- binding.statusText.isVisible = !displayed
- binding.container.isVisible = displayed
- }
- }
-
- override fun onConnected() {
- lifecycleScope.launch(Dispatchers.Main) {
- updateDisplayed(true)
- }
- }
-
- override fun onDisconnected() {
- lifecycleScope.launch(Dispatchers.Main) {
- updateDisplayed(false)
- }
- }
-
- @SuppressLint("NotifyDataSetChanged")
- override fun updateGroups(newGroups: MutableList) {
- val adapter = adapter ?: return
- activity?.runOnUiThread {
- updateDisplayed(newGroups.isNotEmpty())
- adapter.setGroups(newGroups.map(::Group))
- }
- }
-
- private class Adapter : RecyclerView.Adapter() {
- private lateinit var groups: MutableList
-
- @SuppressLint("NotifyDataSetChanged")
- fun setGroups(newGroups: List) {
- if (!::groups.isInitialized || groups.size != newGroups.size) {
- groups = newGroups.toMutableList()
- notifyDataSetChanged()
- } else {
- newGroups.forEachIndexed { index, group ->
- if (this.groups[index] != group) {
- this.groups[index] = group
- notifyItemChanged(index)
- }
- }
- }
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): GroupView {
- return GroupView(
- ViewDashboardGroupBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- ),
- )
- }
-
- override fun getItemCount(): Int {
- if (!::groups.isInitialized) {
- return 0
- }
- return groups.size
- }
-
- override fun onBindViewHolder(
- holder: GroupView,
- position: Int,
- ) {
- holder.bind(groups[position])
- }
- }
-
- private class GroupView(val binding: ViewDashboardGroupBinding) :
- RecyclerView.ViewHolder(binding.root) {
- private lateinit var group: Group
- private lateinit var items: List
- private lateinit var adapter: ItemAdapter
- private var textWatcher: TextWatcher? = null
-
- @OptIn(DelicateCoroutinesApi::class)
- @SuppressLint("NotifyDataSetChanged")
- fun bind(group: Group) {
- this.group = group
- binding.groupName.text = group.tag
- binding.groupType.text = Libbox.proxyDisplayType(group.type)
- binding.urlTestButton.setOnClickListener {
- GlobalScope.launch {
- runCatching {
- Libbox.newStandaloneCommandClient().urlTest(group.tag)
- }.onFailure {
- withContext(Dispatchers.Main) {
- binding.root.context.errorDialogBuilder(it).show()
- }
- }
- }
- }
- items = group.items
- if (!::adapter.isInitialized) {
- adapter = ItemAdapter(this, group, items.toMutableList())
- binding.itemList.adapter = adapter
- (binding.itemList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations =
- false
- binding.itemList.layoutManager = GridLayoutManager(binding.root.context, 2)
- } else {
- adapter.group = group
- adapter.setItems(items)
- }
- updateExpand()
- }
-
- @OptIn(DelicateCoroutinesApi::class)
- private fun updateExpand(isExpand: Boolean? = null) {
- val newExpandStatus = isExpand ?: group.isExpand
- if (isExpand != null) {
- GlobalScope.launch {
- runCatching {
- Libbox.newStandaloneCommandClient().setGroupExpand(group.tag, isExpand)
- }.onFailure {
- withContext(Dispatchers.Main) {
- binding.root.context.errorDialogBuilder(it).show()
- }
- }
- }
- }
- binding.itemList.isVisible = newExpandStatus
- binding.groupSelected.isVisible = !newExpandStatus
- val textView = (binding.groupSelected.editText as MaterialAutoCompleteTextView)
- if (textWatcher != null) {
- textView.removeTextChangedListener(textWatcher)
- }
- if (!newExpandStatus) {
- binding.groupSelected.text = group.selected
- binding.groupSelected.isEnabled = group.selectable
- if (group.selectable) {
- textView.setSimpleItems(group.items.toList().map { it.tag }.toTypedArray())
- textWatcher =
- textView.addTextChangedListener {
- val selected = textView.text.toString()
- if (selected != group.selected) {
- updateSelected(group, selected)
- }
- GlobalScope.launch {
- runCatching {
- Libbox.newStandaloneCommandClient()
- .selectOutbound(group.tag, selected)
- }.onFailure {
- withContext(Dispatchers.Main) {
- binding.root.context.errorDialogBuilder(it).show()
- }
- }
- }
- }
- }
- }
- if (newExpandStatus) {
- binding.urlTestButton.isVisible = true
- binding.expandButton.setImageResource(R.drawable.ic_expand_less_24)
- } else {
- binding.urlTestButton.isVisible = false
- binding.expandButton.setImageResource(R.drawable.ic_expand_more_24)
- }
- binding.expandButton.setOnClickListener {
- updateExpand(!binding.itemList.isVisible)
- }
- }
-
- fun updateSelected(
- group: Group,
- itemTag: String,
- ) {
- val oldSelected = items.indexOfFirst { it.tag == group.selected }
- group.selected = itemTag
- if (oldSelected != -1) {
- adapter.notifyItemChanged(oldSelected)
- }
- }
- }
-
- private class ItemAdapter(
- val groupView: GroupView,
- var group: Group,
- private var items: MutableList = mutableListOf(),
- ) :
- RecyclerView.Adapter() {
- @SuppressLint("NotifyDataSetChanged")
- fun setItems(newItems: List) {
- if (items.size != newItems.size) {
- items = newItems.toMutableList()
- notifyDataSetChanged()
- } else {
- newItems.forEachIndexed { index, item ->
- if (items[index] != item) {
- items[index] = item
- notifyItemChanged(index)
- }
- }
- }
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): ItemGroupView {
- return ItemGroupView(
- ViewDashboardGroupItemBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- ),
- )
- }
-
- override fun getItemCount(): Int {
- return items.size
- }
-
- override fun onBindViewHolder(
- holder: ItemGroupView,
- position: Int,
- ) {
- holder.bind(groupView, group, items[position])
- }
- }
-
- private class ItemGroupView(val binding: ViewDashboardGroupItemBinding) :
- RecyclerView.ViewHolder(binding.root) {
- @OptIn(DelicateCoroutinesApi::class)
- fun bind(
- groupView: GroupView,
- group: Group,
- item: GroupItem,
- ) {
- if (group.selectable) {
- binding.itemCard.setOnClickListener {
- binding.selectedView.isVisible = true
- groupView.updateSelected(group, item.tag)
- GlobalScope.launch {
- runCatching {
- Libbox.newStandaloneCommandClient().selectOutbound(group.tag, item.tag)
- }.onFailure {
- withContext(Dispatchers.Main) {
- binding.root.context.errorDialogBuilder("select outbound: ${it.localizedMessage}")
- .show()
- }
- }
- }
- }
- }
- binding.selectedView.isInvisible = group.selected != item.tag
- binding.itemName.text = item.tag
- binding.itemType.text = Libbox.proxyDisplayType(item.type)
- binding.itemStatus.isVisible = item.urlTestTime > 0
- if (item.urlTestTime > 0) {
- binding.itemStatus.text = "${item.urlTestDelay}ms"
- binding.itemStatus.setTextColor(
- colorForURLTestDelay(
- binding.root.context,
- item.urlTestDelay,
- ),
- )
- }
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt
deleted file mode 100644
index 531d942..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt
+++ /dev/null
@@ -1,389 +0,0 @@
-package io.nekohasekai.sfa.ui.dashboard
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.divider.MaterialDividerItemDecoration
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.libbox.StatusMessage
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.bg.BoxService
-import io.nekohasekai.sfa.constant.Status
-import io.nekohasekai.sfa.database.Profile
-import io.nekohasekai.sfa.database.ProfileManager
-import io.nekohasekai.sfa.database.Settings
-import io.nekohasekai.sfa.databinding.FragmentDashboardOverviewBinding
-import io.nekohasekai.sfa.databinding.ViewClashModeButtonBinding
-import io.nekohasekai.sfa.databinding.ViewProfileItemBinding
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.getAttrColor
-import io.nekohasekai.sfa.ui.MainActivity
-import io.nekohasekai.sfa.utils.CommandClient
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class OverviewFragment : Fragment() {
- private val activity: MainActivity? get() = super.getActivity() as MainActivity?
- private var binding: FragmentDashboardOverviewBinding? = null
- private val statusClient =
- CommandClient(lifecycleScope, CommandClient.ConnectionType.Status, StatusClient())
- private val clashModeClient =
- CommandClient(lifecycleScope, CommandClient.ConnectionType.ClashMode, ClashModeClient())
-
- private var adapter: Adapter? = null
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val binding = FragmentDashboardOverviewBinding.inflate(inflater, container, false)
- this.binding = binding
- onCreate()
- return binding.root
- }
-
- private fun onCreate() {
- val activity = activity ?: return
- val binding = binding ?: return
- binding.profileList.adapter =
- Adapter(lifecycleScope, binding).apply {
- adapter = this
- reload()
- }
- binding.profileList.layoutManager = LinearLayoutManager(requireContext())
- val divider = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
- divider.isLastItemDecorated = false
- binding.profileList.addItemDecoration(divider)
- activity.serviceStatus.observe(viewLifecycleOwner) {
- binding.statusContainer.isVisible = it == Status.Starting || it == Status.Started
- when (it) {
- Status.Stopped -> {
- binding.clashModeCard.isVisible = false
- binding.systemProxyCard.isVisible = false
- }
-
- Status.Started -> {
- statusClient.connect()
- clashModeClient.connect()
- reloadSystemProxyStatus()
- }
-
- else -> {}
- }
- }
-
- ProfileManager.registerCallback(this::updateProfiles)
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- adapter = null
- binding = null
- statusClient.disconnect()
- clashModeClient.disconnect()
- ProfileManager.unregisterCallback(this::updateProfiles)
- }
-
- private fun updateProfiles() {
- adapter?.reload()
- }
-
- private fun reloadSystemProxyStatus() {
- val binding = binding ?: return
- lifecycleScope.launch(Dispatchers.IO) {
- val status = Libbox.newStandaloneCommandClient().systemProxyStatus
- withContext(Dispatchers.Main) {
- binding.systemProxyCard.isVisible = status.available
- binding.systemProxySwitch.setOnCheckedChangeListener(null)
- binding.systemProxySwitch.isChecked = status.enabled
- var reloading = false
- binding.systemProxySwitch.setOnCheckedChangeListener { buttonView, isChecked ->
- synchronized(this@OverviewFragment) {
- if (reloading) return@setOnCheckedChangeListener
- reloading = true
- binding.systemProxySwitch.isEnabled = false
- lifecycleScope.launch(Dispatchers.IO) {
- Settings.systemProxyEnabled = isChecked
- runCatching {
- Libbox.newStandaloneCommandClient().setSystemProxyEnabled(isChecked)
- }.onFailure {
- withContext(Dispatchers.Main) {
- buttonView.context.errorDialogBuilder(it).show()
- }
- }
- withContext(Dispatchers.Main) {
- delay(1000L)
- binding.systemProxySwitch.isEnabled = true
- }
- }
- }
- }
- }
- }
- }
-
- inner class StatusClient : CommandClient.Handler {
- override fun onConnected() {
- val binding = binding ?: return
- lifecycleScope.launch(Dispatchers.Main) {
- binding.memoryText.text = getString(R.string.loading)
- binding.goroutinesText.text = getString(R.string.loading)
- }
- }
-
- override fun onDisconnected() {
- val binding = binding ?: return
- lifecycleScope.launch(Dispatchers.Main) {
- binding.memoryText.text = getString(R.string.loading)
- binding.goroutinesText.text = getString(R.string.loading)
- }
- }
-
- override fun updateStatus(status: StatusMessage) {
- val binding = binding ?: return
- lifecycleScope.launch(Dispatchers.Main) {
- binding.memoryText.text = Libbox.formatBytes(status.memory)
- binding.goroutinesText.text = status.goroutines.toString()
- val trafficAvailable = status.trafficAvailable
- binding.trafficContainer.isVisible = trafficAvailable
- if (trafficAvailable) {
- binding.inboundConnectionsText.text = status.connectionsIn.toString()
- binding.outboundConnectionsText.text = status.connectionsOut.toString()
- binding.uplinkText.text = Libbox.formatBytes(status.uplink) + "/s"
- binding.downlinkText.text = Libbox.formatBytes(status.downlink) + "/s"
- binding.uplinkTotalText.text = Libbox.formatBytes(status.uplinkTotal)
- binding.downlinkTotalText.text = Libbox.formatBytes(status.downlinkTotal)
- }
- }
- }
- }
-
- inner class ClashModeClient : CommandClient.Handler {
- override fun initializeClashMode(
- modeList: List,
- currentMode: String,
- ) {
- val binding = binding ?: return
- if (modeList.size > 1) {
- lifecycleScope.launch(Dispatchers.Main) {
- binding.clashModeCard.isVisible = true
- binding.clashModeList.adapter = ClashModeAdapter(modeList, currentMode)
- binding.clashModeList.layoutManager =
- GridLayoutManager(
- requireContext(),
- if (modeList.size < 3) modeList.size else 3,
- )
- }
- } else {
- lifecycleScope.launch(Dispatchers.Main) {
- binding.clashModeCard.isVisible = false
- }
- }
- }
-
- @SuppressLint("NotifyDataSetChanged")
- override fun updateClashMode(newMode: String) {
- val binding = binding ?: return
- val adapter = binding.clashModeList.adapter as? ClashModeAdapter ?: return
- adapter.selected = newMode
- lifecycleScope.launch(Dispatchers.Main) {
- adapter.notifyDataSetChanged()
- }
- }
- }
-
- private inner class ClashModeAdapter(
- val items: List,
- var selected: String,
- ) :
- RecyclerView.Adapter() {
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): ClashModeItemView {
- val view =
- ClashModeItemView(
- ViewClashModeButtonBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- ),
- )
- view.binding.clashModeButton.clipToOutline = true
- return view
- }
-
- override fun getItemCount(): Int {
- return items.size
- }
-
- override fun onBindViewHolder(
- holder: ClashModeItemView,
- position: Int,
- ) {
- holder.bind(items[position], selected)
- }
- }
-
- private inner class ClashModeItemView(val binding: ViewClashModeButtonBinding) :
- RecyclerView.ViewHolder(binding.root) {
- @OptIn(DelicateCoroutinesApi::class)
- fun bind(
- item: String,
- selected: String,
- ) {
- binding.clashModeButtonText.text = item
- if (item != selected) {
- binding.clashModeButtonText.setTextColor(
- binding.root.context.getAttrColor(com.google.android.material.R.attr.colorOnPrimaryContainer),
- )
- binding.clashModeButton.setBackgroundResource(R.drawable.bg_rounded_rectangle)
- binding.clashModeButton.setOnClickListener {
- runCatching {
- Libbox.newStandaloneCommandClient().setClashMode(item)
- clashModeClient.connect()
- }.onFailure {
- GlobalScope.launch(Dispatchers.Main) {
- binding.root.context.errorDialogBuilder(it).show()
- }
- }
- }
- } else {
- binding.clashModeButtonText.setTextColor(
- binding.root.context.getAttrColor(com.google.android.material.R.attr.colorOnPrimary),
- )
- binding.clashModeButton.setBackgroundResource(R.drawable.bg_rounded_rectangle_active)
- binding.clashModeButton.isClickable = false
- }
- }
- }
-
- class Adapter(
- internal val scope: CoroutineScope,
- internal val parent: FragmentDashboardOverviewBinding,
- ) :
- RecyclerView.Adapter() {
- internal var items: MutableList = mutableListOf()
- internal var selectedProfileID = -1L
- internal var lastSelectedIndex: Int? = null
-
- internal fun reload() {
- scope.launch(Dispatchers.IO) {
- items = ProfileManager.list().toMutableList()
- if (items.isNotEmpty()) {
- selectedProfileID = Settings.selectedProfile
- for ((index, profile) in items.withIndex()) {
- if (profile.id == selectedProfileID) {
- lastSelectedIndex = index
- break
- }
- }
- if (lastSelectedIndex == null) {
- lastSelectedIndex = 0
- selectedProfileID = items[0].id
- Settings.selectedProfile = selectedProfileID
- }
- }
- withContext(Dispatchers.Main) {
- parent.statusText.isVisible = items.isEmpty()
- parent.container.isVisible = items.isNotEmpty()
- notifyDataSetChanged()
- }
- }
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): Holder {
- return Holder(
- this,
- ViewProfileItemBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- ),
- )
- }
-
- override fun onBindViewHolder(
- holder: Holder,
- position: Int,
- ) {
- holder.bind(items[position])
- }
-
- override fun getItemCount(): Int {
- return items.size
- }
- }
-
- class Holder(
- private val adapter: Adapter,
- private val binding: ViewProfileItemBinding,
- ) :
- RecyclerView.ViewHolder(binding.root) {
- internal fun bind(profile: Profile) {
- binding.profileName.text = profile.name
- binding.profileSelected.setOnCheckedChangeListener(null)
- binding.profileSelected.isChecked = profile.id == adapter.selectedProfileID
- binding.profileSelected.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) {
- adapter.parent.profileList.isClickable = false
- adapter.selectedProfileID = profile.id
- adapter.lastSelectedIndex?.let { index ->
- adapter.notifyItemChanged(index)
- }
- adapter.lastSelectedIndex = adapterPosition
- adapter.scope.launch(Dispatchers.IO) {
- switchProfile(profile)
- withContext(Dispatchers.Main) {
- adapter.parent.profileList.isEnabled = true
- }
- }
- }
- }
- binding.root.setOnClickListener {
- binding.profileSelected.toggle()
- }
- }
-
- private suspend fun switchProfile(profile: Profile) {
- Settings.selectedProfile = profile.id
- val mainActivity = (binding.root.context as? MainActivity) ?: return
- val started = mainActivity.serviceStatus.value == Status.Started
- if (!started) {
- return
- }
- val restart = Settings.rebuildServiceMode()
- if (restart) {
- mainActivity.reconnect()
- BoxService.stop()
- delay(1000L)
- mainActivity.startService()
- return
- }
- runCatching {
- Libbox.newStandaloneCommandClient().serviceReload()
- }.onFailure {
- withContext(Dispatchers.Main) {
- mainActivity.errorDialogBuilder(it).show()
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt
deleted file mode 100644
index d7138c1..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.nekohasekai.sfa.ui.debug
-
-import android.content.Intent
-import android.os.Bundle
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.databinding.ActivityDebugBinding
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-
-class DebugActivity : AbstractActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTitle(R.string.title_debug)
- binding.scanVPNButton.setOnClickListener {
- startActivity(Intent(this, VPNScanActivity::class.java))
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt
deleted file mode 100644
index b03581c..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt
+++ /dev/null
@@ -1,274 +0,0 @@
-package io.nekohasekai.sfa.ui.debug
-
-import android.Manifest
-import android.content.pm.PackageInfo
-import android.content.pm.PackageManager
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.view.ViewGroup
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.isVisible
-import androidx.core.view.updatePadding
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.databinding.ActivityVpnScanBinding
-import io.nekohasekai.sfa.databinding.ViewVpnAppItemBinding
-import io.nekohasekai.sfa.ktx.dp2px
-import io.nekohasekai.sfa.ktx.toStringIterator
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-import io.nekohasekai.sfa.vendor.PackageQueryManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.util.zip.ZipFile
-import kotlin.math.roundToInt
-
-class VPNScanActivity : AbstractActivity() {
- private var adapter: Adapter? = null
- private val appInfoList = mutableListOf()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTitle(R.string.title_scan_vpn)
-
- ViewCompat.setOnApplyWindowInsetsListener(binding.scanVPNResult) { view, windowInsets ->
- val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- view.updatePadding(bottom = insets.bottom + dp2px(16))
- WindowInsetsCompat.CONSUMED
- }
-
- binding.scanVPNResult.adapter =
- Adapter().also {
- adapter = it
- }
- binding.scanVPNResult.layoutManager = LinearLayoutManager(this)
- lifecycleScope.launch(Dispatchers.IO) {
- scanVPN()
- }
- }
-
- class VPNType(
- val appType: String?,
- val coreType: VPNCoreType?,
- )
-
- class VPNCoreType(
- val coreType: String,
- val corePath: String,
- val goVersion: String,
- )
-
- class AppInfo(
- val packageInfo: PackageInfo,
- val vpnType: VPNType,
- )
-
- inner class Adapter : RecyclerView.Adapter() {
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): Holder {
- return Holder(ViewVpnAppItemBinding.inflate(layoutInflater, parent, false))
- }
-
- override fun getItemCount(): Int {
- return appInfoList.size
- }
-
- override fun onBindViewHolder(
- holder: Holder,
- position: Int,
- ) {
- holder.bind(appInfoList[position])
- }
- }
-
- class Holder(
- private val binding: ViewVpnAppItemBinding,
- ) :
- RecyclerView.ViewHolder(binding.root) {
- fun bind(element: AppInfo) {
- binding.appIcon.setImageDrawable(element.packageInfo.applicationInfo!!.loadIcon(binding.root.context.packageManager))
- binding.appName.text =
- element.packageInfo.applicationInfo!!.loadLabel(binding.root.context.packageManager)
- binding.packageName.text = element.packageInfo.packageName
- val appType = element.vpnType.appType
- if (appType != null) {
- binding.appTypeText.text = element.vpnType.appType
- } else {
- binding.appTypeText.setText(R.string.vpn_app_type_other)
- }
- val coreType = element.vpnType.coreType?.coreType
- if (coreType != null) {
- binding.coreTypeText.text = element.vpnType.coreType.coreType
- } else {
- binding.coreTypeText.setText(R.string.vpn_core_type_unknown)
- }
- val corePath = element.vpnType.coreType?.corePath.takeIf { !it.isNullOrBlank() }
- if (corePath != null) {
- binding.corePathLayout.isVisible = true
- binding.corePathText.text = corePath
- } else {
- binding.corePathLayout.isVisible = false
- }
-
- val goVersion = element.vpnType.coreType?.goVersion.takeIf { !it.isNullOrBlank() }
- if (goVersion != null) {
- binding.goVersionLayout.isVisible = true
- binding.goVersionText.text = goVersion
- } else {
- binding.goVersionLayout.isVisible = false
- }
- }
- }
-
- private suspend fun scanVPN() {
- val adapter = adapter ?: return
- val flag =
- PackageManager.GET_SERVICES or
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- PackageManager.MATCH_UNINSTALLED_PACKAGES
- } else {
- @Suppress("DEPRECATION")
- PackageManager.GET_UNINSTALLED_PACKAGES
- }
- val installedPackages = PackageQueryManager.getInstalledPackages(flag)
- val vpnAppList =
- installedPackages.filter {
- it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE && it.applicationInfo != null }
- ?: false
- }
- for ((index, packageInfo) in vpnAppList.withIndex()) {
- val appType = runCatching { getVPNAppType(packageInfo) }.getOrNull()
- val coreType = runCatching { getVPNCoreType(packageInfo) }.getOrNull()
- appInfoList.add(AppInfo(packageInfo, VPNType(appType, coreType)))
- withContext(Dispatchers.Main) {
- adapter.notifyItemInserted(index)
- binding.scanVPNResult.scrollToPosition(index)
- binding.scanVPNProgress.setProgressCompat(
- (((index + 1).toFloat() / vpnAppList.size.toFloat()) * 100).roundToInt(),
- true,
- )
- }
- System.gc()
- }
- withContext(Dispatchers.Main) {
- binding.scanVPNProgress.isVisible = false
- }
- }
-
- companion object {
- private val v2rayNGClasses =
- listOf(
- "com.v2ray.ang",
- ".dto.V2rayConfig",
- ".service.V2RayVpnService",
- )
-
- private val clashForAndroidClasses =
- listOf(
- "com.github.kr328.clash",
- ".core.Clash",
- ".service.TunService",
- )
-
- private val sfaClasses =
- listOf(
- "io.nekohasekai.sfa",
- )
-
- private val legacySagerNetClasses =
- listOf(
- "io.nekohasekai.sagernet",
- ".fmt.ConfigBuilder",
- )
-
- private val shadowsocksAndroidClasses =
- listOf(
- "com.github.shadowsocks",
- ".bg.VpnService",
- "GuardedProcessPool",
- )
- }
-
- private fun getVPNAppType(packageInfo: PackageInfo): String? {
- ZipFile(File(packageInfo.applicationInfo!!.publicSourceDir)).use { packageFile ->
- for (packageEntry in packageFile.entries()) {
- if (!(
- packageEntry.name.startsWith("classes") &&
- packageEntry.name.endsWith(
- ".dex",
- )
- )
- ) {
- continue
- }
- if (packageEntry.size > 15000000) {
- continue
- }
- val input = packageFile.getInputStream(packageEntry).buffered()
- val dexFile =
- try {
- DexBackedDexFile.fromInputStream(null, input)
- } catch (e: Exception) {
- Log.e("VPNScanActivity", "Failed to read dex file", e)
- continue
- }
- for (clazz in dexFile.classes) {
- val clazzName =
- clazz.type.substring(1, clazz.type.length - 1)
- .replace("/", ".")
- .replace("$", ".")
- for (v2rayNGClass in v2rayNGClasses) {
- if (clazzName.contains(v2rayNGClass)) {
- return "V2RayNG"
- }
- }
- for (clashForAndroidClass in clashForAndroidClasses) {
- if (clazzName.contains(clashForAndroidClass)) {
- return "ClashForAndroid"
- }
- }
- for (sfaClass in sfaClasses) {
- if (clazzName.contains(sfaClass)) {
- return "sing-box"
- }
- }
- for (legacySagerNetClass in legacySagerNetClasses) {
- if (clazzName.contains(legacySagerNetClass)) {
- return "LegacySagerNet"
- }
- }
- for (shadowsocksAndroidClass in shadowsocksAndroidClasses) {
- if (clazzName.contains(shadowsocksAndroidClass)) {
- return "shadowsocks-android"
- }
- }
- }
- }
- return null
- }
- }
-
- private fun getVPNCoreType(packageInfo: PackageInfo): VPNCoreType? {
- val packageFiles = mutableListOf(packageInfo.applicationInfo!!.publicSourceDir)
- packageInfo.applicationInfo!!.splitPublicSourceDirs?.also {
- packageFiles.addAll(it)
- }
- val vpnType =
- try {
- Libbox.readAndroidVPNType(packageFiles.toStringIterator())
- } catch (ignored: Exception) {
- return null
- }
- return VPNCoreType(vpnType.coreType, vpnType.corePath, vpnType.goVersion)
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt
deleted file mode 100644
index 5981730..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt
+++ /dev/null
@@ -1,294 +0,0 @@
-package io.nekohasekai.sfa.ui.main
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.widget.PopupMenu
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.database.Profile
-import io.nekohasekai.sfa.database.ProfileManager
-import io.nekohasekai.sfa.database.TypedProfile
-import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding
-import io.nekohasekai.sfa.databinding.SheetAddProfileBinding
-import io.nekohasekai.sfa.databinding.ViewConfigutationItemBinding
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.shareProfile
-import io.nekohasekai.sfa.ktx.shareProfileURL
-import io.nekohasekai.sfa.ui.MainActivity
-import io.nekohasekai.sfa.ui.profile.EditProfileActivity
-import io.nekohasekai.sfa.ui.profile.NewProfileActivity
-import io.nekohasekai.sfa.ui.profile.QRScanActivity
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.text.DateFormat
-import java.util.Collections
-
-class ConfigurationFragment : Fragment() {
- private var adapter: Adapter? = null
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val binding = FragmentConfigurationBinding.inflate(inflater, container, false)
- val adapter = Adapter(binding)
- this.adapter = adapter
- binding.profileList.also {
- it.layoutManager = LinearLayoutManager(requireContext())
- it.adapter = adapter
- ItemTouchHelper(
- object :
- ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
- override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder,
- ): Boolean {
- return adapter.move(viewHolder.adapterPosition, target.adapterPosition)
- }
-
- override fun onSwiped(
- viewHolder: RecyclerView.ViewHolder,
- direction: Int,
- ) {
- }
-
- override fun onSelectedChanged(
- viewHolder: RecyclerView.ViewHolder?,
- actionState: Int,
- ) {
- super.onSelectedChanged(viewHolder, actionState)
- if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
- adapter.updateUserOrder()
- }
- }
- },
- ).attachToRecyclerView(it)
- }
- adapter.reload()
- binding.fab.setOnClickListener {
- AddProfileDialog().show(childFragmentManager, "add_profile")
- }
- ProfileManager.registerCallback(this::updateProfiles)
- return binding.root
- }
-
- class AddProfileDialog : BottomSheetDialogFragment(R.layout.sheet_add_profile) {
- private val importFromFile =
- registerForActivityResult(ActivityResultContracts.GetContent(), ::onImportResult)
-
- private val scanQrCode =
- registerForActivityResult(QRScanActivity.Contract(), ::onScanResult)
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- val binding = SheetAddProfileBinding.bind(view)
- binding.importFromFile.setOnClickListener {
- importFromFile.launch("*/*")
- }
- binding.scanQrCode.setOnClickListener {
- scanQrCode.launch(null)
- }
- binding.createManually.setOnClickListener {
- dismiss()
- startActivity(Intent(requireContext(), NewProfileActivity::class.java))
- }
- }
-
- private fun onImportResult(result: Uri?) {
- dismiss()
- (activity as? MainActivity ?: return).onNewIntent(Intent(Intent.ACTION_VIEW, result))
- }
-
- private fun onScanResult(result: Intent?) {
- dismiss()
- (activity as? MainActivity ?: return).onNewIntent(result ?: return)
- }
- }
-
- override fun onResume() {
- super.onResume()
- adapter?.reload()
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- ProfileManager.unregisterCallback(this::updateProfiles)
- adapter = null
- }
-
- private fun updateProfiles() {
- adapter?.reload()
- }
-
- inner class Adapter(
- private val parent: FragmentConfigurationBinding,
- ) :
- RecyclerView.Adapter() {
- internal var items: MutableList = mutableListOf()
- internal val scope = lifecycleScope
- internal val fragmentActivity = requireActivity()
-
- @SuppressLint("NotifyDataSetChanged")
- internal fun reload() {
- lifecycleScope.launch(Dispatchers.IO) {
- val newItems = ProfileManager.list().toMutableList()
- withContext(Dispatchers.Main) {
- items = newItems
- notifyDataSetChanged()
- if (items.isEmpty()) {
- parent.statusText.isVisible = true
- parent.profileList.isVisible = false
- } else if (parent.statusText.isVisible) {
- parent.statusText.isVisible = false
- parent.profileList.isVisible = true
- }
- }
- }
- }
-
- internal fun move(
- from: Int,
- to: Int,
- ): Boolean {
- if (from < to) {
- for (i in from until to) {
- Collections.swap(items, i, i + 1)
- }
- } else {
- for (i in from downTo to + 1) {
- Collections.swap(items, i, i - 1)
- }
- }
- notifyItemMoved(from, to)
- return true
- }
-
- @OptIn(DelicateCoroutinesApi::class)
- internal fun updateUserOrder() {
- items.forEachIndexed { index, profile ->
- profile.userOrder = index.toLong()
- }
- GlobalScope.launch(Dispatchers.IO) {
- ProfileManager.update(items)
- }
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): Holder {
- return Holder(
- this,
- ViewConfigutationItemBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- ),
- )
- }
-
- override fun onBindViewHolder(
- holder: Holder,
- position: Int,
- ) {
- holder.bind(items[position])
- }
-
- override fun getItemCount(): Int {
- return items.size
- }
- }
-
- class Holder(private val adapter: Adapter, private val binding: ViewConfigutationItemBinding) :
- RecyclerView.ViewHolder(binding.root) {
- internal fun bind(profile: Profile) {
- binding.profileName.text = profile.name
- if (profile.typed.type == TypedProfile.Type.Remote) {
- binding.profileLastUpdated.isVisible = true
- binding.profileLastUpdated.text =
- binding.root.context.getString(
- R.string.last_updated_format,
- DateFormat.getDateTimeInstance().format(profile.typed.lastUpdated),
- )
- } else {
- binding.profileLastUpdated.isVisible = false
- }
- binding.root.setOnClickListener {
- val intent = Intent(binding.root.context, EditProfileActivity::class.java)
- intent.putExtra("profile_id", profile.id)
- it.context.startActivity(intent)
- }
- binding.moreButton.setOnClickListener { button ->
- val popup = PopupMenu(button.context, button)
- popup.setForceShowIcon(true)
- popup.menuInflater.inflate(R.menu.profile_menu, popup.menu)
- if (profile.typed.type != TypedProfile.Type.Remote) {
- popup.menu.removeItem(R.id.action_share_url)
- }
- popup.setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_share -> {
- adapter.scope.launch(Dispatchers.IO) {
- try {
- button.context.shareProfile(profile)
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- button.context.errorDialogBuilder(e).show()
- }
- }
- }
- true
- }
-
- R.id.action_share_url -> {
- adapter.scope.launch(Dispatchers.IO) {
- try {
- adapter.fragmentActivity.shareProfileURL(profile)
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- button.context.errorDialogBuilder(e).show()
- }
- }
- }
- true
- }
-
- R.id.action_delete -> {
- adapter.items.remove(profile)
- adapter.notifyItemRemoved(adapterPosition)
- adapter.scope.launch(Dispatchers.IO) {
- runCatching {
- ProfileManager.delete(profile)
- }
- }
- true
- }
-
- else -> false
- }
- }
- popup.show()
- }
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt
deleted file mode 100644
index 194a6e2..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-package io.nekohasekai.sfa.ui.main
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.StringRes
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.viewpager2.adapter.FragmentStateAdapter
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.tabs.TabLayoutMediator
-import io.nekohasekai.libbox.DeprecatedNoteIterator
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.bg.BoxService
-import io.nekohasekai.sfa.constant.Status
-import io.nekohasekai.sfa.databinding.FragmentDashboardBinding
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.launchCustomTab
-import io.nekohasekai.sfa.ui.MainActivity
-import io.nekohasekai.sfa.ui.dashboard.GroupsFragment
-import io.nekohasekai.sfa.ui.dashboard.OverviewFragment
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class DashboardFragment : Fragment(R.layout.fragment_dashboard) {
- private val activity: MainActivity? get() = super.getActivity() as MainActivity?
- private var binding: FragmentDashboardBinding? = null
- private var mediator: TabLayoutMediator? = null
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val binding = FragmentDashboardBinding.inflate(inflater, container, false)
- this.binding = binding
- onCreate()
- return binding.root
- }
-
- private val adapter by lazy { Adapter(this) }
-
- private fun onCreate() {
- val activity = activity ?: return
- val binding = binding ?: return
- binding.dashboardPager.adapter = adapter
- binding.dashboardPager.offscreenPageLimit = Page.values().size
- activity.serviceStatus.observe(viewLifecycleOwner) {
- when (it) {
- Status.Stopped -> {
- disablePager()
- binding.fab.setImageResource(R.drawable.ic_play_arrow_24)
- binding.fab.show()
- binding.fab.isEnabled = true
- }
-
- Status.Starting -> {
- binding.fab.hide()
- }
-
- Status.Started -> {
- checkDeprecatedNotes()
- enablePager()
- binding.fab.setImageResource(R.drawable.ic_stop_24)
- binding.fab.show()
- binding.fab.isEnabled = true
- }
-
- Status.Stopping -> {
- disablePager()
- binding.fab.hide()
- }
-
- else -> {}
- }
- }
- binding.fab.setOnClickListener {
- when (activity.serviceStatus.value) {
- Status.Stopped -> {
- it.isEnabled = false
- activity.startService()
- }
-
- Status.Started -> {
- BoxService.stop()
- }
-
- else -> {}
- }
- }
- }
-
- override fun onStart() {
- super.onStart()
- val activityBinding = activity?.binding ?: return
- val binding = binding ?: return
- if (mediator != null) return
- mediator =
- TabLayoutMediator(
- activityBinding.dashboardTabLayout,
- binding.dashboardPager,
- ) { tab, position ->
- tab.setText(Page.values()[position].titleRes)
- }.apply { attach() }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- mediator?.detach()
- mediator = null
- binding?.dashboardPager?.adapter = null
- binding = null
- }
-
- private fun checkDeprecatedNotes() {
- GlobalScope.launch(Dispatchers.IO) {
- runCatching {
- val notes = Libbox.newStandaloneCommandClient().deprecatedNotes
- if (notes.hasNext()) {
- withContext(Dispatchers.Main) {
- loopShowDeprecatedNotes(notes)
- }
- }
- }.onFailure {
- withContext(Dispatchers.Main) {
- activity?.errorDialogBuilder(it)?.show()
- }
- }
- }
- }
-
- private fun loopShowDeprecatedNotes(notes: DeprecatedNoteIterator) {
- if (notes.hasNext()) {
- val note = notes.next()
- val builder = MaterialAlertDialogBuilder(requireContext())
- builder.setTitle(getString(R.string.service_error_title_deprecated_warning))
- builder.setMessage(note.message())
- builder.setPositiveButton(R.string.ok) { _, _ ->
- loopShowDeprecatedNotes(notes)
- }
- if (!note.migrationLink.isNullOrBlank()) {
- builder.setNeutralButton(R.string.service_error_deprecated_warning_documentation) { _, _ ->
- requireContext().launchCustomTab(note.migrationLink)
- loopShowDeprecatedNotes(notes)
- }
- }
- builder.show()
- }
- }
-
- private fun enablePager() {
- val activity = activity ?: return
- val binding = binding ?: return
- activity.binding.dashboardTabLayout.isVisible = true
- binding.dashboardPager.isUserInputEnabled = true
- }
-
- private fun disablePager() {
- val activity = activity ?: return
- val binding = binding ?: return
- activity.binding.dashboardTabLayout.isVisible = false
- binding.dashboardPager.isUserInputEnabled = false
- binding.dashboardPager.setCurrentItem(0, false)
- }
-
- enum class Page(
- @StringRes val titleRes: Int,
- val fragmentClass: Class,
- ) {
- Overview(R.string.title_overview, OverviewFragment::class.java),
- Groups(R.string.title_groups, GroupsFragment::class.java),
- }
-
- class Adapter(parent: Fragment) : FragmentStateAdapter(parent) {
- override fun getItemCount(): Int {
- return Page.entries.size
- }
-
- override fun createFragment(position: Int): Fragment {
- return Page.entries[position].fragmentClass.getConstructor().newInstance()
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/LogFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/LogFragment.kt
deleted file mode 100644
index e47ee2d..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/main/LogFragment.kt
+++ /dev/null
@@ -1,198 +0,0 @@
-package io.nekohasekai.sfa.ui.main
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import io.nekohasekai.libbox.LogEntry
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.bg.BoxService
-import io.nekohasekai.sfa.constant.Status
-import io.nekohasekai.sfa.databinding.FragmentLogBinding
-import io.nekohasekai.sfa.databinding.ViewLogTextItemBinding
-import io.nekohasekai.sfa.ui.MainActivity
-import io.nekohasekai.sfa.utils.ColorUtils
-import io.nekohasekai.sfa.utils.CommandClient
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.util.LinkedList
-
-class LogFragment : Fragment(), CommandClient.Handler {
- private val activity: MainActivity? get() = super.getActivity() as MainActivity?
- private var binding: FragmentLogBinding? = null
- private var adapter: Adapter? = null
- private val commandClient =
- CommandClient(lifecycleScope, CommandClient.ConnectionType.Log, this)
- private val logList = LinkedList()
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val binding = FragmentLogBinding.inflate(inflater, container, false)
- this.binding = binding
- onCreate()
- return binding.root
- }
-
- private fun onCreate() {
- val activity = activity ?: return
- val binding = binding ?: return
- binding.logView.layoutManager = LinearLayoutManager(requireContext())
- binding.logView.adapter = Adapter(logList).also { adapter = it }
- updateViews()
- activity.serviceStatus.observe(viewLifecycleOwner) {
- when (it) {
- Status.Stopped -> {
- binding.fab.setImageResource(R.drawable.ic_play_arrow_24)
- binding.fab.show()
- binding.statusText.setText(R.string.status_default)
- }
-
- Status.Starting -> {
- binding.fab.hide()
- binding.statusText.setText(R.string.status_starting)
- }
-
- Status.Started -> {
- commandClient.connect()
- binding.fab.setImageResource(R.drawable.ic_stop_24)
- binding.fab.show()
- binding.fab.isEnabled = true
- binding.statusText.setText(R.string.status_started)
- }
-
- Status.Stopping -> {
- binding.fab.hide()
- binding.statusText.setText(R.string.status_stopping)
- }
-
- else -> {}
- }
- }
- binding.fab.setOnClickListener {
- when (activity.serviceStatus.value) {
- Status.Stopped -> {
- it.isEnabled = false
- activity.startService()
- }
-
- Status.Started -> {
- BoxService.stop()
- }
-
- else -> {}
- }
- }
- }
-
- private fun updateViews(
- removeLen: Int = 0,
- insertLen: Int = 0,
- ) {
- val activity = activity ?: return
- val logAdapter = adapter ?: return
- val binding = binding ?: return
- if (logList.isEmpty()) {
- binding.logView.isVisible = false
- binding.statusText.isVisible = true
- } else if (!binding.logView.isVisible) {
- binding.logView.isVisible = true
- binding.statusText.isVisible = false
- }
- if (insertLen == 0) {
- logAdapter.notifyDataSetChanged()
- if (logList.size > 0) {
- binding.logView.scrollToPosition(logList.size - 1)
- }
- } else {
- if (logList.size == 300) {
- logAdapter.notifyItemRangeRemoved(0, removeLen)
- }
- logAdapter.notifyItemRangeInserted(logList.size - insertLen, insertLen)
- binding.logView.scrollToPosition(logList.size - 1)
- }
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- commandClient.disconnect()
- binding = null
- adapter = null
- }
-
- override fun onConnected() {
- lifecycleScope.launch(Dispatchers.Main) {
- logList.clear()
- updateViews()
- }
- }
-
- override fun clearLogs() {
- lifecycleScope.launch(Dispatchers.Main) {
- logList.clear()
- updateViews()
- }
- }
-
- private var defaultLogLevel = 0
-
- override fun setDefaultLogLevel(level: Int) {
- defaultLogLevel = level
- }
-
- override fun appendLogs(messageList: List) {
- val messageList = messageList.filter { it.level <= defaultLogLevel }
- lifecycleScope.launch(Dispatchers.Main) {
- val messageLen = messageList.size
- val removeLen = logList.size + messageLen - 300
- logList.addAll(messageList.map { it.message })
- if (removeLen > 0) {
- repeat(removeLen) {
- logList.removeFirst()
- }
- }
- updateViews(removeLen, messageLen)
- }
- }
-
- class Adapter(private val logList: LinkedList) :
- RecyclerView.Adapter() {
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): LogViewHolder {
- return LogViewHolder(
- ViewLogTextItemBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- ),
- )
- }
-
- override fun onBindViewHolder(
- holder: LogViewHolder,
- position: Int,
- ) {
- holder.bind(logList.getOrElse(position) { "" })
- }
-
- override fun getItemCount(): Int {
- return logList.size
- }
- }
-
- class LogViewHolder(private val binding: ViewLogTextItemBinding) :
- RecyclerView.ViewHolder(binding.root) {
- fun bind(message: String) {
- binding.text.text = ColorUtils.ansiEscapeToSpannable(binding.root.context, message)
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt
deleted file mode 100644
index ccea0a0..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-package io.nekohasekai.sfa.ui.main
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.RequiresApi
-import androidx.core.view.isGone
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.sfa.Application
-import io.nekohasekai.sfa.BuildConfig
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.constant.EnabledType
-import io.nekohasekai.sfa.database.Settings
-import io.nekohasekai.sfa.databinding.FragmentSettingsBinding
-import io.nekohasekai.sfa.ktx.addTextChangedListener
-import io.nekohasekai.sfa.ktx.launchCustomTab
-import io.nekohasekai.sfa.ktx.setSimpleItems
-import io.nekohasekai.sfa.ktx.text
-import io.nekohasekai.sfa.ui.MainActivity
-import io.nekohasekai.sfa.ui.debug.DebugActivity
-import io.nekohasekai.sfa.ui.profileoverride.ProfileOverrideActivity
-import io.nekohasekai.sfa.vendor.Vendor
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class SettingsFragment : Fragment() {
- private lateinit var binding: FragmentSettingsBinding
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- binding = FragmentSettingsBinding.inflate(inflater, container, false)
- onCreate()
- return binding.root
- }
-
- @RequiresApi(Build.VERSION_CODES.M)
- private val requestIgnoreBatteryOptimizations =
- registerForActivityResult(
- ActivityResultContracts.StartActivityForResult(),
- ) { result ->
- if (Application.powerManager.isIgnoringBatteryOptimizations(Application.application.packageName)) {
- binding.backgroundPermissionCard.isGone = true
- }
- }
-
- @SuppressLint("BatteryLife")
- private fun onCreate() {
- val activity = activity as MainActivity? ?: return
- val binding = binding ?: return
- binding.versionText.text = Libbox.version()
- binding.clearButton.setOnClickListener {
- lifecycleScope.launch(Dispatchers.IO) {
- activity.getExternalFilesDir(null)?.deleteRecursively()
- reloadSettings()
- }
- }
- binding.useComposeUIEnabled.addTextChangedListener {
- lifecycleScope.launch(Dispatchers.IO) {
- val newValue = EnabledType.valueOf(requireContext(), it).boolValue
- Settings.useComposeUI = newValue
- if (newValue) {
- withContext(Dispatchers.Main) {
- // Restart with Compose UI
- val intent = Intent(requireContext(), Class.forName("io.nekohasekai.sfa.compose.ComposeActivity"))
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- startActivity(intent)
- activity.finish()
- }
- }
- }
- }
- binding.checkUpdateEnabled.addTextChangedListener {
- lifecycleScope.launch(Dispatchers.IO) {
- val newValue = EnabledType.valueOf(requireContext(), it).boolValue
- Settings.checkUpdateEnabled = newValue
- }
- }
- binding.checkUpdateButton.setOnClickListener {
- Vendor.checkUpdate(activity, true)
- }
- binding.openPrivacyPolicyButton.setOnClickListener {
- activity.launchCustomTab("https://sing-box.sagernet.org/clients/privacy/")
- }
- binding.disableMemoryLimit.addTextChangedListener {
- lifecycleScope.launch(Dispatchers.IO) {
- val newValue = EnabledType.valueOf(requireContext(), it).boolValue
- Settings.disableMemoryLimit = !newValue
- }
- }
- binding.dynamicNotificationEnabled.addTextChangedListener {
- lifecycleScope.launch(Dispatchers.IO) {
- val newValue = EnabledType.valueOf(requireContext(), it).boolValue
- Settings.dynamicNotification = newValue
- }
- }
-
- binding.dontKillMyAppButton.setOnClickListener {
- it.context.launchCustomTab("https://dontkillmyapp.com/")
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- binding.requestIgnoreBatteryOptimizationsButton.setOnClickListener {
- requestIgnoreBatteryOptimizations.launch(
- Intent(
- android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
- Uri.parse("package:${Application.application.packageName}"),
- ),
- )
- }
- }
- binding.configureOverridesButton.setOnClickListener {
- startActivity(Intent(requireContext(), ProfileOverrideActivity::class.java))
- }
- binding.openDebugButton.setOnClickListener {
- startActivity(Intent(requireContext(), DebugActivity::class.java))
- }
- binding.startSponserButton.setOnClickListener {
- activity.launchCustomTab("https://sekai.icu/sponsors/")
- }
- lifecycleScope.launch(Dispatchers.IO) {
- reloadSettings()
- }
- }
-
- private suspend fun reloadSettings() {
- val activity = activity ?: return
- val binding = binding ?: return
- val dataSize =
- Libbox.formatBytes(
- (activity.getExternalFilesDir(null) ?: activity.filesDir)
- .walkTopDown().filter { it.isFile }.map { it.length() }.sum(),
- )
- val checkUpdateEnabled = Settings.checkUpdateEnabled
- val removeBackgroundPermissionPage =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- Application.powerManager.isIgnoringBatteryOptimizations(Application.application.packageName)
- } else {
- true
- }
- val dynamicNotification = Settings.dynamicNotification
- val useComposeUI = Settings.useComposeUI
- withContext(Dispatchers.Main) {
- binding.dataSizeText.text = dataSize
- binding.checkUpdateEnabled.text =
- EnabledType.from(checkUpdateEnabled).getString(requireContext())
- binding.checkUpdateEnabled.setSimpleItems(R.array.enabled)
- binding.disableMemoryLimit.text =
- EnabledType.from(!Settings.disableMemoryLimit).getString(requireContext())
- binding.disableMemoryLimit.setSimpleItems(R.array.enabled)
- binding.backgroundPermissionCard.isGone = removeBackgroundPermissionPage
- binding.dynamicNotificationEnabled.text =
- EnabledType.from(dynamicNotification).getString(requireContext())
- binding.dynamicNotificationEnabled.setSimpleItems(R.array.enabled)
- binding.experimentalFeaturesCard.isVisible = BuildConfig.DEBUG
- binding.useComposeUIEnabled.text =
- EnabledType.from(useComposeUI).getString(requireContext())
- binding.useComposeUIEnabled.setSimpleItems(R.array.enabled)
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt
deleted file mode 100644
index a2e1d42..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt
+++ /dev/null
@@ -1,203 +0,0 @@
-package io.nekohasekai.sfa.ui.profile
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.View
-import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.bg.UpdateProfileWork
-import io.nekohasekai.sfa.compose.util.RelativeTimeFormatter
-import io.nekohasekai.sfa.constant.EnabledType
-import io.nekohasekai.sfa.database.Profile
-import io.nekohasekai.sfa.database.ProfileManager
-import io.nekohasekai.sfa.database.Settings
-import io.nekohasekai.sfa.database.TypedProfile
-import io.nekohasekai.sfa.databinding.ActivityEditProfileBinding
-import io.nekohasekai.sfa.ktx.addTextChangedListener
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.setSimpleItems
-import io.nekohasekai.sfa.ktx.text
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-import io.nekohasekai.sfa.utils.HTTPClient
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.util.Date
-
-class EditProfileActivity : AbstractActivity() {
- private lateinit var profile: Profile
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTitle(R.string.title_edit_profile)
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- loadProfile()
- }.onFailure {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(it)
- .setPositiveButton(R.string.ok) { _, _ -> finish() }
- .show()
- }
- }
- }
- }
-
- private suspend fun loadProfile() {
- delay(200L)
- val profileId = intent.getLongExtra("profile_id", -1L)
- if (profileId == -1L) error("invalid arguments")
- profile = ProfileManager.get(profileId) ?: error("invalid arguments")
- withContext(Dispatchers.Main) {
- binding.name.text = profile.name
- binding.name.addTextChangedListener {
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- profile.name = it
- ProfileManager.update(profile)
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(e).show()
- }
- }
- }
- }
- binding.type.text = profile.typed.type.getString(this@EditProfileActivity)
- binding.editButton.setOnClickListener {
- startActivity(
- Intent(
- this@EditProfileActivity,
- EditProfileContentActivity::class.java,
- ).apply {
- putExtra("profile_id", profile.id)
- },
- )
- }
- when (profile.typed.type) {
- TypedProfile.Type.Local -> {
- binding.editButton.isVisible = true
- binding.remoteFields.isVisible = false
- }
-
- TypedProfile.Type.Remote -> {
- binding.editButton.isVisible = false
- binding.remoteFields.isVisible = true
- binding.remoteURL.text = profile.typed.remoteURL
- binding.lastUpdated.text =
- RelativeTimeFormatter.format(this@EditProfileActivity, profile.typed.lastUpdated)
- binding.autoUpdate.text =
- EnabledType.from(profile.typed.autoUpdate)
- .getString(this@EditProfileActivity)
- binding.autoUpdate.setSimpleItems(R.array.enabled)
- binding.autoUpdateInterval.isVisible = profile.typed.autoUpdate
- binding.autoUpdateInterval.text = profile.typed.autoUpdateInterval.toString()
- }
- }
- binding.remoteURL.addTextChangedListener(this@EditProfileActivity::updateRemoteURL)
- binding.autoUpdate.addTextChangedListener(this@EditProfileActivity::updateAutoUpdate)
- binding.autoUpdateInterval.addTextChangedListener(this@EditProfileActivity::updateAutoUpdateInterval)
- binding.updateButton.setOnClickListener(this@EditProfileActivity::updateProfile)
- binding.profileLayout.isVisible = true
- binding.progressView.isVisible = false
- }
- }
-
- private fun updateRemoteURL(newValue: String) {
- profile.typed.remoteURL = newValue
- updateProfile()
- }
-
- private fun updateAutoUpdate(newValue: String) {
- val boolValue = EnabledType.valueOf(this, newValue).boolValue
- if (profile.typed.autoUpdate == boolValue) {
- return
- }
- binding.autoUpdateInterval.isVisible = boolValue
- profile.typed.autoUpdate = boolValue
- if (boolValue) {
- lifecycleScope.launch(Dispatchers.IO) {
- UpdateProfileWork.reconfigureUpdater()
- }
- }
- updateProfile()
- }
-
- private fun updateAutoUpdateInterval(newValue: String) {
- if (newValue.isBlank()) {
- binding.autoUpdateInterval.error = getString(R.string.profile_input_required)
- return
- }
- val intValue =
- try {
- newValue.toInt()
- } catch (e: Exception) {
- binding.autoUpdateInterval.error = e.localizedMessage
- return
- }
- if (intValue < 15) {
- binding.autoUpdateInterval.error =
- getString(R.string.profile_auto_update_interval_minimum_hint)
- return
- }
- binding.autoUpdateInterval.error = null
- profile.typed.autoUpdateInterval = intValue
- updateProfile()
- }
-
- private fun updateProfile() {
- binding.progressView.isVisible = true
- lifecycleScope.launch(Dispatchers.IO) {
- delay(200L)
- try {
- ProfileManager.update(profile)
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(e).show()
- }
- }
- withContext(Dispatchers.Main) {
- binding.progressView.isVisible = false
- }
- }
- }
-
- @Suppress("UNUSED_PARAMETER")
- private fun updateProfile(view: View) {
- binding.progressView.isVisible = true
- lifecycleScope.launch(Dispatchers.IO) {
- var selectedProfileUpdated = false
- try {
- val content = HTTPClient().use { it.getString(profile.typed.remoteURL) }
- Libbox.checkConfig(content)
- val file = File(profile.typed.path)
- if (file.readText() != content) {
- File(profile.typed.path).writeText(content)
- if (profile.id == Settings.selectedProfile) {
- selectedProfileUpdated = true
- }
- }
- profile.typed.lastUpdated = Date()
- ProfileManager.update(profile)
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(e).show()
- }
- }
- withContext(Dispatchers.Main) {
- binding.lastUpdated.text =
- RelativeTimeFormatter.format(this@EditProfileActivity, profile.typed.lastUpdated)
- binding.progressView.isVisible = false
- }
- if (selectedProfileUpdated) {
- runCatching {
- Libbox.newStandaloneCommandClient().serviceReload()
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt
deleted file mode 100644
index 28c6f08..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-package io.nekohasekai.sfa.ui.profile
-
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.core.view.isInvisible
-import androidx.core.view.isVisible
-import androidx.core.widget.addTextChangedListener
-import androidx.lifecycle.lifecycleScope
-import com.blacksquircle.ui.language.json.JsonLanguage
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.database.Profile
-import io.nekohasekai.sfa.database.ProfileManager
-import io.nekohasekai.sfa.databinding.ActivityEditProfileContentBinding
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.unwrap
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-
-class EditProfileContentActivity : AbstractActivity() {
- private var profile: Profile? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTitle(R.string.title_edit_configuration)
- binding.editor.language = JsonLanguage()
- loadConfiguration()
- }
-
- private fun loadConfiguration() {
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- loadConfiguration0()
- }.onFailure {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(it)
- .setPositiveButton(R.string.ok) { _, _ -> finish() }
- .show()
- }
- }
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.edit_configutation_menu, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.action_undo -> {
- if (binding.editor.canUndo()) binding.editor.undo()
- return true
- }
-
- R.id.action_redo -> {
- if (binding.editor.canRedo()) binding.editor.redo()
- return true
- }
-
- R.id.action_check -> {
- binding.progressView.isVisible = true
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- Libbox.checkConfig(binding.editor.text.toString())
- }.onFailure {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(it).show()
- }
- }
- withContext(Dispatchers.Main) {
- delay(200)
- binding.progressView.isInvisible = true
- }
- }
- return true
- }
-
- R.id.action_format -> {
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- val content = Libbox.formatConfig(binding.editor.text.toString()).unwrap
- if (binding.editor.text.toString() != content) {
- withContext(Dispatchers.Main) {
- binding.editor.setTextContent(content)
- }
- }
- }.onFailure {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(it).show()
- }
- }
- }
- return true
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- private suspend fun loadConfiguration0() {
- delay(200L)
-
- val profileId = intent.getLongExtra("profile_id", -1L)
- if (profileId == -1L) error("invalid arguments")
- val profile = ProfileManager.get(profileId) ?: error("invalid arguments")
- this.profile = profile
- val content = File(profile.typed.path).readText()
- withContext(Dispatchers.Main) {
- binding.editor.setTextContent(content)
- binding.editor.addTextChangedListener {
- binding.progressView.isVisible = true
- val newContent = it.toString()
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- File(profile.typed.path).writeText(newContent)
- }.onFailure {
- withContext(Dispatchers.Main) {
- errorDialogBuilder(it)
- .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
- .show()
- }
- }
- withContext(Dispatchers.Main) {
- delay(200L)
- binding.progressView.isInvisible = true
- }
- }
- }
- binding.progressView.isInvisible = true
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt
deleted file mode 100644
index 62051f6..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt
+++ /dev/null
@@ -1,221 +0,0 @@
-package io.nekohasekai.sfa.ui.profile
-
-import android.content.Context
-import android.net.Uri
-import android.os.Bundle
-import android.view.View
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.StringRes
-import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
-import io.nekohasekai.libbox.Libbox
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.constant.EnabledType
-import io.nekohasekai.sfa.database.Profile
-import io.nekohasekai.sfa.database.ProfileManager
-import io.nekohasekai.sfa.database.TypedProfile
-import io.nekohasekai.sfa.databinding.ActivityAddProfileBinding
-import io.nekohasekai.sfa.ktx.addTextChangedListener
-import io.nekohasekai.sfa.ktx.errorDialogBuilder
-import io.nekohasekai.sfa.ktx.removeErrorIfNotEmpty
-import io.nekohasekai.sfa.ktx.showErrorIfEmpty
-import io.nekohasekai.sfa.ktx.startFilesForResult
-import io.nekohasekai.sfa.ktx.text
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-import io.nekohasekai.sfa.utils.HTTPClient
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.io.InputStream
-import java.util.Date
-
-class NewProfileActivity : AbstractActivity() {
- enum class FileSource(
- @StringRes val formattedRes: Int,
- ) {
- CreateNew(R.string.profile_source_create_new),
- Import(R.string.profile_source_import),
- ;
-
- fun formatted(context: Context): String {
- return context.getString(formattedRes)
- }
- }
-
- private val importFile =
- registerForActivityResult(ActivityResultContracts.GetContent()) { fileURI ->
- if (fileURI != null) {
- binding.sourceURL.editText?.setText(fileURI.toString())
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTitle(R.string.title_new_profile)
-
- intent.getStringExtra("importName")?.also { importName ->
- intent.getStringExtra("importURL")?.also { importURL ->
- binding.name.editText?.setText(importName)
- binding.type.text = TypedProfile.Type.Remote.getString(this)
- binding.remoteURL.editText?.setText(importURL)
- binding.localFields.isVisible = false
- binding.remoteFields.isVisible = true
- binding.autoUpdateInterval.text = "60"
- }
- }
-
- binding.name.removeErrorIfNotEmpty()
- binding.type.addTextChangedListener {
- when (it) {
- TypedProfile.Type.Local.getString(this) -> {
- binding.localFields.isVisible = true
- binding.remoteFields.isVisible = false
- }
-
- TypedProfile.Type.Remote.getString(this) -> {
- binding.localFields.isVisible = false
- binding.remoteFields.isVisible = true
- if (binding.autoUpdateInterval.text.toIntOrNull() == null) {
- binding.autoUpdateInterval.text = "60"
- }
- }
- }
- }
- binding.fileSourceMenu.addTextChangedListener {
- when (it) {
- FileSource.CreateNew.formatted(this) -> {
- binding.importFileButton.isVisible = false
- binding.sourceURL.isVisible = false
- }
-
- FileSource.Import.formatted(this) -> {
- binding.importFileButton.isVisible = true
- binding.sourceURL.isVisible = true
- }
- }
- }
- binding.importFileButton.setOnClickListener {
- startFilesForResult(importFile, "application/json")
- }
- binding.createProfile.setOnClickListener(this::createProfile)
- binding.autoUpdateInterval.addTextChangedListener(this::updateAutoUpdateInterval)
- }
-
- private fun createProfile(
- @Suppress("UNUSED_PARAMETER") view: View,
- ) {
- if (binding.name.showErrorIfEmpty()) {
- return
- }
- when (binding.type.text) {
- TypedProfile.Type.Local.getString(this) -> {
- when (binding.fileSourceMenu.text) {
- FileSource.Import.formatted(this) -> {
- if (binding.sourceURL.showErrorIfEmpty()) {
- return
- }
- }
- }
- }
-
- TypedProfile.Type.Remote.getString(this) -> {
- if (binding.remoteURL.showErrorIfEmpty()) {
- return
- }
- }
- }
- binding.progressView.isVisible = true
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- createProfile0()
- }.onFailure { e ->
- withContext(Dispatchers.Main) {
- binding.progressView.isVisible = false
- errorDialogBuilder(e).show()
- }
- }
- }
- }
-
- private suspend fun createProfile0() {
- val typedProfile = TypedProfile()
- val profile = Profile(name = binding.name.text, typed = typedProfile)
- profile.userOrder = ProfileManager.nextOrder()
- val fileID = ProfileManager.nextFileID()
- val configDirectory = File(filesDir, "configs").also { it.mkdirs() }
- val configFile = File(configDirectory, "$fileID.json")
- typedProfile.path = configFile.path
-
- when (binding.type.text) {
- TypedProfile.Type.Local.getString(this) -> {
- typedProfile.type = TypedProfile.Type.Local
-
- when (binding.fileSourceMenu.text) {
- FileSource.CreateNew.formatted(this) -> {
- configFile.writeText("{}")
- }
-
- FileSource.Import.formatted(this) -> {
- val sourceURL = binding.sourceURL.text
- val content =
- if (sourceURL.startsWith("content://")) {
- val inputStream =
- contentResolver.openInputStream(Uri.parse(sourceURL)) as InputStream
- inputStream.use { it.bufferedReader().readText() }
- } else if (sourceURL.startsWith("file://")) {
- File(sourceURL).readText()
- } else if (sourceURL.startsWith("http://") || sourceURL.startsWith("https://")) {
- HTTPClient().use { it.getString(sourceURL) }
- } else {
- error("unsupported source: $sourceURL")
- }
- Libbox.checkConfig(content)
- configFile.writeText(content)
- }
- }
- }
-
- TypedProfile.Type.Remote.getString(this) -> {
- typedProfile.type = TypedProfile.Type.Remote
- val remoteURL = binding.remoteURL.text
- val content = HTTPClient().use { it.getString(remoteURL) }
- Libbox.checkConfig(content)
- configFile.writeText(content)
- typedProfile.remoteURL = remoteURL
- typedProfile.lastUpdated = Date()
- typedProfile.autoUpdate =
- EnabledType.valueOf(this, binding.autoUpdate.text).boolValue
- binding.autoUpdateInterval.text.toIntOrNull()?.also {
- typedProfile.autoUpdateInterval = it
- }
- }
- }
- ProfileManager.create(profile)
- withContext(Dispatchers.Main) {
- binding.progressView.isVisible = false
- finish()
- }
- }
-
- private fun updateAutoUpdateInterval(newValue: String) {
- if (newValue.isBlank()) {
- binding.autoUpdateInterval.error = getString(R.string.profile_input_required)
- return
- }
- val intValue =
- try {
- newValue.toInt()
- } catch (e: Exception) {
- binding.autoUpdateInterval.error = e.localizedMessage
- return
- }
- if (intValue < 15) {
- binding.autoUpdateInterval.error =
- getString(R.string.profile_auto_update_interval_minimum_hint)
- return
- }
- binding.autoUpdateInterval.error = null
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt
deleted file mode 100644
index dc7d43b..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package io.nekohasekai.sfa.ui.profileoverride
-
-import android.content.Intent
-import android.os.Bundle
-import io.nekohasekai.sfa.R
-import io.nekohasekai.sfa.database.Settings
-import io.nekohasekai.sfa.databinding.ActivityConfigOverrideBinding
-import io.nekohasekai.sfa.ui.shared.AbstractActivity
-
-class ProfileOverrideActivity :
- AbstractActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setTitle(R.string.profile_override)
- binding.switchPerAppProxy.isChecked = Settings.perAppProxyEnabled
- binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
- Settings.perAppProxyEnabled = isChecked
- binding.configureAppListButton.isEnabled = isChecked
- }
- binding.configureAppListButton.isEnabled = binding.switchPerAppProxy.isChecked
-
- binding.configureAppListButton.setOnClickListener {
- startActivity(Intent(this, PerAppProxyActivity::class.java))
- }
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt
index d83c2e2..519cc55 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt
@@ -14,7 +14,6 @@ import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.color.DynamicColors
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.ktx.getAttrColor
-import io.nekohasekai.sfa.ui.MainActivity
import io.nekohasekai.sfa.utils.MIUIUtils
import java.lang.reflect.ParameterizedType
@@ -56,17 +55,15 @@ abstract class AbstractActivity : AppCompatActivity() {
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
- if (this !is MainActivity) {
- supportActionBar?.setHomeAsUpIndicator(
- AppCompatResources.getDrawable(
- this@AbstractActivity,
- R.drawable.ic_arrow_back_24,
- )!!.apply {
- setTint(getAttrColor(com.google.android.material.R.attr.colorOnSurface))
- },
- )
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
+ supportActionBar?.setHomeAsUpIndicator(
+ AppCompatResources.getDrawable(
+ this@AbstractActivity,
+ R.drawable.ic_arrow_back_24,
+ )!!.apply {
+ setTint(getAttrColor(com.google.android.material.R.attr.colorOnSurface))
+ },
+ )
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/shared/QRCodeDialog.kt b/app/src/main/java/io/nekohasekai/sfa/ui/shared/QRCodeDialog.kt
deleted file mode 100644
index 4c01676..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/shared/QRCodeDialog.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.nekohasekai.sfa.ui.shared
-
-import android.graphics.Bitmap
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import io.nekohasekai.sfa.databinding.FragmentQrcodeDialogBinding
-
-class QRCodeDialog(private val bitmap: Bitmap) :
- BottomSheetDialogFragment() {
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val binding = FragmentQrcodeDialogBinding.inflate(inflater, container, false)
- val behavior = BottomSheetBehavior.from(binding.qrcodeLayout)
- behavior.state = BottomSheetBehavior.STATE_EXPANDED
- binding.qrCode.setImageBitmap(bitmap)
- return binding.root
- }
-}
diff --git a/app/src/main/res/layout/activity_add_profile.xml b/app/src/main/res/layout/activity_add_profile.xml
deleted file mode 100644
index 4657c24..0000000
--- a/app/src/main/res/layout/activity_add_profile.xml
+++ /dev/null
@@ -1,188 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_config_override.xml b/app/src/main/res/layout/activity_config_override.xml
deleted file mode 100644
index 33448b0..0000000
--- a/app/src/main/res/layout/activity_config_override.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml
deleted file mode 100644
index cf7dfb2..0000000
--- a/app/src/main/res/layout/activity_debug.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml
deleted file mode 100644
index 6e70138..0000000
--- a/app/src/main/res/layout/activity_edit_profile.xml
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_profile_content.xml b/app/src/main/res/layout/activity_edit_profile_content.xml
deleted file mode 100644
index 6b0eaef..0000000
--- a/app/src/main/res/layout/activity_edit_profile_content.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index fab07a4..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_vpn_scan.xml b/app/src/main/res/layout/activity_vpn_scan.xml
deleted file mode 100644
index 5b3d7f4..0000000
--- a/app/src/main/res/layout/activity_vpn_scan.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_progress.xml b/app/src/main/res/layout/dialog_progress.xml
deleted file mode 100644
index 97cc09b..0000000
--- a/app/src/main/res/layout/dialog_progress.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_configuration.xml b/app/src/main/res/layout/fragment_configuration.xml
deleted file mode 100644
index af149c5..0000000
--- a/app/src/main/res/layout/fragment_configuration.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml
deleted file mode 100644
index eb15c42..0000000
--- a/app/src/main/res/layout/fragment_dashboard.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dashboard_groups.xml b/app/src/main/res/layout/fragment_dashboard_groups.xml
deleted file mode 100644
index 29e9935..0000000
--- a/app/src/main/res/layout/fragment_dashboard_groups.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dashboard_overview.xml b/app/src/main/res/layout/fragment_dashboard_overview.xml
deleted file mode 100644
index 72908df..0000000
--- a/app/src/main/res/layout/fragment_dashboard_overview.xml
+++ /dev/null
@@ -1,527 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_log.xml b/app/src/main/res/layout/fragment_log.xml
deleted file mode 100644
index 2efc797..0000000
--- a/app/src/main/res/layout/fragment_log.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_qrcode_dialog.xml b/app/src/main/res/layout/fragment_qrcode_dialog.xml
deleted file mode 100644
index 6278847..0000000
--- a/app/src/main/res/layout/fragment_qrcode_dialog.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
deleted file mode 100644
index 711e9f2..0000000
--- a/app/src/main/res/layout/fragment_settings.xml
+++ /dev/null
@@ -1,459 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/sheet_add_profile.xml b/app/src/main/res/layout/sheet_add_profile.xml
deleted file mode 100644
index beda7c1..0000000
--- a/app/src/main/res/layout/sheet_add_profile.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_app_list_item0.xml b/app/src/main/res/layout/view_app_list_item0.xml
deleted file mode 100644
index 9ef9dc5..0000000
--- a/app/src/main/res/layout/view_app_list_item0.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/view_clash_mode_button.xml b/app/src/main/res/layout/view_clash_mode_button.xml
deleted file mode 100644
index 7ca8442..0000000
--- a/app/src/main/res/layout/view_clash_mode_button.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_configutation_item.xml b/app/src/main/res/layout/view_configutation_item.xml
deleted file mode 100644
index 8e8ae64..0000000
--- a/app/src/main/res/layout/view_configutation_item.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_dashboard_group.xml b/app/src/main/res/layout/view_dashboard_group.xml
deleted file mode 100644
index a361e1a..0000000
--- a/app/src/main/res/layout/view_dashboard_group.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_dashboard_group_item.xml b/app/src/main/res/layout/view_dashboard_group_item.xml
deleted file mode 100644
index 9b8689b..0000000
--- a/app/src/main/res/layout/view_dashboard_group_item.xml
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_log_text_item.xml b/app/src/main/res/layout/view_log_text_item.xml
deleted file mode 100644
index 856005a..0000000
--- a/app/src/main/res/layout/view_log_text_item.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_prefenence_screen.xml b/app/src/main/res/layout/view_prefenence_screen.xml
deleted file mode 100644
index 090b4e9..0000000
--- a/app/src/main/res/layout/view_prefenence_screen.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/view_profile_item.xml b/app/src/main/res/layout/view_profile_item.xml
deleted file mode 100644
index b2069fd..0000000
--- a/app/src/main/res/layout/view_profile_item.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_vpn_app_item.xml b/app/src/main/res/layout/view_vpn_app_item.xml
deleted file mode 100644
index bc5c994..0000000
--- a/app/src/main/res/layout/view_vpn_app_item.xml
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml
deleted file mode 100644
index 392d555..0000000
--- a/app/src/main/res/menu/bottom_nav_menu.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/edit_configutation_menu.xml b/app/src/main/res/menu/edit_configutation_menu.xml
deleted file mode 100644
index 7f49d78..0000000
--- a/app/src/main/res/menu/edit_configutation_menu.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/profile_menu.xml b/app/src/main/res/menu/profile_menu.xml
deleted file mode 100644
index 70fa4d7..0000000
--- a/app/src/main/res/menu/profile_menu.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
deleted file mode 100644
index cc64ba4..0000000
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file