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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 _ =>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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)
|
||||
}
|
||||
+116
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user