diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index aeda923..2ba672a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -37,7 +37,6 @@
android:name=".ui.MainActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher"
- android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleTask">
+
diff --git a/app/src/main/java/io/nekohasekai/sfa/Application.kt b/app/src/main/java/io/nekohasekai/sfa/Application.kt
index 43c77a7..ac96d0c 100644
--- a/app/src/main/java/io/nekohasekai/sfa/Application.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/Application.kt
@@ -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()!! }
val notificationManager by lazy { application.getSystemService()!! }
val wifiManager by lazy { application.getSystemService()!! }
+ val clipboard by lazy { application.getSystemService()!! }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt b/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt
index db667b9..b788173 100644
--- a/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt
@@ -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
diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Clips.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Clips.kt
new file mode 100644
index 0000000..48ee0f4
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Clips.kt
@@ -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))
+ }
+ }
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt
index 6b91850..aea4807 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Intents.kt
@@ -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, input: String
) {
try {
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
index db20646..37f3f95 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
@@ -53,7 +53,7 @@ import java.io.File
import java.util.Date
import java.util.LinkedList
-class MainActivity : AbstractActivity(),
+class MainActivity : AbstractActivity(),
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()
@@ -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
}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt
index cc98a53..cf83791 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/debug/DebugActivity.kt
@@ -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() {
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))
}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt
index 6de9151..2cb3a6f 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt
@@ -25,19 +25,14 @@ import java.io.File
import java.util.zip.ZipFile
import kotlin.math.roundToInt
-class VPNScanActivity : AbstractActivity() {
+class VPNScanActivity : AbstractActivity() {
- private var binding: ActivityVpnScanBinding? = null
private var adapter: Adapter? = null
private val appInfoList = mutableListOf()
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
}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt
index 4d6fd88..8022009 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt
@@ -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)
}
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt
index 44fcf98..53de9cb 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt
@@ -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() {
- 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()
- }
- }
- }
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt
index 0657972..d87758e 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt
@@ -21,28 +21,17 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
-class EditProfileContentActivity : AbstractActivity() {
+class EditProfileContentActivity : AbstractActivity() {
- 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)
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt
index 6f2ab38..b0b9231 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt
@@ -28,13 +28,12 @@ import java.io.File
import java.io.InputStream
import java.util.Date
-class NewProfileActivity : AbstractActivity() {
+class NewProfileActivity : AbstractActivity() {
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()) {
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt
index ccf9553..e7333cc 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt
@@ -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() {
- private lateinit var binding: ActivityPerAppProxyBinding
private lateinit var adapter: AppListAdapter
private val perAppProxyList = mutableSetOf()
@@ -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()!!
- 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
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity0.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity0.kt
new file mode 100644
index 0000000..3b2b82f
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity0.kt
@@ -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() {
+ 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()
+ private var displayPackages = listOf()
+ private var currentPackages = listOf()
+ private var selectedUIDs = mutableSetOf()
+
+ 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()
+ for (packageInfo in installedPackages) {
+ if (packageInfo.packageName == packageName) continue
+ packages.add(PackageCache(packageInfo))
+ }
+ val selectedPackageNames = Settings.perAppProxyList.toMutableSet()
+ val selectedUIDs = mutableSetOf()
+ for (packageCache in packages) {
+ if (selectedPackageNames.contains(packageCache.packageName)) {
+ selectedUIDs.add(packageCache.uid)
+ }
+ }
+ this.packages = packages
+ this.selectedUIDs = selectedUIDs
+ }
+
+ private fun filterApplicationList(selectedUIDs: Set = this.selectedUIDs) {
+ val displayPackages = mutableListOf()
+ 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 {
+ !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) :
+ RecyclerView.Adapter() {
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun setApplicationList(applicationList: List) {
+ 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
+ ) {
+ 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()
+ 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()
+ 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()
+ 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().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) {
+ 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
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt
index 0ada5fc..1598b53 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt
@@ -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() {
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()
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt
index 82231cc..2ba7da0 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/shared/AbstractActivity.kt
@@ -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() :
+ 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(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
+ val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
+ return method.invoke(null, inflater) as Binding
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_add_profile.xml b/app/src/main/res/layout/activity_add_profile.xml
index 1f51b2f..383e152 100644
--- a/app/src/main/res/layout/activity_add_profile.xml
+++ b/app/src/main/res/layout/activity_add_profile.xml
@@ -1,175 +1,185 @@
-
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
-
+
+
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
-
-
-
-
-
-
-
-
-
-
-
+ android:indeterminate="true"
+ android:visibility="gone" />
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+
+
+
+
+
+ android:hint="@string/profile_type">
+ android:text="@string/profile_type_local"
+ app:simpleItems="@array/profile_type" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_config_override.xml b/app/src/main/res/layout/activity_config_override.xml
index 0839237..163350e 100644
--- a/app/src/main/res/layout/activity_config_override.xml
+++ b/app/src/main/res/layout/activity_config_override.xml
@@ -1,89 +1,98 @@
-
-
+
+
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
-
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical"
+ android:padding="16dp">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content">
+ android:orientation="vertical"
+ android:paddingStart="16dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="16dp"
+ android:paddingBottom="8dp">
-
+
+
+
+ android:layout_marginTop="8dp"
+ android:text="@string/per_app_proxy_description" />
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml
index e9438ee..af6a025 100644
--- a/app/src/main/res/layout/activity_debug.xml
+++ b/app/src/main/res/layout/activity_debug.xml
@@ -1,8 +1,15 @@
-
+ android:layout_height="match_parent">
+
+
+
+
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml
index 0f936ef..17061f0 100644
--- a/app/src/main/res/layout/activity_edit_profile.xml
+++ b/app/src/main/res/layout/activity_edit_profile.xml
@@ -1,82 +1,44 @@
-
+ android:layout_height="match_parent">
-
+
+
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:indeterminate="true" />
+ android:padding="16dp"
+ android:visibility="gone"
+ tools:visibility="visible">
+ android:hint="@string/profile_name">
-
+ android:enabled="false"
+ android:hint="@string/profile_type">
-
-
-
-
-
-
-
-
-
-
-
-
+ android:text="@string/profile_type_local"
+ app:simpleItems="@array/profile_type" />
+ android:text="@string/profile_edit_content" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_profile_content.xml b/app/src/main/res/layout/activity_edit_profile_content.xml
index ad6467c..6b0eaef 100644
--- a/app/src/main/res/layout/activity_edit_profile_content.xml
+++ b/app/src/main/res/layout/activity_edit_profile_content.xml
@@ -1,14 +1,29 @@
-
+ android:layout_height="match_parent">
-
+ android:background="?colorSurfaceContainer"
+ android:fitsSystemWindows="true">
+
+
+
+
+
+
+ android:typeface="monospace"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_per_app_proxy.xml b/app/src/main/res/layout/activity_per_app_proxy.xml
index 62ea8c9..8187229 100644
--- a/app/src/main/res/layout/activity_per_app_proxy.xml
+++ b/app/src/main/res/layout/activity_per_app_proxy.xml
@@ -1,54 +1,66 @@
-
+ android:layout_height="match_parent">
-
+ android:background="?colorSurfaceContainer"
+ android:fitsSystemWindows="true">
-
+ android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
+ app:layout_scrollFlags="scroll|enterAlways|snap">
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_per_app_proxy0.xml b/app/src/main/res/layout/activity_per_app_proxy0.xml
new file mode 100644
index 0000000..4c71b98
--- /dev/null
+++ b/app/src/main/res/layout/activity_per_app_proxy0.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_vpn_scan.xml b/app/src/main/res/layout/activity_vpn_scan.xml
index c3d1a9f..b6430c6 100644
--- a/app/src/main/res/layout/activity_vpn_scan.xml
+++ b/app/src/main/res/layout/activity_vpn_scan.xml
@@ -1,14 +1,31 @@
-
+ android:layout_height="match_parent">
-
+ android:background="?colorSurfaceContainer"
+ android:fitsSystemWindows="true">
+
+
+
+
+
+
+
+ android:paddingBottom="16dp"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_progressbar.xml b/app/src/main/res/layout/dialog_progressbar.xml
new file mode 100644
index 0000000..b9a20b1
--- /dev/null
+++ b/app/src/main/res/layout/dialog_progressbar.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_configuration.xml b/app/src/main/res/layout/fragment_configuration.xml
index 13193e3..600e764 100644
--- a/app/src/main/res/layout/fragment_configuration.xml
+++ b/app/src/main/res/layout/fragment_configuration.xml
@@ -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" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_app_list_item.xml b/app/src/main/res/layout/view_app_list_item.xml
index 33ad540..406045d 100644
--- a/app/src/main/res/layout/view_app_list_item.xml
+++ b/app/src/main/res/layout/view_app_list_item.xml
@@ -1,5 +1,6 @@
-
+
+
+
+
+
-
+ 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" />
+
+
+
+
-
+ android:gravity="top">
+
+
+
+
diff --git a/app/src/main/res/layout/view_app_list_item0.xml b/app/src/main/res/layout/view_app_list_item0.xml
new file mode 100644
index 0000000..9ef9dc5
--- /dev/null
+++ b/app/src/main/res/layout/view_app_list_item0.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_appbar.xml b/app/src/main/res/layout/view_appbar.xml
new file mode 100644
index 0000000..a43317c
--- /dev/null
+++ b/app/src/main/res/layout/view_appbar.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/app_menu.xml b/app/src/main/res/menu/app_menu.xml
new file mode 100644
index 0000000..5e8eb57
--- /dev/null
+++ b/app/src/main/res/menu/app_menu.xml
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/per_app_menu.xml b/app/src/main/res/menu/per_app_menu.xml
index 063f749..7ade0dc 100644
--- a/app/src/main/res/menu/per_app_menu.xml
+++ b/app/src/main/res/menu/per_app_menu.xml
@@ -20,10 +20,10 @@
+ android:title="@string/per_app_proxy_import" />
+ android:title="@string/per_app_proxy_export" />
\ No newline at end of file
diff --git a/app/src/main/res/menu/per_app_menu0.xml b/app/src/main/res/menu/per_app_menu0.xml
new file mode 100644
index 0000000..96d1468
--- /dev/null
+++ b/app/src/main/res/menu/per_app_menu0.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 586b9ee..884d201 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,11 +1,8 @@
-
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5a61900..61a9c04 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -99,22 +99,53 @@
Profile Override
Overrides profile configuration items with platform-specific values.
Configure
+
Per-app Proxy
Override include_package and exclude_package in the configuration.
- Do not proxy selected apps
- Proxy only selected apps
+ Proxy Mode
+ Include
+ Only selected apps are allowed through the VPN
+ Exclude
+ Selected apps will be excluded from VPN
+ Copy
+ Name
+ Package Name
+ UID
+
+ Sort Mode
+ By name
+ By package name
+ By UID
+ By install time
+ By update time
+ Reverse
+
+ Filter
+ Hide system apps
+ Hide offline apps
+ Hide disabled apps
+
+ Select
+ Select all
+ Deselect all
+
+ Backup
+ Import from clipboard
+ Export to clipboard
+
+ Scan
+ China apps
+
App icon
Hide system apps
Show system apps
Scan China apps
- Import from clipboard
- Export to clipboard
Clipboard is empty
App list is empty
- Copied to clipboard
+ Exported to clipboard
Imported from clipboard
Importing app list from clipboard will overwrite your current list. Are you sure to continue?
- Scanning… Please wait
+ Scanning…
Error scanning apps
No matching apps found
Found the following apps, please choose the action you want.
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 3560796..43c102a 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,11 +1,8 @@
-
-
-