From 1ad08c6dba6ad689050ad4da4bb8c328c3de4550 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Mar 2017 18:46:23 +0800 Subject: [PATCH 01/36] Update support lib version --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7fcff54a..ea61879c 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val commonSettings = Seq( resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW") ) -val supportLibsVersion = "25.2.0" +val supportLibsVersion = "25.3.0" lazy val root = Project(id = "shadowsocks-android", base = file(".")) .settings(commonSettings) .aggregate(plugin, mobile) From c09a96a203844fe4829a245043f48d6808e049ee Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Mar 2017 18:59:19 +0800 Subject: [PATCH 02/36] Bump plugin lib version --- plugin/CHANGES.md | 2 ++ plugin/README.md | 2 +- plugin/build.sbt | 2 +- plugin/src/main/AndroidManifest.xml | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugin/CHANGES.md b/plugin/CHANGES.md index 9e277fee..12ebf67e 100644 --- a/plugin/CHANGES.md +++ b/plugin/CHANGES.md @@ -1,3 +1,5 @@ +* 0.0.4: + * Update support library version to 25.3.0. * 0.0.3: * Update support library version to 25.2.0. * 0.0.2: diff --git a/plugin/README.md b/plugin/README.md index cfd062f8..fbd519a0 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -36,7 +36,7 @@ First you need to add this library to your dependencies. This library is written and it's most convenient to use it with SBT: ```scala -libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.2" +libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4" ``` ### Native binary configuration diff --git a/plugin/build.sbt b/plugin/build.sbt index 93e5f588..bc1e7ac8 100644 --- a/plugin/build.sbt +++ b/plugin/build.sbt @@ -2,7 +2,7 @@ enablePlugins(AndroidLib) android.useSupportVectors name := "plugin" -version := "0.0.3" +version := "0.0.4-SNAPSHOT" pomExtra in Global := { https://github.com/shadowsocks/shadowsocks-android diff --git a/plugin/src/main/AndroidManifest.xml b/plugin/src/main/AndroidManifest.xml index bef42e78..1d01f307 100644 --- a/plugin/src/main/AndroidManifest.xml +++ b/plugin/src/main/AndroidManifest.xml @@ -6,6 +6,6 @@ + android:value="0.0.4"/> From 9617418e70788f28b3a6421acd399c536e7a023a Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Mar 2017 19:07:44 +0800 Subject: [PATCH 03/36] Stop calling notifyChanged over aggressively in EditTextPreference --- .../main/scala/be/mygod/preference/EditTextPreference.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala b/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala index e23d261a..a57fb98d 100644 --- a/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala +++ b/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala @@ -60,7 +60,8 @@ class EditTextPreference(context: Context, attrs: AttributeSet = null) extends P } override def setText(text: String): Unit = { + val old = getText super.setText(text) - notifyChanged() + if (old != text) notifyChanged() } } From 53af99eb9854c4f53af8a4e7eb233ca46ec4ec99 Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 30 Mar 2017 21:56:02 +0800 Subject: [PATCH 04/36] Update support lib --- build.sbt | 2 +- plugin/CHANGES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index ea61879c..9256082c 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val commonSettings = Seq( resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW") ) -val supportLibsVersion = "25.3.0" +val supportLibsVersion = "25.3.1" lazy val root = Project(id = "shadowsocks-android", base = file(".")) .settings(commonSettings) .aggregate(plugin, mobile) diff --git a/plugin/CHANGES.md b/plugin/CHANGES.md index 12ebf67e..5b7a8ac8 100644 --- a/plugin/CHANGES.md +++ b/plugin/CHANGES.md @@ -1,5 +1,5 @@ * 0.0.4: - * Update support library version to 25.3.0. + * Update support library version to 25.3.1. * 0.0.3: * Update support library version to 25.2.0. * 0.0.2: From 3a935b3274186784bac69a41e889cb0a820e6894 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 14 Apr 2017 00:14:45 +0800 Subject: [PATCH 05/36] Update scala to 2.11.10 It seems there will be 2.11.11 coming out soon however. Source: https://github.com/scala/scala-dev/issues/359#issuecomment-293474247 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 32e3ca64..030e78c3 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ lazy val commonSettings = Seq( - scalaVersion := "2.11.9", + scalaVersion := "2.11.10", dexMaxHeap := "4g", organization := "com.github.shadowsocks", From 6ca3e8abdaec68ab7531e0a1123095e9ddba68a3 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 21 Apr 2017 11:07:14 +0800 Subject: [PATCH 06/36] Update scala to 2.11.11 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 030e78c3..1a8dceea 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ lazy val commonSettings = Seq( - scalaVersion := "2.11.10", + scalaVersion := "2.11.11", dexMaxHeap := "4g", organization := "com.github.shadowsocks", From 1ec80175ea8ac8b6245780039cf04c8a3cccfb3f Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 17 May 2017 00:54:12 +0800 Subject: [PATCH 07/36] Update target SDK to O preview --- build.sbt | 4 ++-- mobile/src/main/AndroidManifest.xml | 2 +- plugin/src/main/AndroidManifest.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 946d7cf2..ff88b127 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ lazy val commonSettings = Seq( organization := "com.github.shadowsocks", - platformTarget := "android-25", + platformTarget := "android-O", compileOrder := CompileOrder.JavaThenScala, javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil, @@ -21,7 +21,7 @@ lazy val commonSettings = Seq( resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW") ) -val supportLibsVersion = "25.3.1" +val supportLibsVersion = "26.0.0-alpha1" lazy val root = Project(id = "shadowsocks-android", base = file(".")) .settings(commonSettings) .aggregate(plugin, mobile) diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index ff448140..f6072dd4 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ + android:targetSdkVersion="O"/> + android:targetSdkVersion="O"/> Date: Wed, 17 May 2017 01:24:29 +0800 Subject: [PATCH 08/36] Use the new findViewById APIs --- .../com/github/shadowsocks/AboutFragment.scala | 2 +- .../com/github/shadowsocks/AppManager.scala | 4 ++-- .../com/github/shadowsocks/MainActivity.scala | 2 +- .../github/shadowsocks/ProfilesFragment.scala | 16 ++++++++-------- .../com/github/shadowsocks/TaskerActivity.scala | 2 +- .../com/github/shadowsocks/ToolbarFragment.scala | 2 +- .../shadowsocks/acl/CustomRulesFragment.scala | 11 +++++------ .../BottomSheetPreferenceDialogFragment.scala | 6 +++--- .../EditTextPreferenceDialogFragment.scala | 2 +- .../preference/PreferenceGroupAdapter.scala | 2 +- 10 files changed, 24 insertions(+), 25 deletions(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala index bed140d3..785a2110 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala @@ -34,7 +34,7 @@ class AboutFragment extends ToolbarFragment { override def onViewCreated(view: View, savedInstanceState: Bundle) { super.onViewCreated(view, savedInstanceState) toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME)) - val web = view.findViewById(R.id.web_view).asInstanceOf[WebView] + val web = view.findViewById[WebView](R.id.web_view) web.loadUrl("file:///android_asset/pages/about.html") web.setWebViewClient(new WebViewClient() { override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = { diff --git a/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala index ceffb9d3..67e799d6 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { import AppManager._ private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener { - private val icon = itemView.findViewById(R.id.itemicon).asInstanceOf[ImageView] - private val check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch] + private val icon = itemView.findViewById[ImageView](R.id.itemicon) + private val check = itemView.findViewById[Switch](R.id.itemcheck) private var item: ProxiedApp = _ itemView.setOnClickListener(this) diff --git a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala index 61211364..3374eda8 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala @@ -246,7 +246,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe } else drawer = drawerBuilder.build() val header = drawer.getHeader - val title = header.findViewById(R.id.drawer_title).asInstanceOf[TextView] + val title = header.findViewById[TextView](R.id.drawer_title) val tf = Typefaces.get(this, "fonts/Iceland.ttf") if (tf != null) title.setTypeface(tf) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index 81fc22cf..b2a85834 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic var item: Profile = _ - private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView] - private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView] - private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView] - private val edit = itemView.findViewById(R.id.edit) + private val text1 = itemView.findViewById[TextView](android.R.id.text1) + private val text2 = itemView.findViewById[TextView](android.R.id.text2) + private val traffic = itemView.findViewById[TextView](R.id.traffic) + private val edit = itemView.findViewById[View](R.id.edit) edit.setOnClickListener(_ => startConfig(item.id)) edit.setOnLongClickListener(cardButtonLongClickListener) itemView.setOnClickListener(this) // it will not take effect unless set in code - itemView.findViewById(R.id.indicator).setBackgroundResource(R.drawable.background_profile) + itemView.findViewById[View](R.id.indicator).setBackgroundResource(R.drawable.background_profile) private var adView: NativeExpressAdView = _ { - val share = itemView.findViewById(R.id.share) + val share = itemView.findViewById[View](R.id.share) share.setOnClickListener(_ => { val popup = new PopupMenu(getActivity, share) popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu) @@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic adView.setLayoutParams(params) adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521") adView.setAdSize(new AdSize(328, 132)) - itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView) + itemView.findViewById[LinearLayout](R.id.content).addView(adView) // Demographics val random = new Random() @@ -279,7 +279,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic toolbar.setOnMenuItemClickListener(this) if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id) - val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView] + val profilesList = view.findViewById[RecyclerView](R.id.list) val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false) profilesList.setLayoutManager(layoutManager) layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst { diff --git a/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala index 36f405ae..944b27e9 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala @@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity { typedArray.recycle() } private var item: Profile = _ - private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView] + private val text = itemView.findViewById[CheckedTextView](android.R.id.text1) itemView.setOnClickListener(this) def bindDefault() { diff --git a/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala index e21a6b80..e55d5eb7 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala @@ -30,7 +30,7 @@ class ToolbarFragment extends Fragment { override def onViewCreated(view: View, savedInstanceState: Bundle) { super.onViewCreated(view, savedInstanceState) - toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar] + toolbar = view.findViewById[Toolbar](R.id.toolbar) val activity = getActivity.asInstanceOf[MainActivity] if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true) } diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala index 0bf6a523..74a6fdc4 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala @@ -46,8 +46,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi private def createAclRuleDialog(text: CharSequence = "") = { val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null) - val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner] - val editText = view.findViewById(R.id.content).asInstanceOf[EditText] + val templateSelector = view.findViewById[Spinner](R.id.template_selector) + val editText = view.findViewById[EditText](R.id.content) PATTERN_DOMAIN.findFirstMatchIn(text) match { case Some(m) => templateSelector.setSelection(DOMAIN) @@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener with View.OnLongClickListener { var item: AnyRef = _ - private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView] + private val text = itemView.findViewById[TextView](android.R.id.text1) itemView.setOnClickListener(this) itemView.setOnLongClickListener(this) itemView.setBackgroundResource(R.drawable.background_selectable) @@ -252,12 +252,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi toolbar.setOnMenuItemClickListener(this) selectionItem = toolbar.getMenu.findItem(R.id.selection) selectionItem.setVisible(selectedItems.nonEmpty) - list = view.findViewById(R.id.list).asInstanceOf[RecyclerView] + list = view.findViewById(R.id.list) list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)) list.setItemAnimator(new DefaultItemAnimator) list.setAdapter(adapter) - val fastScroller = view.findViewById(R.id.fastscroller).asInstanceOf[FastScroller] - fastScroller.setRecyclerView(list) + view.findViewById[FastScroller](R.id.fastscroller).setRecyclerView(list) undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo) new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) { override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int = diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala index 1cb28118..11521105 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala @@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view) with View.OnClickListener with View.OnLongClickListener { private var index: Int = _ - private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView] - private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView] - private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView] + private val text1 = view.findViewById[TextView](android.R.id.text1) + private val text2 = view.findViewById[TextView](android.R.id.text2) + private val icon = view.findViewById[ImageView](android.R.id.icon) view.setOnClickListener(this) view.setOnLongClickListener(this) diff --git a/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala b/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala index dadf7893..e8ed19e4 100644 --- a/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala +++ b/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala @@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment { val oldParent = editText.getParent.asInstanceOf[ViewGroup] if (oldParent eq view) return if (oldParent != null) oldParent.removeView(editText) - val oldEdit = view.findViewById(android.R.id.edit) + val oldEdit = view.findViewById[View](android.R.id.edit) if (oldEdit == null) return val container = oldEdit.getParent.asInstanceOf[ViewGroup] if (container == null) return diff --git a/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala b/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala index 8819ca37..8df8b6ef 100644 --- a/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala +++ b/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala @@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) { view.setBackground(background) ViewCompat.setPaddingRelative(view, s, t, e, b) } - val widgetFrame = view.findViewById(android.R.id.widget_frame).asInstanceOf[ViewGroup] + val widgetFrame = view.findViewById[ViewGroup](android.R.id.widget_frame) if (widgetFrame != null) { val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int] if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE) From bbf52c4f1db24fb0854b28005906865c8f560289 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 17 May 2017 14:33:36 +0800 Subject: [PATCH 09/36] Add support for notification channels for O preview --- mobile/src/main/res/values/strings.xml | 1 + .../com/github/shadowsocks/ShadowsocksApplication.scala | 6 +++++- .../com/github/shadowsocks/ShadowsocksNotification.scala | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index ca21e907..594b7fc7 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ Forward all DNS requests to remote + Service Shadowsocks started. Invalid server name Failed to connect the remote server diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala index 96840c4b..4dd063a6 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala @@ -24,11 +24,12 @@ import java.io.{File, FileOutputStream, IOException} import java.util.Locale import android.annotation.SuppressLint -import android.app.Application +import android.app.{Application, NotificationChannel, NotificationManager} import android.content._ import android.content.res.Configuration import android.os.{Build, LocaleList} import android.preference.PreferenceManager +import android.support.v4.os.BuildCompat import android.support.v7.app.AppCompatDelegate import android.util.Log import com.evernote.android.job.JobManager @@ -154,6 +155,9 @@ class ShadowsocksApplication extends Application { JobManager.create(this).addJobCreator(DonaldTrump) TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled)) + + if (BuildCompat.isAtLeastO) getSystemService(classOf[NotificationManager]).createNotificationChannel( + new NotificationChannel("service", getText(R.string.service), NotificationManager.IMPORTANCE_MIN)) } def crashRecovery() { diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala index 3d81bd6c..9fc38fc9 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala @@ -28,6 +28,7 @@ import android.os.{Build, PowerManager} import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat.BigTextStyle import android.support.v4.content.ContextCompat +import android.support.v4.os.BuildCompat import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils} @@ -60,6 +61,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity]) .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)) .setSmallIcon(R.drawable.ic_stat_shadowsocks) + .setChannel("service") if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close, service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0)) private lazy val style = new BigTextStyle(builder) @@ -97,7 +99,9 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str show() } else if (forceShow) show() - def show(): Unit = service.startForeground(1, builder.build) + def show(): Unit = + if (BuildCompat.isAtLeastO) nm.startServiceInForeground(new Intent(service, service.getClass), 1, builder.build()) + else service.startForeground(1, builder.build) def destroy() { if (lockReceiver != null) { From 424e1bd1bcfdc4d2d281249a58468f52a42b3067 Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 18 May 2017 14:56:26 +0800 Subject: [PATCH 10/36] Update to Android O preview 2 * Update support library to beta1; * Adapt to new foreground service policy; * Add a separate notification channel for NAT service; * Adapt to `findViewById` method signature changes. Possible issues: NAT notification will possibly not hide in lockscreen. --- .travis.yml | 2 +- build.sbt | 6 ++++-- mobile/src/main/res/values/strings.xml | 3 ++- .../main/scala/com/github/shadowsocks/BaseService.scala | 9 ++++++++- .../main/scala/com/github/shadowsocks/MainActivity.scala | 2 +- .../com/github/shadowsocks/ShadowsocksApplication.scala | 7 +++++-- .../com/github/shadowsocks/ShadowsocksNatService.scala | 6 ++---- .../com/github/shadowsocks/ShadowsocksNotification.scala | 9 ++++----- .../com/github/shadowsocks/ShadowsocksVpnService.scala | 7 ++----- 9 files changed, 29 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index a9a15ed4..469233a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ jdk: env: global: - - NDK_VERSION=r14 + - NDK_VERSION=r15-beta2 - NDK_CCACHE=ccache - GOROOT_BOOTSTRAP=$GOROOT - ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION} diff --git a/build.sbt b/build.sbt index ff88b127..5f6450c5 100644 --- a/build.sbt +++ b/build.sbt @@ -18,10 +18,12 @@ lazy val commonSettings = Seq( shrinkResources := true, typedResources := false, - resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW") + resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW"), + + resolvers += "google" at "https://maven.google.com" ) -val supportLibsVersion = "26.0.0-alpha1" +val supportLibsVersion = "26.0.0-beta1" lazy val root = Project(id = "shadowsocks-android", base = file(".")) .settings(commonSettings) .aggregate(plugin, mobile) diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 594b7fc7..0d937680 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -56,7 +56,8 @@ Forward all DNS requests to remote - Service + VPN Service + NAT Service Shadowsocks started. Invalid server name Failed to connect the remote server diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index b9a8ac02..30e4b977 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -30,6 +30,7 @@ import java.net.Inet6Address import android.app.Service import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} import android.os.{Handler, IBinder, RemoteCallbackList} +import android.support.v4.os.BuildCompat import android.text.TextUtils import android.util.Log import android.widget.Toast @@ -65,6 +66,7 @@ trait BaseService extends Service { lazy val handler = new Handler(getMainLooper) lazy val restartHanlder = new Handler(getMainLooper) + private var notification: ShadowsocksNotification = _ private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => { Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show() stopRunner(stopService = true) @@ -160,10 +162,12 @@ trait BaseService extends Service { pluginPath = PluginManager.init(plugin) } + def createNotification(): ShadowsocksNotification def startRunner(profile: Profile) { this.profile = profile - startService(new Intent(this, getClass)) + if (BuildCompat.isAtLeastO) startForegroundService(new Intent(this, getClass)) + else startService(new Intent(this, getClass)) TrafficMonitor.reset() trafficMonitorThread = new TrafficMonitorThread(getApplicationContext) trafficMonitorThread.start() @@ -177,6 +181,7 @@ trait BaseService extends Service { closeReceiverRegistered = true } + notification = createNotification() app.track(getClass.getSimpleName, "start") changeState(State.CONNECTING) @@ -198,6 +203,8 @@ trait BaseService extends Service { closeReceiverRegistered = false } + if (notification != null) notification.destroy() + // Make sure update total traffic when stopping the runner updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal) diff --git a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala index 3374eda8..b3aae6e0 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala @@ -256,7 +256,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe txRateText = findViewById(R.id.txRate).asInstanceOf[TextView] rxText = findViewById(R.id.rx).asInstanceOf[TextView] rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView] - findViewById(R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) { + findViewById[View](R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) { testCount += 1 statusText.setText(R.string.connection_test_testing) val id = testCount // it would change by other code diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala index 4dd063a6..72f8b006 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala @@ -44,6 +44,7 @@ import com.j256.ormlite.logger.LocalLog import eu.chainfire.libsuperuser.Shell import scala.collection.mutable.ArrayBuffer +import scala.collection.JavaConversions._ object ShadowsocksApplication { var app: ShadowsocksApplication = _ @@ -156,8 +157,10 @@ class ShadowsocksApplication extends Application { TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled)) - if (BuildCompat.isAtLeastO) getSystemService(classOf[NotificationManager]).createNotificationChannel( - new NotificationChannel("service", getText(R.string.service), NotificationManager.IMPORTANCE_MIN)) + if (BuildCompat.isAtLeastO) getSystemService(classOf[NotificationManager]).createNotificationChannels(List( + new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_MIN), + new NotificationChannel("service-nat", getText(R.string.service_nat), NotificationManager.IMPORTANCE_LOW) + )) } def crashRecovery() { diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index d5855834..a71ea95d 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -43,7 +43,6 @@ class ShadowsocksNatService extends BaseService { val CMD_IPTABLES_DNAT_ADD_SOCKS = "iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123" - private var notification: ShadowsocksNotification = _ val myUid: Int = android.os.Process.myUid() var sslocalProcess: GuardedProcess = _ @@ -204,12 +203,11 @@ class ShadowsocksNatService extends BaseService { AclSyncJob.schedule(profile.route) changeState(State.CONNECTED) - notification = new ShadowsocksNotification(this, profile.name, true) } - override def stopRunner(stopService: Boolean, msg: String = null) { + override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-nat", true) - if (notification != null) notification.destroy() + override def stopRunner(stopService: Boolean, msg: String = null) { // channge the state changeState(State.STOPPING) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala index 9fc38fc9..7ca79437 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala @@ -35,7 +35,8 @@ import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils} /** * @author Mygod */ -class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) { +class ShadowsocksNotification(private val service: BaseService, profileName: String, + channel: String, visible: Boolean = false) { 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 { @@ -61,7 +62,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity]) .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)) .setSmallIcon(R.drawable.ic_stat_shadowsocks) - .setChannel("service") + .setChannel(channel) if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close, service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0)) private lazy val style = new BigTextStyle(builder) @@ -99,9 +100,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str show() } else if (forceShow) show() - def show(): Unit = - if (BuildCompat.isAtLeastO) nm.startServiceInForeground(new Intent(service, service.getClass), 1, builder.build()) - else service.startForeground(1, builder.build) + def show(): Unit = service.startForeground(1, builder.build) def destroy() { if (lockReceiver != null) { diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 81ac83ee..5e4d1e34 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService { val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s" var conn: ParcelFileDescriptor = _ var vpnThread: ShadowsocksVpnThread = _ - private var notification: ShadowsocksNotification = _ var sslocalProcess: GuardedProcess = _ var overtureProcess: GuardedProcess = _ @@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService { vpnThread = null } - if (notification != null) notification.destroy() - // channge the state changeState(State.STOPPING) @@ -117,6 +114,8 @@ class ShadowsocksVpnService extends VpnService with BaseService { super.startRunner(profile) } + override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn") + override def connect() { super.connect() @@ -137,8 +136,6 @@ class ShadowsocksVpnService extends VpnService with BaseService { if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES) AclSyncJob.schedule(profile.route) - - notification = new ShadowsocksNotification(this, profile.name) } /** Called when the activity is first created. */ From 574f80aefd77b238d3a70ca1b082766e2159a252 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Fri, 26 May 2017 18:01:07 +0800 Subject: [PATCH 11/36] Fix a crash caused by new background limit on Android O --- .../com/github/shadowsocks/ShadowsocksRunnerService.scala | 8 ++++++++ .../main/scala/com/github/shadowsocks/utils/Utils.scala | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala index 34a2e86a..75f7d832 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala @@ -24,6 +24,8 @@ import android.app.Service import android.content.Intent import android.net.VpnService import android.os.{Handler, IBinder} +import android.support.v4.app.NotificationCompat +import android.support.v4.os.BuildCompat import com.github.shadowsocks.ShadowsocksApplication.app class ShadowsocksRunnerService extends Service with ServiceBoundContext { @@ -47,11 +49,17 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext { override def onCreate() { super.onCreate() + if (BuildCompat.isAtLeastO) { + val builder = new NotificationCompat.Builder(this) + builder.setPriority(NotificationCompat.PRIORITY_MIN) + startForeground(1, builder.build) + } attachService() } override def onDestroy() { super.onDestroy() + stopForeground(true) detachService() } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala index 266703fb..29c54340 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -28,6 +28,7 @@ import android.content.pm.PackageManager import android.content.{Context, Intent} import android.graphics._ import android.os.Build +import android.support.v4.os.BuildCompat import android.util.{Base64, DisplayMetrics, Log} import android.view.View.MeasureSpec import android.view.{Gravity, View, Window} @@ -138,7 +139,8 @@ object Utils { def startSsService(context: Context) { val intent = new Intent(context, classOf[ShadowsocksRunnerService]) - context.startService(intent) + if (BuildCompat.isAtLeastO) context.startForegroundService(intent) + else context.startService(intent) } def stopSsService(context: Context) { From 1e750e45e9ee5aef2215f2f7f7e88a9f461285e6 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Mon, 29 May 2017 17:46:42 +0800 Subject: [PATCH 12/36] Roll back to Google's barcode scanner --- .../main/scala/com/github/shadowsocks/ProfilesFragment.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index b2a85834..02ca811e 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -354,7 +354,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic case R.id.action_scan_qr_code => def installScanner() = { Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show() - try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark"))) catch { + try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.zxing.client.android"))) catch { case exc: ActivityNotFoundException => exc.printStackTrace() } } From 49e30e7b0984701b2d88359b9d2d6412858ef0f5 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Mon, 5 Jun 2017 09:27:59 +0800 Subject: [PATCH 13/36] Add back internal QR scanner. #1273 --- mobile/build.sbt | 1 + mobile/src/main/AndroidManifest.xml | 9 ++ mobile/src/main/res/layout/layout_scanner.xml | 12 ++ mobile/src/main/res/values-ja/strings.xml | 3 +- mobile/src/main/res/values-ru/strings.xml | 3 +- mobile/src/main/res/values-zh-rCN/strings.xml | 3 +- mobile/src/main/res/values-zh-rTW/strings.xml | 1 + mobile/src/main/res/values/strings.xml | 1 + .../github/shadowsocks/ProfilesFragment.scala | 11 +- .../github/shadowsocks/ScannerActivity.scala | 105 ++++++++++++++++++ 10 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 mobile/src/main/res/layout/layout_scanner.xml create mode 100644 mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala diff --git a/mobile/build.sbt b/mobile/build.sbt index bdb07c66..44b43590 100644 --- a/mobile/build.sbt +++ b/mobile/build.sbt @@ -36,6 +36,7 @@ libraryDependencies ++= "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: "dnsjava" % "dnsjava" % "2.1.8" :: "eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" :: + "me.dm7.barcodescanner" % "zxing" % "1.9.3" :: "net.glxn.qrgen" % "android" % "2.0" :: Nil diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index f6072dd4..b39c6259 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + @@ -11,6 +12,8 @@ android:required="false"/> + + + diff --git a/mobile/src/main/res/layout/layout_scanner.xml b/mobile/src/main/res/layout/layout_scanner.xml new file mode 100644 index 00000000..ae575a39 --- /dev/null +++ b/mobile/src/main/res/layout/layout_scanner.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/mobile/src/main/res/values-ja/strings.xml b/mobile/src/main/res/values-ja/strings.xml index 46db6437..366833f1 100644 --- a/mobile/src/main/res/values-ja/strings.xml +++ b/mobile/src/main/res/values-ja/strings.xml @@ -91,6 +91,7 @@ "QR コードを読み取る" "手動設定" "Zxingに準拠したQRコードスキャンアプリをインストールして下さい" +QRコードを読み取るにはカメラの使用権限が必要です。 "削除済み" @@ -132,4 +133,4 @@ "不明なプラグイン" "警告:このプラグインは信頼されていないソースからの可能性があります" "プラグイン" - \ No newline at end of file + diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml index 30d47daf..a36dbe08 100644 --- a/mobile/src/main/res/values-ru/strings.xml +++ b/mobile/src/main/res/values-ru/strings.xml @@ -96,6 +96,7 @@ "Сканировать QR-код" "Ручные настройки" "Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов." +Разрешение камеры требуется для сканирования QR код. "Удалено" "Удалено %d элемента" @@ -140,4 +141,4 @@ "Неизвестный плагин %s" "Предупреждение: этот плагин получен из недоверенного источника." "Плагин: %s" - \ No newline at end of file + diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index 14dcab6c..ed29ac07 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -92,6 +92,7 @@ "扫描二维码" "手动设置" "请安装任意兼容 ZXing 的二维码扫描应用。" +扫描二维码需要相机权限。 "已删除 %d 项" @@ -133,4 +134,4 @@ "未知插件 %s" "警告:该插件似乎并非来自已知的可信源。" "插件:%s" - \ No newline at end of file + diff --git a/mobile/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml index 9e01852e..431d1830 100644 --- a/mobile/src/main/res/values-zh-rTW/strings.xml +++ b/mobile/src/main/res/values-zh-rTW/strings.xml @@ -90,6 +90,7 @@ "掃描 QR 碼" "手動設定" "請安裝任何相容 ZXing 的 QR 碼掃描應用程式。" +掃描 QR 碼需要相機權限。 "已移除 %d 項" diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 0d937680..b6fa8ab3 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -106,6 +106,7 @@ Scan QR code Manual Settings Please install any ZXing-compliant QR code scanning app. + Camera permission is required for scanning QR code. Removed %d items removed diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index 02ca811e..b9f67338 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -352,21 +352,16 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match { case R.id.action_scan_qr_code => - def installScanner() = { - Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show() - try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.zxing.client.android"))) catch { - case exc: ActivityNotFoundException => exc.printStackTrace() - } - } try startActivityForResult(new Intent("com.google.zxing.client.android.SCAN") .addCategory(Intent.CATEGORY_DEFAULT) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_DOCUMENT), REQUEST_SCAN_QR_CODE) catch { - case _: ActivityNotFoundException => installScanner() + case _: ActivityNotFoundException => + startActivity(new Intent(getActivity, classOf[ScannerActivity])) case e: SecurityException => e.printStackTrace() app.track(e) - installScanner() + startActivity(new Intent(getActivity, classOf[ScannerActivity])) } true case R.id.action_import => diff --git a/mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala new file mode 100644 index 00000000..b1dd4de6 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala @@ -0,0 +1,105 @@ +/*******************************************************************************/ +/* */ +/* Copyright (C) 2016 by Max Lv */ +/* Copyright (C) 2016 by Mygod Studio */ +/* */ +/* This program is free software: you can redistribute it and/or modify */ +/* it under the terms of the GNU General Public License as published by */ +/* the Free Software Foundation, either version 3 of the License, or */ +/* (at your option) any later version. */ +/* */ +/* This program is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU General Public License for more details. */ +/* */ +/* You should have received a copy of the GNU General Public License */ +/* along with this program. If not, see . */ +/* */ +/*******************************************************************************/ + +package com.github.shadowsocks + +import android.app.{Activity, TaskStackBuilder} +import android.content.Intent +import android.content.pm.{PackageManager, ShortcutManager} +import android.os.{Build, Bundle} +import android.support.v4.app.ActivityCompat +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.Toolbar +import android.text.TextUtils +import android.widget.Toast +import com.google.zxing.Result +import com.github.shadowsocks.ShadowsocksApplication.app +import com.github.shadowsocks.utils.Parser +import me.dm7.barcodescanner.zxing.ZXingScannerView + +object ScannerActivity { + private final val MY_PERMISSIONS_REQUEST_CAMERA = 1 +} + +class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler { + import ScannerActivity._ + + private var scannerView: ZXingScannerView = _ + + override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String], + grantResults: Array[Int]) { + if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA) { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults(0) == PackageManager.PERMISSION_GRANTED) { + scannerView.setResultHandler(this) + scannerView.startCamera() + } else { + Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show() + finish() + } + } + } + + def navigateUp() { + val intent = getParentActivityIntent + if (shouldUpRecreateTask(intent) || isTaskRoot) + TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities() + else finish() + } + + override def onCreate(state: Bundle) { + super.onCreate(state) + setContentView(R.layout.layout_scanner) + val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] + toolbar.setTitle(getTitle) + toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material) + toolbar.setNavigationOnClickListener(_ => navigateUp()) + scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView] + if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan") + } + + override def onResume() { + super.onResume() + val permissionCheck = ContextCompat.checkSelfPermission(this, + android.Manifest.permission.CAMERA) + if (permissionCheck == PackageManager.PERMISSION_GRANTED) { + scannerView.setResultHandler(this) // Register ourselves as a handler for scan results. + scannerView.startCamera() // Start camera on resume + } else { + ActivityCompat.requestPermissions(this, + Array(android.Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA) + } + } + + override def onPause() { + super.onPause() + scannerView.stopCamera() // Stop camera on pause + } + + override def handleResult(rawResult: Result) = { + val uri = rawResult.getText + if (!TextUtils.isEmpty(uri)) + Parser.findAll(uri).foreach(app.profileManager.createProfile) + navigateUp() + } +} + From dc439b0802342a0eafe153c4099a78cb298b8f57 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 14 Jul 2017 11:05:57 +0800 Subject: [PATCH 14/36] Update to API 26 --- build.sbt | 4 ++-- mobile/build.sbt | 14 +++++++------- mobile/src/main/AndroidManifest.xml | 2 +- plugin/src/main/AndroidManifest.xml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index 5f6450c5..e7a574b7 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ lazy val commonSettings = Seq( organization := "com.github.shadowsocks", - platformTarget := "android-O", + platformTarget := "android-26", compileOrder := CompileOrder.JavaThenScala, javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil, @@ -23,7 +23,7 @@ lazy val commonSettings = Seq( resolvers += "google" at "https://maven.google.com" ) -val supportLibsVersion = "26.0.0-beta1" +val supportLibsVersion = "26.0.0-beta2" lazy val root = Project(id = "shadowsocks-android", base = file(".")) .settings(commonSettings) .aggregate(plugin, mobile) diff --git a/mobile/build.sbt b/mobile/build.sbt index 44b43590..4c00204d 100644 --- a/mobile/build.sbt +++ b/mobile/build.sbt @@ -16,11 +16,11 @@ proguardOptions ++= "-keep class com.github.shadowsocks.JniHelper { *; }" :: Nil -val playServicesVersion = "10.2.6" +val playServicesVersion = "11.0.2" resolvers += Resolver.jcenterRepo libraryDependencies ++= "com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" :: - "com.evernote" % "android-job" % "1.1.10" :: + "com.evernote" % "android-job" % "1.1.11" :: "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" :: "com.google.android.gms" % "play-services-ads" % playServicesVersion :: "com.google.android.gms" % "play-services-analytics" % playServicesVersion :: @@ -28,11 +28,11 @@ libraryDependencies ++= "com.google.firebase" % "firebase-config" % playServicesVersion :: "com.j256.ormlite" % "ormlite-android" % "5.0" :: "com.mikepenz" % "crossfader" % "1.5.0" :: - "com.mikepenz" % "fastadapter" % "2.5.2" :: - "com.mikepenz" % "iconics-core" % "2.8.4" :: - "com.mikepenz" % "materialdrawer" % "5.9.1" :: - "com.mikepenz" % "materialize" % "1.0.1" :: - "com.squareup.okhttp3" % "okhttp" % "3.8.0" :: + "com.mikepenz" % "fastadapter" % "2.6.2" :: + "com.mikepenz" % "iconics-core" % "2.8.9" :: + "com.mikepenz" % "materialdrawer" % "5.9.4" :: + "com.mikepenz" % "materialize" % "1.0.2" :: + "com.squareup.okhttp3" % "okhttp" % "3.8.1" :: "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: "dnsjava" % "dnsjava" % "2.1.8" :: "eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" :: diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index b39c6259..c720b571 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ + android:targetSdkVersion="26"/> + android:targetSdkVersion="26"/> Date: Fri, 14 Jul 2017 11:44:00 +0800 Subject: [PATCH 15/36] Update Travis and README --- .travis.yml | 6 +++--- README.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 469233a1..88ab7c8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ jdk: env: global: - - NDK_VERSION=r15-beta2 + - NDK_VERSION=r15b - NDK_CCACHE=ccache - GOROOT_BOOTSTRAP=$GOROOT - ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION} @@ -13,7 +13,7 @@ env: - PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} scala: - - 2.11.8 + - 2.11.11 before_cache: - find $HOME/.sbt -name "*.lock" | xargs rm @@ -27,7 +27,7 @@ cache: android: components: - tools - - build-tools-25.0.2 + - build-tools-26.0.0 - extra-android-m2repository - extra-google-m2repository diff --git a/README.md b/README.md index f2ea0345..61f65178 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. * SBT 0.13.0+ * Go 1.4+ * Android SDK - - Build Tools 25+ + - Build Tools 26+ - Android Support Repository and Google Repository (see `build.sbt` for version) - - Android NDK r14+ + - Android NDK r15+ ### BUILD From 2e1564b7b93cd1895af4ba6c8fe294f42b88e86e Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 14 Jul 2017 11:46:03 +0800 Subject: [PATCH 16/36] Remove usage of BuildCompat --- .../main/scala/com/github/shadowsocks/BaseService.scala | 8 +++----- .../com/github/shadowsocks/ShadowsocksApplication.scala | 5 ++--- .../com/github/shadowsocks/ShadowsocksRunnerService.scala | 4 ++-- .../main/scala/com/github/shadowsocks/utils/Utils.scala | 3 +-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 30e4b977..3c9835e1 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -21,16 +21,14 @@ package com.github.shadowsocks import java.io.{File, IOException} +import java.net.{Inet6Address, InetAddress} import java.util import java.util.concurrent.TimeUnit import java.util.{Timer, TimerTask} -import java.net.InetAddress -import java.net.Inet6Address import android.app.Service import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} -import android.os.{Handler, IBinder, RemoteCallbackList} -import android.support.v4.os.BuildCompat +import android.os.{Build, Handler, IBinder, RemoteCallbackList} import android.text.TextUtils import android.util.Log import android.widget.Toast @@ -166,7 +164,7 @@ trait BaseService extends Service { def startRunner(profile: Profile) { this.profile = profile - if (BuildCompat.isAtLeastO) startForegroundService(new Intent(this, getClass)) + if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass)) else startService(new Intent(this, getClass)) TrafficMonitor.reset() trafficMonitorThread = new TrafficMonitorThread(getApplicationContext) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala index 72f8b006..9ad88c31 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala @@ -29,7 +29,6 @@ import android.content._ import android.content.res.Configuration import android.os.{Build, LocaleList} import android.preference.PreferenceManager -import android.support.v4.os.BuildCompat import android.support.v7.app.AppCompatDelegate import android.util.Log import com.evernote.android.job.JobManager @@ -43,8 +42,8 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.j256.ormlite.logger.LocalLog import eu.chainfire.libsuperuser.Shell -import scala.collection.mutable.ArrayBuffer import scala.collection.JavaConversions._ +import scala.collection.mutable.ArrayBuffer object ShadowsocksApplication { var app: ShadowsocksApplication = _ @@ -157,7 +156,7 @@ class ShadowsocksApplication extends Application { TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled)) - if (BuildCompat.isAtLeastO) getSystemService(classOf[NotificationManager]).createNotificationChannels(List( + if (Build.VERSION.SDK_INT >= 26) getSystemService(classOf[NotificationManager]).createNotificationChannels(List( new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_MIN), new NotificationChannel("service-nat", getText(R.string.service_nat), NotificationManager.IMPORTANCE_LOW) )) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala index 75f7d832..6c089d02 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala @@ -23,7 +23,7 @@ package com.github.shadowsocks import android.app.Service import android.content.Intent import android.net.VpnService -import android.os.{Handler, IBinder} +import android.os.{Build, Handler, IBinder} import android.support.v4.app.NotificationCompat import android.support.v4.os.BuildCompat import com.github.shadowsocks.ShadowsocksApplication.app @@ -49,7 +49,7 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext { override def onCreate() { super.onCreate() - if (BuildCompat.isAtLeastO) { + if (Build.VERSION.SDK_INT >= 26) { val builder = new NotificationCompat.Builder(this) builder.setPriority(NotificationCompat.PRIORITY_MIN) startForeground(1, builder.build) diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala index 29c54340..5bda5098 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -28,7 +28,6 @@ import android.content.pm.PackageManager import android.content.{Context, Intent} import android.graphics._ import android.os.Build -import android.support.v4.os.BuildCompat import android.util.{Base64, DisplayMetrics, Log} import android.view.View.MeasureSpec import android.view.{Gravity, View, Window} @@ -139,7 +138,7 @@ object Utils { def startSsService(context: Context) { val intent = new Intent(context, classOf[ShadowsocksRunnerService]) - if (BuildCompat.isAtLeastO) context.startForegroundService(intent) + if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent) } From 02c8be58e345f14a4e023bb61a404b8f8d522eef Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 15 Jul 2017 01:01:34 +0800 Subject: [PATCH 17/36] Remove obsolete strip in AndroidManifest --- mobile/src/main/AndroidManifest.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index c720b571..8086c1e9 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -159,9 +159,6 @@ android:process=":bg"/> - - - From 27fc27346cf85a1b40300efdd4402954a2609343 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 15 Jul 2017 01:05:04 +0800 Subject: [PATCH 18/36] Replace SharedPreferences with SQLite Other changes included: * Fix a bug where dirty flag is not set correctly when changing per-app proxy settings; * Old auto connect settings in SharedPreference is now ignored. --- .../com/github/shadowsocks/AppManager.scala | 35 +++--- .../shadowsocks/GlobalConfigFragment.scala | 21 ++-- .../com/github/shadowsocks/MainActivity.scala | 17 +-- .../shadowsocks/ProfileConfigActivity.scala | 2 +- .../shadowsocks/ProfileConfigFragment.scala | 30 ++--- .../github/shadowsocks/ProfilesFragment.scala | 12 +- .../shadowsocks/ShadowsocksApplication.scala | 24 ++-- .../shadowsocks/ShadowsocksBackupAgent.scala | 21 +--- .../ShadowsocksRunnerActivity.scala | 4 +- .../ShadowsocksRunnerService.scala | 2 +- .../shadowsocks/database/DBHelper.scala | 24 +++- .../shadowsocks/database/KeyValuePair.scala | 32 +++++ .../github/shadowsocks/database/Profile.scala | 65 +++++----- .../OnPreferenceDataStoreChangeListener.scala | 10 ++ .../OrmLitePreferenceDataStore.scala | 116 ++++++++++++++++++ 15 files changed, 292 insertions(+), 123 deletions(-) create mode 100644 mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala create mode 100644 mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala create mode 100644 mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala diff --git a/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala index 67e799d6..c4e97ae7 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { proxiedApps.add(item.packageName) check.setChecked(true) } - if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply() + if (!appsLoading.get) { + app.dataStore.individual = proxiedApps.mkString("\n") + app.dataStore.dirty = true + } } } @@ -118,7 +121,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { private val appsLoading = new AtomicBoolean private var handler: Handler = _ - private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) = + private def initProxiedApps(str: String = app.dataStore.individual) = proxiedApps = str.split('\n').to[mutable.HashSet] override def onDestroy() { @@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { case R.id.action_apply_all => app.profileManager.getAllProfiles match { case Some(profiles) => - val proxiedAppString = app.settings.getString(Key.individual, null) + val proxiedAppString = app.dataStore.individual profiles.foreach(p => { p.individual = proxiedAppString app.profileManager.updateProfile(p) @@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { } return true case R.id.action_export => - val bypass = app.settings.getBoolean(Key.bypass, false) - val proxiedAppString = app.settings.getString(Key.individual, null) - val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString) - clipboard.setPrimaryClip(clip) + clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual, + app.dataStore.bypass + "\n" + app.dataStore.individual)) Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() return true case R.id.action_import => @@ -163,7 +164,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { val (enabled, apps) = if (i < 0) (proxiedAppString, "") else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1)) bypassSwitch.setChecked(enabled.toBoolean) - app.editor.putString(Key.individual, apps).putBoolean(Key.dirty, true).apply() + app.dataStore.individual = apps + app.dataStore.dirty = true Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() appListView.setVisibility(View.GONE) fastScroller.setVisibility(View.GONE) @@ -202,18 +204,23 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { toolbar.inflateMenu(R.menu.app_manager_menu) toolbar.setOnMenuItemClickListener(this) - if (!app.settings.getBoolean(Key.proxyApps, false)) - app.editor.putBoolean(Key.proxyApps, true).putBoolean(Key.dirty, true).apply() + if (!app.dataStore.proxyApps) { + app.dataStore.proxyApps = true + app.dataStore.dirty = true + } findViewById(R.id.onSwitch).asInstanceOf[Switch] .setOnCheckedChangeListener((_, checked) => { - app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply() + app.dataStore.proxyApps = checked + app.dataStore.dirty = true finish() }) bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch] - bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false)) - bypassSwitch.setOnCheckedChangeListener((_, checked) => - app.editor.putBoolean(Key.bypass, checked).putBoolean(Key.dirty, true).apply()) + bypassSwitch.setChecked(app.dataStore.bypass) + bypassSwitch.setOnCheckedChangeListener((_, checked) => { + app.dataStore.bypass = checked + app.dataStore.dirty = true + }) initProxiedApps() loadingView = findViewById(R.id.loading) diff --git a/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala index a3de92be..f60e3c79 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala @@ -20,27 +20,24 @@ package com.github.shadowsocks -import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v14.preference.SwitchPreference +import android.support.v7.preference.PreferenceDataStore import be.mygod.preference.PreferenceFragment -import com.github.shadowsocks.utils.{Key, TcpFastOpen} import com.github.shadowsocks.ShadowsocksApplication.app +import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener +import com.github.shadowsocks.utils.{Key, TcpFastOpen} -class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceChangeListener { +class GlobalConfigFragment extends PreferenceFragment with OnPreferenceDataStoreChangeListener { override def onCreatePreferences(bundle: Bundle, key: String) { + getPreferenceManager.setPreferenceDataStore(app.dataStore) addPreferencesFromResource(R.xml.pref_global) val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference] switch.setOnPreferenceChangeListener((_, value) => { BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean]) true }) - if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) { - BootReceiver.setEnabled(getActivity, true) - getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply() - } switch.setChecked(BootReceiver.getEnabled(getActivity)) val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference] @@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha tfo.setEnabled(false) tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version"))) } - app.settings.registerOnSharedPreferenceChangeListener(this) + app.dataStore.registerChangeListener(this) } override def onDestroy() { - app.settings.unregisterOnSharedPreferenceChangeListener(this) + app.dataStore.unregisterChangeListener(this) super.onDestroy() } - def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match { - case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false)) + def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match { + case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(store.getBoolean(key, false)) case _ => } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala index b3aae6e0..4908bf47 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala @@ -26,7 +26,6 @@ import java.util.Locale import android.app.backup.BackupManager import android.app.{Activity, ProgressDialog} -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content._ import android.net.{Uri, VpnService} import android.nfc.{NdefMessage, NfcAdapter} @@ -36,6 +35,7 @@ import android.support.design.widget.{FloatingActionButton, Snackbar} import android.support.v4.content.ContextCompat import android.support.v7.app.AlertDialog import android.support.v7.content.res.AppCompatResources +import android.support.v7.preference.PreferenceDataStore import android.support.v7.widget.RecyclerView.ViewHolder import android.text.TextUtils import android.util.Log @@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.acl.{Acl, CustomRulesFragment} import com.github.shadowsocks.aidl.IShadowsocksServiceCallback +import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.utils.CloseUtils.autoDisconnect import com.github.shadowsocks.utils._ import com.mikepenz.crossfader.Crossfader @@ -67,7 +68,7 @@ object MainActivity { } class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener - with OnSharedPreferenceChangeListener { + with OnPreferenceDataStoreChangeListener { import MainActivity._ // UI @@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe override def onServiceDisconnected(): Unit = changeState(State.IDLE) private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ => - if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false).apply()): View.OnClickListener) + if (state == State.STOPPED) app.dataStore.isNAT = false): View.OnClickListener) override def binderDied(): Unit = handler.post(() => { detachService() @@ -174,7 +175,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe }) override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match { - case Activity.RESULT_OK => bgService.use(app.profileId) + case Activity.RESULT_OK => bgService.use(app.dataStore.profileId) case _ => Log.e(TAG, "Failed to start VpnService") } @@ -299,7 +300,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton] fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle] fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture { - if (app.isNatEnabled) bgService.use(app.profileId) else { + if (app.isNatEnabled) bgService.use(app.dataStore.profileId) else { val intent = VpnService.prepare(this) if (intent != null) startActivityForResult(intent, REQUEST_CONNECT) else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)) @@ -313,7 +314,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe changeState(State.IDLE) // reset everything to init state handler.post(() => attachService(callback)) - app.settings.registerOnSharedPreferenceChangeListener(this) + app.dataStore.registerChangeListener(this) val intent = getIntent if (intent != null) handleShareIntent(intent) @@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe .show() } - def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match { + def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match { case Key.isNAT => handler.post(() => { detachService() attachService(callback) @@ -424,7 +425,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe override def onDestroy() { super.onDestroy() - app.settings.unregisterOnSharedPreferenceChangeListener(this) + app.dataStore.unregisterChangeListener(this) detachService() new BackupManager(this).dataChanged() handler.removeCallbacksAndMessages(null) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala index fd22a282..a99b3b53 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala @@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity { toolbar.setOnMenuItemClickListener(child) } - override def onBackPressed(): Unit = if (app.settings.getBoolean(Key.dirty, false)) new AlertDialog.Builder(this) + override def onBackPressed(): Unit = if (app.dataStore.dirty) new AlertDialog.Builder(this) .setTitle(R.string.unsaved_changes_prompt) .setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener) .setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala index 506f459f..16cdf902 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala @@ -21,13 +21,12 @@ package com.github.shadowsocks import android.app.Activity -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content._ import android.os.{Build, Bundle, UserManager} import android.support.design.widget.Snackbar import android.support.v14.preference.SwitchPreference import android.support.v7.app.AlertDialog -import android.support.v7.preference.Preference +import android.support.v7.preference.{Preference, PreferenceDataStore} import android.support.v7.widget.Toolbar.OnMenuItemClickListener import android.text.TextUtils import android.view.MenuItem @@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment} import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.database.Profile import com.github.shadowsocks.plugin._ -import com.github.shadowsocks.preference.{IconListPreference, PluginConfigurationDialogFragment} +import com.github.shadowsocks.preference.{IconListPreference, OnPreferenceDataStoreChangeListener, PluginConfigurationDialogFragment} import com.github.shadowsocks.utils.{Action, Key, Utils} object ProfileConfigFragment { @@ -43,7 +42,7 @@ object ProfileConfigFragment { } class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener - with OnSharedPreferenceChangeListener { + with OnPreferenceDataStoreChangeListener { import ProfileConfigFragment._ private var profile: Profile = _ @@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe private var pluginConfiguration: PluginConfiguration = _ override def onCreatePreferences(bundle: Bundle, key: String) { + getPreferenceManager.setPreferenceDataStore(app.dataStore) app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match { case Some(p) => profile = p - profile.serialize(app.editor).apply() + profile.serialize(app.dataStore) case None => getActivity.finish() } addPreferencesFromResource(R.xml.pref_profile) @@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe plugin.setOnPreferenceChangeListener((_, value) => { val selected = value.asInstanceOf[String] pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected) - app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply() + app.dataStore.plugin = pluginConfiguration.toString + app.dataStore.dirty = true pluginConfigure.setEnabled(!TextUtils.isEmpty(selected)) pluginConfigure.setText(pluginConfiguration.selectedOptions.toString) if (!PluginManager.fetchPlugins()(selected).trusted) @@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged) initPlugins() app.listenForPackageChanges(initPlugins()) - app.settings.registerOnSharedPreferenceChangeListener(this) + app.dataStore.registerChangeListener(this) } def initPlugins() { @@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray) plugin.setEntryIcons(plugins.map(_._2.icon).toArray) plugin.entryPackageNames = plugins.map(_._2.packageName).toArray - pluginConfiguration = new PluginConfiguration(app.settings.getString(Key.plugin, null)) + pluginConfiguration = new PluginConfiguration(app.dataStore.plugin) plugin.setValue(pluginConfiguration.selected) plugin.init() plugin.checkSummary() @@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe val selected = pluginConfiguration.selected pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions + (pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected) - app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply() + app.dataStore.plugin = pluginConfiguration.toString + app.dataStore.dirty = true true } catch { case exc: IllegalArgumentException => @@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe false } - override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit = - if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply() + def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = + if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true override def onDestroy() { - app.settings.unregisterOnSharedPreferenceChangeListener(this) + app.dataStore.unregisterChangeListener(this) super.onDestroy() } override def onResume() { super.onResume() - isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager + isProxyApps.setChecked(app.dataStore.proxyApps) // fetch proxyApps updated by AppManager } private def showPluginEditor() { @@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe } def saveAndExit() { - profile.deserialize(app.settings) + profile.deserialize(app.dataStore) app.profileManager.updateProfile(profile) if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id) getActivity.finish() diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index b9f67338..2fd177c5 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic case _ => false } private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match { - case State.CONNECTED => id != app.profileId + case State.CONNECTED => id != app.dataStore.profileId case State.STOPPED => true case _ => false } @@ -129,7 +129,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx))) } - if (item.id == app.profileId) { + if (item.id == app.dataStore.profileId) { itemView.setSelected(true) selectedItem = this } else { @@ -166,7 +166,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic def onClick(v: View): Unit = if (isEnabled) { val activity = getActivity.asInstanceOf[MainActivity] - val old = app.profileId + val old = app.dataStore.profileId app.switchProfile(item.id) profilesAdapter.refreshId(old) itemView.setSelected(true) @@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic if (index >= 0) { profiles.remove(index) notifyItemRemoved(index) - if (id == app.profileId) app.profileId(0) // switch to null profile + if (id == app.dataStore.profileId) app.dataStore.profileId = 0 // switch to null profile } } } @@ -278,12 +278,12 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic toolbar.inflateMenu(R.menu.profile_manager_menu) toolbar.setOnMenuItemClickListener(this) - if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id) + if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id val profilesList = view.findViewById[RecyclerView](R.id.list) val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false) profilesList.setLayoutManager(layoutManager) layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst { - case (profile, i) if profile.id == app.profileId => i + case (profile, i) if profile.id == app.dataStore.profileId => i }.getOrElse(-1)) val animator = new DefaultItemAnimator() animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala index 9ad88c31..d8419e0c 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala @@ -28,12 +28,12 @@ import android.app.{Application, NotificationChannel, NotificationManager} import android.content._ import android.content.res.Configuration import android.os.{Build, LocaleList} -import android.preference.PreferenceManager import android.support.v7.app.AppCompatDelegate import android.util.Log import com.evernote.android.job.JobManager import com.github.shadowsocks.acl.DonaldTrump import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager} +import com.github.shadowsocks.preference.OrmLitePreferenceDataStore import com.github.shadowsocks.utils.CloseUtils._ import com.github.shadowsocks.utils._ import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker} @@ -60,13 +60,13 @@ object ShadowsocksApplication { class ShadowsocksApplication extends Application { import ShadowsocksApplication._ - lazy val remoteConfig = FirebaseRemoteConfig.getInstance() + lazy val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance() lazy val tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker) - lazy val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) - lazy val editor: SharedPreferences.Editor = settings.edit - lazy val profileManager = new ProfileManager(new DBHelper(this)) + private lazy val dbHelper = new DBHelper(this) + lazy val profileManager = new ProfileManager(dbHelper) + lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper) - def isNatEnabled: Boolean = settings.getBoolean(Key.isNAT, false) + def isNatEnabled: Boolean = dataStore.isNAT def isVpnEnabled: Boolean = !isNatEnabled // send event @@ -79,13 +79,11 @@ class ShadowsocksApplication extends Application { .setFatal(false) .build()) - def profileId: Int = settings.getInt(Key.id, 0) - def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply() - def currentProfile: Option[Profile] = profileManager.getProfile(profileId) + def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId) def switchProfile(id: Int): Profile = { val result = profileManager.getProfile(id) getOrElse profileManager.createProfile() - profileId(result.id) + dataStore.profileId = result.id result } @@ -154,7 +152,7 @@ class ShadowsocksApplication extends Application { JobManager.create(this).addJobCreator(DonaldTrump) - TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled)) + TcpFastOpen.enabled(dataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled)) if (Build.VERSION.SDK_INT >= 26) getSystemService(classOf[NotificationManager]).createNotificationChannels(List( new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_MIN), @@ -192,10 +190,10 @@ class ShadowsocksApplication extends Application { autoClose(new FileOutputStream(new File(getFilesDir, file)))(out => IOUtils.copy(in, out))) } - editor.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE).apply() + dataStore.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE) } - def updateAssets(): Unit = if (settings.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets() + def updateAssets(): Unit = if (dataStore.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets() def listenForPackageChanges(callback: => Unit): BroadcastReceiver = { val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala index 37e8657a..64441b6e 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala @@ -20,26 +20,11 @@ package com.github.shadowsocks -import android.app.backup.{BackupAgentHelper, FileBackupHelper, SharedPreferencesBackupHelper} +import android.app.backup.{BackupAgentHelper, FileBackupHelper} import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.database.DBHelper class ShadowsocksBackupAgent extends BackupAgentHelper { - - // The names of the SharedPreferences groups that the application maintains. These - // are the same strings that are passed to getSharedPreferences(String, int). - val PREFS_DISPLAY = "com.github.shadowsocks_preferences" - - // An arbitrary string used within the BackupAgentHelper implementation to - // identify the SharedPreferencesBackupHelper's data. - val MY_PREFS_BACKUP_KEY = "com.github.shadowsocks" - - val DATABASE = "com.github.shadowsocks.database.profile" - - override def onCreate() { - val helper = new SharedPreferencesBackupHelper(this, PREFS_DISPLAY) - addHelper(MY_PREFS_BACKUP_KEY, helper) - addHelper(DATABASE, new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, - Acl.CUSTOM_RULES + ".acl")) - } + override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile", + new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl")) } diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala index 9e58c7ea..197cc935 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala @@ -46,7 +46,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { def startBackgroundService() { if (app.isNatEnabled) { - bgService.use(app.profileId) + bgService.use(app.dataStore.profileId) finish() } else { val intent = VpnService.prepare(ShadowsocksRunnerActivity.this) @@ -89,7 +89,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { resultCode match { case Activity.RESULT_OK => if (bgService != null) { - bgService.use(app.profileId) + bgService.use(app.dataStore.profileId) } case _ => Log.e(TAG, "Failed to start VpnService") diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala index 6c089d02..e9079c46 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala @@ -45,7 +45,7 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext { }, 1000) } - def startBackgroundService(): Unit = bgService.useSync(app.profileId) + def startBackgroundService(): Unit = bgService.useSync(app.dataStore.profileId) override def onCreate() { super.onCreate() diff --git a/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala b/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala index 155ddf28..68f176f4 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala @@ -20,10 +20,14 @@ package com.github.shadowsocks.database -import android.content.Context +import java.nio.ByteBuffer + +import android.content.{Context, SharedPreferences} import android.content.pm.ApplicationInfo import android.database.sqlite.SQLiteDatabase +import android.preference.PreferenceManager import com.github.shadowsocks.ShadowsocksApplication.app +import com.github.shadowsocks.utils.Key import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper import com.j256.ormlite.dao.Dao import com.j256.ormlite.support.ConnectionSource @@ -46,17 +50,20 @@ object DBHelper { } class DBHelper(val context: Context) - extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) { + extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 23) { import DBHelper._ lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile]) + lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair]) def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) { TableUtils.createTable(connectionSource, classOf[Profile]) + TableUtils.createTable(connectionSource, classOf[KeyValuePair]) } def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) { TableUtils.dropTable(connectionSource, classOf[Profile], true) + TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true) onCreate(database, connectionSource) } @@ -132,6 +139,19 @@ class DBHelper(val context: Context) profileDao.executeRawNoArgs("DROP TABLE `tmp`;") profileDao.executeRawNoArgs("COMMIT;") } + + if (oldVersion < 23) { + import KeyValuePair._ + val old = PreferenceManager.getDefaultSharedPreferences(app) + kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT, + ByteBuffer.allocate(4).putInt(old.getInt(Key.id, 0)).array())) + kvPairDao.createOrUpdate(new KeyValuePair(Key.isNAT, TYPE_BOOLEAN, + ByteBuffer.allocate(1).put((if (old.getBoolean(Key.isNAT, false)) 1 else 0).toByte).array())) + kvPairDao.createOrUpdate(new KeyValuePair(Key.tfo, TYPE_BOOLEAN, + ByteBuffer.allocate(1).put((if (old.getBoolean(Key.tfo, false)) 1 else 0).toByte).array())) + kvPairDao.createOrUpdate(new KeyValuePair(Key.currentVersionCode, TYPE_INT, + ByteBuffer.allocate(4).putInt(-1).array())) + } } catch { case ex: Exception => app.track(ex) diff --git a/mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala b/mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala new file mode 100644 index 00000000..625f8c41 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala @@ -0,0 +1,32 @@ +package com.github.shadowsocks.database + +import com.j256.ormlite.field.{DataType, DatabaseField} + +/** + * @author Mygod + */ +object KeyValuePair { + val TYPE_UNINITIALIZED = 0 + val TYPE_BOOLEAN = 1 + val TYPE_FLOAT = 2 + val TYPE_INT = 3 + val TYPE_LONG = 4 + val TYPE_STRING = 5 + val TYPE_STRING_SET = 6 +} + +class KeyValuePair { + @DatabaseField(id = true) + var key: String = _ + @DatabaseField + var valueType: Int = _ + @DatabaseField(dataType = DataType.BYTE_ARRAY) + var value: Array[Byte] = _ + + def this(key: String, valueType: Int, value: Array[Byte]) = { + this() + this.key = key + this.valueType = valueType + this.value = value + } +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala b/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala index fa347065..a6560814 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala @@ -22,11 +22,11 @@ package com.github.shadowsocks.database import java.util.Locale -import android.content.SharedPreferences import android.net.Uri import android.os.Binder import android.util.Base64 import com.github.shadowsocks.plugin.PluginConfiguration +import com.github.shadowsocks.preference.OrmLitePreferenceDataStore import com.github.shadowsocks.utils.Key import com.j256.ormlite.field.{DataType, DatabaseField} @@ -108,37 +108,38 @@ class Profile { } override def toString: String = toUri.toString - def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor - .putString(Key.name, name) - .putString(Key.host, host) - .putInt(Key.localPort, localPort) - .putInt(Key.remotePort, remotePort) - .putString(Key.password, password) - .putString(Key.route, route) - .putString(Key.remoteDns, remoteDns) - .putString(Key.method, method) - .putBoolean(Key.proxyApps, proxyApps) - .putBoolean(Key.bypass, bypass) - .putBoolean(Key.udpdns, udpdns) - .putBoolean(Key.ipv6, ipv6) - .putString(Key.individual, individual) - .putString(Key.plugin, plugin) - .remove(Key.dirty) - def deserialize(pref: SharedPreferences) { + def serialize(store: OrmLitePreferenceDataStore) { + store.putString(Key.name, name) + store.putString(Key.host, host) + store.putInt(Key.localPort, localPort) + store.putInt(Key.remotePort, remotePort) + store.putString(Key.password, password) + store.putString(Key.route, route) + store.putString(Key.remoteDns, remoteDns) + store.putString(Key.method, method) + store.proxyApps = proxyApps + store.bypass = bypass + store.putBoolean(Key.udpdns, udpdns) + store.putBoolean(Key.ipv6, ipv6) + store.individual = individual + store.plugin = plugin + store.remove(Key.dirty) + } + def deserialize(store: OrmLitePreferenceDataStore) { // It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case - name = pref.getString(Key.name, null) - host = pref.getString(Key.host, null) - localPort = pref.getInt(Key.localPort, 0) - remotePort = pref.getInt(Key.remotePort, 0) - password = pref.getString(Key.password, null) - method = pref.getString(Key.method, null) - route = pref.getString(Key.route, null) - remoteDns = pref.getString(Key.remoteDns, null) - proxyApps = pref.getBoolean(Key.proxyApps, false) - bypass = pref.getBoolean(Key.bypass, false) - udpdns = pref.getBoolean(Key.udpdns, false) - ipv6 = pref.getBoolean(Key.ipv6, false) - individual = pref.getString(Key.individual, null) - plugin = pref.getString(Key.plugin, null) + name = store.getString(Key.name, null) + host = store.getString(Key.host, null) + localPort = store.getInt(Key.localPort, 0) + remotePort = store.getInt(Key.remotePort, 0) + password = store.getString(Key.password, null) + method = store.getString(Key.method, null) + route = store.getString(Key.route, null) + remoteDns = store.getString(Key.remoteDns, null) + proxyApps = store.proxyApps + bypass = store.bypass + udpdns = store.getBoolean(Key.udpdns, false) + ipv6 = store.getBoolean(Key.ipv6, false) + individual = store.individual + plugin = store.plugin } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala new file mode 100644 index 00000000..6e690b73 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala @@ -0,0 +1,10 @@ +package com.github.shadowsocks.preference + +import android.support.v7.preference.PreferenceDataStore + +/** + * @author Mygod + */ +trait OnPreferenceDataStoreChangeListener { + def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala new file mode 100644 index 00000000..0e48ccaa --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala @@ -0,0 +1,116 @@ +package com.github.shadowsocks.preference + +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import java.util + +import android.support.v7.preference.PreferenceDataStore +import com.github.shadowsocks.database.{DBHelper, KeyValuePair} +import com.github.shadowsocks.utils.Key + +import scala.collection.JavaConversions._ + +/** + * @author Mygod + */ +//noinspection AccessorLikeMethodIsUnit +final class OrmLitePreferenceDataStore(dbHelper: DBHelper) extends PreferenceDataStore { + import KeyValuePair._ + + override def getBoolean(key: String, defValue: Boolean = false): Boolean = dbHelper.kvPairDao.queryForId(key) match { + case pair: KeyValuePair => + if (pair.valueType == TYPE_BOOLEAN) ByteBuffer.wrap(pair.value).get() != 0 else defValue + case _ => defValue + } + override def getFloat(key: String, defValue: Float): Float = dbHelper.kvPairDao.queryForId(key) match { + case pair: KeyValuePair => + if (pair.valueType == TYPE_FLOAT) ByteBuffer.wrap(pair.value).getFloat() else defValue + case _ => defValue + } + override def getInt(key: String, defValue: Int): Int = dbHelper.kvPairDao.queryForId(key) match { + case pair: KeyValuePair => + if (pair.valueType == TYPE_INT) ByteBuffer.wrap(pair.value).getInt() else defValue + case _ => defValue + } + override def getLong(key: String, defValue: Long): Long = dbHelper.kvPairDao.queryForId(key) match { + case pair: KeyValuePair => + if (pair.valueType == TYPE_LONG) ByteBuffer.wrap(pair.value).getLong() else defValue + case _ => defValue + } + override def getString(key: String, defValue: String = null): String = dbHelper.kvPairDao.queryForId(key) match { + case pair: KeyValuePair => + if (pair.valueType == TYPE_STRING) new String(pair.value) else defValue + case _ => defValue + } + override def getStringSet(key: String, defValue: util.Set[String]): util.Set[String] = + dbHelper.kvPairDao.queryForId(key) match { + case pair: KeyValuePair => if (pair.valueType == TYPE_STRING_SET) { + val buffer = ByteBuffer.wrap(pair.value) + val result = new util.HashSet[String]() + while (buffer.hasRemaining) { + val chArr = new Array[Byte](buffer.getInt) + buffer.get(chArr) + result.add(new String(chArr)) + } + result + } else defValue + case _ => defValue + } + + override def putBoolean(key: String, value: Boolean) { + dbHelper.kvPairDao.createOrUpdate( + new KeyValuePair(key, TYPE_BOOLEAN, ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte).array())) + fireChangeListener(key) + } + override def putFloat(key: String, value: Float) { + dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_FLOAT, ByteBuffer.allocate(4).putFloat(value).array())) + fireChangeListener(key) + } + override def putInt(key: String, value: Int) { + dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_INT, ByteBuffer.allocate(4).putInt(value).array())) + fireChangeListener(key) + } + override def putLong(key: String, value: Long) { + dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_LONG, ByteBuffer.allocate(8).putLong(value).array())) + fireChangeListener(key) + } + override def putString(key: String, value: String) { + value match { + case null => remove(key) + case _ => dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING, value.getBytes())) + } + fireChangeListener(key) + } + override def putStringSet(key: String, value: util.Set[String]) { + val stream = new ByteArrayOutputStream() + for (v <- value) { + stream.write(ByteBuffer.allocate(4).putInt(v.length).array()) + stream.write(v.getBytes()) + } + dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING_SET, stream.toByteArray)) + fireChangeListener(key) + } + + def remove(key: String): Int = dbHelper.kvPairDao.deleteById(key) + + private var listeners: Set[OnPreferenceDataStoreChangeListener] = Set.empty + private def fireChangeListener(key: String) = listeners.foreach(_.onPreferenceDataStoreChanged(this, key)) + def registerChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners += listener + def unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners -= listener + + def profileId: Int = getInt(Key.id, 0) + def profileId_=(i: Int): Unit = putInt(Key.id, i) + def isNAT: Boolean = getBoolean(Key.isNAT) + def isNAT_=(value: Boolean): Unit = putBoolean(Key.isNAT, value) + + def proxyApps: Boolean = getBoolean(Key.proxyApps) + def proxyApps_=(value: Boolean): Unit = putBoolean(Key.proxyApps, value) + def bypass: Boolean = getBoolean(Key.bypass) + def bypass_=(value: Boolean): Unit = putBoolean(Key.bypass, value) + def individual: String = getString(Key.individual) + def individual_=(value: String): Unit = putString(Key.individual, value) + def plugin: String = getString(Key.plugin) + def plugin_=(value: String): Unit = putString(Key.plugin, value) + def dirty: Boolean = getBoolean(Key.dirty) + def dirty_=(value: Boolean): Unit = putBoolean(Key.dirty, value) +} From 143afb052de0b357db02d199c1f2f7d4d298d6f8 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 15 Jul 2017 01:42:17 +0800 Subject: [PATCH 19/36] Use startService calls instead of AIDL to start services Because our data store is process-safe, we can remove the `use` function entirely and move more components to :bg process. --- mobile/src/main/AndroidManifest.xml | 12 +- .../shadowsocks/aidl/IShadowsocksService.aidl | 3 - .../com/github/shadowsocks/BaseService.scala | 103 +++++++++--------- .../com/github/shadowsocks/MainActivity.scala | 8 +- .../github/shadowsocks/ProfilesFragment.scala | 2 +- .../shadowsocks/ShadowsocksNatService.scala | 9 +- .../ShadowsocksRunnerActivity.scala | 8 +- .../ShadowsocksRunnerService.scala | 65 ----------- .../shadowsocks/ShadowsocksVpnService.scala | 11 +- .../github/shadowsocks/TaskerReceiver.scala | 11 +- .../github/shadowsocks/utils/Constants.scala | 1 + .../com/github/shadowsocks/utils/Utils.scala | 10 +- 12 files changed, 90 insertions(+), 153 deletions(-) delete mode 100644 mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 8086c1e9..f4d102ed 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -96,18 +96,14 @@ android:label="@string/quick_toggle" android:theme="@android:style/Theme.Translucent.NoTitleBar" android:excludeFromRecents="true" - android:taskAffinity="" + android:taskAffinity="" + android:process=":bg" android:launchMode="singleTask"> - - - - + - + diff --git a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl index 17545289..5735de28 100644 --- a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl +++ b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl @@ -10,7 +10,4 @@ interface IShadowsocksService { oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb); oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb); oneway void unregisterCallback(IShadowsocksServiceCallback cb); - - oneway void use(in int profileId); - void useSync(in int profileId); } diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 3c9835e1..83765492 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit import java.util.{Timer, TimerTask} import android.app.Service -import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} +import android.content._ import android.os.{Build, Handler, IBinder, RemoteCallbackList} import android.text.TextUtils import android.util.Log @@ -65,9 +65,11 @@ trait BaseService extends Service { lazy val restartHanlder = new Handler(getMainLooper) private var notification: ShadowsocksNotification = _ - private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => { - Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show() - stopRunner(stopService = true) + private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match { + case Action.RELOAD => forceLoad() + case _ => + Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show() + stopRunner(stopService = true) } var closeReceiverRegistered: Boolean = _ @@ -103,20 +105,6 @@ trait BaseService extends Service { stopListeningForBandwidth(cb) // saves an RPC, and safer callbacks.unregister(cb) } - - override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else { - val profile = app.profileManager.getProfile(profileId).orNull - if (profile == null) stopRunner(stopService = true, getString(R.string.profile_empty)) else state match { - case State.STOPPED => if (checkProfile(profile)) startRunner(profile) - case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) { - stopRunner(stopService = false) - startRunner(profile) - } - case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state) - } - }) - - override def useSync(profileId: Int): Unit = use(profileId) } def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) { @@ -124,6 +112,17 @@ trait BaseService extends Service { false } else true + def forceLoad(): Unit = app.currentProfile.orNull match { + case null => stopRunner(stopService = true, getString(R.string.profile_empty)) + case p => if (checkProfile(p)) state match { + case State.STOPPED => startRunner() + case State.CONNECTED => + stopRunner(stopService = false) + startRunner() + case s => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + s) + } + } + def connect() { profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses @@ -161,38 +160,8 @@ trait BaseService extends Service { } def createNotification(): ShadowsocksNotification - def startRunner(profile: Profile) { - this.profile = profile - - if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass)) + def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass)) else startService(new Intent(this, getClass)) - TrafficMonitor.reset() - trafficMonitorThread = new TrafficMonitorThread(getApplicationContext) - trafficMonitorThread.start() - - if (!closeReceiverRegistered) { - // register close receiver - val filter = new IntentFilter() - filter.addAction(Intent.ACTION_SHUTDOWN) - filter.addAction(Action.CLOSE) - registerReceiver(closeReceiver, filter) - closeReceiverRegistered = true - } - - notification = createNotification() - app.track(getClass.getSimpleName, "start") - - changeState(State.CONNECTING) - - Utils.ThrowableFuture(try connect() catch { - case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server)) - case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required)) - case exc: Throwable => - stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage) - exc.printStackTrace() - app.track(exc) - }) - } def stopRunner(stopService: Boolean, msg: String = null) { // clean up recevier @@ -281,7 +250,41 @@ trait BaseService extends Service { } // Service of shadowsocks should always be started explicitly - override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY + override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = { + state match { + case State.STOPPED | State.IDLE => + case _ => return Service.START_NOT_STICKY // ignore request + } + profile = app.currentProfile.orNull + TrafficMonitor.reset() + trafficMonitorThread = new TrafficMonitorThread(getApplicationContext) + trafficMonitorThread.start() + + if (!closeReceiverRegistered) { + // register close receiver + val filter = new IntentFilter() + filter.addAction(Action.RELOAD) + filter.addAction(Intent.ACTION_SHUTDOWN) + filter.addAction(Action.CLOSE) + registerReceiver(closeReceiver, filter) + closeReceiverRegistered = true + } + + notification = createNotification() + app.track(getClass.getSimpleName, "start") + + changeState(State.CONNECTING) + + Utils.ThrowableFuture(try connect() catch { + case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server)) + case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required)) + case exc: Throwable => + stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage) + exc.printStackTrace() + app.track(exc) + }) + Service.START_NOT_STICKY + } protected def changeState(s: Int, msg: String = null) { val handler = new Handler(getMainLooper) diff --git a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala index 4908bf47..b3768b3b 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala @@ -175,7 +175,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe }) override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match { - case Activity.RESULT_OK => bgService.use(app.dataStore.profileId) + case Activity.RESULT_OK => Utils.startSsService(this) case _ => Log.e(TAG, "Failed to start VpnService") } @@ -299,8 +299,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton] fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle] - fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture { - if (app.isNatEnabled) bgService.use(app.dataStore.profileId) else { + fab.setOnClickListener(_ => if (state == State.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture { + if (app.isNatEnabled) Utils.startSsService(this) else { val intent = VpnService.prepare(this) if (intent != null) startActivityForResult(intent, REQUEST_CONNECT) else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)) @@ -369,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe case DRAWER_PROFILES => displayFragment(new ProfilesFragment) case DRAWER_RECOVERY => app.track("GlobalConfigFragment", "reset") - if (bgService != null) bgService.use(-1) + Utils.stopSsService(this) val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false) val handler = new Handler { override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss() diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index 2fd177c5..618a7931 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -170,7 +170,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic app.switchProfile(item.id) profilesAdapter.refreshId(old) itemView.setSelected(true) - if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile + if (activity.state == State.CONNECTED) Utils.reloadSsService(activity) } override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match { diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index a71ea95d..a1db6d74 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -24,11 +24,12 @@ import java.io.File import java.net.{Inet6Address, InetAddress} import java.util.Locale +import android.app.Service import android.content._ import android.os._ import android.util.Log import com.github.shadowsocks.ShadowsocksApplication.app -import com.github.shadowsocks.acl.{AclSyncJob, Acl} +import com.github.shadowsocks.acl.{Acl, AclSyncJob} import com.github.shadowsocks.database.Profile import com.github.shadowsocks.utils._ import eu.chainfire.libsuperuser.Shell @@ -176,15 +177,17 @@ class ShadowsocksNatService extends BaseService { su.addCommand((init_sb ++ http_sb).toArray) } - override def startRunner(profile: Profile): Unit = if (su == null) + override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = if (su == null) { su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) => - if (exitCode == 0) super.startRunner(profile) else { + if (exitCode == 0) super.onStartCommand(intent, flags, startId) else { if (su != null) { su.close() su = null } super.stopRunner(stopService = true, getString(R.string.nat_no_root)) }) + Service.START_NOT_STICKY + } else super.onStartCommand(intent, flags, startId) override def connect() { super.connect() diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala index 197cc935..749560be 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala @@ -26,6 +26,7 @@ import android.net.VpnService import android.os.{Bundle, Handler} import android.util.Log import com.github.shadowsocks.ShadowsocksApplication.app +import com.github.shadowsocks.utils.Utils object ShadowsocksRunnerActivity { private final val TAG = "ShadowsocksRunnerActivity" @@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { def startBackgroundService() { if (app.isNatEnabled) { - bgService.use(app.dataStore.profileId) + Utils.startSsService(this) finish() } else { val intent = VpnService.prepare(ShadowsocksRunnerActivity.this) @@ -87,10 +88,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { resultCode match { - case Activity.RESULT_OK => - if (bgService != null) { - bgService.use(app.dataStore.profileId) - } + case Activity.RESULT_OK => Utils.startSsService(this) case _ => Log.e(TAG, "Failed to start VpnService") } diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala deleted file mode 100644 index e9079c46..00000000 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala +++ /dev/null @@ -1,65 +0,0 @@ -/*******************************************************************************/ -/* */ -/* Copyright (C) 2017 by Max Lv */ -/* Copyright (C) 2017 by Mygod Studio */ -/* */ -/* This program is free software: you can redistribute it and/or modify */ -/* it under the terms of the GNU General Public License as published by */ -/* the Free Software Foundation, either version 3 of the License, or */ -/* (at your option) any later version. */ -/* */ -/* This program is distributed in the hope that it will be useful, */ -/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ -/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ -/* GNU General Public License for more details. */ -/* */ -/* You should have received a copy of the GNU General Public License */ -/* along with this program. If not, see . */ -/* */ -/*******************************************************************************/ - -package com.github.shadowsocks - -import android.app.Service -import android.content.Intent -import android.net.VpnService -import android.os.{Build, Handler, IBinder} -import android.support.v4.app.NotificationCompat -import android.support.v4.os.BuildCompat -import com.github.shadowsocks.ShadowsocksApplication.app - -class ShadowsocksRunnerService extends Service with ServiceBoundContext { - val handler = new Handler() - - override def onBind(intent: Intent): IBinder = { - null - } - - override def onServiceConnected() { - handler.postDelayed(() => { - if (bgService != null) { - if (app.isNatEnabled) startBackgroundService() - else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService() - handler.postDelayed(() => stopSelf(), 3000) - } - }, 1000) - } - - def startBackgroundService(): Unit = bgService.useSync(app.dataStore.profileId) - - override def onCreate() { - super.onCreate() - if (Build.VERSION.SDK_INT >= 26) { - val builder = new NotificationCompat.Builder(this) - builder.setPriority(NotificationCompat.PRIORITY_MIN) - startForeground(1, builder.build) - } - attachService() - } - - override def onDestroy() { - super.onDestroy() - stopForeground(true) - detachService() - } -} diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 5e4d1e34..1f124776 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -23,6 +23,7 @@ package com.github.shadowsocks import java.io.File import java.util.Locale +import android.app.Service import android.content._ import android.content.pm.PackageManager.NameNotFoundException import android.net.VpnService @@ -30,7 +31,6 @@ import android.os._ import android.util.Log import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet} -import com.github.shadowsocks.database.Profile import com.github.shadowsocks.utils._ import scala.collection.mutable.ArrayBuffer @@ -100,18 +100,15 @@ class ShadowsocksVpnService extends VpnService with BaseService { } } - override def startRunner(profile: Profile) { - + override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = { // ensure the VPNService is prepared if (VpnService.prepare(this) != null) { val i = new Intent(this, classOf[ShadowsocksRunnerActivity]) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(i) stopRunner(stopService = true) - return - } - - super.startRunner(profile) + Service.START_NOT_STICKY + } else super.onStartCommand(intent, flags, startId) } override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn") diff --git a/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala index b015eb65..fa9017b8 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala @@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app class TaskerReceiver extends BroadcastReceiver { override def onReceive(context: Context, intent: Intent) { val settings = TaskerSettings.fromIntent(intent) + var changed = false app.profileManager.getProfile(settings.profileId) match { - case Some(_) => app.switchProfile(settings.profileId) + case Some(_) => if (app.dataStore.profileId != settings.profileId) { + app.switchProfile(settings.profileId) + changed = true + } case _ => } - if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context) + if (settings.switchOn) { + Utils.startSsService(context) + if (changed) Utils.reloadSsService(context) + } else Utils.stopSsService(context) } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala index 80bd0f4a..e7e30a92 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -94,6 +94,7 @@ object State { object Action { final val SERVICE = "com.github.shadowsocks.SERVICE" final val CLOSE = "com.github.shadowsocks.CLOSE" + final val RELOAD = "com.github.shadowsocks.RELOAD" final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID" } diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala index 5bda5098..eae40a19 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -33,7 +33,7 @@ import android.view.View.MeasureSpec import android.view.{Gravity, View, Window} import android.widget.Toast import com.github.shadowsocks.ShadowsocksApplication.app -import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService} +import com.github.shadowsocks.{BuildConfig, ShadowsocksNatService, ShadowsocksVpnService} import org.xbill.DNS._ import scala.collection.JavaConversions._ @@ -137,11 +137,11 @@ object Utils { } def startSsService(context: Context) { - val intent = new Intent(context, classOf[ShadowsocksRunnerService]) - if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) - else context.startService(intent) + val intent = + new Intent(context, if (app.dataStore.isNAT) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]) + if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent) } - + def reloadSsService(context: Context): Unit = context.sendBroadcast(new Intent(Action.RELOAD)) def stopSsService(context: Context) { val intent = new Intent(Action.CLOSE) context.sendBroadcast(intent) From 9c36f84b40719f8018b06231c0e743a0fc936530 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 15 Jul 2017 02:06:13 +0800 Subject: [PATCH 20/36] Update shortcut APIs --- .../github/shadowsocks/QuickToggleShortcut.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala b/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala index 62d73999..eadc041a 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala @@ -24,6 +24,8 @@ import android.app.Activity import android.content.Intent import android.content.pm.ShortcutManager import android.os.{Build, Bundle} +import android.support.v4.content.pm.{ShortcutInfoCompat, ShortcutManagerCompat} +import android.support.v4.graphics.drawable.IconCompat import com.github.shadowsocks.utils.{State, Utils} /** @@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext { super.onCreate(savedInstanceState) getIntent.getAction match { case Intent.ACTION_CREATE_SHORTCUT => - setResult(Activity.RESULT_OK, new Intent() - .putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut])) - .putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle)) - .putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher))) + setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, + new ShortcutInfoCompat.Builder(this, "toggle") + .setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN)) + .setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher)) + .setShortLabel(getString(R.string.quick_toggle)) + .build())) finish() case _ => attachService() From e1ee53567a78259ad9187310f94a0e00f36a8723 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 16 Jul 2017 15:08:52 +0800 Subject: [PATCH 21/36] Implement adaptive icons --- mobile/src/main/ic_launcher-web.png | Bin 35161 -> 42352 bytes .../ic_launcher_foreground_shadow.xml | 9 +++++++++ .../ic_launcher_foreground.xml | 13 +++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3213 -> 3908 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2001 -> 2442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4613 -> 5546 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7451 -> 8732 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10880 -> 12263 bytes mobile/src/main/res/values/colors.xml | 4 ++++ 10 files changed, 31 insertions(+) create mode 100644 mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml create mode 100644 mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml create mode 100644 mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/src/main/res/values/colors.xml diff --git a/mobile/src/main/ic_launcher-web.png b/mobile/src/main/ic_launcher-web.png index b394e2aefd0989100c8826abadaa44b0aa008ec7..26b9aeccd298c4d4ad132e319a7334d46015b2a8 100644 GIT binary patch literal 42352 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

