Improve per proxy app selector

This commit is contained in:
世界 2024-03-14 23:51:19 +08:00
parent 3b72cddd2a
commit f26458ba68
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
35 changed files with 1559 additions and 512 deletions

View file

@ -37,7 +37,6 @@
android:name=".ui.MainActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleTask">
<meta-data
@ -120,6 +119,9 @@
<activity
android:name="io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity"
android:exported="false" />
<activity
android:name="io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity0"
android:exported="false" />
<activity
android:name="io.nekohasekai.sfa.ui.debug.DebugActivity"
android:exported="false" />

View file

@ -2,6 +2,7 @@ package io.nekohasekai.sfa
import android.app.Application
import android.app.NotificationManager
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@ -48,6 +49,7 @@ class Application : Application() {
val powerManager by lazy { application.getSystemService<PowerManager>()!! }
val notificationManager by lazy { application.getSystemService<NotificationManager>()!! }
val wifiManager by lazy { application.getSystemService<WifiManager>()!! }
val clipboard by lazy { application.getSystemService<ClipboardManager>()!! }
}
}

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity0
class AppChangeReceiver : BroadcastReceiver() {
@ -37,7 +37,7 @@ class AppChangeReceiver : BroadcastReceiver() {
Log.d(TAG, "missing package name in intent")
return
}
val isChinaApp = PerAppProxyActivity.scanChinaApps(listOf(packageName)).isNotEmpty()
val isChinaApp = PerAppProxyActivity0.scanChinaPackage(packageName)
Log.d(TAG, "scan china app result for $packageName: $isChinaApp")
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE) xor !isChinaApp) {
Settings.perAppProxyList += packageName

View file

@ -0,0 +1,12 @@
package io.nekohasekai.sfa.ktx
import android.content.ClipData
import io.nekohasekai.sfa.Application
var clipboardText: String?
get() = Application.clipboard.primaryClip?.getItemAt(0)?.text?.toString()
set(plainText) {
if (plainText != null) {
Application.clipboard.setPrimaryClip(ClipData.newPlainText(null, plainText))
}
}

View file

@ -1,12 +1,12 @@
package io.nekohasekai.sfa.ktx
import android.app.Activity
import android.content.ActivityNotFoundException
import androidx.activity.result.ActivityResultLauncher
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.ui.shared.AbstractActivity
fun AbstractActivity.startFilesForResult(
fun Activity.startFilesForResult(
launcher: ActivityResultLauncher<String>, input: String
) {
try {

View file

@ -53,7 +53,7 @@ import java.io.File
import java.util.Date
import java.util.LinkedList
class MainActivity : AbstractActivity(),
class MainActivity : AbstractActivity<ActivityMainBinding>(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
ServiceConnection.Callback {
@ -61,7 +61,6 @@ class MainActivity : AbstractActivity(),
private const val TAG = "MainActivity"
}
internal lateinit var binding: ActivityMainBinding
private val connection = ServiceConnection(this, this)
val logList = LinkedList<String>()
@ -71,10 +70,6 @@ class MainActivity : AbstractActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navController = findNavController(R.id.nav_host_fragment_activity_my)
navController.setGraph(R.navigation.mobile_navigation)
navController.navigate(R.id.navigation_dashboard)
@ -103,6 +98,7 @@ class MainActivity : AbstractActivity(),
navDestination: NavDestination,
bundle: Bundle?
) {
val binding = binding ?: return
val destinationId = navDestination.id
binding.dashboardTabContainer.isVisible = destinationId == R.id.navigation_dashboard
}

View file

@ -6,19 +6,12 @@ import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.databinding.ActivityDebugBinding
import io.nekohasekai.sfa.ui.shared.AbstractActivity
class DebugActivity : AbstractActivity() {
private var binding: ActivityDebugBinding? = null
class DebugActivity : AbstractActivity<ActivityDebugBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_debug)
val binding = ActivityDebugBinding.inflate(layoutInflater)
this.binding = binding
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.scanVPNButton.setOnClickListener {
startActivity(Intent(this, VPNScanActivity::class.java))
}

View file

@ -25,19 +25,14 @@ import java.io.File
import java.util.zip.ZipFile
import kotlin.math.roundToInt
class VPNScanActivity : AbstractActivity() {
class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
private var binding: ActivityVpnScanBinding? = null
private var adapter: Adapter? = null
private val appInfoList = mutableListOf<AppInfo>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_scan_vpn)
val binding = ActivityVpnScanBinding.inflate(layoutInflater)
this.binding = binding
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.scanVPNResult.adapter = Adapter().also {
adapter = it
}

View file

@ -79,11 +79,11 @@ class DashboardFragment : Fragment(R.layout.fragment_dashboard) {
override fun onStart() {
super.onStart()
val activity = activity ?: return
val activityBinding = activity?.binding ?: return
val binding = binding ?: return
if (mediator != null) return
mediator = TabLayoutMediator(
activity.binding.dashboardTabLayout,
activityBinding.dashboardTabLayout,
binding.dashboardPager
) { tab, position ->
tab.setText(Page.values()[position].titleRes)
@ -93,20 +93,21 @@ class DashboardFragment : Fragment(R.layout.fragment_dashboard) {
override fun onDestroyView() {
super.onDestroyView()
mediator?.detach()
mediator = null
binding = null
}
private fun enablePager() {
val activity = activity ?: return
val binding = binding ?: return
activity.binding.dashboardTabLayout.isVisible = true
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
activity.binding?.dashboardTabLayout?.isVisible = false
binding.dashboardPager.isUserInputEnabled = false
binding.dashboardPager.setCurrentItem(0, false)
}

View file

@ -16,7 +16,6 @@ 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.shareProfile
import io.nekohasekai.sfa.ktx.text
import io.nekohasekai.sfa.ui.shared.AbstractActivity
import io.nekohasekai.sfa.utils.HTTPClient
@ -28,20 +27,13 @@ import java.io.File
import java.text.DateFormat
import java.util.Date
class EditProfileActivity : AbstractActivity() {
class EditProfileActivity : AbstractActivity<ActivityEditProfileBinding>() {
private var binding: ActivityEditProfileBinding? = null
private var _profile: Profile? = null
private val profile get() = _profile!!
private lateinit var profile: Profile
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_edit_profile)
val binding = ActivityEditProfileBinding.inflate(layoutInflater)
this.binding = binding
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lifecycleScope.launch(Dispatchers.IO) {
runCatching {
loadProfile()
@ -55,18 +47,11 @@ class EditProfileActivity : AbstractActivity() {
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
private suspend fun loadProfile() {
val binding = binding ?: return
delay(200L)
val profileId = intent.getLongExtra("profile_id", -1L)
if (profileId == -1L) error("invalid arguments")
_profile = ProfileManager.get(profileId) ?: error("invalid arguments")
profile = ProfileManager.get(profileId) ?: error("invalid arguments")
withContext(Dispatchers.Main) {
binding.name.text = profile.name
binding.name.addTextChangedListener {
@ -203,16 +188,4 @@ class EditProfileActivity : AbstractActivity() {
}
}
private fun shareProfile(button: View) {
lifecycleScope.launch(Dispatchers.IO) {
try {
shareProfile(profile)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
errorDialogBuilder(e).show()
}
}
}
}
}

View file

@ -21,28 +21,17 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class EditProfileContentActivity : AbstractActivity() {
class EditProfileContentActivity : AbstractActivity<ActivityEditProfileContentBinding>() {
private var binding: ActivityEditProfileContentBinding? = null
private var _profile: Profile? = null
private val profile get() = _profile!!
private var profile: Profile? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_edit_configuration)
val binding = ActivityEditProfileContentBinding.inflate(layoutInflater)
this.binding = binding
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.editor.language = JsonLanguage()
loadConfiguration()
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
private fun loadConfiguration() {
lifecycleScope.launch(Dispatchers.IO) {
runCatching {
@ -120,7 +109,8 @@ class EditProfileContentActivity : AbstractActivity() {
val profileId = intent.getLongExtra("profile_id", -1L)
if (profileId == -1L) error("invalid arguments")
_profile = ProfileManager.get(profileId) ?: 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)

View file

@ -28,13 +28,12 @@ import java.io.File
import java.io.InputStream
import java.util.Date
class NewProfileActivity : AbstractActivity() {
class NewProfileActivity : AbstractActivity<ActivityAddProfileBinding>() {
enum class FileSource(val formatted: String) {
CreateNew("Create New"),
Import("Import");
}
private var binding: ActivityAddProfileBinding? = null
private val importFile =
registerForActivityResult(ActivityResultContracts.GetContent()) { fileURI ->
val binding = binding ?: return@registerForActivityResult
@ -47,10 +46,6 @@ class NewProfileActivity : AbstractActivity() {
super.onCreate(savedInstanceState)
setTitle(R.string.title_new_profile)
val binding = ActivityAddProfileBinding.inflate(layoutInflater)
this.binding = binding
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
intent.getStringExtra("importName")?.also { importName ->
intent.getStringExtra("importURL")?.also { importURL ->
@ -100,11 +95,6 @@ class NewProfileActivity : AbstractActivity() {
binding.autoUpdateInterval.addTextChangedListener(this::updateAutoUpdateInterval)
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
private fun createProfile(view: View) {
val binding = binding ?: return
if (binding.name.showErrorIfEmpty()) {

View file

@ -3,7 +3,6 @@ package io.nekohasekai.sfa.ui.profileoverride
import android.Manifest
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
@ -20,7 +19,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.core.content.getSystemService
import androidx.core.view.isGone
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -29,6 +27,7 @@ import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.ActivityPerAppProxyBinding
import io.nekohasekai.sfa.databinding.ViewAppListItemBinding
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.ui.shared.AbstractActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -37,10 +36,9 @@ import org.jf.dexlib2.dexbacked.DexBackedDexFile
import java.io.File
import java.util.zip.ZipFile
class PerAppProxyActivity : AbstractActivity() {
class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
private lateinit var binding: ActivityPerAppProxyBinding
private lateinit var adapter: AppListAdapter
private val perAppProxyList = mutableSetOf<String>()
@ -52,10 +50,8 @@ class PerAppProxyActivity : AbstractActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_per_app_proxy)
binding = ActivityPerAppProxyBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val proxyMode = Settings.perAppProxyMode
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
@ -87,8 +83,8 @@ class PerAppProxyActivity : AbstractActivity() {
@SuppressLint("NotifyDataSetChanged")
private fun loadAppList() {
binding.recyclerViewAppList.isGone = true
binding.layoutProgress.isGone = false
// binding.recyclerViewAppList.isGone = true
// binding.layoutProgress.isGone = false
lifecycleScope.launch {
val list = withContext(Dispatchers.IO) {
@ -142,8 +138,8 @@ class PerAppProxyActivity : AbstractActivity() {
adapter.notifyDataSetChanged()
binding.recyclerViewAppList.scrollToPosition(0)
binding.layoutProgress.isGone = true
binding.recyclerViewAppList.isGone = false
// binding.layoutProgress.isGone = true
// binding.recyclerViewAppList.isGone = false
}
}
@ -183,7 +179,7 @@ class PerAppProxyActivity : AbstractActivity() {
R.id.action_import -> {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.menu_import_from_clipboard)
.setTitle(R.string.per_app_proxy_import)
.setMessage(R.string.message_import_from_clipboard)
.setPositiveButton(R.string.ok) { _, _ ->
importFromClipboard()
@ -247,9 +243,7 @@ class PerAppProxyActivity : AbstractActivity() {
return
}
val content = perAppProxyList.joinToString("\n")
val clipboardManager = getSystemService<ClipboardManager>()!!
val clip = ClipData.newPlainText(null, content)
clipboardManager.setPrimaryClip(clip)
clipboardText = content
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Toast.makeText(this, R.string.toast_copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
@ -477,14 +471,14 @@ class PerAppProxyActivity : AbstractActivity() {
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AppItem) {
binding.imageAppIcon.setImageDrawable(item.icon)
binding.textAppName.text = item.name
binding.textAppPackageName.text = item.packageName
binding.checkboxAppSelected.isChecked = item.selected
binding.appIcon.setImageDrawable(item.icon)
binding.applicationLabel.text = item.name
binding.packageName.text = item.packageName
binding.selected.isChecked = item.selected
}
fun bindCheck(item: AppItem) {
binding.checkboxAppSelected.isChecked = item.selected
binding.selected.isChecked = item.selected
}
}
}

View file

@ -0,0 +1,669 @@
package io.nekohasekai.sfa.ui.profileoverride
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.ActivityPerAppProxy0Binding
import io.nekohasekai.sfa.databinding.DialogProgressbarBinding
import io.nekohasekai.sfa.databinding.ViewAppListItemBinding
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.ui.shared.AbstractActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jf.dexlib2.dexbacked.DexBackedDexFile
import java.io.File
import java.util.zip.ZipFile
class PerAppProxyActivity0 : AbstractActivity<ActivityPerAppProxy0Binding>() {
enum class SortMode {
NAME, PACKAGE_NAME, UID, INSTALL_TIME, UPDATE_TIME,
}
private var proxyMode = Settings.PER_APP_PROXY_INCLUDE
private var sortMode = SortMode.NAME
private var sortReverse = false
private var hideSystemApps = false
private var hideOfflineApps = true
private var hideDisabledApps = true
inner class PackageCache(
private val packageInfo: PackageInfo,
) {
val packageName: String get() = packageInfo.packageName
val uid get() = packageInfo.applicationInfo.uid
val installTime get() = packageInfo.firstInstallTime
val updateTime get() = packageInfo.lastUpdateTime
val isSystem get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
val isOffline get() = packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) != true
val isDisabled get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0
val applicationIcon by lazy {
packageInfo.applicationInfo.loadIcon(packageManager)
}
val applicationLabel by lazy {
packageInfo.applicationInfo.loadLabel(packageManager).toString()
}
}
private lateinit var adapter: ApplicationAdapter
private var packages = listOf<PackageCache>()
private var displayPackages = listOf<PackageCache>()
private var currentPackages = listOf<PackageCache>()
private var selectedUIDs = mutableSetOf<Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_per_app_proxy)
lifecycleScope.launch {
proxyMode = if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
Settings.PER_APP_PROXY_EXCLUDE
} else {
Settings.PER_APP_PROXY_INCLUDE
}
withContext(Dispatchers.Main) {
if (proxyMode != Settings.PER_APP_PROXY_EXCLUDE) {
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_include_description)
} else {
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
}
}
reloadApplicationList()
filterApplicationList()
withContext(Dispatchers.Main) {
adapter = ApplicationAdapter(displayPackages)
binding.appList.adapter = adapter
delay(500L)
binding.progress.isVisible = false
}
}
}
private fun reloadApplicationList() {
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
} else {
@Suppress("DEPRECATION") PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
}
val installedPackages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getInstalledPackages(
PackageManager.PackageInfoFlags.of(
packageManagerFlags.toLong()
)
)
} else {
@Suppress("DEPRECATION") packageManager.getInstalledPackages(packageManagerFlags)
}
val packages = mutableListOf<PackageCache>()
for (packageInfo in installedPackages) {
if (packageInfo.packageName == packageName) continue
packages.add(PackageCache(packageInfo))
}
val selectedPackageNames = Settings.perAppProxyList.toMutableSet()
val selectedUIDs = mutableSetOf<Int>()
for (packageCache in packages) {
if (selectedPackageNames.contains(packageCache.packageName)) {
selectedUIDs.add(packageCache.uid)
}
}
this.packages = packages
this.selectedUIDs = selectedUIDs
}
private fun filterApplicationList(selectedUIDs: Set<Int> = this.selectedUIDs) {
val displayPackages = mutableListOf<PackageCache>()
for (packageCache in packages) {
if (hideSystemApps && packageCache.isSystem) continue
if (hideOfflineApps && packageCache.isOffline) continue
if (hideDisabledApps && packageCache.isDisabled) continue
displayPackages.add(packageCache)
}
displayPackages.sortWith(compareBy<PackageCache> {
!selectedUIDs.contains(it.uid)
}.let {
if (!sortReverse) it.thenBy {
when (sortMode) {
SortMode.NAME -> it.applicationLabel
SortMode.PACKAGE_NAME -> it.packageName
SortMode.UID -> it.uid
SortMode.INSTALL_TIME -> it.installTime
SortMode.UPDATE_TIME -> it.updateTime
}
} else it.thenByDescending {
when (sortMode) {
SortMode.NAME -> it.applicationLabel
SortMode.PACKAGE_NAME -> it.packageName
SortMode.UID -> it.uid
SortMode.INSTALL_TIME -> it.installTime
SortMode.UPDATE_TIME -> it.updateTime
}
}
})
this.displayPackages = displayPackages
this.currentPackages = displayPackages
}
private fun updateApplicationSelection(packageCache: PackageCache, selected: Boolean) {
val performed = if (selected) {
selectedUIDs.add(packageCache.uid)
} else {
selectedUIDs.remove(packageCache.uid)
}
if (!performed) return
currentPackages.forEachIndexed { index, it ->
if (it.uid == packageCache.uid) {
adapter.notifyItemChanged(index, PayloadUpdateSelection(selected))
}
}
saveSelectedApplications()
}
data class PayloadUpdateSelection(val selected: Boolean)
inner class ApplicationAdapter(private var applicationList: List<PackageCache>) :
RecyclerView.Adapter<ApplicationViewHolder>() {
@SuppressLint("NotifyDataSetChanged")
fun setApplicationList(applicationList: List<PackageCache>) {
this.applicationList = applicationList
notifyDataSetChanged()
}
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
): ApplicationViewHolder {
return ApplicationViewHolder(
ViewAppListItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
override fun getItemCount(): Int {
return applicationList.size
}
override fun onBindViewHolder(
holder: ApplicationViewHolder, position: Int
) {
holder.bind(applicationList[position])
}
override fun onBindViewHolder(
holder: ApplicationViewHolder, position: Int, payloads: MutableList<Any>
) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
return
}
payloads.forEach {
when (it) {
is PayloadUpdateSelection -> holder.updateSelection(it.selected)
}
}
}
}
inner class ApplicationViewHolder(
private val binding: ViewAppListItemBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(packageCache: PackageCache) {
binding.appIcon.setImageDrawable(packageCache.applicationIcon)
binding.applicationLabel.text = packageCache.applicationLabel
binding.packageName.text = "${packageCache.packageName} (${packageCache.uid})"
binding.selected.isChecked = selectedUIDs.contains(packageCache.uid)
binding.root.setOnClickListener {
updateApplicationSelection(packageCache, !binding.selected.isChecked)
}
binding.root.setOnLongClickListener {
val popup = PopupMenu(it.context, it)
popup.setForceShowIcon(true)
popup.gravity = Gravity.END
popup.menuInflater.inflate(R.menu.app_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_copy_application_label -> {
clipboardText = packageCache.applicationLabel
true
}
R.id.action_copy_package_name -> {
clipboardText = packageCache.packageName
true
}
R.id.action_copy_uid -> {
clipboardText = packageCache.uid.toString()
true
}
else -> false
}
}
popup.show()
true
}
}
fun updateSelection(selected: Boolean) {
binding.selected.isChecked = selected
}
}
private fun searchApplications(searchText: String) {
currentPackages = if (searchText.isEmpty()) {
displayPackages
} else {
displayPackages.filter {
it.applicationLabel.contains(
searchText, ignoreCase = true
) || it.packageName.contains(
searchText, ignoreCase = true
) || it.uid.toString().contains(searchText)
}
}
adapter.setApplicationList(currentPackages)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.per_app_menu0, menu)
if (menu != null) {
val searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
searchApplications(newText)
return true
}
})
searchView.setOnCloseListener {
searchApplications("")
true
}
when (proxyMode) {
Settings.PER_APP_PROXY_INCLUDE -> {
menu.findItem(R.id.action_mode_include).isChecked = true
}
Settings.PER_APP_PROXY_EXCLUDE -> {
menu.findItem(R.id.action_mode_exclude).isChecked = true
}
}
when (sortMode) {
SortMode.NAME -> {
menu.findItem(R.id.action_sort_by_name).isChecked = true
}
SortMode.PACKAGE_NAME -> {
menu.findItem(R.id.action_sort_by_package_name).isChecked = true
}
SortMode.UID -> {
menu.findItem(R.id.action_sort_by_uid).isChecked = true
}
SortMode.INSTALL_TIME -> {
menu.findItem(R.id.action_sort_by_install_time).isChecked = true
}
SortMode.UPDATE_TIME -> {
menu.findItem(R.id.action_sort_by_update_time).isChecked = true
}
}
menu.findItem(R.id.action_sort_reverse).isChecked = sortReverse
menu.findItem(R.id.action_hide_system_apps).isChecked = hideSystemApps
menu.findItem(R.id.action_hide_offline_apps).isChecked = hideOfflineApps
menu.findItem(R.id.action_hide_disabled_apps).isChecked = hideDisabledApps
}
return super.onCreateOptionsMenu(menu)
}
@SuppressLint("NotifyDataSetChanged")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_mode_include -> {
item.isChecked = true
proxyMode = Settings.PER_APP_PROXY_INCLUDE
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_include_description)
lifecycleScope.launch {
Settings.perAppProxyMode = Settings.PER_APP_PROXY_INCLUDE
}
}
R.id.action_mode_exclude -> {
item.isChecked = true
proxyMode = Settings.PER_APP_PROXY_EXCLUDE
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
lifecycleScope.launch {
Settings.perAppProxyMode = Settings.PER_APP_PROXY_EXCLUDE
}
}
R.id.action_sort_by_name -> {
item.isChecked = true
sortMode = SortMode.NAME
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_sort_by_package_name -> {
item.isChecked = true
sortMode = SortMode.PACKAGE_NAME
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_sort_by_uid -> {
item.isChecked = true
sortMode = SortMode.UID
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_sort_by_install_time -> {
item.isChecked = true
sortMode = SortMode.INSTALL_TIME
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_sort_by_update_time -> {
item.isChecked = true
sortMode = SortMode.UPDATE_TIME
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_sort_reverse -> {
item.isChecked = !item.isChecked
sortReverse = item.isChecked
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_hide_system_apps -> {
item.isChecked = !item.isChecked
hideSystemApps = item.isChecked
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_hide_offline_apps -> {
item.isChecked = !item.isChecked
hideOfflineApps = item.isChecked
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_hide_disabled_apps -> {
item.isChecked = !item.isChecked
hideDisabledApps = item.isChecked
filterApplicationList()
adapter.setApplicationList(currentPackages)
}
R.id.action_select_all -> {
val selectedUIDs = mutableSetOf<Int>()
for (packageCache in packages) {
selectedUIDs.add(packageCache.uid)
}
this.selectedUIDs = selectedUIDs
saveSelectedApplications()
}
R.id.action_deselect_all -> {
selectedUIDs = mutableSetOf()
saveSelectedApplications()
}
R.id.action_export -> {
lifecycleScope.launch {
val packageList = mutableListOf<String>()
for (packageCache in packages) {
if (selectedUIDs.contains(packageCache.uid)) {
packageList.add(packageCache.packageName)
}
}
clipboardText = packageList.joinToString("\n")
withContext(Dispatchers.Main) {
Toast.makeText(
this@PerAppProxyActivity0,
R.string.toast_copied_to_clipboard,
Toast.LENGTH_SHORT
).show()
}
}
}
R.id.action_import -> {
val packageNames = clipboardText?.split("\n")?.distinct()
?.takeIf { it.isNotEmpty() && it[0].isNotEmpty() }
if (packageNames.isNullOrEmpty()) {
Toast.makeText(
this@PerAppProxyActivity0,
R.string.toast_clipboard_empty,
Toast.LENGTH_SHORT
).show()
return true
}
val selectedUIDs = mutableSetOf<Int>()
for (packageCache in packages) {
if (packageNames.contains(packageCache.packageName)) {
selectedUIDs.add(packageCache.uid)
}
}
lifecycleScope.launch {
postSaveSelectedApplications(selectedUIDs)
withContext(Dispatchers.Main) {
Toast.makeText(
this@PerAppProxyActivity0,
R.string.toast_imported_from_clipboard,
Toast.LENGTH_SHORT
).show()
}
}
}
R.id.action_scan_china_apps -> {
scanChinaApps()
}
}
return true
}
@SuppressLint("NotifyDataSetChanged")
private fun scanChinaApps() {
val binding = DialogProgressbarBinding.inflate(layoutInflater)
binding.progress.max = currentPackages.size
binding.message.setText(R.string.message_scanning)
val progress = MaterialAlertDialogBuilder(
this, com.google.android.material.R.style.Theme_MaterialComponents_Dialog
).setView(binding.root).setCancelable(false).create()
progress.show()
lifecycleScope.launch {
val foundApps = withContext(Dispatchers.IO) {
mutableMapOf<String, PackageCache>().also { foundApps ->
currentPackages.forEachIndexed { index, it ->
if (scanChinaPackage(it.packageName)) {
foundApps[it.packageName] = it
}
withContext(Dispatchers.Main) {
binding.progress.progress = index + 1
}
}
}
}
withContext(Dispatchers.Main) {
progress.dismiss()
if (foundApps.isEmpty()) {
MaterialAlertDialogBuilder(this@PerAppProxyActivity0).setTitle(R.string.title_scan_result)
.setMessage(R.string.message_scan_app_no_apps_found)
.setPositiveButton(R.string.ok, null).show()
return@withContext
}
val dialogContent =
getString(R.string.message_scan_app_found) + "\n\n" + foundApps.entries.joinToString(
"\n"
) {
"${it.value.applicationLabel} (${it.key})"
}
MaterialAlertDialogBuilder(this@PerAppProxyActivity0).setTitle(R.string.title_scan_result)
.setMessage(dialogContent)
.setPositiveButton(R.string.action_select) { dialog, _ ->
dialog.dismiss()
lifecycleScope.launch {
val selectedUIDs = selectedUIDs.toMutableSet()
foundApps.values.forEach {
selectedUIDs.add(it.uid)
}
postSaveSelectedApplications(selectedUIDs)
}
}.setNegativeButton(R.string.action_deselect) { dialog, _ ->
dialog.dismiss()
lifecycleScope.launch {
val selectedUIDs = selectedUIDs.toMutableSet()
foundApps.values.forEach {
selectedUIDs.remove(it.uid)
}
postSaveSelectedApplications(selectedUIDs)
}
}.setNeutralButton(android.R.string.cancel, null).show()
}
}
}
@SuppressLint("NotifyDataSetChanged")
private suspend fun postSaveSelectedApplications(newUIDs: MutableSet<Int>) {
filterApplicationList(newUIDs)
withContext(Dispatchers.Main) {
selectedUIDs = newUIDs
adapter.notifyDataSetChanged()
}
val packageList = selectedUIDs.mapNotNull { uid ->
packages.find { it.uid == uid }?.packageName
}
Settings.perAppProxyList = packageList.toSet()
}
private fun saveSelectedApplications() {
lifecycleScope.launch {
val packageList = selectedUIDs.mapNotNull { uid ->
packages.find { it.uid == uid }?.packageName
}
Settings.perAppProxyList = packageList.toSet()
}
}
companion object {
private val chinaAppPrefixList by lazy {
runCatching {
Application.application.assets.open("prefix-china-apps.txt").reader().readLines()
}.getOrNull() ?: emptyList()
}
private val chinaAppRegex by lazy {
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
fun scanChinaPackage(packageName: String): Boolean {
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION") PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
if (packageName.matches(chinaAppRegex)) {
return true
}
try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Application.packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
@Suppress("DEPRECATION") Application.packageManager.getPackageInfo(
packageName, packageManagerFlags
)
}
if (packageInfo.services?.find { it.name.matches(chinaAppRegex) } != null || packageInfo.activities?.find {
it.name.matches(
chinaAppRegex
)
} != null || packageInfo.receivers?.find { it.name.matches(chinaAppRegex) } != null || packageInfo.providers?.find {
it.name.matches(
chinaAppRegex
)
} != null) {
return true
}
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) {
return true
}
}
}
}
} catch (ignored: Exception) {
}
return false
}
}
}

View file

@ -15,17 +15,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ProfileOverrideActivity : AbstractActivity() {
private lateinit var binding: ActivityConfigOverrideBinding
class ProfileOverrideActivity :
AbstractActivity<ActivityConfigOverrideBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.title_profile_override)
binding = ActivityConfigOverrideBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setTitle(R.string.title_profile_override)
binding.switchPerAppProxy.isChecked = Settings.perAppProxyEnabled
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
Settings.perAppProxyEnabled = isChecked
@ -42,7 +38,7 @@ class ProfileOverrideActivity : AbstractActivity() {
}
binding.configureAppListButton.setOnClickListener {
startActivity(Intent(this, PerAppProxyActivity::class.java))
startActivity(Intent(this, PerAppProxyActivity0::class.java))
}
lifecycleScope.launch(Dispatchers.IO) {
reloadSettings()

View file

@ -1,16 +1,26 @@
package io.nekohasekai.sfa.ui.shared
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.viewbinding.ViewBinding
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
abstract class AbstractActivity<Binding : ViewBinding>() :
AppCompatActivity() {
private var _binding: Binding? = null
internal val binding get() = _binding!!
abstract class AbstractActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -21,17 +31,28 @@ abstract class AbstractActivity : AppCompatActivity() {
window.statusBarColor = colorSurfaceContainer
window.navigationBarColor = colorSurfaceContainer
_binding = createBindingInstance(layoutInflater).also {
setContentView(it.root)
}
findViewById<MaterialToolbar>(R.id.toolbar)?.also {
setSupportActionBar(it)
}
// MIUI overrides colorSurfaceContainer to colorSurface without below flags
@Suppress("DEPRECATION") if (MIUIUtils.isMIUI) {
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
supportActionBar?.setHomeAsUpIndicator(AppCompatResources.getDrawable(
this@AbstractActivity, R.drawable.ic_arrow_back_24
)!!.apply {
setTint(getAttrColor(com.google.android.material.R.attr.colorOnSurface))
})
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)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -44,4 +65,14 @@ abstract class AbstractActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item)
}
@Suppress("UNCHECKED_CAST")
private fun createBindingInstance(
inflater: LayoutInflater,
): Binding {
val vbType = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
val vbClass = vbType as Class<Binding>
val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
return method.invoke(null, inflater) as Binding
}
}

View file

@ -1,175 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<include layout="@layout/view_appbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<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:orientation="vertical"
android:clipToPadding="false"
android:clipChildren="false"
android:padding="16dp">
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name"
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressView"
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" />
</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>
android:indeterminate="true"
android:visibility="gone" />
<LinearLayout
android:id="@+id/localFields"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
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/fileSourceMenu"
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" />
</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_source">
android:hint="@string/profile_type">
<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" />
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" />
</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" />
</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/importFileButton"
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/createProfile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:visibility="visible"
android:text="@string/profile_import_file"
android:visibility="gone">
android:text="@string/profile_create">
</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" />
</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" />
</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>
</ScrollView>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,89 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<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">
<LinearLayout
<include layout="@layout/view_appbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="16dp">
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.card.MaterialCardView
android:id="@+id/perAppProxyCard"
style="?attr/materialCardViewElevatedStyle"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
<com.google.android.material.card.MaterialCardView
android:id="@+id/perAppProxyCard"
style="?attr/materialCardViewElevatedStyle"
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/title_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" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/perAppProxyUpdateOnChange"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/per_app_proxy_update_on_change">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:text="@string/disabled"
app:simpleItems="@array/per_app_proxy_update_on_change_value" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical|end"
android:orientation="horizontal">
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<Button
android:id="@+id/configureAppListButton"
style="@style/Widget.Material3.Button.TextButton"
<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/title_per_app_proxy"
android:textAppearance="?attr/textAppearanceTitleLarge" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/profile_override_configure" />
android:layout_marginTop="8dp"
android:text="@string/per_app_proxy_description" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/perAppProxyUpdateOnChange"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/per_app_proxy_update_on_change">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:text="@string/disabled"
app:simpleItems="@array/per_app_proxy_update_on_change_value" />
</com.google.android.material.textfield.TextInputLayout>
<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>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>
</ScrollView>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,8 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<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"
android:orientation="vertical">
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"
@ -63,4 +70,6 @@
</LinearLayout>
</ScrollView>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,82 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<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="wrap_content">
android:layout_height="match_parent">
<LinearLayout
<include layout="@layout/view_appbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<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:clipToPadding="false"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible">
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name"
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressView"
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" />
</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" />
android:indeterminate="true" />
<LinearLayout
android:id="@+id/remoteFields"
android:id="@+id/profileLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="8dp">
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/remoteURL"
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/profile_url">
android:hint="@string/profile_name">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@ -84,66 +46,112 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/autoUpdate"
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_auto_update">
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/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" />
android:text="@string/profile_type_local"
app:simpleItems="@array/profile_type" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/updateButton"
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_update" />
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: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>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,14 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
android:orientation="vertical">
android:layout_height="match_parent">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressView"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true" />
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"
@ -18,6 +33,8 @@
android:gravity="top|start"
android:padding="8dp"
android:textSize="14sp"
android:typeface="monospace" />
android:typeface="monospace"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,54 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
android:orientation="vertical">
android:layout_height="match_parent">
<RadioGroup
android:id="@+id/radio_group_per_app_mode"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp">
android:background="?colorSurfaceContainer"
android:fitsSystemWindows="true">
<RadioButton
android:id="@+id/radio_per_app_exclude"
<com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/per_app_proxy_mode_exclude" />
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|enterAlways|snap">
<RadioButton
android:id="@+id/radio_per_app_include"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/per_app_proxy_mode_include" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</RadioGroup>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp" />
<RadioGroup
android:id="@+id/radio_group_per_app_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp">
<RadioButton
android:id="@+id/radio_per_app_exclude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/per_app_proxy_mode_exclude" />
<RadioButton
android:id="@+id/radio_per_app_include"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/per_app_proxy_mode_include" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_app_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<RelativeLayout
android:id="@+id/layout_progress"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,55 @@
<?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">
<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/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="?colorSurfaceContainerLow"
android:gravity="center_vertical"
android:paddingLeft="16dp">
<TextView
android:id="@+id/per_app_proxy_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/per_app_proxy_mode_include" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/view_app_list_item" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,14 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
android:orientation="vertical">
android:layout_height="match_parent">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/scanVPNProgress"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true" />
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"
@ -17,6 +34,7 @@
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp" />
android:paddingBottom="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,18 @@
<?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:orientation="vertical">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp" />
</LinearLayout>

View file

@ -36,8 +36,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_note_add_24"
app:iconTint="@android:color/white" />
app:icon="@drawable/ic_note_add_24" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,5 +1,6 @@
<?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"
@ -7,40 +8,59 @@
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<ImageView
android:id="@+id/image_app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/content_description_app_icon" />
<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_gravity="center"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="?android:textColorPrimary"
android:textSize="16sp" />
<TextView
android:id="@+id/text_app_package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
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="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="io.nekohasekai.sfa" />
</LinearLayout>
<CheckBox
android:id="@+id/checkbox_app_selected"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false" />
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>

View file

@ -0,0 +1,64 @@
<?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>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
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.appbar.AppBarLayout>

View file

@ -0,0 +1,25 @@
<?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_copy"
android:icon="@drawable/ic_insert_drive_file_24"
android:title="@string/per_app_proxy_action_copy"
app:iconTint="?colorPrimary"
app:iconTintMode="src_in">
<menu>
<item
android:id="@+id/action_copy_application_label"
android:title="@string/per_app_proxy_action_copy_application_label" />
<item
android:id="@+id/action_copy_package_name"
android:title="@string/per_app_proxy_action_copy_package_name" />
<item
android:id="@+id/action_copy_uid"
android:title="@string/per_app_proxy_action_copy_uid" />
</menu>
</item>
</menu>

View file

@ -20,10 +20,10 @@
<item
android:id="@+id/action_import"
android:title="@string/menu_import_from_clipboard" />
android:title="@string/per_app_proxy_import" />
<item
android:id="@+id/action_export"
android:title="@string/menu_export_to_clipboard" />
android:title="@string/per_app_proxy_export" />
</menu>

View file

@ -0,0 +1,99 @@
<?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_search"
android:icon="@drawable/ic_find_in_page_24"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?colorControlNormal"
app:showAsAction="collapseActionView|always" />
<item android:title="@string/per_app_proxy_mode">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_mode_include"
android:title="@string/per_app_proxy_mode_include" />
<item
android:id="@+id/action_mode_exclude"
android:title="@string/per_app_proxy_mode_exclude" />
</group>
</menu>
</item>
<item android:title="@string/per_app_proxy_sort_mode">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/action_sort_by_name"
android:title="@string/per_app_proxy_sort_mode_name" />
<item
android:id="@+id/action_sort_by_package_name"
android:title="@string/per_app_proxy_sort_mode_package_name" />
<item
android:id="@+id/action_sort_by_uid"
android:title="@string/per_app_proxy_sort_mode_uid" />
<item
android:id="@+id/action_sort_by_install_time"
android:title="@string/per_app_proxy_sort_mode_install_time" />
<item
android:id="@+id/action_sort_by_update_time"
android:title="@string/per_app_proxy_sort_mode_update_time" />
</group>
<item
android:id="@+id/action_sort_reverse"
android:checkable="true"
android:title="@string/per_app_proxy_sort_mode_reverse" />
</menu>
</item>
<item android:title="@string/per_app_proxy_filter">
<menu>
<item
android:id="@+id/action_hide_system_apps"
android:checkable="true"
android:title="@string/per_app_proxy_hide_system_apps" />
<item
android:id="@+id/action_hide_offline_apps"
android:checkable="true"
android:title="@string/per_app_proxy_hide_offline_apps" />
<item
android:id="@+id/action_hide_disabled_apps"
android:checkable="true"
android:title="@string/per_app_proxy_hide_disabled_apps" />
</menu>
</item>
<item android:title="@string/action_select">
<menu>
<item
android:id="@+id/action_select_all"
android:title="@string/per_app_proxy_select_all" />
<item
android:id="@+id/action_deselect_all"
android:title="@string/per_app_proxy_select_none" />
</menu>
</item>
<item android:title="@string/per_app_proxy_backup">
<menu>
<item
android:id="@+id/action_import"
android:title="@string/per_app_proxy_import" />
<item
android:id="@+id/action_export"
android:title="@string/per_app_proxy_export" />
</menu>
</item>
<item android:title="@string/per_app_proxy_scan">
<menu>
<item
android:id="@+id/action_scan_china_apps"
android:title="@string/per_app_proxy_scan_china_apps" />
</menu>
</item>
</menu>

View file

@ -1,11 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.Material3.DayNight.NoActionBar">
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="m">false</item>
</style>
</resources>

View file

@ -99,22 +99,53 @@
<string name="title_profile_override">Profile Override</string>
<string name="profile_override_description">Overrides profile configuration items with platform-specific values.</string>
<string name="profile_override_configure">Configure</string>
<string name="title_per_app_proxy">Per-app Proxy</string>
<string name="per_app_proxy_description">Override include_package and exclude_package in the configuration.</string>
<string name="per_app_proxy_mode_exclude">Do not proxy selected apps</string>
<string name="per_app_proxy_mode_include">Proxy only selected apps</string>
<string name="per_app_proxy_mode">Proxy Mode</string>
<string name="per_app_proxy_mode_include">Include</string>
<string name="per_app_proxy_mode_include_description">Only selected apps are allowed through the VPN</string>
<string name="per_app_proxy_mode_exclude">Exclude</string>
<string name="per_app_proxy_mode_exclude_description">Selected apps will be excluded from VPN</string>
<string name="per_app_proxy_action_copy">Copy</string>
<string name="per_app_proxy_action_copy_application_label">Name</string>
<string name="per_app_proxy_action_copy_package_name">Package Name</string>
<string name="per_app_proxy_action_copy_uid">UID</string>
<string name="per_app_proxy_sort_mode">Sort Mode</string>
<string name="per_app_proxy_sort_mode_name">By name</string>
<string name="per_app_proxy_sort_mode_package_name">By package name</string>
<string name="per_app_proxy_sort_mode_uid">By UID</string>
<string name="per_app_proxy_sort_mode_install_time">By install time</string>
<string name="per_app_proxy_sort_mode_update_time">By update time</string>
<string name="per_app_proxy_sort_mode_reverse">Reverse</string>
<string name="per_app_proxy_filter">Filter</string>
<string name="per_app_proxy_hide_system_apps">Hide system apps</string>
<string name="per_app_proxy_hide_offline_apps">Hide offline apps</string>
<string name="per_app_proxy_hide_disabled_apps">Hide disabled apps</string>
<string name="per_app_proxy_select">Select</string>
<string name="per_app_proxy_select_all">Select all</string>
<string name="per_app_proxy_select_none">Deselect all</string>
<string name="per_app_proxy_backup">Backup</string>
<string name="per_app_proxy_import">Import from clipboard</string>
<string name="per_app_proxy_export">Export to clipboard</string>
<string name="per_app_proxy_scan">Scan</string>
<string name="per_app_proxy_scan_china_apps">China apps</string>
<string name="content_description_app_icon">App icon</string>
<string name="menu_hide_system">Hide system apps</string>
<string name="menu_show_system">Show system apps</string>
<string name="menu_scan_china_apps">Scan China apps</string>
<string name="menu_import_from_clipboard">Import from clipboard</string>
<string name="menu_export_to_clipboard">Export to clipboard</string>
<string name="toast_clipboard_empty">Clipboard is empty</string>
<string name="toast_app_list_empty">App list is empty</string>
<string name="toast_copied_to_clipboard">Copied to clipboard</string>
<string name="toast_copied_to_clipboard">Exported to clipboard</string>
<string name="toast_imported_from_clipboard">Imported from clipboard</string>
<string name="message_import_from_clipboard">Importing app list from clipboard will overwrite your current list. Are you sure to continue?</string>
<string name="message_scanning">Scanning… Please wait</string>
<string name="message_scanning">Scanning…</string>
<string name="message_scan_app_error">Error scanning apps</string>
<string name="message_scan_app_no_apps_found">No matching apps found</string>
<string name="message_scan_app_found">Found the following apps, please choose the action you want.</string>

View file

@ -1,11 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.Material3.DayNight.NoActionBar">
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="m">true</item>
</style>
<style name="AppTheme.Translucent" parent="Theme.Material3.DayNight.Dialog.Alert">