Files
shadowsocks-android/mobile/src/main/java/com/github/shadowsocks/AppManager.kt
T
2017-12-03 10:20:57 -08:00

270 lines
12 KiB
Kotlin

/*******************************************************************************
* *
* 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.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.TaskStackBuilder
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.*
import android.widget.ImageView
import android.widget.Switch
import android.widget.Toast
import com.futuremind.recyclerviewfastscroll.FastScroller
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.thread
import java.util.concurrent.atomic.AtomicBoolean
class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
companion object {
data class ProxiedApp(val name: String, val packageName: String, val icon: Drawable)
@SuppressLint("StaticFieldLeak")
private var instance: AppManager? = null
private var receiver: BroadcastReceiver? = null
private var cachedApps: Array<ProxiedApp>? = null
private fun getApps(pm: PackageManager): Array<ProxiedApp> {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) { cachedApps = null }
AppManager.instance?.reloadApps()
}
return synchronized(AppManager) {
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
.map { ProxiedApp(pm.getApplicationLabel(it.applicationInfo).toString(), it.packageName,
it.applicationInfo.loadIcon(pm)) }
.toTypedArray()
this.cachedApps = cachedApps
cachedApps
}
}
}
private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private val icon = view.findViewById<ImageView>(R.id.itemicon)
private val check = view.findViewById<Switch>(R.id.itemcheck)
private lateinit var item: ProxiedApp
private val proxied get() = proxiedApps.contains(item.packageName)
init {
view.setOnClickListener(this)
}
fun bind(app: ProxiedApp) {
this.item = app
icon.setImageDrawable(app.icon)
check.text = app.name
check.isChecked = proxied
}
override fun onClick(v: View?) {
if (proxied) {
proxiedApps.remove(item.packageName)
check.isChecked = false
} else {
proxiedApps.add(item.packageName)
check.isChecked = true
}
if (!appsLoading.get()) {
DataStore.individual = proxiedApps.joinToString("\n")
DataStore.dirty = true
}
}
}
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(), SectionTitleProvider {
private val apps = getApps(packageManager).sortedWith(Comparator { a, b ->
when (Pair(proxiedApps.contains(a.packageName), proxiedApps.contains(b.packageName))) {
Pair(true, false) -> -1
Pair(false, true) -> 1
else -> a.name.compareTo(b.name, true)
}
})
override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(apps[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 = apps.size
override fun getSectionTitle(position: Int): String = apps[position].name.substring(0, 1)
}
private lateinit var proxiedApps: HashSet<String>
private lateinit var toolbar: Toolbar
private lateinit var bypassSwitch: Switch
private lateinit var appListView: RecyclerView
private lateinit var fastScroller: FastScroller
private lateinit var loadingView: View
private val appsLoading = AtomicBoolean()
private val handler = Handler()
private val clipboard by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet()
}
private fun reloadApps() {
if (!appsLoading.compareAndSet(true, false)) loadAppsAsync()
}
private fun loadAppsAsync() {
if (!appsLoading.compareAndSet(false, true)) return
thread {
var adapter: AppsAdapter?
do {
appsLoading.set(true)
adapter = AppsAdapter()
} while (!appsLoading.compareAndSet(true, false))
handler.post {
appListView.adapter = adapter
fastScroller.setRecyclerView(appListView)
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
appListView.alpha = 0F
appListView.visibility = View.VISIBLE
appListView.animate().alpha(1F).duration = shortAnimTime.toLong()
fastScroller.alpha = 0F
fastScroller.visibility = View.VISIBLE
fastScroller.animate().alpha(1F).duration = shortAnimTime.toLong()
loadingView.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
loadingView.visibility = View.GONE
}
}).duration = shortAnimTime.toLong()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_apps)
toolbar = findViewById(R.id.toolbar)
toolbar.setTitle(R.string.proxied_apps)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener {
val intent = parentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities() else finish()
}
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!DataStore.proxyApps) {
DataStore.proxyApps = true
DataStore.dirty = true
}
findViewById<Switch>(R.id.onSwitch).setOnCheckedChangeListener { _, checked ->
DataStore.proxyApps = checked
DataStore.dirty = true
finish()
}
bypassSwitch = findViewById(R.id.bypassSwitch)
bypassSwitch.isChecked = DataStore.bypass
bypassSwitch.setOnCheckedChangeListener { _, checked ->
DataStore.bypass = checked
DataStore.dirty = true
}
initProxiedApps()
loadingView = findViewById(R.id.loading)
appListView = findViewById(R.id.list)
appListView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
appListView.itemAnimator = DefaultItemAnimator()
fastScroller = findViewById(R.id.fastscroller)
instance = this
loadAppsAsync()
}
override fun onMenuItemClick(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
ProfileManager.updateProfile(it)
}
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
} else Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
return true
}
R.id.action_export -> {
clipboard.primaryClip = ClipData.newPlainText(Key.individual,
"${DataStore.bypass}\n${DataStore.individual}")
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
return true
}
R.id.action_import -> {
val proxiedAppString = clipboard.primaryClip?.getItemAt(0)?.text?.toString()
if (!proxiedAppString.isNullOrEmpty()) {
val i = proxiedAppString!!.indexOf('\n')
try {
val (enabled, apps) = if (i < 0) Pair(proxiedAppString, "") else
Pair(proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.isChecked = enabled.toBoolean()
DataStore.individual = apps
DataStore.dirty = true
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.visibility = View.GONE
fastScroller.visibility = View.GONE
loadingView.visibility = View.VISIBLE
initProxiedApps(apps)
reloadApps()
return true
} catch (_: IllegalArgumentException) { }
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
return false
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return if (keyCode == KeyEvent.KEYCODE_MENU)
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
else super.onKeyUp(keyCode, event)
}
override fun onDestroy() {
instance = null
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
}