Improve per proxy app selector
This commit is contained in:
@@ -2,6 +2,7 @@ package io.nekohasekai.sfa
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
@@ -48,6 +49,7 @@ class Application : Application() {
|
||||
val powerManager by lazy { application.getSystemService<PowerManager>()!! }
|
||||
val notificationManager by lazy { application.getSystemService<NotificationManager>()!! }
|
||||
val wifiManager by lazy { application.getSystemService<WifiManager>()!! }
|
||||
val clipboard by lazy { application.getSystemService<ClipboardManager>()!! }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
12
app/src/main/java/io/nekohasekai/sfa/ktx/Clips.kt
Normal file
12
app/src/main/java/io/nekohasekai/sfa/ktx/Clips.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package io.nekohasekai.sfa.ktx
|
||||
|
||||
import android.content.ClipData
|
||||
import io.nekohasekai.sfa.Application
|
||||
|
||||
var clipboardText: String?
|
||||
get() = Application.clipboard.primaryClip?.getItemAt(0)?.text?.toString()
|
||||
set(plainText) {
|
||||
if (plainText != null) {
|
||||
Application.clipboard.setPrimaryClip(ClipData.newPlainText(null, plainText))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package io.nekohasekai.sfa.ktx
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
|
||||
fun AbstractActivity.startFilesForResult(
|
||||
fun Activity.startFilesForResult(
|
||||
launcher: ActivityResultLauncher<String>, input: String
|
||||
) {
|
||||
try {
|
||||
|
||||
@@ -53,7 +53,7 @@ import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.LinkedList
|
||||
|
||||
class MainActivity : AbstractActivity(),
|
||||
class MainActivity : AbstractActivity<ActivityMainBinding>(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
||||
ServiceConnection.Callback {
|
||||
|
||||
@@ -61,7 +61,6 @@ class MainActivity : AbstractActivity(),
|
||||
private const val TAG = "MainActivity"
|
||||
}
|
||||
|
||||
internal lateinit var binding: ActivityMainBinding
|
||||
private val connection = ServiceConnection(this, this)
|
||||
|
||||
val logList = LinkedList<String>()
|
||||
@@ -71,10 +70,6 @@ class MainActivity : AbstractActivity(),
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
val navController = findNavController(R.id.nav_host_fragment_activity_my)
|
||||
navController.setGraph(R.navigation.mobile_navigation)
|
||||
navController.navigate(R.id.navigation_dashboard)
|
||||
@@ -103,6 +98,7 @@ class MainActivity : AbstractActivity(),
|
||||
navDestination: NavDestination,
|
||||
bundle: Bundle?
|
||||
) {
|
||||
val binding = binding ?: return
|
||||
val destinationId = navDestination.id
|
||||
binding.dashboardTabContainer.isVisible = destinationId == R.id.navigation_dashboard
|
||||
}
|
||||
|
||||
@@ -6,19 +6,12 @@ import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.databinding.ActivityDebugBinding
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
|
||||
class DebugActivity : AbstractActivity() {
|
||||
|
||||
private var binding: ActivityDebugBinding? = null
|
||||
class DebugActivity : AbstractActivity<ActivityDebugBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_debug)
|
||||
val binding = ActivityDebugBinding.inflate(layoutInflater)
|
||||
this.binding = binding
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
binding.scanVPNButton.setOnClickListener {
|
||||
startActivity(Intent(this, VPNScanActivity::class.java))
|
||||
}
|
||||
|
||||
@@ -25,19 +25,14 @@ import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VPNScanActivity : AbstractActivity() {
|
||||
class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
||||
|
||||
private var binding: ActivityVpnScanBinding? = null
|
||||
private var adapter: Adapter? = null
|
||||
private val appInfoList = mutableListOf<AppInfo>()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_scan_vpn)
|
||||
val binding = ActivityVpnScanBinding.inflate(layoutInflater)
|
||||
this.binding = binding
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
binding.scanVPNResult.adapter = Adapter().also {
|
||||
adapter = it
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import io.nekohasekai.sfa.databinding.ActivityEditProfileBinding
|
||||
import io.nekohasekai.sfa.ktx.addTextChangedListener
|
||||
import io.nekohasekai.sfa.ktx.errorDialogBuilder
|
||||
import io.nekohasekai.sfa.ktx.setSimpleItems
|
||||
import io.nekohasekai.sfa.ktx.shareProfile
|
||||
import io.nekohasekai.sfa.ktx.text
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
import io.nekohasekai.sfa.utils.HTTPClient
|
||||
@@ -28,20 +27,13 @@ import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
class EditProfileActivity : AbstractActivity() {
|
||||
class EditProfileActivity : AbstractActivity<ActivityEditProfileBinding>() {
|
||||
|
||||
private var binding: ActivityEditProfileBinding? = null
|
||||
private var _profile: Profile? = null
|
||||
private val profile get() = _profile!!
|
||||
private lateinit var profile: Profile
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_edit_profile)
|
||||
val binding = ActivityEditProfileBinding.inflate(layoutInflater)
|
||||
this.binding = binding
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
loadProfile()
|
||||
@@ -55,18 +47,11 @@ class EditProfileActivity : AbstractActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
}
|
||||
|
||||
private suspend fun loadProfile() {
|
||||
val binding = binding ?: return
|
||||
delay(200L)
|
||||
|
||||
val profileId = intent.getLongExtra("profile_id", -1L)
|
||||
if (profileId == -1L) error("invalid arguments")
|
||||
_profile = ProfileManager.get(profileId) ?: error("invalid arguments")
|
||||
profile = ProfileManager.get(profileId) ?: error("invalid arguments")
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.name.text = profile.name
|
||||
binding.name.addTextChangedListener {
|
||||
@@ -203,16 +188,4 @@ class EditProfileActivity : AbstractActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareProfile(button: View) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
shareProfile(profile)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
errorDialogBuilder(e).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,28 +21,17 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class EditProfileContentActivity : AbstractActivity() {
|
||||
class EditProfileContentActivity : AbstractActivity<ActivityEditProfileContentBinding>() {
|
||||
|
||||
private var binding: ActivityEditProfileContentBinding? = null
|
||||
private var _profile: Profile? = null
|
||||
private val profile get() = _profile!!
|
||||
private var profile: Profile? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_edit_configuration)
|
||||
val binding = ActivityEditProfileContentBinding.inflate(layoutInflater)
|
||||
this.binding = binding
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
binding.editor.language = JsonLanguage()
|
||||
loadConfiguration()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
}
|
||||
|
||||
private fun loadConfiguration() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
@@ -120,7 +109,8 @@ class EditProfileContentActivity : AbstractActivity() {
|
||||
|
||||
val profileId = intent.getLongExtra("profile_id", -1L)
|
||||
if (profileId == -1L) error("invalid arguments")
|
||||
_profile = ProfileManager.get(profileId) ?: error("invalid arguments")
|
||||
val profile = ProfileManager.get(profileId) ?: error("invalid arguments")
|
||||
this.profile = profile
|
||||
val content = File(profile.typed.path).readText()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.editor.setTextContent(content)
|
||||
|
||||
@@ -28,13 +28,12 @@ import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.Date
|
||||
|
||||
class NewProfileActivity : AbstractActivity() {
|
||||
class NewProfileActivity : AbstractActivity<ActivityAddProfileBinding>() {
|
||||
enum class FileSource(val formatted: String) {
|
||||
CreateNew("Create New"),
|
||||
Import("Import");
|
||||
}
|
||||
|
||||
private var binding: ActivityAddProfileBinding? = null
|
||||
private val importFile =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) { fileURI ->
|
||||
val binding = binding ?: return@registerForActivityResult
|
||||
@@ -47,10 +46,6 @@ class NewProfileActivity : AbstractActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_new_profile)
|
||||
val binding = ActivityAddProfileBinding.inflate(layoutInflater)
|
||||
this.binding = binding
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
intent.getStringExtra("importName")?.also { importName ->
|
||||
intent.getStringExtra("importURL")?.also { importURL ->
|
||||
@@ -100,11 +95,6 @@ class NewProfileActivity : AbstractActivity() {
|
||||
binding.autoUpdateInterval.addTextChangedListener(this::updateAutoUpdateInterval)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
}
|
||||
|
||||
private fun createProfile(view: View) {
|
||||
val binding = binding ?: return
|
||||
if (binding.name.showErrorIfEmpty()) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.nekohasekai.sfa.ui.profileoverride
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
@@ -20,7 +19,6 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.isGone
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -29,6 +27,7 @@ import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.databinding.ActivityPerAppProxyBinding
|
||||
import io.nekohasekai.sfa.databinding.ViewAppListItemBinding
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -37,10 +36,9 @@ import org.jf.dexlib2.dexbacked.DexBackedDexFile
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class PerAppProxyActivity : AbstractActivity() {
|
||||
class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
||||
|
||||
|
||||
private lateinit var binding: ActivityPerAppProxyBinding
|
||||
private lateinit var adapter: AppListAdapter
|
||||
|
||||
private val perAppProxyList = mutableSetOf<String>()
|
||||
@@ -52,10 +50,8 @@ class PerAppProxyActivity : AbstractActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_per_app_proxy)
|
||||
binding = ActivityPerAppProxyBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val proxyMode = Settings.perAppProxyMode
|
||||
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||
@@ -87,8 +83,8 @@ class PerAppProxyActivity : AbstractActivity() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun loadAppList() {
|
||||
binding.recyclerViewAppList.isGone = true
|
||||
binding.layoutProgress.isGone = false
|
||||
// binding.recyclerViewAppList.isGone = true
|
||||
// binding.layoutProgress.isGone = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
@@ -142,8 +138,8 @@ class PerAppProxyActivity : AbstractActivity() {
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
binding.recyclerViewAppList.scrollToPosition(0)
|
||||
binding.layoutProgress.isGone = true
|
||||
binding.recyclerViewAppList.isGone = false
|
||||
// binding.layoutProgress.isGone = true
|
||||
// binding.recyclerViewAppList.isGone = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +179,7 @@ class PerAppProxyActivity : AbstractActivity() {
|
||||
|
||||
R.id.action_import -> {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.menu_import_from_clipboard)
|
||||
.setTitle(R.string.per_app_proxy_import)
|
||||
.setMessage(R.string.message_import_from_clipboard)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
importFromClipboard()
|
||||
@@ -247,9 +243,7 @@ class PerAppProxyActivity : AbstractActivity() {
|
||||
return
|
||||
}
|
||||
val content = perAppProxyList.joinToString("\n")
|
||||
val clipboardManager = getSystemService<ClipboardManager>()!!
|
||||
val clip = ClipData.newPlainText(null, content)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
clipboardText = content
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
Toast.makeText(this, R.string.toast_copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -477,14 +471,14 @@ class PerAppProxyActivity : AbstractActivity() {
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: AppItem) {
|
||||
binding.imageAppIcon.setImageDrawable(item.icon)
|
||||
binding.textAppName.text = item.name
|
||||
binding.textAppPackageName.text = item.packageName
|
||||
binding.checkboxAppSelected.isChecked = item.selected
|
||||
binding.appIcon.setImageDrawable(item.icon)
|
||||
binding.applicationLabel.text = item.name
|
||||
binding.packageName.text = item.packageName
|
||||
binding.selected.isChecked = item.selected
|
||||
}
|
||||
|
||||
fun bindCheck(item: AppItem) {
|
||||
binding.checkboxAppSelected.isChecked = item.selected
|
||||
binding.selected.isChecked = item.selected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
package io.nekohasekai.sfa.ui.profileoverride
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.databinding.ActivityPerAppProxy0Binding
|
||||
import io.nekohasekai.sfa.databinding.DialogProgressbarBinding
|
||||
import io.nekohasekai.sfa.databinding.ViewAppListItemBinding
|
||||
import io.nekohasekai.sfa.ktx.clipboardText
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jf.dexlib2.dexbacked.DexBackedDexFile
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class PerAppProxyActivity0 : AbstractActivity<ActivityPerAppProxy0Binding>() {
|
||||
enum class SortMode {
|
||||
NAME, PACKAGE_NAME, UID, INSTALL_TIME, UPDATE_TIME,
|
||||
}
|
||||
|
||||
private var proxyMode = Settings.PER_APP_PROXY_INCLUDE
|
||||
private var sortMode = SortMode.NAME
|
||||
private var sortReverse = false
|
||||
private var hideSystemApps = false
|
||||
private var hideOfflineApps = true
|
||||
private var hideDisabledApps = true
|
||||
|
||||
inner class PackageCache(
|
||||
private val packageInfo: PackageInfo,
|
||||
) {
|
||||
|
||||
val packageName: String get() = packageInfo.packageName
|
||||
|
||||
val uid get() = packageInfo.applicationInfo.uid
|
||||
|
||||
val installTime get() = packageInfo.firstInstallTime
|
||||
val updateTime get() = packageInfo.lastUpdateTime
|
||||
val isSystem get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
|
||||
val isOffline get() = packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) != true
|
||||
val isDisabled get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0
|
||||
|
||||
val applicationIcon by lazy {
|
||||
packageInfo.applicationInfo.loadIcon(packageManager)
|
||||
}
|
||||
|
||||
val applicationLabel by lazy {
|
||||
packageInfo.applicationInfo.loadLabel(packageManager).toString()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var adapter: ApplicationAdapter
|
||||
private var packages = listOf<PackageCache>()
|
||||
private var displayPackages = listOf<PackageCache>()
|
||||
private var currentPackages = listOf<PackageCache>()
|
||||
private var selectedUIDs = mutableSetOf<Int>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.title_per_app_proxy)
|
||||
|
||||
lifecycleScope.launch {
|
||||
proxyMode = if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
|
||||
Settings.PER_APP_PROXY_EXCLUDE
|
||||
} else {
|
||||
Settings.PER_APP_PROXY_INCLUDE
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (proxyMode != Settings.PER_APP_PROXY_EXCLUDE) {
|
||||
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_include_description)
|
||||
} else {
|
||||
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
|
||||
}
|
||||
}
|
||||
reloadApplicationList()
|
||||
filterApplicationList()
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter = ApplicationAdapter(displayPackages)
|
||||
binding.appList.adapter = adapter
|
||||
delay(500L)
|
||||
binding.progress.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reloadApplicationList() {
|
||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
} else {
|
||||
@Suppress("DEPRECATION") PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
}
|
||||
val installedPackages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(
|
||||
packageManagerFlags.toLong()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") packageManager.getInstalledPackages(packageManagerFlags)
|
||||
}
|
||||
val packages = mutableListOf<PackageCache>()
|
||||
for (packageInfo in installedPackages) {
|
||||
if (packageInfo.packageName == packageName) continue
|
||||
packages.add(PackageCache(packageInfo))
|
||||
}
|
||||
val selectedPackageNames = Settings.perAppProxyList.toMutableSet()
|
||||
val selectedUIDs = mutableSetOf<Int>()
|
||||
for (packageCache in packages) {
|
||||
if (selectedPackageNames.contains(packageCache.packageName)) {
|
||||
selectedUIDs.add(packageCache.uid)
|
||||
}
|
||||
}
|
||||
this.packages = packages
|
||||
this.selectedUIDs = selectedUIDs
|
||||
}
|
||||
|
||||
private fun filterApplicationList(selectedUIDs: Set<Int> = this.selectedUIDs) {
|
||||
val displayPackages = mutableListOf<PackageCache>()
|
||||
for (packageCache in packages) {
|
||||
if (hideSystemApps && packageCache.isSystem) continue
|
||||
if (hideOfflineApps && packageCache.isOffline) continue
|
||||
if (hideDisabledApps && packageCache.isDisabled) continue
|
||||
displayPackages.add(packageCache)
|
||||
}
|
||||
displayPackages.sortWith(compareBy<PackageCache> {
|
||||
!selectedUIDs.contains(it.uid)
|
||||
}.let {
|
||||
if (!sortReverse) it.thenBy {
|
||||
when (sortMode) {
|
||||
SortMode.NAME -> it.applicationLabel
|
||||
SortMode.PACKAGE_NAME -> it.packageName
|
||||
SortMode.UID -> it.uid
|
||||
SortMode.INSTALL_TIME -> it.installTime
|
||||
SortMode.UPDATE_TIME -> it.updateTime
|
||||
}
|
||||
} else it.thenByDescending {
|
||||
when (sortMode) {
|
||||
SortMode.NAME -> it.applicationLabel
|
||||
SortMode.PACKAGE_NAME -> it.packageName
|
||||
SortMode.UID -> it.uid
|
||||
SortMode.INSTALL_TIME -> it.installTime
|
||||
SortMode.UPDATE_TIME -> it.updateTime
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.displayPackages = displayPackages
|
||||
this.currentPackages = displayPackages
|
||||
}
|
||||
|
||||
private fun updateApplicationSelection(packageCache: PackageCache, selected: Boolean) {
|
||||
val performed = if (selected) {
|
||||
selectedUIDs.add(packageCache.uid)
|
||||
} else {
|
||||
selectedUIDs.remove(packageCache.uid)
|
||||
}
|
||||
if (!performed) return
|
||||
currentPackages.forEachIndexed { index, it ->
|
||||
if (it.uid == packageCache.uid) {
|
||||
adapter.notifyItemChanged(index, PayloadUpdateSelection(selected))
|
||||
}
|
||||
}
|
||||
saveSelectedApplications()
|
||||
}
|
||||
|
||||
data class PayloadUpdateSelection(val selected: Boolean)
|
||||
|
||||
inner class ApplicationAdapter(private var applicationList: List<PackageCache>) :
|
||||
RecyclerView.Adapter<ApplicationViewHolder>() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setApplicationList(applicationList: List<PackageCache>) {
|
||||
this.applicationList = applicationList
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup, viewType: Int
|
||||
): ApplicationViewHolder {
|
||||
return ApplicationViewHolder(
|
||||
ViewAppListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return applicationList.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ApplicationViewHolder, position: Int
|
||||
) {
|
||||
holder.bind(applicationList[position])
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ApplicationViewHolder, position: Int, payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
payloads.forEach {
|
||||
when (it) {
|
||||
is PayloadUpdateSelection -> holder.updateSelection(it.selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ApplicationViewHolder(
|
||||
private val binding: ViewAppListItemBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(packageCache: PackageCache) {
|
||||
binding.appIcon.setImageDrawable(packageCache.applicationIcon)
|
||||
binding.applicationLabel.text = packageCache.applicationLabel
|
||||
binding.packageName.text = "${packageCache.packageName} (${packageCache.uid})"
|
||||
binding.selected.isChecked = selectedUIDs.contains(packageCache.uid)
|
||||
binding.root.setOnClickListener {
|
||||
updateApplicationSelection(packageCache, !binding.selected.isChecked)
|
||||
}
|
||||
binding.root.setOnLongClickListener {
|
||||
val popup = PopupMenu(it.context, it)
|
||||
popup.setForceShowIcon(true)
|
||||
popup.gravity = Gravity.END
|
||||
popup.menuInflater.inflate(R.menu.app_menu, popup.menu)
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_copy_application_label -> {
|
||||
clipboardText = packageCache.applicationLabel
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_copy_package_name -> {
|
||||
clipboardText = packageCache.packageName
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_copy_uid -> {
|
||||
clipboardText = packageCache.uid.toString()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
popup.show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelection(selected: Boolean) {
|
||||
binding.selected.isChecked = selected
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchApplications(searchText: String) {
|
||||
currentPackages = if (searchText.isEmpty()) {
|
||||
displayPackages
|
||||
} else {
|
||||
displayPackages.filter {
|
||||
it.applicationLabel.contains(
|
||||
searchText, ignoreCase = true
|
||||
) || it.packageName.contains(
|
||||
searchText, ignoreCase = true
|
||||
) || it.uid.toString().contains(searchText)
|
||||
}
|
||||
}
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.per_app_menu0, menu)
|
||||
|
||||
if (menu != null) {
|
||||
val searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
searchApplications(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
searchView.setOnCloseListener {
|
||||
searchApplications("")
|
||||
true
|
||||
}
|
||||
when (proxyMode) {
|
||||
Settings.PER_APP_PROXY_INCLUDE -> {
|
||||
menu.findItem(R.id.action_mode_include).isChecked = true
|
||||
}
|
||||
|
||||
Settings.PER_APP_PROXY_EXCLUDE -> {
|
||||
menu.findItem(R.id.action_mode_exclude).isChecked = true
|
||||
}
|
||||
}
|
||||
when (sortMode) {
|
||||
SortMode.NAME -> {
|
||||
menu.findItem(R.id.action_sort_by_name).isChecked = true
|
||||
}
|
||||
|
||||
SortMode.PACKAGE_NAME -> {
|
||||
menu.findItem(R.id.action_sort_by_package_name).isChecked = true
|
||||
}
|
||||
|
||||
SortMode.UID -> {
|
||||
menu.findItem(R.id.action_sort_by_uid).isChecked = true
|
||||
}
|
||||
|
||||
SortMode.INSTALL_TIME -> {
|
||||
menu.findItem(R.id.action_sort_by_install_time).isChecked = true
|
||||
}
|
||||
|
||||
SortMode.UPDATE_TIME -> {
|
||||
menu.findItem(R.id.action_sort_by_update_time).isChecked = true
|
||||
}
|
||||
}
|
||||
menu.findItem(R.id.action_sort_reverse).isChecked = sortReverse
|
||||
menu.findItem(R.id.action_hide_system_apps).isChecked = hideSystemApps
|
||||
menu.findItem(R.id.action_hide_offline_apps).isChecked = hideOfflineApps
|
||||
menu.findItem(R.id.action_hide_disabled_apps).isChecked = hideDisabledApps
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_mode_include -> {
|
||||
item.isChecked = true
|
||||
proxyMode = Settings.PER_APP_PROXY_INCLUDE
|
||||
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_include_description)
|
||||
lifecycleScope.launch {
|
||||
Settings.perAppProxyMode = Settings.PER_APP_PROXY_INCLUDE
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_mode_exclude -> {
|
||||
item.isChecked = true
|
||||
proxyMode = Settings.PER_APP_PROXY_EXCLUDE
|
||||
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
|
||||
lifecycleScope.launch {
|
||||
Settings.perAppProxyMode = Settings.PER_APP_PROXY_EXCLUDE
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_sort_by_name -> {
|
||||
item.isChecked = true
|
||||
sortMode = SortMode.NAME
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_sort_by_package_name -> {
|
||||
item.isChecked = true
|
||||
sortMode = SortMode.PACKAGE_NAME
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_sort_by_uid -> {
|
||||
item.isChecked = true
|
||||
sortMode = SortMode.UID
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_sort_by_install_time -> {
|
||||
item.isChecked = true
|
||||
sortMode = SortMode.INSTALL_TIME
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_sort_by_update_time -> {
|
||||
item.isChecked = true
|
||||
sortMode = SortMode.UPDATE_TIME
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_sort_reverse -> {
|
||||
item.isChecked = !item.isChecked
|
||||
sortReverse = item.isChecked
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_hide_system_apps -> {
|
||||
item.isChecked = !item.isChecked
|
||||
hideSystemApps = item.isChecked
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_hide_offline_apps -> {
|
||||
item.isChecked = !item.isChecked
|
||||
hideOfflineApps = item.isChecked
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_hide_disabled_apps -> {
|
||||
item.isChecked = !item.isChecked
|
||||
hideDisabledApps = item.isChecked
|
||||
filterApplicationList()
|
||||
adapter.setApplicationList(currentPackages)
|
||||
}
|
||||
|
||||
R.id.action_select_all -> {
|
||||
val selectedUIDs = mutableSetOf<Int>()
|
||||
for (packageCache in packages) {
|
||||
selectedUIDs.add(packageCache.uid)
|
||||
}
|
||||
this.selectedUIDs = selectedUIDs
|
||||
saveSelectedApplications()
|
||||
}
|
||||
|
||||
R.id.action_deselect_all -> {
|
||||
selectedUIDs = mutableSetOf()
|
||||
saveSelectedApplications()
|
||||
}
|
||||
|
||||
R.id.action_export -> {
|
||||
lifecycleScope.launch {
|
||||
val packageList = mutableListOf<String>()
|
||||
for (packageCache in packages) {
|
||||
if (selectedUIDs.contains(packageCache.uid)) {
|
||||
packageList.add(packageCache.packageName)
|
||||
}
|
||||
}
|
||||
clipboardText = packageList.joinToString("\n")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@PerAppProxyActivity0,
|
||||
R.string.toast_copied_to_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_import -> {
|
||||
val packageNames = clipboardText?.split("\n")?.distinct()
|
||||
?.takeIf { it.isNotEmpty() && it[0].isNotEmpty() }
|
||||
if (packageNames.isNullOrEmpty()) {
|
||||
Toast.makeText(
|
||||
this@PerAppProxyActivity0,
|
||||
R.string.toast_clipboard_empty,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return true
|
||||
}
|
||||
val selectedUIDs = mutableSetOf<Int>()
|
||||
for (packageCache in packages) {
|
||||
if (packageNames.contains(packageCache.packageName)) {
|
||||
selectedUIDs.add(packageCache.uid)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
postSaveSelectedApplications(selectedUIDs)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
this@PerAppProxyActivity0,
|
||||
R.string.toast_imported_from_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
R.id.action_scan_china_apps -> {
|
||||
scanChinaApps()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun scanChinaApps() {
|
||||
val binding = DialogProgressbarBinding.inflate(layoutInflater)
|
||||
binding.progress.max = currentPackages.size
|
||||
binding.message.setText(R.string.message_scanning)
|
||||
val progress = MaterialAlertDialogBuilder(
|
||||
this, com.google.android.material.R.style.Theme_MaterialComponents_Dialog
|
||||
).setView(binding.root).setCancelable(false).create()
|
||||
progress.show()
|
||||
lifecycleScope.launch {
|
||||
val foundApps = withContext(Dispatchers.IO) {
|
||||
mutableMapOf<String, PackageCache>().also { foundApps ->
|
||||
currentPackages.forEachIndexed { index, it ->
|
||||
if (scanChinaPackage(it.packageName)) {
|
||||
foundApps[it.packageName] = it
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.progress.progress = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
progress.dismiss()
|
||||
if (foundApps.isEmpty()) {
|
||||
MaterialAlertDialogBuilder(this@PerAppProxyActivity0).setTitle(R.string.title_scan_result)
|
||||
.setMessage(R.string.message_scan_app_no_apps_found)
|
||||
.setPositiveButton(R.string.ok, null).show()
|
||||
return@withContext
|
||||
}
|
||||
val dialogContent =
|
||||
getString(R.string.message_scan_app_found) + "\n\n" + foundApps.entries.joinToString(
|
||||
"\n"
|
||||
) {
|
||||
"${it.value.applicationLabel} (${it.key})"
|
||||
}
|
||||
MaterialAlertDialogBuilder(this@PerAppProxyActivity0).setTitle(R.string.title_scan_result)
|
||||
.setMessage(dialogContent)
|
||||
.setPositiveButton(R.string.action_select) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
lifecycleScope.launch {
|
||||
val selectedUIDs = selectedUIDs.toMutableSet()
|
||||
foundApps.values.forEach {
|
||||
selectedUIDs.add(it.uid)
|
||||
}
|
||||
postSaveSelectedApplications(selectedUIDs)
|
||||
}
|
||||
}.setNegativeButton(R.string.action_deselect) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
lifecycleScope.launch {
|
||||
val selectedUIDs = selectedUIDs.toMutableSet()
|
||||
foundApps.values.forEach {
|
||||
selectedUIDs.remove(it.uid)
|
||||
}
|
||||
postSaveSelectedApplications(selectedUIDs)
|
||||
}
|
||||
}.setNeutralButton(android.R.string.cancel, null).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private suspend fun postSaveSelectedApplications(newUIDs: MutableSet<Int>) {
|
||||
filterApplicationList(newUIDs)
|
||||
withContext(Dispatchers.Main) {
|
||||
selectedUIDs = newUIDs
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
val packageList = selectedUIDs.mapNotNull { uid ->
|
||||
packages.find { it.uid == uid }?.packageName
|
||||
}
|
||||
Settings.perAppProxyList = packageList.toSet()
|
||||
}
|
||||
|
||||
private fun saveSelectedApplications() {
|
||||
lifecycleScope.launch {
|
||||
val packageList = selectedUIDs.mapNotNull { uid ->
|
||||
packages.find { it.uid == uid }?.packageName
|
||||
}
|
||||
Settings.perAppProxyList = packageList.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val chinaAppPrefixList by lazy {
|
||||
runCatching {
|
||||
Application.application.assets.open("prefix-china-apps.txt").reader().readLines()
|
||||
}.getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
private val chinaAppRegex by lazy {
|
||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||
}
|
||||
|
||||
fun scanChinaPackage(packageName: String): Boolean {
|
||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION") PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
if (packageName.matches(chinaAppRegex)) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Application.packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") Application.packageManager.getPackageInfo(
|
||||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
if (packageInfo.services?.find { it.name.matches(chinaAppRegex) } != null || packageInfo.activities?.find {
|
||||
it.name.matches(
|
||||
chinaAppRegex
|
||||
)
|
||||
} != null || packageInfo.receivers?.find { it.name.matches(chinaAppRegex) } != null || packageInfo.providers?.find {
|
||||
it.name.matches(
|
||||
chinaAppRegex
|
||||
)
|
||||
} != null) {
|
||||
return true
|
||||
}
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,17 +15,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ProfileOverrideActivity : AbstractActivity() {
|
||||
|
||||
private lateinit var binding: ActivityConfigOverrideBinding
|
||||
class ProfileOverrideActivity :
|
||||
AbstractActivity<ActivityConfigOverrideBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setTitle(R.string.title_profile_override)
|
||||
binding = ActivityConfigOverrideBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
setTitle(R.string.title_profile_override)
|
||||
binding.switchPerAppProxy.isChecked = Settings.perAppProxyEnabled
|
||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||
Settings.perAppProxyEnabled = isChecked
|
||||
@@ -42,7 +38,7 @@ class ProfileOverrideActivity : AbstractActivity() {
|
||||
}
|
||||
|
||||
binding.configureAppListButton.setOnClickListener {
|
||||
startActivity(Intent(this, PerAppProxyActivity::class.java))
|
||||
startActivity(Intent(this, PerAppProxyActivity0::class.java))
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
reloadSettings()
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
package io.nekohasekai.sfa.ui.shared
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.ktx.getAttrColor
|
||||
import io.nekohasekai.sfa.ui.MainActivity
|
||||
import io.nekohasekai.sfa.utils.MIUIUtils
|
||||
import java.lang.reflect.ParameterizedType
|
||||
|
||||
abstract class AbstractActivity<Binding : ViewBinding>() :
|
||||
AppCompatActivity() {
|
||||
|
||||
private var _binding: Binding? = null
|
||||
internal val binding get() = _binding!!
|
||||
|
||||
abstract class AbstractActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -21,17 +31,28 @@ abstract class AbstractActivity : AppCompatActivity() {
|
||||
window.statusBarColor = colorSurfaceContainer
|
||||
window.navigationBarColor = colorSurfaceContainer
|
||||
|
||||
_binding = createBindingInstance(layoutInflater).also {
|
||||
setContentView(it.root)
|
||||
}
|
||||
|
||||
findViewById<MaterialToolbar>(R.id.toolbar)?.also {
|
||||
setSupportActionBar(it)
|
||||
}
|
||||
|
||||
// MIUI overrides colorSurfaceContainer to colorSurface without below flags
|
||||
@Suppress("DEPRECATION") if (MIUIUtils.isMIUI) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||
}
|
||||
|
||||
supportActionBar?.setHomeAsUpIndicator(AppCompatResources.getDrawable(
|
||||
this@AbstractActivity, R.drawable.ic_arrow_back_24
|
||||
)!!.apply {
|
||||
setTint(getAttrColor(com.google.android.material.R.attr.colorOnSurface))
|
||||
})
|
||||
if (this !is MainActivity) {
|
||||
supportActionBar?.setHomeAsUpIndicator(AppCompatResources.getDrawable(
|
||||
this@AbstractActivity, R.drawable.ic_arrow_back_24
|
||||
)!!.apply {
|
||||
setTint(getAttrColor(com.google.android.material.R.attr.colorOnSurface))
|
||||
})
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -44,4 +65,14 @@ abstract class AbstractActivity : AppCompatActivity() {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun createBindingInstance(
|
||||
inflater: LayoutInflater,
|
||||
): Binding {
|
||||
val vbType = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
|
||||
val vbClass = vbType as Class<Binding>
|
||||
val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
|
||||
return method.invoke(null, inflater) as Binding
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user