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">