Remove legacy View-based UI
Keep only Compose UI, removing old View-based Activities, Fragments, and layouts. QRScanActivity and PerAppProxyActivity are retained as they are still used by Compose UI.
This commit is contained in:
@@ -95,21 +95,6 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".compose.ComposeActivity"
|
||||
android:exported="false"
|
||||
@@ -122,21 +107,6 @@
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.ShortcutActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/quick_toggle"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/AppTheme.Translucent">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.profile.NewProfileActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.compose.NewProfileComposeActivity"
|
||||
android:exported="false"
|
||||
@@ -149,25 +119,9 @@
|
||||
android:name="io.nekohasekai.sfa.compose.GroupsComposeActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.profile.EditProfileActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.profile.EditProfileContentActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.profileoverride.ProfileOverrideActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.debug.DebugActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.debug.VPNScanActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="io.nekohasekai.sfa.ui.profile.QRScanActivity"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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<ActivityMainBinding>(),
|
||||
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<Intent, Boolean>() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<KeyguardManager>()
|
||||
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<ShortcutManager>()?.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()
|
||||
}
|
||||
}
|
||||
@@ -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<OutboundGroup>) {
|
||||
val adapter = adapter ?: return
|
||||
activity?.runOnUiThread {
|
||||
updateDisplayed(newGroups.isNotEmpty())
|
||||
adapter.setGroups(newGroups.map(::Group))
|
||||
}
|
||||
}
|
||||
|
||||
private class Adapter : RecyclerView.Adapter<GroupView>() {
|
||||
private lateinit var groups: MutableList<Group>
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setGroups(newGroups: List<Group>) {
|
||||
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<GroupItem>
|
||||
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<GroupItem> = mutableListOf(),
|
||||
) :
|
||||
RecyclerView.Adapter<ItemGroupView>() {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setItems(newItems: List<GroupItem>) {
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
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<String>,
|
||||
var selected: String,
|
||||
) :
|
||||
RecyclerView.Adapter<ClashModeItemView>() {
|
||||
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<Holder>() {
|
||||
internal var items: MutableList<Profile> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityDebugBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_debug)
|
||||
binding.scanVPNButton.setOnClickListener {
|
||||
startActivity(Intent(this, VPNScanActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityVpnScanBinding>() {
|
||||
private var adapter: Adapter? = null
|
||||
private val appInfoList = mutableListOf<AppInfo>()
|
||||
|
||||
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<Holder>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<Holder>() {
|
||||
internal var items: MutableList<Profile> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<out Fragment>,
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>()
|
||||
|
||||
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<LogEntry>) {
|
||||
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<String>) :
|
||||
RecyclerView.Adapter<LogViewHolder>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityEditProfileBinding>() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityEditProfileContentBinding>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityAddProfileBinding>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityConfigOverrideBinding>() {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Binding : ViewBinding> : 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/view_appbar" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_name">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/type"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_type">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/profile_type_local"
|
||||
app:simpleItems="@array/profile_type" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/localFields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/fileSourceMenu"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_source">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/profile_source_create_new"
|
||||
app:simpleItems="@array/profile_source" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/importFileButton"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/profile_import_file"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
</Button>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/sourceURL"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_url"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/remoteFields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/remoteURL"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_url">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/autoUpdate"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_auto_update">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/enabled"
|
||||
app:simpleItems="@array/enabled" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/autoUpdateInterval"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_auto_update_interval">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/createProfile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/profile_create">
|
||||
|
||||
</Button>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/view_appbar" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/perAppProxyCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchPerAppProxy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/per_app_proxy"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/per_app_proxy_description" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/configureAppListButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profile_override_configure" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/view_appbar" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_scan_vpn"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/message_scan_vpn" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/scanVPNButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/per_app_proxy_scan" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,160 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/view_appbar" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/profileLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_name">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:maxLines="1"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/type"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:enabled="false"
|
||||
android:hint="@string/profile_type">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/profile_type_local"
|
||||
app:simpleItems="@array/profile_type" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/profile_edit_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/remoteFields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/remoteURL"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_url">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:maxLines="1"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/autoUpdate"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_auto_update">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/disabled"
|
||||
app:simpleItems="@array/enabled" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/autoUpdateInterval"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/profile_auto_update_interval">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/lastUpdated"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:enabled="false"
|
||||
android:hint="@string/profile_last_updated">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/updateButton"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/profile_update" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorSurfaceContainer"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="?attr/toolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.blacksquircle.ui.editorkit.widget.TextProcessor
|
||||
android:id="@+id/editor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:gravity="top|start"
|
||||
android:padding="8dp"
|
||||
android:textSize="14sp"
|
||||
android:typeface="monospace"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,73 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorSurfaceContainer"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="?attr/toolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/dashboardTabContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/dashboardTabLayout"
|
||||
style="@style/Widget.Material3.TabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
app:tabIndicatorFullWidth="false"
|
||||
app:tabUnboundedRipple="true">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_overview" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_groups" />
|
||||
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment_activity_my"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:defaultNavHost="true" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorSurfaceContainer"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="?attr/toolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/scanVPNProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/scanVPNResult"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.main.DashboardFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/profile_empty"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profileList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:icon="@drawable/ic_note_add_24" />
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.main.DashboardFragment">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/dashboardPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:saveEnabled="false" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:srcCompat="@drawable/ic_play_arrow_24" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/status_default"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/container"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp"
|
||||
android:clipChildren="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,527 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/profile_empty"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/statusContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/status"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall">
|
||||
|
||||
</TextView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/memory" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/memoryText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/goroutines" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/goroutinesText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<View
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/title_connections"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall">
|
||||
|
||||
</TextView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/connections_in" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/inboundConnectionsText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/connections_out" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outboundConnectionsText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/trafficContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/traffic"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall">
|
||||
|
||||
</TextView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/status_uplink" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uplinkText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/status_downlink" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downlinkText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<View
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/status_traffic_total"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall">
|
||||
|
||||
</TextView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/status_uplink" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uplinkTotalText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/status_downlink" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downlinkTotalText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/clashModeCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:text="@string/mode"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall">
|
||||
|
||||
</TextView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/clashModeList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
tools:listitem="@layout/view_clash_mode_button" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/systemProxyCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/systemProxySwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/http_proxy"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/profileCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profile"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall">
|
||||
|
||||
</TextView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profileList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="16dp"
|
||||
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/view_profile_item" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.main.LogFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/status_default"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/logView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:srcCompat="@drawable/ic_play_arrow_24" />
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/qrcode_layout"
|
||||
style="@style/Widget.Material3.BottomSheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
||||
android:id="@+id/drag_handle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageView
|
||||
android:paddingTop="16dp"
|
||||
android:id="@+id/qr_code"
|
||||
android:scaleType="centerCrop"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,459 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_app_settings"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/dynamicNotificationEnabled"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/dynamic_notification">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/disabled"
|
||||
app:simpleItems="@array/enabled" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/checkUpdateEnabled"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/check_update_automatic">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/disabled"
|
||||
app:simpleItems="@array/enabled" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/checkUpdateButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/check_update" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/openPrivacyPolicyButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/privacy_policy" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/statusCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/core"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceTitleSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/core_version" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/versionText"
|
||||
style="?attr/textAppearanceBodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="@string/loading" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceTitleSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/core_data_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dataSizeText"
|
||||
style="?attr/textAppearanceBodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/loading" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/disableMemoryLimit"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/memory_limit">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/enabled"
|
||||
app:simpleItems="@array/enabled" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/clearButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_clear_working_directory"
|
||||
android:textColor="#E91E63" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/backgroundPermissionCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/background_permission"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/background_permission_description" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/dontKillMyAppButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/read_more" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/requestIgnoreBatteryOptimizationsButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/request_background_permission" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/configOverrideCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profile_override"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/profile_override_description" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/configureOverridesButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profile_override_configure" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/experimentalFeaturesCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_experimental_features"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/message_experimental_features" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/useComposeUIEnabled"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/use_compose_ui">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/disabled" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_debug"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/message_debug_tools" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/openDebugButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_open" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sponsor"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge">
|
||||
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/sponsor_message" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/startSponserButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_start" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -1,95 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
||||
android:id="@+id/drag_handle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/import_from_file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:padding="18dp"
|
||||
app:tint="?colorControlNormal"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_baseline_file_open_24" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:text="@string/profile_add_import_file"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</TextView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/scan_qr_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:padding="18dp"
|
||||
app:tint="?colorControlNormal"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_qr_code_2_24" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:text="@string/profile_add_scan_qr_code"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</TextView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/create_manually"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:padding="18dp"
|
||||
app:tint="?colorControlNormal"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_baseline_create_new_folder_24" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:text="@string/profile_add_create_manually"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</TextView>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/app_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:contentDescription="@string/content_description_app_icon"
|
||||
tools:src="@drawable/ic_launcher_foreground" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/application_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?colorOnSurface"
|
||||
android:textSize="16sp"
|
||||
tools:text="sing-box" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/package_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="io.nekohasekai.sfa" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/selected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/clashModeButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/bg_rounded_rectangle_active">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clashModeButtonText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="8dp"
|
||||
android:textColor="?android:textColorPrimaryInverse"
|
||||
tools:text="Direct" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?textAppearanceTitleMedium"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="Profile name" />
|
||||
|
||||
<TextView
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/profile_last_updated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="?textAppearanceBodySmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="Last updated at now" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="top|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/moreButton"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_more_vert_24" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/groupName"
|
||||
style="?attr/textAppearanceTitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Group Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/groupType"
|
||||
style="?attr/textAppearanceBodyLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="Group Name" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/urlTestButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:backgroundTint="?colorSurfaceContainerLow"
|
||||
android:contentDescription="@string/urltest"
|
||||
android:src="@drawable/ic_electric_bolt_24"
|
||||
app:tint="?colorControlNormal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/expandButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:backgroundTint="?colorSurfaceContainerLow"
|
||||
android:contentDescription="@string/expand"
|
||||
android:src="@drawable/ic_expand_less_24"
|
||||
app:tint="?colorControlNormal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/group_selected"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/group_selected_title">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/itemList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:listitem="@layout/view_dashboard_group_item" />
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/item_card"
|
||||
style="?materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="2dp"
|
||||
app:cardBackgroundColor="?colorSurfaceContainer"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/selected_view"
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
tools:text="Name" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Type" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="Status" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:checkable="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="14dp" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/recycler_view"
|
||||
style="?attr/preferenceFragmentListStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp" />
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/profile_selected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="Profile name" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,186 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:layout_marginTop="16dp"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/appIcon"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
tools:src="@android:drawable/sym_def_app_icon" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
tools:text="App Name" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/packageName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
tools:text="com.myapp" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/vpn_app_type" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appTypeText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/vpn_core_type" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/coreTypeText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/corePathLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/vpn_core_path" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/corePathText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/goVersionLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/vpn_golang_version" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/goVersionText"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textColor="?android:colorForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_dashboard"
|
||||
android:icon="@drawable/ic_dashboard_black_24dp"
|
||||
android:title="@string/title_dashboard" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_log"
|
||||
android:icon="@drawable/ic_message_24"
|
||||
android:title="@string/title_log" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_configuration"
|
||||
android:icon="@drawable/ic_insert_drive_file_24"
|
||||
android:title="@string/title_configuration" />
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_settings"
|
||||
android:icon="@drawable/ic_settings_24"
|
||||
android:title="@string/title_settings" />
|
||||
</menu>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_undo"
|
||||
android:title="@string/menu_undo" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_redo"
|
||||
android:title="@string/menu_redo" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_check"
|
||||
android:title="@string/profile_check" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_format"
|
||||
android:title="@string/menu_format" />
|
||||
|
||||
</menu>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:title="@string/menu_share"
|
||||
android:icon="@drawable/ic_ios_share_24"
|
||||
app:iconTintMode="src_in"
|
||||
app:iconTint="?colorPrimary" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share_url"
|
||||
android:icon="@drawable/ic_qr_code_2_24"
|
||||
android:title="@string/profile_share_url"
|
||||
app:iconTint="?colorPrimary"
|
||||
app:iconTintMode="src_in" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/menu_delete"
|
||||
android:icon="@drawable/ic_delete_24"
|
||||
app:iconTintMode="src_in"
|
||||
app:iconTint="?colorPrimary" />
|
||||
|
||||
</menu>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/mobile_navigation"
|
||||
app:startDestination="@+id/navigation_dashboard">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_dashboard"
|
||||
android:name="io.nekohasekai.sfa.ui.main.DashboardFragment"
|
||||
android:label="@string/title_dashboard"
|
||||
tools:layout="@layout/fragment_dashboard" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_log"
|
||||
android:name="io.nekohasekai.sfa.ui.main.LogFragment"
|
||||
android:label="@string/title_log"
|
||||
tools:layout="@layout/fragment_log" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_configuration"
|
||||
android:name="io.nekohasekai.sfa.ui.main.ConfigurationFragment"
|
||||
android:label="@string/title_configuration"
|
||||
tools:layout="@layout/fragment_configuration" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_settings"
|
||||
android:name="io.nekohasekai.sfa.ui.main.SettingsFragment"
|
||||
android:label="@string/title_settings"
|
||||
tools:layout="@layout/fragment_settings" />
|
||||
|
||||
|
||||
</navigation>
|
||||
Reference in New Issue
Block a user