From 27fc27346cf85a1b40300efdd4402954a2609343 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 15 Jul 2017 01:05:04 +0800 Subject: [PATCH] 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) +}