diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2ba672a..915fffb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -119,9 +119,6 @@
-
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 b788173..3088f0a 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.PerAppProxyActivity0
+import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
class AppChangeReceiver : BroadcastReceiver() {
@@ -37,7 +37,7 @@ class AppChangeReceiver : BroadcastReceiver() {
Log.d(TAG, "missing package name in intent")
return
}
- val isChinaApp = PerAppProxyActivity0.scanChinaPackage(packageName)
+ val isChinaApp = PerAppProxyActivity.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/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
index 37f3f95..e9c6f70 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
@@ -19,7 +19,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
+import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.Preference
@@ -61,6 +63,10 @@ class MainActivity : AbstractActivity(),
private const val TAG = "MainActivity"
}
+ private lateinit var navHostFragment: NavHostFragment
+ private lateinit var navController: NavController
+ private lateinit var appBarConfiguration: AppBarConfiguration
+
private val connection = ServiceConnection(this, this)
val logList = LinkedList()
@@ -70,11 +76,13 @@ class MainActivity : AbstractActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val navController = findNavController(R.id.nav_host_fragment_activity_my)
+ navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_my) as NavHostFragment
+ navController = navHostFragment.navController
navController.setGraph(R.navigation.mobile_navigation)
navController.navigate(R.id.navigation_dashboard)
navController.addOnDestinationChangedListener(::onDestinationChanged)
- val appBarConfiguration =
+ appBarConfiguration =
AppBarConfiguration(
setOf(
R.id.navigation_dashboard,
@@ -85,13 +93,15 @@ class MainActivity : AbstractActivity(),
)
setupActionBarWithNavController(navController, appBarConfiguration)
binding.navView.setupWithNavController(navController)
-
reconnect()
startIntegration()
onNewIntent(intent)
}
+ override fun onSupportNavigateUp(): Boolean {
+ return navController.navigateUp(appBarConfiguration)
+ }
private fun onDestinationChanged(
navController: NavController,
diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt
index d9a15c4..e95c75a 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/GroupsFragment.kt
@@ -12,6 +12,7 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -175,7 +176,7 @@ class GroupsFragment : Fragment(), CommandClient.Handler {
binding.itemList.adapter = adapter
(binding.itemList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations =
false
- binding.itemList.layoutManager = LinearLayoutManager(binding.root.context)
+ binding.itemList.layoutManager = GridLayoutManager(binding.root.context, 2)
} else {
adapter.setItems(items)
}
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 e7333cc..89b0d98 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
@@ -2,23 +2,20 @@ package io.nekohasekai.sfa.ui.profileoverride
import android.Manifest
import android.annotation.SuppressLint
-import android.app.Dialog
-import android.content.ClipboardManager
import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
import android.content.pm.PackageManager
-import android.content.pm.PackageManager.PackageInfoFlags
-import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
-import android.util.Log
+import android.view.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup
-import android.widget.TextView
import android.widget.Toast
+import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
-import androidx.core.content.getSystemService
+import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -26,10 +23,12 @@ import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.ActivityPerAppProxyBinding
+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
@@ -37,114 +36,266 @@ import java.io.File
import java.util.zip.ZipFile
class PerAppProxyActivity : 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
- private lateinit var adapter: AppListAdapter
+ inner class PackageCache(
+ private val packageInfo: PackageInfo,
+ ) {
- private val perAppProxyList = mutableSetOf()
- private val appList = mutableListOf()
+ val packageName: String get() = packageInfo.packageName
- private var hideSystem = false
- private var searchKeyword = ""
- private val filteredAppList = mutableListOf()
+ 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)
- val proxyMode = Settings.perAppProxyMode
- if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
- binding.radioPerAppInclude.isChecked = true
- } else {
- binding.radioPerAppExclude.isChecked = true
- }
- binding.radioGroupPerAppMode.setOnCheckedChangeListener { _, checkedId ->
- if (checkedId == R.id.radio_per_app_include) {
- Settings.perAppProxyMode = Settings.PER_APP_PROXY_INCLUDE
+ lifecycleScope.launch {
+ proxyMode = if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
+ Settings.PER_APP_PROXY_EXCLUDE
} else {
- Settings.perAppProxyMode = Settings.PER_APP_PROXY_EXCLUDE
+ 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
}
}
-
- perAppProxyList.addAll(Settings.perAppProxyList.toMutableSet())
- adapter = AppListAdapter(filteredAppList) {
- val item = filteredAppList[it]
- if (item.selected) {
- perAppProxyList.add(item.packageName)
- } else {
- perAppProxyList.remove(item.packageName)
- }
- Settings.perAppProxyList = perAppProxyList
- }
- binding.recyclerViewAppList.adapter = adapter
- loadAppList()
}
- @SuppressLint("NotifyDataSetChanged")
- private fun loadAppList() {
-// binding.recyclerViewAppList.isGone = true
-// binding.layoutProgress.isGone = 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
+ }
- lifecycleScope.launch {
- val list = withContext(Dispatchers.IO) {
- val flag = 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
+ 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
}
- val installedPackages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- packageManager.getInstalledPackages(PackageInfoFlags.of(flag.toLong()))
- } else {
- @Suppress("DEPRECATION")
- packageManager.getInstalledPackages(flag)
+ } 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
}
- val list = mutableListOf()
- installedPackages.forEach {
- if (it.packageName != packageName &&
- (it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
- || it.packageName == "android")
- ) {
- list.add(
- AppItem(
- it.packageName,
- it.applicationInfo.loadLabel(packageManager).toString(),
- it.applicationInfo.loadIcon(packageManager),
- it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1,
- perAppProxyList.contains(it.packageName)
- )
- )
+ }
+ })
+
+ 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
}
}
- list.sortedWith(compareBy({ !it.selected }, { it.name }))
+ popup.show()
+ true
}
- appList.clear()
- appList.addAll(list)
-
- perAppProxyList.toSet().forEach {
- if (appList.find { app -> app.packageName == it } == null) {
- perAppProxyList.remove(it)
- }
- }
- Settings.perAppProxyList = perAppProxyList
-
- filteredAppList.clear()
- if (hideSystem) {
- filteredAppList.addAll(appList.filter { !it.isSystemApp })
- } else {
- filteredAppList.addAll(appList)
- }
- adapter.notifyDataSetChanged()
-
- binding.recyclerViewAppList.scrollToPosition(0)
-// binding.layoutProgress.isGone = true
-// binding.recyclerViewAppList.isGone = false
}
+
+ 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_menu, menu)
+ menuInflater.inflate(R.menu.per_app_menu0, menu)
if (menu != null) {
val searchView = menu.findItem(R.id.action_search).actionView as SearchView
@@ -154,225 +305,336 @@ class PerAppProxyActivity : AbstractActivity() {
}
override fun onQueryTextChange(newText: String): Boolean {
- searchKeyword = newText
- filterApps()
+ 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_hide_system -> {
- hideSystem = !hideSystem
- if (hideSystem) {
- item.setTitle(R.string.menu_show_system)
- } else {
- item.setTitle(R.string.menu_hide_system)
+ 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
}
- filterApps()
- return true
}
- R.id.action_import -> {
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.per_app_proxy_import)
- .setMessage(R.string.message_import_from_clipboard)
- .setPositiveButton(R.string.ok) { _, _ ->
- importFromClipboard()
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- return true
+ 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 -> {
- exportToClipboard()
- return true
+ 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@PerAppProxyActivity,
+ 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@PerAppProxyActivity,
+ 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@PerAppProxyActivity,
+ R.string.toast_imported_from_clipboard,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
}
R.id.action_scan_china_apps -> {
scanChinaApps()
- return true
}
}
- return super.onOptionsItemSelected(item)
+ return true
}
@SuppressLint("NotifyDataSetChanged")
- private fun filterApps() {
- filteredAppList.clear()
- if (searchKeyword.isNotEmpty()) {
- filteredAppList.addAll(appList.filter {
- (!hideSystem || !it.isSystemApp) &&
- (it.name.contains(searchKeyword, true)
- || it.packageName.contains(searchKeyword, true))
- })
- adapter.notifyDataSetChanged()
- } else if (hideSystem) {
- filteredAppList.addAll(appList.filter { !it.isSystemApp })
- } else {
- filteredAppList.addAll(appList)
- }
- adapter.notifyDataSetChanged()
- }
-
- private fun importFromClipboard() {
- val clipboardManager = getSystemService()!!
- if (!clipboardManager.hasPrimaryClip()) {
- Toast.makeText(this, R.string.toast_clipboard_empty, Toast.LENGTH_SHORT).show()
- return
- }
- val content = clipboardManager.primaryClip?.getItemAt(0)?.text?.split("\n")?.distinct()
- if (content.isNullOrEmpty()) {
- Toast.makeText(this, R.string.toast_clipboard_empty, Toast.LENGTH_SHORT).show()
- return
- }
- perAppProxyList.clear()
- perAppProxyList.addAll(content)
- loadAppList()
- Toast.makeText(this, R.string.toast_imported_from_clipboard, Toast.LENGTH_SHORT).show()
- }
-
- private fun exportToClipboard() {
- if (perAppProxyList.isEmpty()) {
- Toast.makeText(this, R.string.toast_app_list_empty, Toast.LENGTH_SHORT).show()
- return
- }
- val content = perAppProxyList.joinToString("\n")
- clipboardText = content
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- Toast.makeText(this, R.string.toast_copied_to_clipboard, Toast.LENGTH_SHORT).show()
- }
- }
-
private fun scanChinaApps() {
- val progressDialog = MaterialAlertDialogBuilder(this)
- .setView(R.layout.dialog_progress)
- .setCancelable(false)
- .create()
- progressDialog.setOnShowListener {
- val dialog = it as Dialog
- dialog.findViewById(R.id.text_message).setText(R.string.message_scanning)
- }
- progressDialog.show()
-
+ 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 scanResult = withContext(Dispatchers.IO) {
- val appNameMap = mutableMapOf()
- appList.forEach {
- appNameMap[it.packageName] = it.name
- }
- val foundChinaApps = mutableMapOf()
- scanChinaApps(appList.map { it.packageName }).forEach { packageName ->
- foundChinaApps[packageName] = appNameMap[packageName] ?: "Unknown"
- }
- foundChinaApps
- }
- progressDialog.dismiss()
-
- if (scanResult.isEmpty()) {
- MaterialAlertDialogBuilder(this@PerAppProxyActivity)
- .setTitle(R.string.title_scan_result)
- .setMessage(R.string.message_scan_app_no_apps_found)
- .setPositiveButton(R.string.ok, null)
- .show()
- return@launch
- }
-
- val dialogContent = getString(R.string.message_scan_app_found) + "\n\n" +
- scanResult.entries.joinToString("\n") {
- "${it.value} (${it.key})"
+ 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
+ }
}
- MaterialAlertDialogBuilder(this@PerAppProxyActivity)
- .setTitle(R.string.title_scan_result)
- .setMessage(dialogContent)
- .setPositiveButton(R.string.action_select) { dialog, _ ->
- perAppProxyList.addAll(scanResult.keys)
- Settings.perAppProxyList = perAppProxyList
- loadAppList()
- dialog.dismiss()
}
- .setNegativeButton(R.string.action_deselect) { dialog, _ ->
- perAppProxyList.removeAll(scanResult.keys)
- Settings.perAppProxyList = perAppProxyList
- loadAppList()
- dialog.dismiss()
+ }
+ withContext(Dispatchers.Main) {
+ progress.dismiss()
+ if (foundApps.isEmpty()) {
+ MaterialAlertDialogBuilder(this@PerAppProxyActivity).setTitle(R.string.title_scan_result)
+ .setMessage(R.string.message_scan_app_no_apps_found)
+ .setPositiveButton(R.string.ok, null).show()
+ return@withContext
}
- .setNeutralButton(android.R.string.cancel, null)
- .show()
+ val dialogContent =
+ getString(R.string.message_scan_app_found) + "\n\n" + foundApps.entries.joinToString(
+ "\n"
+ ) {
+ "${it.value.applicationLabel} (${it.key})"
+ }
+ MaterialAlertDialogBuilder(this@PerAppProxyActivity).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 const val TAG = "PerAppProxyActivity"
- fun scanChinaApps(packageNameList: List): List {
- val chinaAppPrefixList = try {
+ private val chinaAppPrefixList by lazy {
+ runCatching {
Application.application.assets.open("prefix-china-apps.txt").reader().readLines()
- } catch (e: Exception) {
- Log.w(
- TAG,
- "scan china apps: failed to load prefix from assets, error = ${e.message}"
- )
- null
- }
- if (chinaAppPrefixList.isNullOrEmpty()) {
- return listOf()
- }
- val chinaAppRegex =
- ("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
+ }.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
+ 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
+ @Suppress("DEPRECATION") PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
- val foundChinaApps = mutableListOf()
- for (packageName in packageNameList) {
- if (packageName == "android") {
- continue
+ 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 (packageName.matches(chinaAppRegex)) {
- foundChinaApps.add(packageName)
- continue
+ 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
}
-
- try {
- val packageInfo =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Application.packageManager.getPackageInfo(
- packageName,
- 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) {
- foundChinaApps.add(packageName)
- continue
- }
- val packageFile = ZipFile(File(packageInfo.applicationInfo.publicSourceDir))
- for (packageEntry in packageFile.entries()) {
+ ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
+ for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
@@ -380,105 +642,28 @@ class PerAppProxyActivity : AbstractActivity() {
continue
}
if (packageEntry.size > 15000000) {
- foundChinaApps.add(packageName)
- break
+ return true
}
- val input = packageFile.getInputStream(packageEntry).buffered()
+ val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
- foundChinaApps.add(packageName)
- Log.w(
- TAG,
- "scan china apps: failed to read dex file, error = ${e.message}"
- )
- break
+ return false
}
for (clazz in dexFile.classes) {
- val clazzName = clazz.type.substring(1, clazz.type.length - 1)
- .replace("/", ".")
- .replace("$", ".")
+ val clazzName =
+ clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
+ .replace("$", ".")
if (clazzName.matches(chinaAppRegex)) {
- foundChinaApps.add(packageName)
- break
+ return true
}
}
}
- packageFile.close()
- } catch (e: Exception) {
- Log.w(
- TAG,
- "scan china apps: something went wrong when scanning package ${packageName}, error = ${e.message}"
- )
- continue
}
- System.gc()
+ } catch (ignored: Exception) {
}
- return foundChinaApps
+ return false
}
}
- data class AppItem(
- val packageName: String,
- val name: String,
- val icon: Drawable,
- val isSystemApp: Boolean,
- var selected: Boolean
- )
-
- class AppListAdapter(
- private val list: List,
- private val onSelectChange: (Int) -> Unit
- ) :
- RecyclerView.Adapter() {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppListViewHolder {
- val binding =
- ViewAppListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return AppListViewHolder(binding)
- }
-
- override fun getItemCount(): Int {
- return list.size
- }
-
- override fun onBindViewHolder(holder: AppListViewHolder, position: Int) {
- val item = list[position]
- holder.bind(item)
- holder.itemView.setOnClickListener {
- item.selected = !item.selected
- onSelectChange.invoke(position)
- notifyItemChanged(position, "check")
- }
- }
-
- override fun onBindViewHolder(
- holder: AppListViewHolder,
- position: Int,
- payloads: MutableList
- ) {
- if (payloads.contains("check")) {
- holder.bindCheck(list[position])
- } else {
- super.onBindViewHolder(holder, position, payloads)
- }
- }
-
- }
-
- class AppListViewHolder(
- private val binding: ViewAppListItemBinding
- ) : RecyclerView.ViewHolder(binding.root) {
-
- fun bind(item: AppItem) {
- 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.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
deleted file mode 100644
index 3b2b82f..0000000
--- a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity0.kt
+++ /dev/null
@@ -1,669 +0,0 @@
-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 1598b53..8c8d1c4 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
@@ -38,7 +38,7 @@ class ProfileOverrideActivity :
}
binding.configureAppListButton.setOnClickListener {
- startActivity(Intent(this, PerAppProxyActivity0::class.java))
+ startActivity(Intent(this, PerAppProxyActivity::class.java))
}
lifecycleScope.launch(Dispatchers.IO) {
reloadSettings()
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 8187229..4c71b98 100644
--- a/app/src/main/res/layout/activity_per_app_proxy.xml
+++ b/app/src/main/res/layout/activity_per_app_proxy.xml
@@ -1,6 +1,7 @@
@@ -11,56 +12,44 @@
android:background="?colorSurfaceContainer"
android:fitsSystemWindows="true">
-
+ android:layout_height="?actionBarSize" />
-
+
+
+
+
+ android:text="@string/per_app_proxy_mode_include" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:listitem="@layout/view_app_list_item" />
\ 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
deleted file mode 100644
index 4c71b98..0000000
--- a/app/src/main/res/layout/activity_per_app_proxy0.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_dashboard_group_item.xml b/app/src/main/res/layout/view_dashboard_group_item.xml
index 8dba8b1..ebba5a8 100644
--- a/app/src/main/res/layout/view_dashboard_group_item.xml
+++ b/app/src/main/res/layout/view_dashboard_group_item.xml
@@ -6,9 +6,10 @@
style="?materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_margin="4dp"
app:cardBackgroundColor="?colorSurfaceContainer"
app:cardCornerRadius="0dp"
- app:cardElevation="0dp">
+ app:cardElevation="4dp">