Files
shadowsocks-android/src/main/scala/com/github/shadowsocks/AppManager.scala
T
2014-12-15 17:19:40 +08:00

381 lines
14 KiB
Scala

/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2014 <max.c.lv@gmail.com>
*
* 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/>.
*
*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* HERE BE DRAGONS
*
*/
package com.github.shadowsocks
import android.app.{Activity, ProgressDialog}
import android.content.ClipboardManager
import android.content.ClipData
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.{Bitmap, PixelFormat}
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.view.WindowManager
import android.widget._
import android.widget.AbsListView.OnScrollListener
import android.widget.CompoundButton.OnCheckedChangeListener
import com.nostra13.universalimageloader.core.download.BaseImageDownloader
import java.io.{ByteArrayOutputStream, ByteArrayInputStream, InputStream}
import com.nostra13.universalimageloader.core.{DisplayImageOptions, ImageLoader, ImageLoaderConfiguration}
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer
import com.github.shadowsocks.utils.{Utils, Scheme, Key}
import scala.concurrent.ops._
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(box: CheckBox, 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 Activity 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 box = convertView.findViewById(R.id.itemcheck).asInstanceOf[CheckBox]
val text = convertView.findViewById(R.id.itemtext).asInstanceOf[TextView]
entry = new ListEntry(box, text, icon)
entry.text.setOnClickListener(AppManager.this)
entry.text.setOnClickListener(AppManager.this)
convertView.setTag(entry)
entry.box.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 box: CheckBox = entry.box
box.setTag(app)
box.setChecked(app.proxied)
entry.text.setTag(box)
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 cbox: CheckBox = v.getTag.asInstanceOf[CheckBox]
val app: ProxiedApp = cbox.getTag.asInstanceOf[ProxiedApp]
if (app != null) {
app.proxied = !app.proxied
cbox.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 proxiedAppString = prefs.getString(Key.proxied, "")
val clip = ClipData.newPlainText(Key.proxied, 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) {
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
prefs.edit.putString(Key.proxied, proxiedAppString).commit()
// 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 = {
val inflater = getMenuInflater()
inflater.inflate(R.menu.app_manager_menu, menu)
return super.onCreateOptionsMenu(menu)
}
protected override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
handler = new Handler()
getActionBar.setTitle(R.string.proxied_help)
this.setContentView(R.layout.layout_apps)
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(new OnCheckedChangeListener {
def onCheckedChanged(button: CompoundButton, checked: Boolean) {
prefs.edit().putBoolean(Key.isBypassApps, checked).commit()
}
})
bypassSwitch.setChecked(prefs.getBoolean(Key.isBypassApps, false))
appListView = findViewById(R.id.applistview).asInstanceOf[ListView]
}
protected override def onResume() {
super.onResume()
spawn {
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.commit
}
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)
}
}
}