/* * Shadowsocks - A shadowsocks client for Android * Copyright (C) 2014 * * 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 . * * * ___====-_ _-====___ * _--^^^#####// \\#####^^^--_ * _-^##########// ( ) \\##########^-_ * -############// |\^^/| \\############- * _/############// (@::@) \\############\_ * /#############(( \\// ))#############\ * -###############\\ (oo) //###############- * -#################\\ / VV \ //#################- * -###################\\/ \//###################- * _#/|##########/\######( /\ )######/\##########|\#_ * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| * ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' * ` ` ` ` / | | | | \ ' ' ' ' * ( | | | | ) * __\ | | | | /__ * (vvv(VVV)(VVV)vvv) * * HERE BE DRAGONS * */ package com.github.shadowsocks import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} import android.app.ProgressDialog import android.content.{ClipData, ClipboardManager, Context, SharedPreferences} import android.content.pm.PackageManager import android.graphics.{Bitmap, PixelFormat} import android.os.{Bundle, Handler} import android.preference.PreferenceManager import android.support.v7.app.AppCompatActivity import android.support.v7.widget.Toolbar import android.view.View.OnClickListener import android.view.ViewGroup.LayoutParams import android.view.{Menu, MenuItem, View, ViewGroup, WindowManager} import android.widget.AbsListView.OnScrollListener import android.widget.CompoundButton.OnCheckedChangeListener import android.widget._ import com.github.shadowsocks.utils.{Key, Scheme, Utils} import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer import com.nostra13.universalimageloader.core.download.BaseImageDownloader import com.nostra13.universalimageloader.core.{DisplayImageOptions, ImageLoader, ImageLoaderConfiguration} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.language.implicitConversions case class ProxiedApp(uid: Int, name: String, packageName: String, var proxied: Boolean) class ObjectArrayTools[T <: AnyRef](a: Array[T]) { def binarySearch(key: T) = { java.util.Arrays.binarySearch(a.asInstanceOf[Array[AnyRef]], key) } } case class ListEntry(switch: Switch, text: TextView, icon: ImageView) object AppManager { implicit def anyrefarray_tools[T <: AnyRef](a: Array[T]): ObjectArrayTools[T] = new ObjectArrayTools(a) def getProxiedApps(context: Context, proxiedAppString: String): Array[ProxiedApp] = { val proxiedApps = proxiedAppString.split('|').sortWith(_ < _) import scala.collection.JavaConversions._ val packageManager: PackageManager = context.getPackageManager val appList = packageManager.getInstalledApplications(0) appList.filter(_.uid >= 10000).map { case a => val uid = a.uid val userName = uid.toString val name = packageManager.getApplicationLabel(a).toString val packageName = a.packageName val proxied = proxiedApps.binarySearch(userName) >= 0 new ProxiedApp(uid, name, packageName, proxied) }.toArray } } class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnClickListener { val MSG_LOAD_START = 1 val MSG_LOAD_FINISH = 2 val STUB = android.R.drawable.sym_def_app_icon implicit def anyrefarray_tools[T <: AnyRef](a: Array[T]): ObjectArrayTools[T] = new ObjectArrayTools(a) var apps: Array[ProxiedApp] = null var appListView: ListView = null var overlay: TextView = null var progressDialog: ProgressDialog = null var adapter: ListAdapter = null var appsLoaded: Boolean = false def loadApps(context: Context): Array[ProxiedApp] = { val prefs = PreferenceManager.getDefaultSharedPreferences(context) val proxiedAppString = prefs.getString(Key.proxied, "") val proxiedApps = proxiedAppString.split('|').sortWith(_ < _) import scala.collection.JavaConversions._ val packageManager: PackageManager = context.getPackageManager val appList = packageManager.getInstalledApplications(0) appList.filter(a => a.uid >= 10000 && packageManager.getApplicationLabel(a) != null && packageManager.getApplicationIcon(a) != null).map { a => val uid = a.uid val userName = uid.toString val name = packageManager.getApplicationLabel(a).toString val packageName = a.packageName val proxied = (proxiedApps binarySearch userName) >= 0 new ProxiedApp(uid, name, packageName, proxied) }.toArray } def loadApps() { apps = loadApps(this).sortWith((a, b) => { if (a == null || b == null || a.name == null || b.name == null) { true } else if (a.proxied == b.proxied) { a.name < b.name } else if (a.proxied) { true } else { false } }) adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) { override def getView(position: Int, view: View, parent: ViewGroup): View = { var convertView = view var entry: ListEntry = null if (convertView == null) { convertView = getLayoutInflater.inflate(R.layout.layout_apps_item, parent, false) val icon = convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView] val switch = convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch] val text = convertView.findViewById(R.id.itemtext).asInstanceOf[TextView] entry = new ListEntry(switch, text, icon) entry.text.setOnClickListener(AppManager.this) entry.text.setOnClickListener(AppManager.this) convertView.setTag(entry) entry.switch.setOnCheckedChangeListener(AppManager.this) } else { entry = convertView.getTag.asInstanceOf[ListEntry] } val app: ProxiedApp = apps(position) val options = new DisplayImageOptions.Builder() .showStubImage(STUB) .showImageForEmptyUri(STUB) .showImageOnFail(STUB) .resetViewBeforeLoading() .cacheInMemory() .cacheOnDisc() .displayer(new FadeInBitmapDisplayer(300)) .build() ImageLoader.getInstance().displayImage(Scheme.APP + app.packageName, entry.icon, options) entry.text.setText(app.name) val switch = entry.switch switch.setTag(app) switch.setChecked(app.proxied) entry.text.setTag(switch) convertView } } appsLoaded = true } /** Called an application is check/unchecked */ def onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { val app: ProxiedApp = buttonView.getTag.asInstanceOf[ProxiedApp] if (app != null) { app.proxied = isChecked } saveAppSettings(this) } def onClick(v: View) { val switch = v.getTag.asInstanceOf[Switch] val app: ProxiedApp = switch.getTag.asInstanceOf[ProxiedApp] if (app != null) { app.proxied = !app.proxied switch.setChecked(app.proxied) } saveAppSettings(this) } override def onDestroy() { super.onDestroy() if (handler != null) { handler.removeCallbacksAndMessages(null) handler = null } } protected override def onOptionsItemSelected(item: MenuItem): Boolean = { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager] val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext) item.getItemId match { case R.id.action_export => val bypass = prefs.getBoolean(Key.isBypassApps, false) val proxiedAppString = prefs.getString(Key.proxied, "") val clip = ClipData.newPlainText(Key.proxied, bypass + " " + proxiedAppString) clipboard.setPrimaryClip(clip) Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() return true case R.id.action_import => if (clipboard.hasPrimaryClip) { val clipdata = clipboard.getPrimaryClip val label = clipdata.getDescription.getLabel if (label == Key.proxied) { val proxiedAppSequence = clipdata.getItemAt(0).getText if (proxiedAppSequence != null) { val proxiedAppString = proxiedAppSequence.toString if (!proxiedAppString.isEmpty) { val array = proxiedAppString.split(" ") val bypass = array(0).toBoolean val apps = if (array.size > 1) array(1) else "" prefs.edit.putBoolean(Key.isBypassApps, bypass).apply() prefs.edit.putString(Key.proxied, apps).apply() Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() // Restart activity val intent = getIntent finish() startActivity(intent) return true } } } } Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show() return false } super.onOptionsItemSelected(item) } protected override def onCreateOptionsMenu(menu: Menu): Boolean = { getMenuInflater.inflate(R.menu.app_manager_menu, menu) super.onCreateOptionsMenu(menu) } protected override def onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) handler = new Handler() this.setContentView(R.layout.layout_apps) val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] toolbar.setTitle(R.string.proxied_help) toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_mtrl_am_alpha) toolbar.setNavigationOnClickListener((v: View) => { val intent = getParentActivityIntent if (intent == null) finish else navigateUpTo(intent) }) this.overlay = View.inflate(this, R.layout.overlay, null).asInstanceOf[TextView] getWindowManager.addView(overlay, new WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT)) val config = new ImageLoaderConfiguration.Builder(this) .imageDownloader(new AppIconDownloader(this)) .build() ImageLoader.getInstance().init(config) val bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch] val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext) bypassSwitch.setOnCheckedChangeListener((button: CompoundButton, checked: Boolean) => prefs.edit().putBoolean(Key.isBypassApps, checked).apply()) bypassSwitch.setChecked(prefs.getBoolean(Key.isBypassApps, false)) appListView = findViewById(R.id.applistview).asInstanceOf[ListView] } protected override def onResume() { super.onResume() Future { handler.post(loadStartRunnable) if (!appsLoaded) loadApps() handler.post(loadFinishRunnable) } } def saveAppSettings(context: Context) { if (apps == null) return val prefs = PreferenceManager.getDefaultSharedPreferences(this) val proxiedApps = new StringBuilder apps.foreach(app => if (app.proxied) { proxiedApps ++= app.uid.toString proxiedApps += '|' }) val edit: SharedPreferences.Editor = prefs.edit edit.putString(Key.proxied, proxiedApps.toString()) edit.apply } val loadStartRunnable = new Runnable { override def run() { progressDialog = ProgressDialog .show(AppManager.this, "", getString(R.string.loading), true, true) } } val loadFinishRunnable = new Runnable { override def run() = { appListView.setAdapter(adapter) appListView.setOnScrollListener(new AbsListView.OnScrollListener { def onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) { if (visible) { val name: String = apps(firstVisibleItem).name if (name != null && name.length > 1) { overlay.setText(apps(firstVisibleItem).name.substring(0, 1)) } else { overlay.setText("*") } overlay.setVisibility(View.VISIBLE) } } def onScrollStateChanged(view: AbsListView, scrollState: Int) { visible = true if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { overlay.setVisibility(View.INVISIBLE) } } var visible = false }) if (progressDialog != null) { progressDialog.dismiss() progressDialog = null } } } var handler: Handler = null class AppIconDownloader(context: Context, connectTimeout: Int, readTimeout: Int) extends BaseImageDownloader(context, connectTimeout, readTimeout) { def this(context: Context) { this(context, 0, 0) } override def getStreamFromOtherSource(imageUri: String, extra: AnyRef): InputStream = { val packageName = imageUri.substring(Scheme.APP.length) val drawable = Utils.getAppIcon(getBaseContext, packageName) val bitmap = Utils.drawableToBitmap(drawable) val os = new ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 100, os) new ByteArrayInputStream(os.toByteArray) } } }