Compare commits

...
41 Commits
Author SHA1 Message Date
Max Lv 5f268caa0a Update dependency 2015-12-31 21:45:41 +08:00
Max Lv 04c86a537a Merge pull request #502 from Mygod/master
Fix dynamic traffic stat in profile selector
2015-12-29 20:49:02 +08:00
Mygod f9667d767e Fix dynamic traffic stat in profile selector
Fix #495
2015-12-29 20:27:30 +08:00
Max Lv 5ddd8a1686 Bump version 2015-12-27 16:59:11 +08:00
Max Lv 7d44e4fc9f Catch NameNotFoundException 2015-12-27 16:48:46 +08:00
Max Lv 92b9c52944 Merge branch 'master' of github.com:shadowsocks/shadowsocks-android 2015-12-27 16:33:55 +08:00
Max Lv 4f3ebf9395 Merge pull request #497 from Mygod/master
Fix #496
2015-12-27 10:40:31 +08:00
Mygod bf135107d8 Fix individual and userOrder not updated when upgrading from <12 2015-12-27 10:26:00 +08:00
Mygod 5a3d2c07bc Merge branch 'master' of https://github.com/shadowsocks/shadowsocks-android 2015-12-27 10:22:57 +08:00
Mygod d8ab3733dc Fix getCallbackCount is not available on API 16 2015-12-27 10:22:43 +08:00
Mygod b6fc013a06 Unregister preference change callback in onDestroy instead of onPause
Fix #496
2015-12-27 10:17:07 +08:00
Max Lv 7d01ca81c8 Fix #496 2015-12-27 10:08:23 +08:00
Max Lv 9feb5bd96a Fix an issue when updating database from version < 12 2015-12-27 10:01:23 +08:00
Max Lv 2641de436d Merge pull request #494 from Mygod/master
Reduce battery usage
2015-12-27 08:53:51 +08:00
Mygod 4d7bf8d707 Reduce battery usage - Unregister callbacks onStop 2015-12-26 23:46:18 +08:00
Mygod 0018a7ca9e Fix NAT mode not working after task removed from recent 2015-12-26 23:27:21 +08:00
Mygod 51ceb3dac5 Refine callback handling 2015-12-26 23:10:54 +08:00
Mygod 179aab8094 Reduce battery usage - Reduce expensive reflections 2015-12-26 22:32:23 +08:00
Mygod 6bff4f1a44 Reduce battery usage - transmit traffic stat with binary 2015-12-26 20:41:23 +08:00
Mygod 8dcfd5a8c5 Reduce battery usage - prevent unnecessary notification updates 2015-12-26 20:00:12 +08:00
Mygod ef162cd47e Reload apps if a change is detected 2015-12-26 19:21:03 +08:00
Mygod 569f056a22 Reduce battery usage - register broadcast at runtime 2015-12-26 18:56:12 +08:00
Mygod 971ea09077 Reduce battery usage - optimize performance critical block 2015-12-26 17:29:26 +08:00
Mygod fd01712b6c Reduce battery usage - use notify instead of startForeground 2015-12-26 17:14:47 +08:00
Max Lv 6a0955c082 Merge pull request #491 from Mygod/master
Hide adview if default profile is no longer active
2015-12-26 07:52:18 +08:00
Mygod 92c8e0856c Hide adview if default profile is no longer active 2015-12-26 00:00:11 +08:00
Mygod a5ef0cd6f3 Fix upgrading database from 13 2015-12-25 23:27:38 +08:00
Mygod 0612a00474 Remove deprecated permission 2015-12-25 23:00:39 +08:00
Max Lv 0282975e6c Merge pull request #490 from Mygod/master
Enhancements for profile selector, and bugfixes
2015-12-25 22:48:29 +08:00
Mygod c4583bea22 Add a friendly dialog 2015-12-25 22:33:31 +08:00
Mygod 0d582f2392 Update profile default values 2015-12-25 22:03:15 +08:00
Mygod 8cf37954d7 Revert "Use brighter accent color for FAB"
This reverts commit edd5fb4a4c.
2015-12-25 19:08:38 +08:00
Mygod eadb95ca2d Fix creating profiles 2015-12-25 17:43:18 +08:00
Mygod 0f9b686e55 Fix per-app proxy not enabled when NAT is on 2015-12-25 17:29:00 +08:00
Mygod c93a0348ad Fix isDestroyed is missing on API 16 2015-12-25 16:37:30 +08:00
Mygod 0cf509833a Fix dismissing ProgressDialog after activity destroyed 2015-12-25 16:33:44 +08:00
Mygod aed6308841 Fix a race condition 2015-12-25 16:25:39 +08:00
Mygod 9fb0639f86 Move preference related logic to ShadowsocksSettings 2015-12-25 16:07:07 +08:00
Mygod 164becfed0 Implement dragging to move profiles 2015-12-25 15:51:18 +08:00
Mygod eed7ec6eba Update library dependencies 2015-12-25 14:24:37 +08:00
Mygod 7e5b44d36f Scroll to selected profile 2015-12-25 14:20:13 +08:00
24 changed files with 429 additions and 378 deletions
+4 -4
View File
@@ -29,13 +29,13 @@ resolvers += "JRAF" at "http://JRAF.org/static/maven/2"
libraryDependencies ++= Seq(
"dnsjava" % "dnsjava" % "2.1.7",
"com.github.kevinsawicki" % "http-request" % "6.0",
"commons-net" % "commons-net" % "3.3",
"commons-net" % "commons-net" % "3.4",
"eu.chainfire" % "libsuperuser" % "1.0.0.201510071325",
"com.google.zxing" % "android-integration" % "3.2.1",
"net.glxn.qrgen" % "android" % "2.0",
"com.google.android.gms" % "play-services-base" % "8.3.0",
"com.google.android.gms" % "play-services-ads" % "8.3.0",
"com.google.android.gms" % "play-services-analytics" % "8.3.0",
"com.google.android.gms" % "play-services-base" % "8.4.0",
"com.google.android.gms" % "play-services-ads" % "8.4.0",
"com.google.android.gms" % "play-services-analytics" % "8.4.0",
"com.android.support" % "design" % "23.1.1",
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
"com.j256.ormlite" % "ormlite-core" % "4.48",
+1 -1
View File
@@ -1,5 +1,5 @@
resolvers += Resolver.url("scalasbt releases", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-snapshots"))(Resolver.ivyStylePatterns)
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.11")
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.12")
resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.7.0-SNAPSHOT")
+2 -11
View File
@@ -1,12 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.shadowsocks"
android:versionCode="126"
android:versionName="2.9.4">
android:versionCode="127"
android:versionName="2.9.5">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>
<uses-feature
android:name="android.hardware.touchscreen"
@@ -109,14 +108,6 @@
</intent-filter>
</service>
<receiver android:name=".AppManagerReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package"/>
</intent-filter>
</receiver>
<receiver android:name=".ShadowsocksReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
@@ -2,6 +2,5 @@ package com.github.shadowsocks.aidl;
interface IShadowsocksServiceCallback {
oneway void stateChanged(int state, String msg);
oneway void trafficUpdated(String txRate, String rxRate,
String txTotal, String rxTotal);
oneway void trafficUpdated(long txRate, long rxRate, long txTotal, long rxTotal);
}
+4
View File
@@ -85,6 +85,10 @@
<string name="connection_test_error_status_code">状态码无效。(#%d)</string>
<!-- profile -->
<string name="profile_manager_dialog">配置文件</string>
<string name="profile_manager_dialog_content">&#8226; 点击选择配置文件;\n&#8226; 滑动删除;\n&#8226;
长按拖动改变顺序。</string>
<string name="gotcha">好的</string>
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
<string name="add_profile_methods_manual_settings">手动设置</string>
+1 -1
View File
@@ -2,7 +2,7 @@
<resources>
<color name="material_green_a700">#64DD17</color>
<color name="material_green_700">#388E3C</color>
<color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_700">#455A64</color>
+4
View File
@@ -103,6 +103,10 @@
<string name="action_import_err">Failed to import.</string>
<!-- profile -->
<string name="profile_manager_dialog">Profiles</string>
<string name="profile_manager_dialog_content">&#8226; Tap to select a profile;\n&#8226; Swipe to remove;\n&#8226;
Long press to reorder profiles.</string>
<string name="gotcha">Gotcha!</string>
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
@@ -39,8 +39,11 @@
package com.github.shadowsocks
import java.util.concurrent.atomic.AtomicBoolean
import android.Manifest.permission
import android.content._
import android.content.pm.PackageManager
import android.content.{ClipData, ClipboardManager, Context}
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.os.{Bundle, Handler}
@@ -54,7 +57,6 @@ import android.view._
import android.widget.AbsListView.OnScrollListener
import android.widget.CompoundButton.OnCheckedChangeListener
import android.widget._
import android.Manifest.permission
import com.github.shadowsocks.utils.{Key, Utils}
import scala.collection.JavaConversions._
@@ -65,13 +67,31 @@ object AppManager {
case class ProxiedApp(name: String, packageName: String, icon: Drawable)
private case class ListEntry(switch: Switch, text: TextView, icon: ImageView)
var cachedApps: Array[ProxiedApp] = _
private var instance: AppManager = _
private var receiverRegistered: Boolean = _
private var cachedApps: Array[ProxiedApp] = _
private def getApps(pm: PackageManager) = {
if (cachedApps == null) cachedApps = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter(p => p.requestedPermissions != null && p.requestedPermissions.contains(permission.INTERNET))
.map(p => new ProxiedApp(pm.getApplicationLabel(p.applicationInfo).toString, p.packageName,
p.applicationInfo.loadIcon(pm))).toArray
cachedApps
if (!receiverRegistered) {
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
ShadowsocksApplication.instance.registerReceiver((context: Context, intent: Intent) =>
if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED ||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
synchronized(cachedApps = null)
val instance = AppManager.instance
if (instance != null) instance.reloadApps()
}, filter)
receiverRegistered = true
}
synchronized {
if (cachedApps == null) cachedApps = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter(p => p.requestedPermissions != null && p.requestedPermissions.contains(permission.INTERNET))
.map(p => new ProxiedApp(pm.getApplicationLabel(p.applicationInfo).toString, p.packageName,
p.applicationInfo.loadIcon(pm))).toArray
cachedApps
}
}
}
@@ -86,43 +106,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
private var loadingView: View = _
private var overlay: TextView = _
private var adapter: ListAdapter = _
@volatile private var appsLoading: Boolean = _
def loadApps() {
appsLoading = true
proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
apps = getApps(getPackageManager).sortWith((a, b) => {
val aProxied = proxiedApps.contains(a.packageName)
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
})
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)
entry = new ListEntry(convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch],
convertView.findViewById(R.id.itemtext).asInstanceOf[TextView],
convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView])
convertView.setOnClickListener(AppManager.this)
convertView.setTag(entry)
entry.switch.setOnCheckedChangeListener(AppManager.this)
} else {
entry = convertView.getTag.asInstanceOf[ListEntry]
}
val app: ProxiedApp = apps(position)
entry.text.setText(app.name)
entry.icon.setImageDrawable(app.icon)
val switch = entry.switch
switch.setTag(app)
switch.setChecked(proxiedApps.contains(app.packageName))
entry.text.setTag(switch)
convertView
}
}
}
private val appsLoading = new AtomicBoolean
private def setProxied(pn: String, proxied: Boolean) = if (proxied) proxiedApps.add(pn) else proxiedApps.remove(pn)
@@ -145,6 +129,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
}
override def onDestroy() {
instance = null
super.onDestroy()
if (handler != null) {
handler.removeCallbacksAndMessages(null)
@@ -186,7 +171,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
// Restart activity
appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
if (appsLoading) appsLoading = false else loadAppsAsync()
reloadApps()
return true
}
}
@@ -252,13 +237,49 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
}
}
})
instance = this
loadAppsAsync()
}
def reloadApps() = if (!appsLoading.compareAndSet(true, false)) loadAppsAsync()
def loadAppsAsync() {
if (!appsLoading.compareAndSet(false, true)) return
ThrowableFuture {
while (!appsLoading) loadApps()
appsLoading = false
do {
proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
appsLoading.set(true)
apps = getApps(getPackageManager).sortWith((a, b) => {
val aProxied = proxiedApps.contains(a.packageName)
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
})
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)
entry = new ListEntry(convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch],
convertView.findViewById(R.id.itemtext).asInstanceOf[TextView],
convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView])
convertView.setOnClickListener(AppManager.this)
convertView.setTag(entry)
entry.switch.setOnCheckedChangeListener(AppManager.this)
} else {
entry = convertView.getTag.asInstanceOf[ListEntry]
}
val app: ProxiedApp = apps(position)
entry.text.setText(app.name)
entry.icon.setImageDrawable(app.icon)
val switch = entry.switch
switch.setTag(app)
switch.setChecked(proxiedApps.contains(app.packageName))
entry.text.setTag(switch)
convertView
}
}
} while (!appsLoading.compareAndSet(true, false))
handler.post(() => {
appListView.setAdapter(adapter)
Utils.crossFade(AppManager.this, loadingView, appListView)
@@ -267,7 +288,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
}
def saveAppSettings(context: Context) {
if (!appsLoading) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
if (!appsLoading.get) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
}
var handler: Handler = null
@@ -1,11 +0,0 @@
package com.github.shadowsocks
import android.content.{Intent, Context, BroadcastReceiver}
/**
* @author Mygod
*/
class AppManagerReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent) = if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED ||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) AppManager.synchronized(AppManager.cachedApps = null)
}
@@ -42,7 +42,7 @@ package com.github.shadowsocks
import java.util.{Timer, TimerTask}
import android.app.Service
import android.content.Context
import android.content.{Intent, Context}
import android.os.{Handler, RemoteCallbackList}
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread}
@@ -50,13 +50,13 @@ import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread
trait BaseService extends Service {
@volatile private var state = State.STOPPED
@volatile private var callbackCount = 0
@volatile protected var config: Config = null
var timer: Timer = null
var trafficMonitorThread: TrafficMonitorThread = null
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
var callbacksCount: Int = _
val binder = new IShadowsocksService.Stub {
override def getMode: Int = {
@@ -68,34 +68,29 @@ trait BaseService extends Service {
}
override def unregisterCallback(cb: IShadowsocksServiceCallback) {
if (cb != null ) {
callbacks.unregister(cb)
callbackCount -= 1
}
if (callbackCount == 0 && timer != null) {
timer.cancel()
timer = null
}
if (callbackCount == 0 && state == State.STOPPED) {
stopBackgroundService()
if (cb != null && callbacks.unregister(cb)) {
callbacksCount -= 1
if (callbacksCount == 0 && timer != null) {
timer.cancel()
timer = null
}
}
}
override def registerCallback(cb: IShadowsocksServiceCallback) {
if (cb != null) {
if (callbackCount == 0 && timer == null) {
if (cb != null && callbacks.register(cb)) {
callbacksCount += 1
if (callbacksCount != 0 && timer == null) {
val task = new TimerTask {
def run {
TrafficMonitor.updateRate()
updateTrafficRate(TrafficMonitor.getTxRate, TrafficMonitor.getRxRate,
TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal)
if (TrafficMonitor.updateRate()) updateTrafficRate()
}
}
timer = new Timer(true)
timer.schedule(task, 1000, 1000)
}
callbacks.register(cb)
callbackCount += 1
TrafficMonitor.updateRate()
cb.trafficUpdated(TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
}
}
@@ -115,6 +110,7 @@ trait BaseService extends Service {
def startRunner(config: Config) {
this.config = config
startService(new Intent(getContext, getClass))
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread()
trafficMonitorThread.start()
@@ -122,38 +118,38 @@ trait BaseService extends Service {
def stopRunner() {
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.getDeltaTx, TrafficMonitor.getDeltaRx)
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
TrafficMonitor.reset()
if (trafficMonitorThread != null) {
trafficMonitorThread.stopThread()
trafficMonitorThread = null
}
// change the state
changeState(State.STOPPED)
// stop the service if nothing has bound to it
stopSelf()
}
def updateTrafficTotal(tx: Long, rx: Long) {
val handler = new Handler(getContext.getMainLooper)
handler.post(() => {
if (config != null) {
ShadowsocksApplication.profileManager.getProfile(config.profileId) match {
case Some(profile) =>
profile.tx += tx
profile.rx += rx
ShadowsocksApplication.profileManager.updateProfile(profile)
case None => // Ignore
}
val config = this.config // avoid race conditions without locking
if (config != null) {
ShadowsocksApplication.profileManager.getProfile(config.profileId) match {
case Some(profile) =>
profile.tx += tx
profile.rx += rx
ShadowsocksApplication.profileManager.updateProfile(profile)
case None => // Ignore
}
})
}
}
def stopBackgroundService()
def getServiceMode: Int
def getTag: String
def getContext: Context
def getCallbackCount: Int = {
callbackCount
}
def getState: Int = {
state
}
@@ -161,10 +157,14 @@ trait BaseService extends Service {
changeState(s, null)
}
def updateTrafficRate(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
def updateTrafficRate() {
val handler = new Handler(getContext.getMainLooper)
handler.post(() => {
if (callbackCount > 0) {
if (callbacksCount > 0) {
val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate
val txTotal = TrafficMonitor.txTotal
val rxTotal = TrafficMonitor.rxTotal
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
@@ -181,7 +181,7 @@ trait BaseService extends Service {
protected def changeState(s: Int, msg: String) {
val handler = new Handler(getContext.getMainLooper)
handler.post(() => if (state != s) {
if (callbackCount > 0) {
if (callbacksCount > 0) {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
@@ -1,7 +1,7 @@
package com.github.shadowsocks
import android.content.Intent
import android.os.{Handler, Bundle}
import android.os.{Bundle, Handler}
import android.support.design.widget.Snackbar
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.RecyclerView.ViewHolder
@@ -25,7 +25,13 @@ import scala.collection.mutable.ArrayBuffer
/**
* @author Mygod
*/
object ProfileManagerActivity {
private final val profileTip = "profileTip"
}
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext {
import ProfileManagerActivity._
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
private var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
@@ -55,28 +61,27 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
})
}
def updateText(refetch: Boolean = false) {
def updateText(txTotal: Long = 0, rxTotal: Long = 0) {
val builder = new SpannableStringBuilder
val item = if (refetch) ShadowsocksApplication.profileManager.getProfile(this.item.id) match {
case Some(profile) => profile
case None => return
} else this.item
val tx = item.tx + txTotal
val rx = item.rx + rxTotal
builder.append(item.name)
if (item.tx != 0 || item.rx != 0) {
if (tx != 0 || rx != 0) {
val start = builder.length
builder.append(getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(item.tx), TrafficMonitor.formatTraffic(item.rx)))
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
builder.setSpan(new TextAppearanceSpan(ProfileManagerActivity.this, android.R.style.TextAppearance_Small),
start + 1, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
handler.post(() => text.setText(builder))
}
def bind(item: Profile) {
def bind(item: Profile, i: Int) {
this.item = item
updateText()
if (item.id == ShadowsocksApplication.profileId) {
text.setChecked(true)
selectedIndex = i
selectedItem = this
} else {
text.setChecked(false)
@@ -92,12 +97,12 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
private val recycleBin = new ArrayBuffer[(Int, Profile)]
private var profiles = new ArrayBuffer[Profile]
var profiles = new ArrayBuffer[Profile]
profiles ++= ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
def getItemCount = profiles.length
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = vh.bind(profiles(i))
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = vh.bind(profiles(i), i)
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profiles_item, vg, false))
@@ -110,6 +115,24 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
notifyItemInserted(pos)
}
def move(from: Int, to: Int) {
val step = if (from < to) 1 else -1
val first = profiles(from)
var previousOrder = profiles(from).userOrder
for (i <- from until to by step) {
val next = profiles(i + step)
val order = next.userOrder
next.userOrder = previousOrder
previousOrder = order
profiles(i) = next
ShadowsocksApplication.profileManager.updateProfile(next)
}
first.userOrder = previousOrder
profiles(to) = first
ShadowsocksApplication.profileManager.updateProfile(first)
notifyItemMoved(from, to)
}
def remove(pos: Int) {
recycleBin.append((pos, profiles(pos)))
profiles.remove(pos)
@@ -131,6 +154,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
}
}
private var selectedIndex = -1
private var selectedItem: ProfileViewHolder = _
private val handler = new Handler
@@ -153,28 +177,52 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
ShadowsocksApplication.profileManager.setProfileAddedListener(profilesAdapter.add)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
profilesList.setLayoutManager(new LinearLayoutManager(this))
val layoutManager = new LinearLayoutManager(this)
profilesList.setLayoutManager(layoutManager)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
if (selectedIndex < 0) selectedIndex = profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == ShadowsocksApplication.profileId => i
}.getOrElse(-1)
layoutManager.scrollToPosition(selectedIndex)
removedSnackbar = Snackbar.make(profilesList, R.string.removed, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, ((v: View) => profilesAdapter.undoRemoves): OnClickListener)
removedSnackbar.getView.addOnAttachStateChangeListener(new OnAttachStateChangeListener {
def onViewDetachedFromWindow(v: View) = profilesAdapter.commitRemoves
def onViewAttachedToWindow(v: View) = ()
})
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
def onSwiped(viewHolder: ViewHolder, direction: Int) = {
profilesAdapter.remove(viewHolder.getAdapterPosition)
removedSnackbar.show
}
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = false // TODO?
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = {
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
true
}
}).attachToRecyclerView(profilesList)
attachService(new IShadowsocksServiceCallback.Stub {
def stateChanged(state: Int, msg: String) = () // ignore
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) =
if (selectedItem != null) selectedItem.updateText(true)
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) =
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
})
if (ShadowsocksApplication.settings.getBoolean(profileTip, true)) {
ShadowsocksApplication.settings.edit.putBoolean(profileTip, false).commit
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create.show
}
}
override def onStart() {
super.onStart()
registerCallback
}
override def onStop() {
super.onStop()
unregisterCallback
}
override def onDestroy {
@@ -13,20 +13,12 @@ trait ServiceBoundContext extends Context {
class ShadowsocksServiceConnection extends ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) {
bgService = IShadowsocksService.Stub.asInterface(service)
if (callback != null) try {
bgService.registerCallback(callback)
} catch {
case ignored: RemoteException => // Nothing
}
registerCallback
ServiceBoundContext.this.onServiceConnected()
}
override def onServiceDisconnected(name: ComponentName) {
if (callback != null) {
try {
if (bgService != null) bgService.unregisterCallback(callback)
} catch {
case ignored: RemoteException => // Nothing
}
unregisterCallback
callback = null
}
ServiceBoundContext.this.onServiceDisconnected()
@@ -34,11 +26,25 @@ trait ServiceBoundContext extends Context {
}
}
def registerCallback = if (bgService != null && callback != null && !callbackRegistered) try {
bgService.registerCallback(callback)
callbackRegistered = true
} catch {
case ignored: RemoteException => // Nothing
}
def unregisterCallback = if (bgService != null && callback != null && callbackRegistered) try {
bgService.unregisterCallback(callback)
callbackRegistered = false
} catch {
case ignored: RemoteException => // Nothing
}
def onServiceConnected() = ()
def onServiceDisconnected() = ()
private var callback: IShadowsocksServiceCallback.Stub = _
private var connection: ShadowsocksServiceConnection = _
private var callbackRegistered: Boolean = _
// Variables
var bgService: IShadowsocksService = _
@@ -54,19 +60,13 @@ trait ServiceBoundContext extends Context {
connection = new ShadowsocksServiceConnection()
bindService(intent, connection, Context.BIND_AUTO_CREATE)
startService(new Intent(this, s))
}
}
def deattachService() {
if (bgService != null) {
if (callback != null) {
try {
bgService.unregisterCallback(callback)
} catch {
case ignored: RemoteException => // Nothing
}
unregisterCallback
callback = null
}
if (connection != null) {
@@ -49,7 +49,6 @@ import android.content.res.AssetManager
import android.graphics.Typeface
import android.net.VpnService
import android.os._
import android.preference.{Preference, SwitchPreference}
import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
@@ -60,7 +59,6 @@ import android.widget._
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database._
import com.github.shadowsocks.preferences.{DropDownPreference, NumberPickerPreference, PasswordEditTextPreference, SummaryEditTextPreference}
import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
@@ -91,52 +89,9 @@ object Shadowsocks {
// Constants
val TAG = "Shadowsocks"
val REQUEST_CONNECT = 1
val PREFS_NAME = "Shadowsocks"
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey, Key.encMethod,
Key.isAuth)
val FEATURE_PREFS = Array(Key.route, Key.isProxyApps, Key.isUdpDns, Key.isIpv6)
val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL,
Executable.TUN2SOCKS)
// Helper functions
def updateDropDownPreference(pref: Preference, value: String) {
pref.asInstanceOf[DropDownPreference].setValue(value)
}
def updatePasswordEditTextPreference(pref: Preference, value: String) {
pref.setSummary(value)
pref.asInstanceOf[PasswordEditTextPreference].setText(value)
}
def updateNumberPickerPreference(pref: Preference, value: Int) {
pref.asInstanceOf[NumberPickerPreference].setValue(value)
}
def updateSummaryEditTextPreference(pref: Preference, value: String) {
pref.setSummary(value)
pref.asInstanceOf[SummaryEditTextPreference].setText(value)
}
def updateSwitchPreference(pref: Preference, value: Boolean) {
pref.asInstanceOf[SwitchPreference].setChecked(value)
}
def updatePreference(pref: Preference, name: String, profile: Profile) {
name match {
case Key.profileName => updateSummaryEditTextPreference(pref, profile.name)
case Key.proxy => updateSummaryEditTextPreference(pref, profile.host)
case Key.remotePort => updateNumberPickerPreference(pref, profile.remotePort)
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
case Key.sitekey => updatePasswordEditTextPreference(pref, profile.password)
case Key.encMethod => updateDropDownPreference(pref, profile.method)
case Key.route => updateDropDownPreference(pref, profile.route)
case Key.isProxyApps => updateSwitchPreference(pref, profile.proxyApps)
case Key.isUdpDns => updateSwitchPreference(pref, profile.udpdns)
case Key.isAuth => updateSwitchPreference(pref, profile.auth)
case Key.isIpv6 => updateSwitchPreference(pref, profile.ipv6)
}
}
}
class Shadowsocks
@@ -149,7 +104,6 @@ class Shadowsocks
var progressDialog: ProgressDialog = _
var progressTag = -1
var state = State.STOPPED
var prepared = false
var currentProfile = new Profile
var vpnEnabled = -1
var trafficCache: String = _
@@ -166,7 +120,7 @@ class Shadowsocks
fab.setImageResource(R.drawable.ic_cloud_queue)
fab.setEnabled(false)
fabProgressCircle.show()
setPreferenceEnabled(enabled = false)
preferences.setEnabled(false)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint)
if (state == State.CONNECTING) {
@@ -176,7 +130,7 @@ class Shadowsocks
}
fab.setEnabled(true)
changeSwitch(checked = true)
setPreferenceEnabled(enabled = false)
preferences.setEnabled(false)
case State.STOPPED =>
fab.setBackgroundTintList(greyTint)
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
@@ -189,25 +143,26 @@ class Shadowsocks
(_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
snackbar.show
}
setPreferenceEnabled(enabled = true)
preferences.setEnabled(true)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_cloud_queue)
fab.setEnabled(false)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
setPreferenceEnabled(enabled = false)
preferences.setEnabled(false)
}
state = s
})
}
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
trafficCache = getString(R.string.stat_summary).formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal)
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
trafficCache = getString(R.string.stat_summary).formatLocal(Locale.ENGLISH,
TrafficMonitor.formatTraffic(txRate), TrafficMonitor.formatTraffic(rxRate),
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal))
handler.post(updateTraffic)
}
}
def updateTraffic(): Unit = if (trafficCache == null) callback.trafficUpdated(TrafficMonitor.getTxRate,
TrafficMonitor.getRxRate, TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal) else {
def updateTraffic(): Unit = if (trafficCache == null) callback.trafficUpdated(0, 0, 0, 0) else {
if (connectionTestResult == null) connectionTestResult = getString(R.string.connection_test_pending)
if (preferences.natSwitch.isChecked) {
preferences.stat.setSummary(trafficCache)
@@ -255,7 +210,7 @@ class Shadowsocks
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
private var adView: AdView = _
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_blue_grey_700)
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_a700)
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
var handler = new Handler()
@@ -367,12 +322,7 @@ class Shadowsocks
changeSwitch(checked = false)
}
def isReady: Boolean = {
if (!checkText(Key.proxy)) return false
if (!checkText(Key.sitekey)) return false
if (bgService == null) return false
true
}
def isReady: Boolean = checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null
def prepareStartService() {
ThrowableFuture {
@@ -438,9 +388,10 @@ class Shadowsocks
currentProfile = ShadowsocksApplication.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List[Profile]())
if (profiles.isEmpty) ShadowsocksApplication.profileManager.createDefault()
else ShadowsocksApplication.switchProfile(profiles.head.id)
ShadowsocksApplication.profileManager.getFirstProfile match {
case Some(first) => ShadowsocksApplication.switchProfile(first.id)
case None => ShadowsocksApplication.profileManager.createDefault()
}
}
updatePreferenceScreen()
@@ -451,7 +402,6 @@ class Shadowsocks
protected override def onPause() {
super.onPause()
ShadowsocksApplication.profileManager.save
prepared = false
}
private def stateUpdate() {
@@ -461,25 +411,25 @@ class Shadowsocks
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_cloud_queue)
setPreferenceEnabled(false)
preferences.setEnabled(false)
fabProgressCircle.show()
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint)
serviceStarted = true
fab.setImageResource(R.drawable.ic_cloud)
setPreferenceEnabled(false)
preferences.setEnabled(false)
fabProgressCircle.postDelayed(fabProgressCircle.hide, 100)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_cloud_queue)
setPreferenceEnabled(false)
preferences.setEnabled(false)
fabProgressCircle.show()
case _ =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_cloud_off)
setPreferenceEnabled(true)
preferences.setEnabled(true)
fabProgressCircle.postDelayed(fabProgressCircle.hide, 100)
}
state = bgService.getState
@@ -497,53 +447,34 @@ class Shadowsocks
updateTraffic()
}
private def setPreferenceEnabled(enabled: Boolean) {
preferences.findPreference(Key.isNAT).setEnabled(enabled)
for (name <- Shadowsocks.PROXY_PREFS) {
val pref = preferences.findPreference(name)
if (pref != null) {
pref.setEnabled(enabled)
}
}
for (name <- Shadowsocks.FEATURE_PREFS) {
val pref = preferences.findPreference(name)
if (pref != null) {
if (name == Key.isProxyApps) {
pref.setEnabled(enabled && (Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
} else {
pref.setEnabled(enabled)
}
}
}
}
private def updatePreferenceScreen() {
val profile = currentProfile
if (profile.host == "198.199.101.152" && adView == null) {
if (profile.host == "198.199.101.152") if (adView == null) {
adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.SMART_BANNER)
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 0)
adView.loadAd(new AdRequest.Builder().build())
}
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
for (name <- Shadowsocks.PROXY_PREFS) {
val pref = preferences.findPreference(name)
Shadowsocks.updatePreference(pref, name, profile)
}
for (name <- Shadowsocks.FEATURE_PREFS) {
val pref = preferences.findPreference(name)
Shadowsocks.updatePreference(pref, name, profile)
}
preferences.update(profile)
}
override def onStart() {
super.onStart()
registerCallback
}
override def onStop() {
super.onStop()
unregisterCallback
clearDialog()
}
private var _isDestroyed: Boolean = _
override def isDestroyed = if (Build.VERSION.SDK_INT >= 17) super.isDestroyed else _isDestroyed
override def onDestroy() {
super.onDestroy()
_isDestroyed = true
deattachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
@@ -585,7 +516,6 @@ class Shadowsocks
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) = resultCode match {
case Activity.RESULT_OK =>
prepared = true
serviceStart()
case _ =>
cancelStart()
@@ -613,8 +543,8 @@ class Shadowsocks
}
def clearDialog() {
if (progressDialog != null) {
progressDialog.dismiss()
if (progressDialog != null && progressDialog.isShowing) {
if (!isDestroyed) progressDialog.dismiss()
progressDialog = null
progressTag = -1
}
@@ -44,6 +44,7 @@ import java.lang.Process
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
import android.app.Service
import android.content._
import android.content.pm.{PackageInfo, PackageManager}
import android.net.{ConnectivityManager, Network}
@@ -331,6 +332,8 @@ class ShadowsocksNatService extends BaseService {
ConfigUtils.refresh(this)
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_STICKY
def killProcesses() {
if (sslocalProcess != null) {
sslocalProcess.destroy()
@@ -458,8 +461,6 @@ class ShadowsocksNatService extends BaseService {
override def stopRunner() {
super.stopRunner()
// channge the state
changeState(State.STOPPING)
@@ -476,17 +477,7 @@ class ShadowsocksNatService extends BaseService {
// reset NAT
killProcesses()
// stop the service if no callback registered
if (getCallbackCount == 0) {
stopSelf()
}
// change the state
changeState(State.STOPPED)
}
override def stopBackgroundService() {
stopSelf()
super.stopRunner()
}
override def getTag = TAG
@@ -2,31 +2,35 @@ package com.github.shadowsocks
import java.util.Locale
import android.app.{KeyguardManager, PendingIntent}
import android.app.{KeyguardManager, NotificationManager, PendingIntent}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.content.ContextCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{Action, State, Utils}
import com.github.shadowsocks.utils.{TrafficMonitor, Action, State, Utils}
/**
* @author Mygod
*/
class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) {
private lazy val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private lazy val callback = new Stub {
override def stateChanged(state: Int, msg: String) = () // ignore
override def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txRate, rxRate))
style.bigText(service.getString(R.string.stat_summary)
.formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal))
override def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
val txr = TrafficMonitor.formatTraffic(txRate)
val rxr = TrafficMonitor.formatTraffic(rxRate)
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txr, rxr))
style.bigText(service.getString(R.string.stat_summary).formatLocal(Locale.ENGLISH, txr, rxr,
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal)))
show()
}
}
private var lockReceiver: BroadcastReceiver = _
private var callbackRegistered: Boolean = _
private var started: Boolean = _
private val builder = new NotificationCompat.Builder(service)
.setWhen(0)
@@ -73,8 +77,10 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
show()
} else if (forceShow) show()
def notification = builder.build()
def show() = service.startForeground(1, notification)
def show() = if (started) nm.notify(1, builder.build) else {
service.startForeground(1, builder.build)
started = true
}
def destroy() {
if (lockReceiver != null) {
@@ -11,11 +11,61 @@ import android.os.{Build, Bundle}
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
import android.support.v7.app.AlertDialog
import android.webkit.{WebView, WebViewClient}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.{DropDownPreference, NumberPickerPreference, PasswordEditTextPreference, SummaryEditTextPreference}
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.{Key, Utils}
object ShadowsocksSettings {
// Constants
val PREFS_NAME = "Shadowsocks"
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey, Key.encMethod,
Key.isAuth)
val FEATURE_PREFS = Array(Key.route, Key.isProxyApps, Key.isUdpDns, Key.isIpv6)
// Helper functions
def updateDropDownPreference(pref: Preference, value: String) {
pref.asInstanceOf[DropDownPreference].setValue(value)
}
def updatePasswordEditTextPreference(pref: Preference, value: String) {
pref.setSummary(value)
pref.asInstanceOf[PasswordEditTextPreference].setText(value)
}
def updateNumberPickerPreference(pref: Preference, value: Int) {
pref.asInstanceOf[NumberPickerPreference].setValue(value)
}
def updateSummaryEditTextPreference(pref: Preference, value: String) {
pref.setSummary(value)
pref.asInstanceOf[SummaryEditTextPreference].setText(value)
}
def updateSwitchPreference(pref: Preference, value: Boolean) {
pref.asInstanceOf[SwitchPreference].setChecked(value)
}
def updatePreference(pref: Preference, name: String, profile: Profile) {
name match {
case Key.profileName => updateSummaryEditTextPreference(pref, profile.name)
case Key.proxy => updateSummaryEditTextPreference(pref, profile.host)
case Key.remotePort => updateNumberPickerPreference(pref, profile.remotePort)
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
case Key.sitekey => updatePasswordEditTextPreference(pref, profile.password)
case Key.encMethod => updateDropDownPreference(pref, profile.method)
case Key.route => updateDropDownPreference(pref, profile.route)
case Key.isProxyApps => updateSwitchPreference(pref, profile.proxyApps)
case Key.isUdpDns => updateSwitchPreference(pref, profile.udpdns)
case Key.isAuth => updateSwitchPreference(pref, profile.auth)
case Key.isIpv6 => updateSwitchPreference(pref, profile.ipv6)
}
}
}
// TODO: Move related logic here
class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
import ShadowsocksSettings._
private def activity = getActivity.asInstanceOf[Shadowsocks]
lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
var stat: Preference = _
@@ -116,8 +166,8 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
isProxyApps.setChecked(ShadowsocksApplication.settings.getBoolean(Key.isProxyApps, false)) // update
}
override def onPause {
super.onPause()
override def onDestroy {
super.onDestroy()
ShadowsocksApplication.settings.unregisterOnSharedPreferenceChangeListener(this)
}
@@ -128,4 +178,30 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
})
case _ =>
}
private var enabled = true
def setEnabled(enabled: Boolean) {
this.enabled = enabled
findPreference(Key.isNAT).setEnabled(enabled)
for (name <- PROXY_PREFS) {
val pref = findPreference(name)
if (pref != null) pref.setEnabled(enabled)
}
for (name <- FEATURE_PREFS) {
val pref = findPreference(name)
if (pref != null) pref.setEnabled(enabled &&
(name != Key.isProxyApps || Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
}
}
def update(profile: Profile) {
for (name <- PROXY_PREFS) {
val pref = findPreference(name)
updatePreference(pref, name, profile)
}
for (name <- FEATURE_PREFS) {
val pref = findPreference(name)
updatePreference(pref, name, profile)
}
}
}
@@ -49,6 +49,8 @@ import android.net.VpnService
import android.os._
import android.util.Log
import android.widget.Toast
import android.content.pm.PackageManager.NameNotFoundException
import com.github.shadowsocks.aidl.Config
import com.github.shadowsocks.utils._
import org.apache.commons.net.util.SubnetUtils
@@ -113,8 +115,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
override def stopRunner() {
super.stopRunner()
if (vpnThread != null) {
vpnThread.stopThread()
vpnThread = null
@@ -136,19 +136,13 @@ class ShadowsocksVpnService extends VpnService with BaseService {
conn = null
}
// stop the service if no callback registered
if (getCallbackCount == 0) {
stopSelf()
}
// clean up recevier
if (closeReceiver != null) {
unregisterReceiver(closeReceiver)
closeReceiver = null
}
// channge the state
changeState(State.STOPPED)
super.stopRunner()
}
def getVersionName: String = {
@@ -389,10 +383,15 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (config.isProxyApps) {
for (pkg <- config.proxiedAppString.split('\n')) {
if (!config.isBypassApps) {
builder.addAllowedApplication(pkg)
} else {
builder.addDisallowedApplication(pkg)
try {
if (!config.isBypassApps) {
builder.addAllowedApplication(pkg)
} else {
builder.addDisallowedApplication(pkg)
}
} catch {
case ex: NameNotFoundException =>
Log.e(TAG, "Invalid package name", ex);
}
}
}
@@ -458,10 +457,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
fd
}
override def stopBackgroundService() {
stopSelf()
}
override def getTag = TAG
override def getServiceMode = Mode.VPN
@@ -45,7 +45,12 @@ import java.util.concurrent.Executors
import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress}
import android.util.Log
object ShadowsocksVpnThread {
val getInt = classOf[FileDescriptor].getDeclaredMethod("getInt$")
}
class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
import ShadowsocksVpnThread._
val TAG = "ShadowsocksVpnService"
val PATH = "/data/data/com.github.shadowsocks/protect_path"
@@ -103,11 +108,8 @@ class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
val fds = socket.getAncillaryFileDescriptors
if (fds.nonEmpty) {
var ret = false
val getInt = classOf[FileDescriptor].getDeclaredMethod("getInt$")
val fd = getInt.invoke(fds(0)).asInstanceOf[Int]
ret = vpnService.protect(fd)
val ret = vpnService.protect(fd)
// Trick to close file decriptor
System.jniclose(fd)
@@ -64,7 +64,7 @@ object DBHelper {
}
class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 14) {
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 15) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
@@ -110,10 +110,15 @@ class DBHelper(val context: Context)
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
}
if (oldVersion < 14) {
if (oldVersion < 15) {
if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;")
var i = 0
for (profile <- profileDao.queryForAll.asScala) {
profile.individual = updateProxiedApps(context, profile.individual)
if (oldVersion < 14) profile.individual = updateProxiedApps(context, profile.individual)
profile.userOrder = i
profileDao.update(profile)
i += 1
}
}
}
@@ -55,13 +55,13 @@ class Profile {
var localPort: Int = 1080
@DatabaseField
var remotePort: Int = 8338
var remotePort: Int = 8388
@DatabaseField
var password: String = ""
@DatabaseField
var method: String = "rc4"
var method: String = "aes-256-cfb"
@DatabaseField
var route: String = "all"
@@ -92,4 +92,7 @@ class Profile {
@DatabaseField
val date: java.util.Date = new java.util.Date()
@DatabaseField
var userOrder: Long = _
}
@@ -64,6 +64,9 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
profile.udpdns = oldProfile.udpdns
case _ =>
}
val last = dbHelper.profileDao.queryRaw(dbHelper.profileDao.queryBuilder.selectRaw("MAX(userOrder)")
.prepareStatementString).getFirstResult
if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1
dbHelper.profileDao.createOrUpdate(profile)
if (profileAddedListener != null) profileAddedListener(profile)
profile
@@ -109,10 +112,21 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
}
}
def getFirstProfile = {
try {
val result = dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.limit(1L).prepare)
if (result.size == 1) Option(result.get(0)) else None
} catch {
case ex: Exception =>
Log.e(Shadowsocks.TAG, "getAllProfiles", ex)
None
}
}
def getAllProfiles: Option[List[Profile]] = {
try {
import scala.collection.JavaConversions._
Option(dbHelper.profileDao.queryForAll().toList)
Option(dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.orderBy("userOrder", true).prepare).toList)
} catch {
case ex: Exception =>
Log.e(Shadowsocks.TAG, "getAllProfiles", ex)
@@ -5,26 +5,20 @@ import java.text.DecimalFormat
import com.github.shadowsocks.{R, ShadowsocksApplication}
case class Traffic(tx: Long, rx: Long, timestamp: Long)
object TrafficMonitor {
var last: Traffic = getTraffic(0, 0)
// Bytes per second
var txRate: Long = 0
var rxRate: Long = 0
var txRate: Long = _
var rxRate: Long = _
// Bytes for the current session
var txTotal: Long = 0
var rxTotal: Long = 0
var txTotal: Long = _
var rxTotal: Long = _
// Bytes for the last query
var txLast: Long = 0
var rxLast: Long = 0
def getTraffic(tx: Long, rx: Long): Traffic = {
new Traffic(tx, rx, System.currentTimeMillis())
}
var txLast: Long = _
var rxLast: Long = _
var timestampLast: Long = _
@volatile var dirty = true
private val units = Array("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
private val numberFormat = new DecimalFormat("@@@")
@@ -39,21 +33,42 @@ object TrafficMonitor {
else numberFormat.format(n) + ' ' + units(i)
}
def updateRate() {
val now = getTraffic(txTotal, rxTotal)
val delta = now.timestamp - last.timestamp
val deltaTx = now.tx - last.tx
val deltaRx = now.rx - last.rx
def updateRate() = {
val now = System.currentTimeMillis()
val delta = now - timestampLast
var updated = false
if (delta != 0) {
txRate = deltaTx * 1000 / delta
rxRate = deltaRx * 1000 / delta
if (dirty) {
txRate = (txTotal - txLast) * 1000 / delta
rxRate = (rxTotal - rxLast) * 1000 / delta
txLast = txTotal
rxLast = rxTotal
dirty = false
updated = true
} else {
if (txRate != 0) {
txRate = 0
updated = true
}
if (rxRate != 0) {
rxRate = 0
updated = true
}
}
timestampLast = now
}
last = now
updated
}
def update(tx: Long, rx: Long) {
txTotal = tx
rxTotal = rx
if (txTotal != tx) {
txTotal = tx
dirty = true
}
if (rxTotal != rx) {
rxTotal = rx
dirty = true
}
}
def reset() {
@@ -63,43 +78,7 @@ object TrafficMonitor {
rxTotal = 0
txLast = 0
rxLast = 0
last = getTraffic(0, 0)
}
def getTxTotal(): String = {
formatTraffic(txTotal)
}
def getRxTotal(): String = {
formatTraffic(rxTotal)
}
def getTotal(): String = {
formatTraffic(txTotal + rxTotal)
}
def getTxRate(): String = {
formatTraffic(txRate)
}
def getRxRate(): String = {
formatTraffic(rxRate)
}
def getRate(): String = {
formatTraffic(txRate + rxRate)
}
def getDeltaTx(): Long = {
val last = txLast
txLast = txTotal
txTotal - last
}
def getDeltaRx(): Long = {
val last = rxLast
rxLast = rxTotal
rxTotal - last
dirty = true
}
}
@@ -40,11 +40,11 @@
package com.github.shadowsocks.utils
import java.io.{File, IOException}
import java.nio.{ByteBuffer, ByteOrder}
import java.util.concurrent.Executors
import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress}
import android.util.Log
import com.github.shadowsocks.BaseService
class TrafficMonitorThread() extends Thread {
@@ -99,16 +99,10 @@ class TrafficMonitorThread() extends Thread {
val input = socket.getInputStream
val output = socket.getOutputStream
val buffer = new Array[Byte](256)
val size = input.read(buffer)
val array = new Array[Byte](size)
Array.copy(buffer, 0, array, 0, size)
val stat = new String(array, "UTF-8").split("\\|")
if (stat.length == 2) {
TrafficMonitor.update(stat(0).toLong, stat(1).toLong)
}
val buffer = new Array[Byte](16)
if (input.read(buffer) != 16) throw new IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
output.write(0)