diff --git a/build.sbt b/build.sbt index 2194c008..3abae4dd 100644 --- a/build.sbt +++ b/build.sbt @@ -37,6 +37,7 @@ libraryDependencies ++= Seq( "com.google.android.gms" % "play-services-analytics" % "8.1.0", "com.android.support" % "support-v4" % "23.1.0", "com.android.support" % "design" % "23.1.0", + "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01", "com.nostra13.universalimageloader" % "universal-image-loader" % "1.9.4", "com.j256.ormlite" % "ormlite-core" % "4.48", "com.j256.ormlite" % "ormlite-android" % "4.48" diff --git a/src/main/res/drawable-hdpi-v21/ic_drawer.png b/src/main/res/drawable-hdpi-v21/ic_drawer.png deleted file mode 100755 index 5c2c7f1e..00000000 Binary files a/src/main/res/drawable-hdpi-v21/ic_drawer.png and /dev/null differ diff --git a/src/main/res/drawable-hdpi/ic_drawer.png b/src/main/res/drawable-hdpi/ic_drawer.png index ec8c51f8..5c2c7f1e 100644 Binary files a/src/main/res/drawable-hdpi/ic_drawer.png and b/src/main/res/drawable-hdpi/ic_drawer.png differ diff --git a/src/main/res/drawable-mdpi-v21/ic_drawer.png b/src/main/res/drawable-mdpi-v21/ic_drawer.png deleted file mode 100755 index c9121257..00000000 Binary files a/src/main/res/drawable-mdpi-v21/ic_drawer.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_drawer.png b/src/main/res/drawable-mdpi/ic_drawer.png index 7420a68d..c9121257 100644 Binary files a/src/main/res/drawable-mdpi/ic_drawer.png and b/src/main/res/drawable-mdpi/ic_drawer.png differ diff --git a/src/main/res/drawable-xhdpi-v21/ic_drawer.png b/src/main/res/drawable-xhdpi-v21/ic_drawer.png deleted file mode 100755 index 56f01f3e..00000000 Binary files a/src/main/res/drawable-xhdpi-v21/ic_drawer.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_drawer.png b/src/main/res/drawable-xhdpi/ic_drawer.png index 7ffbe5c5..56f01f3e 100644 Binary files a/src/main/res/drawable-xhdpi/ic_drawer.png and b/src/main/res/drawable-xhdpi/ic_drawer.png differ diff --git a/src/main/res/drawable-xxhdpi-v21/ic_drawer.png b/src/main/res/drawable-xxhdpi/ic_drawer.png old mode 100755 new mode 100644 similarity index 100% rename from src/main/res/drawable-xxhdpi-v21/ic_drawer.png rename to src/main/res/drawable-xxhdpi/ic_drawer.png diff --git a/src/main/res/layout/layout_apps.xml b/src/main/res/layout/layout_apps.xml index 0f02c21a..f84d4bbd 100644 --- a/src/main/res/layout/layout_apps.xml +++ b/src/main/res/layout/layout_apps.xml @@ -1,35 +1,37 @@ - - + + + android:text="@string/bypass_apps"/> - + android:layout_marginEnd="6dp" + android:layout_marginRight="6dp"/> + - + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" android:layout_height="match_parent" + tools:ignore="UnknownIdInLayout" android:clipChildren="false"> - + android:layout_width="match_parent" android:layout_height="match_parent"/> + + + diff --git a/src/main/res/values-v21/dimen.xml b/src/main/res/values-v21/dimen.xml new file mode 100644 index 00000000..4c95daeb --- /dev/null +++ b/src/main/res/values-v21/dimen.xml @@ -0,0 +1,4 @@ + + + -28dp + diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml index c9457d6f..195090d5 100755 --- a/src/main/res/values-zh/strings.xml +++ b/src/main/res/values-zh/strings.xml @@ -26,7 +26,6 @@ 国内路由 绕过所有位于中国的网站 路由 - 绕过指定路由列表中的地址 全局代理 设置系统代理 分应用代理 @@ -54,7 +53,7 @@ 端口号不能为空 代理服务器地址不能为空 检测到意外退出,状态已被重置。 - 正在连接... + 连接 正在初始化... 重置中... 正在加载... diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml new file mode 100644 index 00000000..ca0adbc5 --- /dev/null +++ b/src/main/res/values/attrs.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 2ce8dac8..157d3c4a 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -12,7 +12,7 @@ #00000000 #FF555555 - #2196F3 + #43A047 #90A4AE #607D8B #455A64 diff --git a/src/main/res/values/colors_shadow.xml b/src/main/res/values/colors_shadow.xml deleted file mode 100644 index 87e9fcf5..00000000 --- a/src/main/res/values/colors_shadow.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - #CC33B5E5 - #EEE - diff --git a/src/main/res/values/dimen.xml b/src/main/res/values/dimen.xml new file mode 100644 index 00000000..f8e782e7 --- /dev/null +++ b/src/main/res/values/dimen.xml @@ -0,0 +1,5 @@ + + + + -44dp + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 7b8660ee..0fa48668 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -37,7 +37,6 @@ Bypass all sites located in China Route - Bypass IP addresses in the route list Global Proxy Set up system-wide proxy Per-App Proxy @@ -75,7 +74,7 @@ Proxy should not be empty An unexpected exit detected, the context has been reset. - Connecting… + Connect Initializing… Resetting… Loading… diff --git a/src/main/res/xml/pref_all.xml b/src/main/res/xml/pref_all.xml index f6471c9a..c4df91c0 100644 --- a/src/main/res/xml/pref_all.xml +++ b/src/main/res/xml/pref_all.xml @@ -1,5 +1,6 @@ - + @@ -37,40 +38,41 @@ android:summary="@string/sitekey_summary" android:title="@string/sitekey"> - - + - - - + - - + - + - - - + - - + - + diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala index d6c16be8..88004783 100644 --- a/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -78,7 +78,7 @@ class ObjectArrayTools[T <: AnyRef](a: Array[T]) { } } -case class ListEntry(box: CheckBox, text: TextView, icon: ImageView) +case class ListEntry(switch: Switch, text: TextView, icon: ImageView) object AppManager { @@ -162,13 +162,13 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe 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 switch = convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch] val text = convertView.findViewById(R.id.itemtext).asInstanceOf[TextView] - entry = new ListEntry(box, text, icon) + entry = new ListEntry(switch, text, icon) entry.text.setOnClickListener(AppManager.this) entry.text.setOnClickListener(AppManager.this) convertView.setTag(entry) - entry.box.setOnCheckedChangeListener(AppManager.this) + entry.switch.setOnCheckedChangeListener(AppManager.this) } else { entry = convertView.getTag.asInstanceOf[ListEntry] } @@ -187,10 +187,10 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe 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) + val switch = entry.switch + switch.setTag(app) + switch.setChecked(app.proxied) + entry.text.setTag(switch) convertView } } @@ -207,11 +207,11 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe } def onClick(v: View) { - val cbox: CheckBox = v.getTag.asInstanceOf[CheckBox] - val app: ProxiedApp = cbox.getTag.asInstanceOf[ProxiedApp] + val switch = v.getTag.asInstanceOf[Switch] + val app: ProxiedApp = switch.getTag.asInstanceOf[ProxiedApp] if (app != null) { app.proxied = !app.proxied - cbox.setChecked(app.proxied) + switch.setChecked(app.proxied) } saveAppSettings(this) } @@ -247,8 +247,8 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe val array = proxiedAppString.split(" ") val bypass = array(0).toBoolean val apps = if (array.size > 1) array(1) else "" - prefs.edit.putBoolean(Key.isBypassApps, bypass).commit() - prefs.edit.putString(Key.proxied, apps).commit() + 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 @@ -299,7 +299,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe 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).commit()) + prefs.edit().putBoolean(Key.isBypassApps, checked).apply()) bypassSwitch.setChecked(prefs.getBoolean(Key.isBypassApps, false)) appListView = findViewById(R.id.applistview).asInstanceOf[ListView] @@ -325,7 +325,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe }) val edit: SharedPreferences.Editor = prefs.edit edit.putString(Key.proxied, proxiedApps.toString()) - edit.commit + edit.apply } val loadStartRunnable = new Runnable { diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala index a10b19d6..82f77202 100644 --- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala +++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala @@ -50,17 +50,18 @@ import android.content.res.AssetManager import android.graphics.{Bitmap, Color, Typeface} import android.net.{Uri, VpnService} import android.os._ -import android.preference._ +import android.preference.{Preference, PreferenceManager, SwitchPreference} import android.support.design.widget.{FloatingActionButton, Snackbar} import android.support.v7.app.AppCompatActivity import android.support.v7.widget.Toolbar import android.util.{DisplayMetrics, Log} -import android.view._ +import android.view.{View, ViewGroup, ViewParent} import android.webkit.{WebView, WebViewClient} import android.widget._ +import com.github.jorgecastilloprz.FABProgressCircle import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback} import com.github.shadowsocks.database._ -import com.github.shadowsocks.preferences.{PasswordEditTextPreference, ProfileEditTextPreference, SummaryEditTextPreference} +import com.github.shadowsocks.preferences.{DropDownPreference, PasswordEditTextPreference, ProfileEditTextPreference, SummaryEditTextPreference} import com.github.shadowsocks.utils._ import com.google.android.gms.ads.{AdRequest, AdSize, AdView} import com.google.android.gms.analytics.HitBuilders @@ -132,8 +133,8 @@ object Shadowsocks { val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL, Executable.TUN2SOCKS) // Helper functions - def updateListPreference(pref: Preference, value: String) { - pref.asInstanceOf[ListPreference].setValue(value) + def updateDropDownPreference(pref: Preference, value: String) { + pref.asInstanceOf[DropDownPreference].setValue(value) } def updatePasswordEditTextPreference(pref: Preference, value: String) { @@ -151,8 +152,8 @@ object Shadowsocks { pref.asInstanceOf[ProfileEditTextPreference].setText(value) } - def updateCheckBoxPreference(pref: Preference, value: Boolean) { - pref.asInstanceOf[CheckBoxPreference].setChecked(value) + def updateSwitchPreference(pref: Preference, value: Boolean) { + pref.asInstanceOf[SwitchPreference].setChecked(value) } def updatePreference(pref: Preference, name: String, profile: Profile) { @@ -162,12 +163,12 @@ object Shadowsocks { case Key.remotePort => updateSummaryEditTextPreference(pref, profile.remotePort.toString) case Key.localPort => updateSummaryEditTextPreference(pref, profile.localPort.toString) case Key.sitekey => updatePasswordEditTextPreference(pref, profile.password) - case Key.encMethod => updateListPreference(pref, profile.method) - case Key.route => updateListPreference(pref, profile.route) - case Key.isGlobalProxy => updateCheckBoxPreference(pref, profile.global) - case Key.isUdpDns => updateCheckBoxPreference(pref, profile.udpdns) - case Key.isAuth => updateCheckBoxPreference(pref, profile.auth) - case Key.isIpv6 => updateCheckBoxPreference(pref, profile.ipv6) + case Key.encMethod => updateDropDownPreference(pref, profile.method) + case Key.route => updateDropDownPreference(pref, profile.route) + case Key.isGlobalProxy => updateSwitchPreference(pref, profile.global) + case Key.isUdpDns => updateSwitchPreference(pref, profile.udpdns) + case Key.isAuth => updateSwitchPreference(pref, profile.auth) + case Key.isIpv6 => updateSwitchPreference(pref, profile.ipv6) case _ => } } @@ -186,6 +187,7 @@ class Shadowsocks // Variables private var serviceStarted = false var fab: FloatingActionButton = _ + var fabProgressCircle: FABProgressCircle = _ var progressDialog: ProgressDialog = _ var progressTag = -1 var state = State.INIT @@ -221,7 +223,7 @@ class Shadowsocks state = bgService.getState if (!status.getBoolean(getVersionName, false)) { - status.edit.putBoolean(getVersionName, true).commit() + status.edit.putBoolean(getVersionName, true).apply() recovery() } } @@ -394,7 +396,6 @@ class Shadowsocks } def prepareStartService() { - showProgress(R.string.connecting) Future { if (isVpnEnabled) { val intent = VpnService.prepare(this) @@ -485,6 +486,7 @@ class Shadowsocks val tf: Typeface = Typefaces.get(this, "fonts/Iceland.ttf") if (tf != null) title.setTypeface(tf) fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton] + fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle] fab.setOnClickListener((v: View) => { serviceStarted = !serviceStarted serviceStarted match { @@ -495,24 +497,31 @@ class Shadowsocks changeSwitch(checked = false) case false => serviceStop() + if (fab.isEnabled) { + fab.setEnabled(false) + handler.postDelayed(() => { + fab.setEnabled(true) + }, 1000) + } } - if (fab.isEnabled) { - fab.setEnabled(false) - handler.postDelayed(() => fab.setEnabled(true), 1000) - } + }) + fab.setOnLongClickListener((v: View) => { + Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect, + Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8).toInt).show + true }) toolbar.setNavigationIcon(R.drawable.ic_drawer) toolbar.setNavigationOnClickListener((v: View) => drawer.toggleMenu()) title.setOnLongClickListener((v: View) => { if (Utils.isLollipopOrAbove && bgService != null && (bgService.getState == State.INIT || bgService.getState == State.STOPPED)) { - val natEnabled = status.getBoolean(Key.isNAT, false) - status.edit().putBoolean(Key.isNAT, !natEnabled).commit() - if (!natEnabled) { - Toast.makeText(getBaseContext, R.string.enable_nat, Toast.LENGTH_LONG).show() - } else { - Toast.makeText(getBaseContext, R.string.disable_nat, Toast.LENGTH_LONG).show() - } + val natEnabled = !status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove) + status.edit().putBoolean(Key.isNAT, natEnabled).apply() + deattachService() + attachService() + if (!Utils.isLollipopOrAbove) setPreferenceEnabled(State.isAvailable(bgService.getState)) + Toast.makeText(getBaseContext, if (natEnabled) R.string.enable_nat else R.string.disable_nat, Toast.LENGTH_LONG) + .show() true } else { false @@ -523,13 +532,9 @@ class Shadowsocks registerReceiver(preferenceReceiver, new IntentFilter(Action.UPDATE_PREFS)) // Bind to the service - Future { - val isRoot = (!Utils.isLollipopOrAbove || status.getBoolean(Key.isNAT, false)) && Console.isRoot - handler.post(() => { - status.edit.putBoolean(Key.isRoot, isRoot).commit() - attachService() - }) - } + handler.post(() => { + attachService() + }) } def attachService() { @@ -777,11 +782,17 @@ class Shadowsocks super.onResume() if (bgService != null) { bgService.getState match { - case State.CONNECTED => - changeSwitch(checked = true) case State.CONNECTING => + fabProgressCircle.show() changeSwitch(checked = true) + case State.CONNECTED => + fabProgressCircle.hide() + changeSwitch(checked = true) + case State.STOPPING => + fabProgressCircle.show() + changeSwitch(checked = false) case _ => + fabProgressCircle.hide() changeSwitch(checked = false) } state = bgService.getState @@ -886,7 +897,6 @@ class Shadowsocks private def showQrCode() { val image = new ImageView(this) - image.setPadding(0, dp2px(20), 0, dp2px(20)) image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)) val qrcode = QRCode.from(Parser.generate(currentProfile)) .withSize(dp2px(250), dp2px(250)).asInstanceOf[QRCode] @@ -928,15 +938,10 @@ class Shadowsocks } } + var isRoot: Option[Boolean] = None def isVpnEnabled: Boolean = { - if (vpnEnabled < 0) { - vpnEnabled = if (!status.getBoolean(Key.isRoot, false)) { - 1 - } else { - 0 - } - } - if (vpnEnabled == 1) true else false + if (isRoot.isEmpty) isRoot = Some(Console.isRoot) + !(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove)) } def serviceStop() { @@ -1015,36 +1020,31 @@ class Shadowsocks def onStateChanged(s: Int, m: String) { handler.post(() => if (state != s) { - state = s - state match { + s match { case State.CONNECTING => - if (progressDialog == null) { - progressDialog = ProgressDialog - .show(Shadowsocks.this, "", getString(R.string.connecting), true, true) - progressTag = R.string.connecting - } + fab.setImageResource(R.drawable.ic_cloud_queue) + fab.setEnabled(false) + fabProgressCircle.show() setPreferenceEnabled(enabled = false) case State.CONNECTED => - if (progressTag == R.string.connecting) { - clearDialog() - } + if (state == State.CONNECTING) fabProgressCircle.beginFinalAnimation() else fabProgressCircle.hide() + fab.setEnabled(true) changeSwitch(checked = true) setPreferenceEnabled(enabled = false) case State.STOPPED => - if (progressTag == R.string.stopping || progressTag == R.string.connecting) { - clearDialog() - } + handler.post(() => fabProgressCircle.hide()) + fab.setEnabled(true) changeSwitch(checked = false) if (m != null) Snackbar.make(getWindow.getDecorView.findViewById(android.R.id.content), getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show setPreferenceEnabled(enabled = true) case State.STOPPING => - if (progressDialog == null) { - progressDialog = ProgressDialog - .show(Shadowsocks.this, "", getString(R.string.stopping), true, true) - progressTag = R.string.stopping - } + fab.setImageResource(R.drawable.ic_cloud_queue) + fab.setEnabled(false) + fabProgressCircle.show() + setPreferenceEnabled(enabled = false) } + state = s }) } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index 8ca63951..6029337c 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -50,6 +50,7 @@ import android.content.pm.{PackageInfo, PackageManager} import android.net.{Network, ConnectivityManager} import android.os._ import android.support.v4.app.NotificationCompat +import android.support.v4.content.ContextCompat import android.util.{SparseArray, Log} import android.widget.Toast import com.github.shadowsocks.aidl.Config @@ -319,6 +320,7 @@ class ShadowsocksNatService extends Service with BaseService { builder .setWhen(0) + .setColor(ContextCompat.getColor(this, R.color.material_accent_500)) .setTicker(title) .setContentTitle(getString(R.string.app_name)) .setContentText(info) diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala index 133c0add..7b2d3817 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala @@ -65,19 +65,13 @@ class ShadowsocksRunnerActivity extends Activity { } // Variables - var vpnEnabled = -1 - var bgService: IShadowsocksService = null - var receiver:BroadcastReceiver = null + var isRoot: Option[Boolean] = None + var bgService: IShadowsocksService = _ + var receiver:BroadcastReceiver = _ def isVpnEnabled: Boolean = { - if (vpnEnabled < 0) { - vpnEnabled = if ((!Utils.isLollipopOrAbove || status.getBoolean(Key.isNAT, false)) && Console.isRoot) { - 0 - } else { - 1 - } - } - if (vpnEnabled == 1) true else false + if (isRoot.isEmpty) isRoot = Some(Console.isRoot) + !(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove)) } @@ -127,6 +121,7 @@ class ShadowsocksRunnerActivity extends Activity { } else { attachService() } + finish } override def onDestroy() { diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala index 981c5d30..e27168f2 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala @@ -64,18 +64,12 @@ class ShadowsocksRunnerService extends Service { } // Variables - var vpnEnabled = -1 - var bgService: IShadowsocksService = null + var isRoot: Option[Boolean] = None + var bgService: IShadowsocksService = _ def isVpnEnabled: Boolean = { - if (vpnEnabled < 0) { - vpnEnabled = if ((!Utils.isLollipopOrAbove || status.getBoolean(Key.isNAT, false)) && Console.isRoot) { - 0 - } else { - 1 - } - } - if (vpnEnabled == 1) true else false + if (isRoot.isEmpty) isRoot = Some(Console.isRoot) + !(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove)) } override def onBind(intent: Intent): IBinder = { diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index d7e11665..72e9449d 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -48,6 +48,7 @@ import android.content.pm.{PackageInfo, PackageManager} import android.net.VpnService import android.os._ import android.support.v4.app.NotificationCompat +import android.support.v4.content.ContextCompat import android.util.Log import android.widget.Toast import com.github.shadowsocks.aidl.Config @@ -113,6 +114,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { builder .setWhen(0) + .setColor(ContextCompat.getColor(this, R.color.material_accent_500)) .setTicker(title) .setContentTitle(getString(R.string.app_name)) .setContentText(info) diff --git a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala b/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala index 149dead4..08ea17d3 100644 --- a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala +++ b/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala @@ -133,7 +133,7 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) { edit.putString(Key.proxied, profile.individual) edit.putInt(Key.profileId, profile.id) edit.putString(Key.route, profile.route) - edit.commit() + edit.apply() profile } diff --git a/src/main/scala/com/github/shadowsocks/preferences/DropDownPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/DropDownPreference.scala new file mode 100644 index 00000000..142e384a --- /dev/null +++ b/src/main/scala/com/github/shadowsocks/preferences/DropDownPreference.scala @@ -0,0 +1,180 @@ +package com.github.shadowsocks.preferences + +import android.annotation.TargetApi +import android.content.Context +import android.content.res.TypedArray +import android.os.Build +import android.preference.Preference +import android.support.annotation.NonNull +import android.support.v7.widget.AppCompatSpinner +import android.util.AttributeSet +import android.view.{View, ViewGroup} +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.{AdapterView, ArrayAdapter} +import com.github.shadowsocks.R + +/** + * Based on: https://github.com/Mygod/mygod-lib-android/blob/683c41cb86d10e99ab0f457d41a218c6915fccc8/lib/src/main/scala/tk/mygod/preference/DropDownPreference.scala + * @author Mygod + */ +final class DropDownPreference(private val mContext: Context, attrs: AttributeSet = null) + extends Preference(mContext, attrs) with SummaryPreference { + + private val mAdapter = new ArrayAdapter[String](mContext, android.R.layout.simple_spinner_dropdown_item) + private val mSpinner = new AppCompatSpinner(mContext) + private var mEntries: Array[CharSequence] = _ + private var mEntryValues: Array[CharSequence] = _ + private var mSelectedIndex: Int = _ + private var mValueSet: Boolean = _ + + mSpinner.setVisibility(View.INVISIBLE) + mSpinner.setAdapter(mAdapter) + mSpinner.setOnItemSelectedListener(new OnItemSelectedListener { + override def onNothingSelected(parent: AdapterView[_]) { } + override def onItemSelected(parent: AdapterView[_], view: View, position: Int, id: Long) = setValueIndex(position) + }) + setOnPreferenceClickListener((preference: Preference) => { + // TODO: not working with scrolling + // mSpinner.setDropDownVerticalOffset(Utils.dpToPx(getContext, -48 * mSelectedIndex).toInt) + mSpinner.performClick + true + }) + initSummary(mContext, attrs) + val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference) + setEntries(a.getTextArray(R.styleable.DropDownPreference_entries)) + mEntryValues = a.getTextArray(R.styleable.DropDownPreference_entryValues) + a.recycle + + protected override def getSummaryValue = getEntry + + /** + * Sets the human-readable entries to be shown in the list. This will be shown in subsequent dialogs. + * + * Each entry must have a corresponding index in [[setEntryValues(CharSequence[])]]. + * + * @param entries The entries. + * @see [[setEntryValues(CharSequence[])]] + */ + def setEntries(entries: Array[CharSequence]) { + mEntries = entries + mAdapter.clear + if (entries != null) for (entry <- entries) mAdapter.add(entry.toString) + } + /** + * @see #setEntries(CharSequence[]) + * @param entriesResId The entries array as a resource. + */ + def setEntries(entriesResId: Int): Unit = setEntries(getContext.getResources.getTextArray(entriesResId)) + /** + * The list of entries to be shown in the list in subsequent dialogs. + * + * @return The list as an array. + */ + def getEntries = mEntries + + /** + * The array to find the value to save for a preference when an entry from + * entries is selected. If a user clicks on the second item in entries, the + * second item in this array will be saved to the preference. + * + * @param entryValues The array to be used as values to save for the preference. + */ + def setEntryValues(entryValues: Array[CharSequence]) = mEntryValues = entryValues + /** + * @see #setEntryValues(CharSequence[]) + * @param entryValuesResId The entry values array as a resource. + */ + def setEntryValues(entryValuesResId: Int): Unit = + setEntryValues(getContext.getResources.getTextArray(entryValuesResId)) + /** + * Returns the array of values to be saved for the preference. + * + * @return The array of values. + */ + def getEntryValues: Array[CharSequence] = mEntryValues + + /** + * Sets the value of the key. This should be one of the entries in [[getEntryValues]]. + * + * @param value The value to set for the key. + */ + def setValue(value: String) { + val i = findIndexOfValue(value) + if (i > -1) setValueIndex(i) + } + + /** + * Sets the value to the given index from the entry values. + * + * @param index The index of the value to set. + */ + def setValueIndex(index: Int) { + val changed = mSelectedIndex != index + if (changed || !mValueSet) { + mSelectedIndex = index + mValueSet = true + if (mEntryValues != null) { + val value = mEntryValues(index).toString + persistString(value) + setValue(value) + } + if (changed) notifyChanged + } + mSpinner.setSelection(index) + } + + /** + * Returns the value of the key. This should be one of the entries in [[getEntryValues]]. + * + * @return The value of the key. + */ + def getValue = if (mEntryValues == null) null else mEntryValues(mSelectedIndex).toString + + /** + * Returns the entry corresponding to the current value. + * + * @return The entry corresponding to the current value, or null. + */ + def getEntry = { + val index = getValueIndex + if (index >= 0 && mEntries != null) mEntries(index) else null + } + + /** + * Returns the index of the given value (in the entry values array). + * + * @param value The value whose index should be returned. + * @return The index of the value, or -1 if not found. + */ + def findIndexOfValue(value: String): Int = { + if (value != null && mEntryValues != null) { + var i = mEntryValues.length - 1 + while (i >= 0) { + if (mEntryValues(i) == value) return i + i -= 1 + } + } + -1 + } + + def getValueIndex = mSelectedIndex + + protected override def onGetDefaultValue(a: TypedArray, index: Int) = a.getString(index) + protected override def onSetInitialValue (restoreValue: Boolean, defaultValue: AnyRef) = + setValue(if (restoreValue) getPersistedString(getValue) else defaultValue.asInstanceOf[String]) + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + def setDropDownWidth(dimenResId: Int) = + mSpinner.setDropDownWidth(mContext.getResources.getDimensionPixelSize(dimenResId)) + + protected override def onBindView(@NonNull view: View) { + super.onBindView(view) + val parent = mSpinner.getParent.asInstanceOf[ViewGroup] + if (view eq parent) return + if (parent != null) parent.removeView(mSpinner) + view.asInstanceOf[ViewGroup].addView(mSpinner, 0) + val lp = mSpinner.getLayoutParams + lp.width = 0 + mSpinner.setLayoutParams(lp) + } +} diff --git a/src/main/scala/com/github/shadowsocks/preferences/SummaryListPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/SummaryListPreference.scala deleted file mode 100644 index 4cf92bac..00000000 --- a/src/main/scala/com/github/shadowsocks/preferences/SummaryListPreference.scala +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.preferences - -import android.preference.ListPreference -import android.content.Context -import android.util.AttributeSet - -class SummaryListPreference(context: Context, attrs: AttributeSet) - extends ListPreference(context, attrs) { - - override def setValue(text: String) { - super.setValue(text) - val entry = getEntry - if (entry != null) - setSummary(entry) - else - setSummary(text) - } - - override def setSummary(summary: CharSequence) { - if (summary == null || summary.toString.isEmpty) { - super.setSummary("") - } else { - super.setSummary(summary) - } - } - -} diff --git a/src/main/scala/com/github/shadowsocks/preferences/SummaryPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/SummaryPreference.scala new file mode 100644 index 00000000..56523494 --- /dev/null +++ b/src/main/scala/com/github/shadowsocks/preferences/SummaryPreference.scala @@ -0,0 +1,45 @@ +package com.github.shadowsocks.preferences + +import android.content.Context +import android.preference.Preference +import android.util.AttributeSet +import com.github.shadowsocks.R + +/** + * Make your preference support %s in summary. Override getSummaryValue to customize what to put in. + * Based on: https://github.com/Mygod/mygod-lib-android/blob/eb20c57a0da70bb5dbc27104002f20fb1eb31d76/lib/src/main/scala/tk/mygod/preference/SummaryPreference.scala + * @author Mygod + */ +trait SummaryPreference extends Preference { + private var mSummary: String = _ + + protected def initSummary(context: Context, attrs: AttributeSet) { + val a = context.obtainStyledAttributes(attrs, R.styleable.SummaryPreference) + mSummary = a.getString(R.styleable.SummaryPreference_summary) + a.recycle + } + + protected def getSummaryValue: AnyRef + + /** + * Returns the summary of this SummaryPreference. If the summary has a String formatting marker in it + * (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place. + * + * @return the summary with appropriate string substitution + */ + override def getSummary = { + val entry = getSummaryValue + if (mSummary == null || entry == null) super.getSummary else String.format(mSummary, entry) + } + /** + * Sets the summary for this Preference with a CharSequence. If the summary has a String formatting marker in it + * (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place when it's retrieved. + * + * @param summary The summary for the preference. + */ + override def setSummary(summary: CharSequence) { + super.setSummary(summary) + if (summary == null && mSummary != null) mSummary = null + else if (summary != null && summary != mSummary) mSummary = summary.toString + } +} diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/src/main/scala/com/github/shadowsocks/utils/Constants.scala index 83ab3cfb..3d812e9b 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -66,7 +66,6 @@ object Key { val proxied = "Proxyed" val isNAT = "isNAT" - val isRoot = "isRoot" val status = "status" val proxyedApps = "proxyedApps" val route = "route" diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/src/main/scala/com/github/shadowsocks/utils/Utils.scala index e455effc..85b14a47 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -50,7 +50,10 @@ import android.graphics.drawable.{BitmapDrawable, Drawable} import android.os.Build import android.provider.Settings import android.support.v4.content.ContextCompat -import android.util.{Base64, Log} +import android.util.{DisplayMetrics, Base64, Log} +import android.view.View.MeasureSpec +import android.view.{Gravity, View, Window} +import android.widget.Toast import com.github.shadowsocks.BuildConfig import org.xbill.DNS._ @@ -72,20 +75,13 @@ object Utils { val TIME_OUT: Int = -99 var initialized: Boolean = false var hasRedirectSupport: Int = -1 - var isRoot: Int = -1 var shell: String = null var root_shell: String = null var iptables: String = null var data_path: String = null var rootTries = 0 - def isLollipopOrAbove: Boolean = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - true - } else { - false - } - } + def isLollipopOrAbove: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP def getSignature(context: Context): String = { val info = context @@ -126,6 +122,23 @@ object Utils { bitmap } + // Based on: http://stackoverflow.com/a/21026866/2245107 + def positionToast(toast: Toast, view: View, window: Window, offsetX: Int = 0, offsetY: Int = 0) = { + val rect = new Rect + window.getDecorView.getWindowVisibleDisplayFrame(rect) + val viewLocation = new Array[Int](2) + view.getLocationInWindow(viewLocation) + val metrics = new DisplayMetrics + window.getWindowManager.getDefaultDisplay.getMetrics(metrics) + val toastView = toast.getView + toastView.measure(MeasureSpec.makeMeasureSpec(metrics.widthPixels, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(metrics.heightPixels, MeasureSpec.UNSPECIFIED)) + toast.setGravity(Gravity.LEFT | Gravity.TOP, + viewLocation(0) - rect.left + (view.getWidth - toast.getView.getMeasuredWidth) / 2 + offsetX, + viewLocation(1) - rect.top + view.getHeight + offsetY) + toast + } + // Blocked > 3 seconds def toggleAirplaneMode(context: Context) { if (Build.VERSION.SDK_INT >= 17) {