t 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 diff --git a/mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml b/mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml new file mode 100644 index 00000000..a6a7266d --- /dev/null +++ b/mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml @@ -0,0 +1,9 @@ + + diff --git a/mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml b/mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..f5007c3d --- /dev/null +++ b/mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..5ed0a2df --- /dev/null +++ b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/src/main/res/mipmap-hdpi/ic_launcher.png index 7aa455779269fbf8f92b6eef7775fed7363da78c..576274b8f3a5c161d0f49c35d52112292c3e8522 100644 GIT binary patch literal 3908 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 Sn56dr0000R%lI delta 3210 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 diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/src/main/res/mipmap-mdpi/ic_launcher.png index 7f252778f867449d902b0b07b2ca5166edbd41cc..176e2c83a5a4bad1956033ecd09ea5ea48c84be6 100644 GIT binary patch delta 2432 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 literal 4613 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 diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png index d4f7d1a9d852ccef1e672826298cf8be7dff26d3..01c60ec31072f157db7747f74b9c719262213cd8 100644 GIT binary patch literal 8732 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 Z{{zALq_)~Hy88eC002ovPDHLkV1gcz7w!N6 diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png index da26ce19fdb0d6bb4e929f28fcd60ce7cfb1a44e..0711dbd46b3b3d664e7cd422caf7fb2d7cd4391f 100644 GIT binary patch literal 12263 zcmYjXRag|>(_VI$?k?%2JEXfil}>4;l69&bLKpA-ZSsa#Ov#-;p5QY0001d4RvM1rxpI+z`}T%4cFZ;0Ra9c4P}KF0jqzk zFil^Kua3oXFh-WoE|-sN{2eQ0Q>gf)G8&0f4*l_aD3XJ*32sQ%!`!!G@oH(r`qlBL zQR{NHjoppAj=izqWP!&J?>&T_nQd^DD zZOaxT^kw3e4L3u_Q!s2vCMRRREU![V9ZgB;@&qMpgqrWLH!lC;vuqD~`D5!gc$ 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 literal 10880 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 + + #7488A1 + From 422924f3f4772c1f4eb41cb9264668f5a2cefa7e Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 16 Jul 2017 15:36:20 +0800 Subject: [PATCH 22/36] Opt-in for safe browsing --- mobile/src/main/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index f4d102ed..4ecff499 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -19,6 +19,9 @@ android:minSdkVersion="19" android:targetSdkVersion="26"/> + + Date: Sun, 16 Jul 2017 15:52:55 +0800 Subject: [PATCH 23/36] Opt out of cleartext traffic --- mobile/src/main/AndroidManifest.xml | 1 + mobile/src/main/res/xml/network_security_config.xml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 mobile/src/main/res/xml/network_security_config.xml diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 4ecff499..4f610940 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ android:icon="@mipmap/ic_launcher" android:backupAgent=".ShadowsocksBackupAgent" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:banner="@drawable/ic_start_connected"> + + + From 6978b921bbfd631d624d11de449ab8da7f2aae59 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sun, 16 Jul 2017 16:03:58 +0800 Subject: [PATCH 24/36] Fix a crash in RunnerActivity --- .../scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala index 749560be..9fd86e8e 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala @@ -74,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { } else { attachService() } - finish() } override def onDestroy() { From 5549dfd20a0b384d3bd804363a68fd70cffb5933 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sun, 16 Jul 2017 16:04:45 +0800 Subject: [PATCH 25/36] Bump version --- mobile/build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/build.sbt b/mobile/build.sbt index 4c00204d..76383a72 100644 --- a/mobile/build.sbt +++ b/mobile/build.sbt @@ -4,8 +4,8 @@ enablePlugins(AndroidGms) android.useSupportVectors name := "shadowsocks" -version := "4.1.7" -versionCode := Some(187) +version := "4.2.0" +versionCode := Some(190) proguardOptions ++= "-dontwarn com.google.android.gms.internal.**" :: From d701fb4cee159e62426398e61882eb42e6e6402d Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 19 Jul 2017 09:16:56 +0800 Subject: [PATCH 26/36] Enlarge the text size of number picker --- plugin/src/main/res/values/styles.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/res/values/styles.xml b/plugin/src/main/res/values/styles.xml index 4db00a18..7f86d629 100644 --- a/plugin/src/main/res/values/styles.xml +++ b/plugin/src/main/res/values/styles.xml @@ -40,6 +40,6 @@ vertical From d8179b5b17b5cbcd3825cd76e35a0d88e3a9d9e2 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 19 Jul 2017 09:17:18 +0800 Subject: [PATCH 27/36] Fix a crash caused by evernote/android-job --- mobile/build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/build.sbt b/mobile/build.sbt index 76383a72..b70b1627 100644 --- a/mobile/build.sbt +++ b/mobile/build.sbt @@ -20,7 +20,7 @@ val playServicesVersion = "11.0.2" resolvers += Resolver.jcenterRepo libraryDependencies ++= "com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" :: - "com.evernote" % "android-job" % "1.1.11" :: + "com.evernote" % "android-job" % "1.2.0-alpha3" :: "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" :: "com.google.android.gms" % "play-services-ads" % playServicesVersion :: "com.google.android.gms" % "play-services-analytics" % playServicesVersion :: From 230a018326588a9e4743ae1ce7f6c0474b901873 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 14 Jun 2017 15:27:35 +0800 Subject: [PATCH 28/36] Allow adding URLs to custom rules. #1276 --- mobile/src/main/res/values-ja/strings.xml | 2 +- mobile/src/main/res/values-ko/strings.xml | 2 +- mobile/src/main/res/values-ru/strings.xml | 2 +- mobile/src/main/res/values-zh-rCN/strings.xml | 2 +- mobile/src/main/res/values-zh-rTW/strings.xml | 2 +- mobile/src/main/res/values/strings.xml | 2 +- .../com/github/shadowsocks/BaseService.scala | 2 +- .../com/github/shadowsocks/acl/Acl.scala | 61 ++++++++++++++++--- 8 files changed, 58 insertions(+), 17 deletions(-) diff --git a/mobile/src/main/res/values-ja/strings.xml b/mobile/src/main/res/values-ja/strings.xml index 366833f1..fa52639a 100644 --- a/mobile/src/main/res/values-ja/strings.xml +++ b/mobile/src/main/res/values-ja/strings.xml @@ -123,7 +123,7 @@ "LAN 及び中国本土のアドレスを迂回する" "中国本土のアドレス以外を迂回する" "選択したアプリにプロキシを設定する" -"サブネット/ホスト名 PCRE パターン" +"URL/サブネット/ホスト名 PCRE パターン" "ドメイン及び全てのサブドメイン" diff --git a/mobile/src/main/res/values-ko/strings.xml b/mobile/src/main/res/values-ko/strings.xml index c7eab897..013fb557 100644 --- a/mobile/src/main/res/values-ko/strings.xml +++ b/mobile/src/main/res/values-ko/strings.xml @@ -45,4 +45,4 @@ "지원하지 않는 버전의 커널입니다: %s < 3.7.1" "DNS 포워딩" "모든 DNS 요청을 외부로 포워딩 합니다" - \ No newline at end of file + diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml index a36dbe08..37db95cf 100644 --- a/mobile/src/main/res/values-ru/strings.xml +++ b/mobile/src/main/res/values-ru/strings.xml @@ -131,7 +131,7 @@ "Все, кроме LAN и Китая" "Список Китай" "Установить прокси для выбранных приложений" -"Подсеть/Регулярное выражение (PCRE) имени хоста" +"URL/Подсеть/Регулярное выражение (PCRE) имени хоста" "Доменное имя и все его поддомены" diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index ed29ac07..390e2e3e 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -124,7 +124,7 @@ "绕过局域网及中国大陆地址" "仅代理中国大陆地址" "为应用程序分别设置代理" -"子网/域名 PCRE 正则表达式" +"URL/子网/域名 PCRE 正则表达式" "域名及其子域名" diff --git a/mobile/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml index 431d1830..3b05c65d 100644 --- a/mobile/src/main/res/values-zh-rTW/strings.xml +++ b/mobile/src/main/res/values-zh-rTW/strings.xml @@ -122,7 +122,7 @@ "略過區域網路及中國大陸" "China List" "為已選擇的應用程式設定 Proxy" -"子網路/主機名稱 PCRE 模式" +"URL/子網路/主機名稱 PCRE 模式" "網域及其所有子網域" diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index b6fa8ab3..6de18162 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -132,7 +132,7 @@ Custom rules Selection… Add rule(s)… - Subnet/Hostname PCRE pattern + URL, Subnet or Hostname PCRE pattern Domain name and all its subdomain names Edit rule diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 83765492..b77d35b9 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -153,7 +153,7 @@ trait BaseService extends Service { } if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules - Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES)) + Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES), true) plugin = new PluginConfiguration(profile.plugin).selectedOptions pluginPath = PluginManager.init(plugin) diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala index 676f09e4..dc953f6c 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala @@ -1,6 +1,6 @@ package com.github.shadowsocks.acl -import java.io.{File, FileNotFoundException} +import java.io.{File, FileNotFoundException, IOException} import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.utils.IOUtils @@ -22,6 +22,7 @@ class Acl { val bypassHostnames = new mutable.SortedList[String]() val proxyHostnames = new mutable.SortedList[String]() val subnets = new mutable.SortedList[Subnet]() + val urls = new mutable.SortedList[String]() @DatabaseField var bypass: Boolean = _ @@ -40,6 +41,10 @@ class Acl { subnets.clear() subnets ++= value.split("\n").map(Subnet.fromString) } + def setUrlRules(value: String) { + urls.clear() + urls ++= value.split("\n") + } def fromAcl(other: Acl): Acl = { bypassHostnames.clear() @@ -48,21 +53,35 @@ class Acl { proxyHostnames ++= other.proxyHostnames subnets.clear() subnets ++= other.subnets + urls.clear() + urls ++= other.urls bypass = other.bypass this } - def fromSource(value: Source, defaultBypass: Boolean = false): Acl = { + def fromSource(value: Source, defaultBypass: Boolean = true): Acl = { bypassHostnames.clear() proxyHostnames.clear() this.subnets.clear() + this.urls.clear() bypass = defaultBypass lazy val bypassSubnets = new mutable.SortedList[Subnet]() lazy val proxySubnets = new mutable.SortedList[Subnet]() var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets + var in_urls = false for (line <- value.getLines()) (line.indexOf('#') match { - case -1 => line - case index => line.substring(0, index) // trim comments + case -1 => if (!in_urls) line else "" + case index => { + line.indexOf("URLS_BEGIN") match { + case -1 => + case index => in_urls = true + } + line.indexOf("URLS_END") match { + case -1 => + case index => in_urls = false + } + "" // ignore any comment lines + } }).trim match { case "[outbound_block_list]" => hostnames = null @@ -76,7 +95,10 @@ class Acl { case "[reject_all]" | "[bypass_all]" => bypass = true case "[accept_all]" | "[proxy_all]" => bypass = false case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch { - case _: IllegalArgumentException => hostnames += input + case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://")) { + urls += input + } + hostnames += input } case _ => } @@ -85,9 +107,24 @@ class Acl { } final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id))) - override def toString: String = { + def getAclString(network: Boolean): String = { val result = new StringBuilder() - result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n") + if (urls.nonEmpty) { + result.append("#URLS_BEGIN\n") + result.append(urls.mkString("\n")) + if (network) { + try { + urls.foreach((url: String) => result.append(Source.fromURL(url).mkString)) + } catch { + case e: IOException => // ignore + } + } + result.append("#URLS_END\n") + + } + if (result.isEmpty) { + result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n") + } val (bypassList, proxyList) = if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream) else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream) @@ -104,6 +141,10 @@ class Acl { result.toString } + override def toString: String = { + getAclString(false) + } + def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty // Don't change: dummy fields for OrmLite interaction @@ -134,9 +175,9 @@ object Acl { try acl.fromId(CUSTOM_RULES) catch { case _: FileNotFoundException => } - acl.bypass = true - acl.bypassHostnames.clear() // everything is bypassed acl } - def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString) + def save(id: String, acl: Acl, network: Boolean = false): Unit = { + IOUtils.writeString(getFile(id), acl.getAclString(network)) + } } From 52499f5c2d5e7cebe675796a269ebca88f4bfd5f Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sun, 18 Jun 2017 15:59:47 +0800 Subject: [PATCH 29/36] Add local DNS to dns list. #1281 --- .../com/github/shadowsocks/utils/Dns.java | 73 +++++++++++++++++++ .../com/github/shadowsocks/BaseService.scala | 17 ++++- 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 mobile/src/main/java/com/github/shadowsocks/utils/Dns.java diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java new file mode 100644 index 00000000..d203f978 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010 Brave New Software Project, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.shadowsocks.utils; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.annotation.TargetApi; +import android.util.Log; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.os.Build; + +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.net.InetAddress; + +public class Dns { + + public static String getDnsResolver(Context context) throws Exception { + Collection dnsResolvers = getDnsResolvers(context); + if (dnsResolvers.isEmpty()) { + throw new Exception("Couldn't find an active DNS resolver"); + } + String dnsResolver = dnsResolvers.iterator().next().toString(); + if (dnsResolver.startsWith("/")) { + dnsResolver = dnsResolver.substring(1); + } + return dnsResolver; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static Collection getDnsResolvers(Context context) throws Exception { + ArrayList addresses = new ArrayList(); + ConnectivityManager connectivityManager = + (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + Class LinkPropertiesClass = Class.forName("android.net.LinkProperties"); + Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{}); + Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager); + if (linkProperties != null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{}); + Collection dnses = (Collection)getDnsesMethod.invoke(linkProperties); + for (Object dns : dnses) { + addresses.add((InetAddress)dns); + } + } else { + for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) { + addresses.add(dns); + } + } + } + + return addresses; + } +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index b77d35b9..1e6e8f24 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -349,12 +349,21 @@ trait BaseService extends Service { val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map { case (dns, i) => makeDns("UserDef-" + i, dns.trim) }) + val localDns = new JSONArray(Array( + makeDns("Primary-1", "119.29.29.29", edns = false), + makeDns("Primary-2", "114.114.114.114", edns = false) + )) + + try { + val localLinkDns = com.github.shadowsocks.utils.Dns.getDnsResolver(this) + localDns.put(makeDns("Primary-3", localLinkDns, edns = false)) + } catch { + case _: Exception => // Ignore + } + profile.route match { case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config - .put("PrimaryDNS", new JSONArray(Array( - makeDns("Primary-1", "119.29.29.29", edns = false), - makeDns("Primary-2", "114.114.114.114", edns = false) - ))) + .put("PrimaryDNS", localDns) .put("AlternativeDNS", remoteDns) .put("IPNetworkFile", "china_ip_list.txt") .put("DomainFile", "gfwlist.txt") From af057d07407ebfbb0f3c734515d8474ec978700c Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 19 Jul 2017 16:08:55 +0800 Subject: [PATCH 30/36] Add missing proguard rules #1300 --- mobile/build.sbt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mobile/build.sbt b/mobile/build.sbt index b70b1627..c5e20eee 100644 --- a/mobile/build.sbt +++ b/mobile/build.sbt @@ -14,6 +14,13 @@ proguardOptions ++= "-dontwarn okio.**" :: "-dontwarn org.xbill.**" :: "-keep class com.github.shadowsocks.JniHelper { *; }" :: + "-dontwarn com.evernote.android.job.gcm.**" :: + "-dontwarn com.evernote.android.job.util.GcmAvailableHelper" :: + "-keep public class com.evernote.android.job.v21.PlatformJobService" :: + "-keep public class com.evernote.android.job.v14.PlatformAlarmService" :: + "-keep public class com.evernote.android.job.v14.PlatformAlarmReceiver" :: + "-keep public class com.evernote.android.job.JobBootReceiver" :: + "-keep public class com.evernote.android.job.JobRescheduleService" :: Nil val playServicesVersion = "11.0.2" From 258cb0c9c9ad0427380ac95e04993e5e3798d49b Mon Sep 17 00:00:00 2001 From: Max Lv Date: Fri, 28 Jul 2017 12:58:31 +0800 Subject: [PATCH 31/36] Enlarge the button's padding --- mobile/src/main/res/layout/layout_profile.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/src/main/res/layout/layout_profile.xml b/mobile/src/main/res/layout/layout_profile.xml index aacb3bd8..3ba50070 100644 --- a/mobile/src/main/res/layout/layout_profile.xml +++ b/mobile/src/main/res/layout/layout_profile.xml @@ -37,7 +37,7 @@ Date: Sat, 29 Jul 2017 13:14:35 +0800 Subject: [PATCH 32/36] Update dependencies --- build.sbt | 2 +- mobile/build.sbt | 6 +++--- .../com/github/shadowsocks/ShadowsocksNotification.scala | 3 +-- project/plugins.sbt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index e7a574b7..db86a192 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ lazy val commonSettings = Seq( resolvers += "google" at "https://maven.google.com" ) -val supportLibsVersion = "26.0.0-beta2" +val supportLibsVersion = "26.0.0" lazy val root = Project(id = "shadowsocks-android", base = file(".")) .settings(commonSettings) .aggregate(plugin, mobile) diff --git a/mobile/build.sbt b/mobile/build.sbt index c5e20eee..54c86540 100644 --- a/mobile/build.sbt +++ b/mobile/build.sbt @@ -23,7 +23,7 @@ proguardOptions ++= "-keep public class com.evernote.android.job.JobRescheduleService" :: Nil -val playServicesVersion = "11.0.2" +val playServicesVersion = "11.0.4" resolvers += Resolver.jcenterRepo libraryDependencies ++= "com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" :: @@ -38,12 +38,12 @@ libraryDependencies ++= "com.mikepenz" % "fastadapter" % "2.6.2" :: "com.mikepenz" % "iconics-core" % "2.8.9" :: "com.mikepenz" % "materialdrawer" % "5.9.4" :: - "com.mikepenz" % "materialize" % "1.0.2" :: + "com.mikepenz" % "materialize" % "1.0.3" :: "com.squareup.okhttp3" % "okhttp" % "3.8.1" :: "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: "dnsjava" % "dnsjava" % "2.1.8" :: "eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" :: - "me.dm7.barcodescanner" % "zxing" % "1.9.3" :: + "me.dm7.barcodescanner" % "zxing" % "1.9.5" :: "net.glxn.qrgen" % "android" % "2.0" :: Nil diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala index 7ca79437..6433d985 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala @@ -54,7 +54,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str private var lockReceiver: BroadcastReceiver = _ private var callbackRegistered: Boolean = _ - private val builder = new NotificationCompat.Builder(service) + private val builder = new NotificationCompat.Builder(service, channel) .setWhen(0) .setColor(ContextCompat.getColor(service, R.color.material_primary_500)) .setTicker(service.getString(R.string.forward_success)) @@ -62,7 +62,6 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity]) .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)) .setSmallIcon(R.drawable.ic_stat_shadowsocks) - .setChannel(channel) if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close, service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0)) private lazy val style = new BigTextStyle(builder) diff --git a/project/plugins.sbt b/project/plugins.sbt index 8fd06146..8246189c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7") +addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.8") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10") From 8b57b53788e51423243e26af82f6e8eb46de52ce Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 29 Jul 2017 14:36:51 +0800 Subject: [PATCH 33/36] Misc refinements --- .../com/github/shadowsocks/utils/Dns.java | 19 ++------ .../com/github/shadowsocks/acl/Acl.scala | 47 ++++--------------- 2 files changed, 13 insertions(+), 53 deletions(-) diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java index d203f978..921c652d 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java @@ -16,24 +16,18 @@ package com.github.shadowsocks.utils; -import android.annotation.SuppressLint; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; import android.annotation.TargetApi; -import android.util.Log; +import android.content.Context; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.os.Build; import java.lang.reflect.Method; -import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Collection; -import java.net.InetAddress; public class Dns { - public static String getDnsResolver(Context context) throws Exception { Collection dnsResolvers = getDnsResolvers(context); if (dnsResolvers.isEmpty()) { @@ -46,25 +40,22 @@ public class Dns { return dnsResolver; } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static Collection getDnsResolvers(Context context) throws Exception { ArrayList addresses = new ArrayList(); ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); Class LinkPropertiesClass = Class.forName("android.net.LinkProperties"); - Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{}); + Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties"); Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager); if (linkProperties != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{}); + Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses"); Collection dnses = (Collection)getDnsesMethod.invoke(linkProperties); for (Object dns : dnses) { addresses.add((InetAddress)dns); } } else { - for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) { - addresses.add(dns); - } + addresses.addAll(((LinkProperties) linkProperties).getDnsServers()); } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala index dc953f6c..26d0fe24 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala @@ -26,26 +26,6 @@ class Acl { @DatabaseField var bypass: Boolean = _ - def getBypassHostnamesString: String = bypassHostnames.mkString("\n") - def getProxyHostnamesString: String = proxyHostnames.mkString("\n") - def getSubnetsString: String = subnets.mkString("\n") - def setBypassHostnamesString(value: String) { - bypassHostnames.clear() - bypassHostnames ++= value.split("\n") - } - def setProxyHostnamesString(value: String) { - proxyHostnames.clear() - proxyHostnames ++= value.split("\n") - } - def setSubnetsString(value: String) { - subnets.clear() - subnets ++= value.split("\n").map(Subnet.fromString) - } - def setUrlRules(value: String) { - urls.clear() - urls ++= value.split("\n") - } - def fromAcl(other: Acl): Acl = { bypassHostnames.clear() bypassHostnames ++= other.bypassHostnames @@ -71,17 +51,10 @@ class Acl { var in_urls = false for (line <- value.getLines()) (line.indexOf('#') match { case -1 => if (!in_urls) line else "" - case index => { - line.indexOf("URLS_BEGIN") match { - case -1 => - case index => in_urls = true - } - line.indexOf("URLS_END") match { - case -1 => - case index => in_urls = false - } - "" // ignore any comment lines - } + case index => + if (line.contains("URLS_BEGIN")) in_urls = true + if (line.contains("URLS_END")) in_urls = false + line.substring(0, index) // trim comments }).trim match { case "[outbound_block_list]" => hostnames = null @@ -95,10 +68,8 @@ class Acl { case "[reject_all]" | "[bypass_all]" => bypass = true case "[accept_all]" | "[proxy_all]" => bypass = false case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch { - case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://")) { - urls += input - } - hostnames += input + case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://")) + urls += input else hostnames += input } case _ => } @@ -116,7 +87,7 @@ class Acl { try { urls.foreach((url: String) => result.append(Source.fromURL(url).mkString)) } catch { - case e: IOException => // ignore + case _: IOException => // ignore } } result.append("#URLS_END\n") @@ -141,9 +112,7 @@ class Acl { result.toString } - override def toString: String = { - getAclString(false) - } + override def toString: String = getAclString(false) def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty From 010fd6158ff1c733e07d4eb91b452551c3195084 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 29 Jul 2017 14:43:42 +0800 Subject: [PATCH 34/36] Fix profile name display in notification --- .../src/main/scala/com/github/shadowsocks/BaseService.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 1e6e8f24..f5682e57 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -124,8 +124,6 @@ trait BaseService extends Service { } def connect() { - profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses - if (profile.host == "198.199.101.152") { val client = new OkHttpClient.Builder() .dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match { @@ -256,6 +254,8 @@ trait BaseService extends Service { case _ => return Service.START_NOT_STICKY // ignore request } profile = app.currentProfile.orNull + profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses + TrafficMonitor.reset() trafficMonitorThread = new TrafficMonitorThread(getApplicationContext) trafficMonitorThread.start() From adaca76b12740798f681704887db08bbd673db97 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 29 Jul 2017 15:50:24 +0800 Subject: [PATCH 35/36] Update plugin changes --- plugin/CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/CHANGES.md b/plugin/CHANGES.md index 5b7a8ac8..e7250c3d 100644 --- a/plugin/CHANGES.md +++ b/plugin/CHANGES.md @@ -1,5 +1,6 @@ * 0.0.4: - * Update support library version to 25.3.1. + * Enlarge text size of number pickers; + * Update support library version to 26.0.0. * 0.0.3: * Update support library version to 25.2.0. * 0.0.2: From f19f8d8ffa9b6c7c4fde5d23bc863338561bd4ce Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sun, 30 Jul 2017 15:32:48 +0800 Subject: [PATCH 36/36] Fix a crash when no profile selected --- .../src/main/scala/com/github/shadowsocks/BaseService.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index f5682e57..cd08966d 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -253,7 +253,13 @@ trait BaseService extends Service { case State.STOPPED | State.IDLE => case _ => return Service.START_NOT_STICKY // ignore request } + profile = app.currentProfile.orNull + if (profile == null) { + stopRunner(stopService = true) + return Service.START_NOT_STICKY + } + profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses TrafficMonitor.reset()