/
githubmirror
/
shadowsocks-android
Обзор
Документация
Войти
/
githubmirror
/
shadowsocks-android
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
mobile/src/main/java/com/github/shadowsocks/AppManager.kt
334 строки
15 KB
Mygod
Target API 35
28 авг 2024, 22:03
28 авг 2024, 22:03
712148e
Код
Авторство
О чём код?
/******************************************************************************* * * * Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> * * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * * * *******************************************************************************/ package com.github.shadowsocks import android.Manifest import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.Bundle import android.util.SparseBooleanArray import android.view.* import android.widget.Filter import android.widget.Filterable import android.widget.ImageView import android.widget.RadioGroup import android.widget.SearchView import android.widget.Switch import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.util.set import androidx.core.view.ViewCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.github.shadowsocks.Core.app import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.listenForPackageChanges import com.github.shadowsocks.widget.ListHolderListener import com.github.shadowsocks.widget.ListListener import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.PopupTextProvider import kotlin.coroutines.coroutineContext class AppManager : AppCompatActivity() { companion object { @SuppressLint("StaticFieldLeak") private var instance: AppManager? = null private const val SWITCH = "switch" private var receiver: BroadcastReceiver? = null private var cachedApps: Map<String, PackageInfo>? = null private fun getCachedApps(pm: PackageManager) = synchronized(AppManager) { if (receiver == null) receiver = app.listenForPackageChanges { synchronized(AppManager) { receiver = null cachedApps = null } instance?.loadApps() } // Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached. val cachedApps = cachedApps ?: pm.getInstalledPackages( PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES) .filter { when (it.packageName) { app.packageName -> false "android" -> true else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true } } .associateBy { it.packageName } this.cachedApps = cachedApps cachedApps } } private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo, val packageName: String) { val name: CharSequence = appInfo.loadLabel(pm) // cached for sorting val icon: Drawable get() = appInfo.loadIcon(pm) val uid get() = appInfo.uid } private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener { private lateinit var item: ProxiedApp init { view.setOnClickListener(this) } fun bind(app: ProxiedApp) { item = app itemView.findViewById<ImageView>(R.id.itemicon).setImageDrawable(app.icon) itemView.findViewById<TextView>(R.id.title).text = app.name itemView.findViewById<TextView>(R.id.desc).text = "${app.packageName} (${app.uid})" itemView.findViewById<Switch>(R.id.itemcheck).isChecked = isProxiedApp(app) } fun handlePayload(payloads: List<String>) { if (payloads.contains(SWITCH)) itemView.findViewById<Switch>(R.id.itemcheck).isChecked = isProxiedApp(item) } override fun onClick(v: View?) { if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName } DataStore.dirty = true appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH) } } private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(), Filterable, PopupTextProvider { private var filteredApps = apps suspend fun reload() { apps = getCachedApps(packageManager).map { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() ProxiedApp(packageManager, packageInfo.applicationInfo!!, packageName) }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) } override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(filteredApps[position]) override fun onBindViewHolder(holder: AppViewHolder, position: Int, payloads: List<Any>) { if (payloads.isNotEmpty()) { @Suppress("UNCHECKED_CAST") holder.handlePayload(payloads as List<String>) return } onBindViewHolder(holder, position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder = AppViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_apps_item, parent, false)) override fun getItemCount(): Int = filteredApps.size private val filterImpl = object : Filter() { override fun performFiltering(constraint: CharSequence) = FilterResults().apply { val filteredApps = if (constraint.isEmpty()) apps else apps.filter { it.name.contains(constraint, true) || it.packageName.contains(constraint, true) || it.uid.toString().contains(constraint) } count = filteredApps.size values = filteredApps } override fun publishResults(constraint: CharSequence, results: FilterResults) { @Suppress("UNCHECKED_CAST") filteredApps = results.values as List<ProxiedApp> notifyDataSetChanged() } } override fun getFilter(): Filter = filterImpl override fun getPopupText(view: View, position: Int) = filteredApps[position].name.firstOrNull()?.toString() ?: "" } private val loading by lazy { findViewById<View>(R.id.loading) } private lateinit var toolbar: Toolbar private lateinit var bypassGroup: RadioGroup private lateinit var list: RecyclerView private lateinit var search: SearchView private val proxiedUids = SparseBooleanArray() private var loader: Job? = null private var apps = emptyList<ProxiedApp>() private val appsAdapter = AppsAdapter() private val shortAnimTime by lazy { resources.getInteger(android.R.integer.config_shortAnimTime).toLong() } private fun View.crossFadeFrom(other: View) { clearAnimation() other.clearAnimation() if (visibility == View.VISIBLE && other.visibility == View.GONE) return alpha = 0F visibility = View.VISIBLE animate().alpha(1F).duration = shortAnimTime other.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { other.visibility = View.GONE } }).duration = shortAnimTime } private fun initProxiedUids(str: String = DataStore.individual) { proxiedUids.clear() val apps = getCachedApps(packageManager) for (line in str.lineSequence()) proxiedUids[(apps[line] ?: continue).applicationInfo!!.uid] = true } private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid] @UiThread private fun loadApps() { loader?.cancel() loader = lifecycleScope.launch { loading.crossFadeFrom(list) val adapter = list.adapter as AppsAdapter withContext(Dispatchers.IO) { adapter.reload() } adapter.filter.filter(search.query) list.crossFadeFrom(loading) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.layout_apps) ListHolderListener.setup(this) toolbar = findViewById(R.id.toolbar) setSupportActionBar(toolbar) supportActionBar!!.setDisplayHomeAsUpEnabled(true) if (!DataStore.proxyApps) { DataStore.proxyApps = true DataStore.dirty = true } bypassGroup = findViewById(R.id.bypassGroup) bypassGroup.check(if (DataStore.bypass) R.id.btn_bypass else R.id.btn_on) bypassGroup.setOnCheckedChangeListener { _, checkedId -> DataStore.dirty = true when (checkedId) { R.id.btn_off -> { DataStore.proxyApps = false finish() } R.id.btn_on -> DataStore.bypass = false R.id.btn_bypass -> DataStore.bypass = true } } initProxiedUids() list = findViewById(R.id.list) ViewCompat.setOnApplyWindowInsetsListener(list, ListListener) list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) list.itemAnimator = DefaultItemAnimator() list.adapter = appsAdapter FastScrollerBuilder(list).useMd2Style().build() search = findViewById(R.id.search) search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?) = false override fun onQueryTextChange(newText: String?) = true.also { appsAdapter.filter.filter(newText) } }) instance = this loadApps() } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.app_manager_menu, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_apply_all -> { val profiles = ProfileManager.getAllProfiles() if (profiles != null) { val proxiedAppString = DataStore.individual profiles.forEach { it.individual = proxiedAppString it.bypass = DataStore.bypass ProfileManager.updateProfile(it) } if (DataStore.directBootAware) DirectBoot.update() Snackbar.make(list, R.string.action_apply_all, Snackbar.LENGTH_LONG).show() } else Snackbar.make(list, R.string.action_export_err, Snackbar.LENGTH_LONG).show() return true } R.id.action_export_clipboard -> { val success = Core.trySetPrimaryClip("${DataStore.bypass}\n${DataStore.individual}") Snackbar.make(list, if (success) R.string.action_export_msg else R.string.action_export_err, Snackbar.LENGTH_LONG).show() return true } R.id.action_import_clipboard -> { val proxiedAppString = Core.clipboard.primaryClip?.getItemAt(0)?.text?.toString() if (!proxiedAppString.isNullOrEmpty()) { val i = proxiedAppString.indexOf('\n') try { val (enabled, apps) = if (i < 0) { proxiedAppString to "" } else proxiedAppString.substring(0, i) to proxiedAppString.substring(i + 1) bypassGroup.check(if (enabled.toBoolean()) R.id.btn_bypass else R.id.btn_on) DataStore.individual = apps DataStore.dirty = true Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show() initProxiedUids(apps) appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH) return true } catch (_: IllegalArgumentException) { } } Snackbar.make(list, R.string.action_import_err, Snackbar.LENGTH_LONG).show() } } return super.onOptionsItemSelected(item) } override fun supportNavigateUpTo(upIntent: Intent) = super.supportNavigateUpTo(upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU) { if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu() } else super.onKeyUp(keyCode, event) override fun onDestroy() { instance = null loader?.cancel() super.onDestroy() } }