/******************************************************************************* * * * Copyright (C) 2017 by Max Lv * * Copyright (C) 2017 by Mygod Studio * * * * 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 . * * * *******************************************************************************/ 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? = null private fun getApps(pm: PackageManager): Array { 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(R.id.itemicon) private val check = view.findViewById(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(), 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 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(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() } }