Implement direct boot awareness
To do direct boot, important files need to be moved to device storage (i.e. not as safe storage). The only thing sensitive is user credentials (server settings in each profile) and the config file passed to libss-*.so. In order to implement direct boot, at least the active profile has to be moved/copied to device storage. This commit keeps profile information (and temporary data used by ProfileConfigFragment) and Shadowsocks config in credential storage and move everything else to device storage. In order to make direct boot work, active profile is also copied into device storage and Shadowsocks config file is created in device storage during direct boot. Cleanup of those information is also implemented. Fixes #1457. NB: * `android:directBootAware` can't be used on application element. That seems to be used by system apps. * Works fine with multi-user; * Old GSM files are not migrated (see App.kt) to device storage.
This commit is contained in:
@@ -24,6 +24,7 @@ import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -34,9 +35,12 @@ import android.os.Handler
|
||||
import android.os.LocaleList
|
||||
import android.os.Looper
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.os.UserManagerCompat
|
||||
import android.support.v7.app.AppCompatDelegate
|
||||
import android.util.Log
|
||||
import com.evernote.android.job.JobConstants
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.acl.AclSyncJob
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.database.Profile
|
||||
@@ -44,10 +48,7 @@ import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.preference.IconListPreference
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.TcpFastOpen
|
||||
import com.github.shadowsocks.utils.broadcastReceiver
|
||||
import com.github.shadowsocks.utils.*
|
||||
import com.google.android.gms.analytics.GoogleAnalytics
|
||||
import com.google.android.gms.analytics.HitBuilders
|
||||
import com.google.android.gms.analytics.StandardExceptionParser
|
||||
@@ -75,8 +76,9 @@ class App : Application() {
|
||||
}
|
||||
|
||||
val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
val deviceContext: Context by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceContext(this) }
|
||||
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
|
||||
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker) }
|
||||
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) }
|
||||
val info: PackageInfo by lazy { packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) }
|
||||
|
||||
fun startService() {
|
||||
@@ -86,7 +88,8 @@ class App : Application() {
|
||||
fun reloadService() = sendBroadcast(Intent(Action.RELOAD))
|
||||
fun stopService() = sendBroadcast(Intent(Action.CLOSE))
|
||||
|
||||
val currentProfile: Profile? get() = ProfileManager.getProfile(DataStore.profileId)
|
||||
val currentProfile: Profile? get() =
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId)
|
||||
|
||||
fun switchProfile(id: Int): Profile {
|
||||
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
|
||||
@@ -162,31 +165,42 @@ class App : Application() {
|
||||
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
checkChineseLocale(resources.configuration)
|
||||
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
|
||||
BottomSheetPreferenceDialogFragment::class.java)
|
||||
|
||||
FirebaseApp.initializeApp(this)
|
||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||
deviceContext.moveDatabaseFrom(this, Key.DB_PUBLIC)
|
||||
deviceContext.moveDatabaseFrom(this, JobConstants.DATABASE_NAME)
|
||||
deviceContext.moveSharedPreferencesFrom(this, JobConstants.PREF_FILE_NAME)
|
||||
val old = Acl.getFile(Acl.CUSTOM_RULES, this)
|
||||
if (old.canRead()) {
|
||||
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
||||
old.delete()
|
||||
}
|
||||
}
|
||||
|
||||
FirebaseApp.initializeApp(deviceContext)
|
||||
remoteConfig.setDefaults(R.xml.default_configs)
|
||||
remoteConfig.fetch().addOnCompleteListener {
|
||||
if (it.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config")
|
||||
}
|
||||
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
|
||||
|
||||
JobManager.create(this).addJobCreator(AclSyncJob)
|
||||
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
|
||||
BottomSheetPreferenceDialogFragment::class.java)
|
||||
|
||||
TcpFastOpen.enabled(DataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
|
||||
if (DataStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
|
||||
// handle data restored
|
||||
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
|
||||
TcpFastOpen.enabled(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
|
||||
val assetManager = assets
|
||||
for (dir in arrayOf("acl", "overture"))
|
||||
try {
|
||||
for (file in assetManager.list(dir)) assetManager.open(dir + '/' + file).use { input ->
|
||||
File(filesDir, file).outputStream().use { output -> input.copyTo(output) }
|
||||
File(deviceContext.filesDir, file).outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.message)
|
||||
app.track(e)
|
||||
}
|
||||
DataStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
|
||||
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.thread
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -231,6 +232,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
it.individual = proxiedAppString
|
||||
ProfileManager.updateProfile(it)
|
||||
}
|
||||
if (DataStore.directBootAware) DirectBoot.update()
|
||||
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
|
||||
} else Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
@@ -39,6 +40,11 @@ class BootReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) app.startService()
|
||||
val locked = when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> false
|
||||
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
|
||||
else -> return
|
||||
}
|
||||
if (DataStore.directBootAware == locked) app.startService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ import com.github.shadowsocks.utils.Key
|
||||
|
||||
@Deprecated("Only used in API level < 23. For 6.0+, Auto Backup for Apps is used.")
|
||||
class ConfigBackupHelper : BackupAgentHelper() {
|
||||
override fun onCreate() = addHelper("com.github.shadowsocks.database.profile",
|
||||
FileBackupHelper(this, "../databases/" + Key.PROFILE, Acl.CUSTOM_RULES + ".acl"))
|
||||
override fun onCreate() = addHelper("com.github.shadowsocks.database.profile", FileBackupHelper(this,
|
||||
"../databases/" + Key.DB_PROFILE, "../databases/" + Key.DB_PUBLIC, Acl.CUSTOM_RULES + ".acl"))
|
||||
}
|
||||
|
||||
@@ -20,27 +20,40 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v14.preference.SwitchPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.TcpFastOpen
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
|
||||
|
||||
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompatDividers() {
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.preferenceDataStore = DataStore
|
||||
preferenceManager.preferenceDataStore = DataStore.publicStore
|
||||
DataStore.initGlobal()
|
||||
addPreferencesFromResource(R.xml.pref_global)
|
||||
val switch = findPreference(Key.isAutoConnect) as SwitchPreference
|
||||
switch.setOnPreferenceChangeListener { _, value ->
|
||||
BootReceiver.enabled = value as Boolean
|
||||
val boot = findPreference(Key.isAutoConnect) as SwitchPreference
|
||||
val directBootAwareListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
|
||||
true
|
||||
}
|
||||
switch.isChecked = BootReceiver.enabled
|
||||
boot.setOnPreferenceChangeListener { _, value ->
|
||||
BootReceiver.enabled = value as Boolean
|
||||
directBootAwareListener.onPreferenceChange(null, DataStore.directBootAware)
|
||||
}
|
||||
boot.isChecked = BootReceiver.enabled
|
||||
|
||||
val dba = findPreference(Key.directBootAware)
|
||||
if (Build.VERSION.SDK_INT >= 24 && context!!.getSystemService(DevicePolicyManager::class.java)
|
||||
.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER)
|
||||
dba.onPreferenceChangeListener = directBootAwareListener else dba.parent!!.removePreference(dba)
|
||||
|
||||
val tfo = findPreference(Key.tfo) as SwitchPreference
|
||||
tfo.isChecked = TcpFastOpen.sendEnabled
|
||||
|
||||
@@ -335,7 +335,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
|
||||
changeState(BaseService.IDLE) // reset everything to init state
|
||||
app.handler.post { connection.connect() }
|
||||
DataStore.registerChangeListener(this)
|
||||
DataStore.publicStore.registerChangeListener(this)
|
||||
|
||||
val intent = this.intent
|
||||
if (intent != null) handleShareIntent(intent)
|
||||
@@ -434,7 +434,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
DataStore.unregisterChangeListener(this)
|
||||
DataStore.publicStore.unregisterChangeListener(this)
|
||||
connection.disconnect()
|
||||
BackupManager(this).dataChanged()
|
||||
app.handler.removeCallbacksAndMessages(null)
|
||||
|
||||
@@ -45,6 +45,7 @@ import com.github.shadowsocks.preference.IconListPreference
|
||||
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
|
||||
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.takisoft.fix.support.v7.preference.EditTextPreference
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
|
||||
@@ -62,7 +63,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
private lateinit var pluginConfiguration: PluginConfiguration
|
||||
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.preferenceDataStore = DataStore
|
||||
preferenceManager.preferenceDataStore = DataStore.privateStore
|
||||
val activity = activity!!
|
||||
val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1))
|
||||
if (profile == null) {
|
||||
@@ -102,7 +103,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
pluginConfigure.onPreferenceChangeListener = this
|
||||
initPlugins()
|
||||
app.listenForPackageChanges { initPlugins() }
|
||||
DataStore.registerChangeListener(this)
|
||||
DataStore.privateStore.registerChangeListener(this)
|
||||
}
|
||||
|
||||
private fun initPlugins() {
|
||||
@@ -130,6 +131,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
profile.deserialize()
|
||||
ProfileManager.updateProfile(profile)
|
||||
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id)
|
||||
if (DataStore.profileId == profile.id && DataStore.directBootAware) DirectBoot.update()
|
||||
activity!!.finish()
|
||||
}
|
||||
|
||||
@@ -201,7 +203,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
DataStore.unregisterChangeListener(this)
|
||||
DataStore.privateStore.unregisterChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.util.SortedList
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
@@ -45,7 +46,7 @@ class Acl {
|
||||
|
||||
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||
|
||||
fun getFile(id: String) = File(app.filesDir, id + ".acl")
|
||||
fun getFile(id: String, context: Context = app.deviceContext) = File(context.filesDir, id + ".acl")
|
||||
|
||||
val customRules: Acl get() {
|
||||
val acl = Acl()
|
||||
@@ -56,7 +57,7 @@ class Acl {
|
||||
acl.bypassHostnames.clear() // everything is bypassed
|
||||
return acl
|
||||
}
|
||||
fun save(id: String, acl: Acl): Unit = getFile(id).bufferedWriter().use { it.write(acl.toString()) }
|
||||
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
|
||||
}
|
||||
|
||||
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||
|
||||
@@ -20,11 +20,15 @@
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteCallbackList
|
||||
import android.support.v4.os.UserManagerCompat
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
@@ -66,6 +70,8 @@ object BaseService {
|
||||
const val STOPPING = 3
|
||||
const val STOPPED = 4
|
||||
|
||||
const val CONFIG_FILE = "shadowsocks.conf"
|
||||
|
||||
class Data internal constructor(private val service: Interface) {
|
||||
@Volatile var profile: Profile? = null
|
||||
@Volatile var state = STOPPED
|
||||
@@ -163,7 +169,8 @@ object BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildShadowsocksConfig() {
|
||||
internal var shadowsocksConfigFile: File? = null
|
||||
internal fun buildShadowsocksConfig(): File {
|
||||
val profile = profile!!
|
||||
val config = JSONObject()
|
||||
.put("server", profile.host)
|
||||
@@ -178,7 +185,13 @@ object BaseService {
|
||||
.put("plugin", Commandline.toString(service.buildAdditionalArguments(pluginCmd)))
|
||||
.put("plugin_opts", plugin.toString())
|
||||
}
|
||||
File(app.filesDir, "shadowsocks.json").bufferedWriter().use { it.write(config.toString()) }
|
||||
// sensitive Shadowsocks config is stored in
|
||||
val file = File((if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
|
||||
app.deviceContext.noBackupFilesDir // only API 24+ will be in locked state
|
||||
}), CONFIG_FILE)
|
||||
shadowsocksConfigFile = file
|
||||
file.writeText(config.toString())
|
||||
return file
|
||||
}
|
||||
|
||||
val aclFile: File? get() {
|
||||
@@ -228,14 +241,13 @@ object BaseService {
|
||||
|
||||
fun startNativeProcesses() {
|
||||
val data = data
|
||||
data.buildShadowsocksConfig()
|
||||
val cmd = buildAdditionalArguments(arrayListOf(
|
||||
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||
"-u",
|
||||
"-b", "127.0.0.1",
|
||||
"-l", DataStore.portProxy.toString(),
|
||||
"-t", "600",
|
||||
"-c", "shadowsocks.json"))
|
||||
"-c", data.buildShadowsocksConfig().absolutePath))
|
||||
|
||||
val acl = data.aclFile
|
||||
if (acl != null) {
|
||||
@@ -278,6 +290,9 @@ object BaseService {
|
||||
data.closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
data.shadowsocksConfigFile?.delete() // remove old config possibly in device storage
|
||||
data.shadowsocksConfigFile = null
|
||||
|
||||
data.notification?.destroy()
|
||||
data.notification = null
|
||||
|
||||
|
||||
@@ -37,8 +37,7 @@ object Executable {
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
val exe = File(File(process, "cmdline").bufferedReader().use { it.readText() }
|
||||
.split(Character.MIN_VALUE, limit = 2).first())
|
||||
val exe = File(File(process, "cmdline").readText().split(Character.MIN_VALUE, limit = 2).first())
|
||||
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) {
|
||||
val errno = JniHelper.sigkill(process.name.toInt())
|
||||
if (errno != 0) Log.w("kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno")
|
||||
|
||||
@@ -62,7 +62,7 @@ class GuardedProcess(private val cmd: List<String>) {
|
||||
|
||||
process = ProcessBuilder(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.directory(app.filesDir)
|
||||
.directory(app.deviceContext.filesDir)
|
||||
.start()
|
||||
|
||||
streamLogger(process.inputStream, Log::i)
|
||||
|
||||
@@ -99,7 +99,7 @@ object LocalDnsService {
|
||||
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
|
||||
.put("OnlyPrimaryDNS", true)
|
||||
}
|
||||
File(filesDir, file).bufferedWriter().use { it.write(config.toString()) }
|
||||
File(app.deviceContext.filesDir, file).writeText(config.toString())
|
||||
return file
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
|
||||
override val socketFile = File(app.filesDir, "/stat_path")
|
||||
override val socketFile = File(app.deviceContext.filesDir, "stat_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.github.shadowsocks.bg
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import java.io.File
|
||||
|
||||
@@ -47,14 +48,14 @@ class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
"-t", "10",
|
||||
"-b", "127.0.0.1",
|
||||
"-u",
|
||||
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
|
||||
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
|
||||
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
|
||||
"-c", "shadowsocks.json") // config is already built by BaseService.Interface
|
||||
"-c", data.shadowsocksConfigFile!!.absolutePath) // config is already built by BaseService.Interface
|
||||
sstunnelProcess = GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
val config = """base {
|
||||
File(app.deviceContext.filesDir, "redsocks.conf").writeText("""base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
@@ -68,8 +69,7 @@ redsocks {
|
||||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
"""
|
||||
File(filesDir, "redsocks.conf").bufferedWriter().use { it.write(config) }
|
||||
""")
|
||||
redsocksProcess = GuardedProcess(arrayListOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c", "redsocks.conf")
|
||||
|
||||
@@ -52,7 +52,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
}
|
||||
|
||||
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
|
||||
override val socketFile: File = File(filesDir, "protect_path")
|
||||
override val socketFile: File = File(app.deviceContext.filesDir, "protect_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
try {
|
||||
@@ -197,7 +197,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
var tries = 1
|
||||
while (tries < 5) {
|
||||
Thread.sleep(1000L * tries)
|
||||
if (JniHelper.sendFd(fd, File(filesDir, "sock_path").absolutePath) != -1) return true
|
||||
if (JniHelper.sendFd(fd, File(app.deviceContext.filesDir, "sock_path").absolutePath) != -1) return true
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -31,7 +31,7 @@ import com.j256.ormlite.dao.Dao
|
||||
import com.j256.ormlite.support.ConnectionSource
|
||||
import com.j256.ormlite.table.TableUtils
|
||||
|
||||
object DBHelper : OrmLiteSqliteOpenHelper(app, Key.PROFILE, null, 24) {
|
||||
object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val profileDao: Dao<Profile, Int> by lazy { getDao(Profile::class.java) as Dao<Profile, Int> }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -134,8 +134,14 @@ object DBHelper : OrmLiteSqliteOpenHelper(app, Key.PROFILE, null, 24) {
|
||||
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
|
||||
profileDao.executeRawNoArgs("COMMIT;")
|
||||
val old = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
|
||||
kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
|
||||
PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
|
||||
PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
|
||||
}
|
||||
|
||||
if (oldVersion < 25) {
|
||||
kvPairDao.queryBuilder().where().`in`("key",
|
||||
Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query()
|
||||
.forEach { PublicDatabase.kvPairDao.createOrUpdate(it) }
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
app.track(ex)
|
||||
@@ -29,10 +29,11 @@ import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.parsePort
|
||||
import com.j256.ormlite.field.DataType
|
||||
import com.j256.ormlite.field.DatabaseField
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
|
||||
class Profile {
|
||||
class Profile : Serializable {
|
||||
companion object {
|
||||
private const val TAG = "ShadowParser"
|
||||
private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex()
|
||||
@@ -153,34 +154,34 @@ class Profile {
|
||||
override fun toString() = toUri().toString()
|
||||
|
||||
fun serialize() {
|
||||
DataStore.putString(Key.name, name)
|
||||
DataStore.putString(Key.host, host)
|
||||
DataStore.putString(Key.remotePort, remotePort.toString())
|
||||
DataStore.putString(Key.password, password)
|
||||
DataStore.putString(Key.route, route)
|
||||
DataStore.putString(Key.remoteDns, remoteDns)
|
||||
DataStore.putString(Key.method, method)
|
||||
DataStore.privateStore.putString(Key.name, name)
|
||||
DataStore.privateStore.putString(Key.host, host)
|
||||
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
|
||||
DataStore.privateStore.putString(Key.password, password)
|
||||
DataStore.privateStore.putString(Key.route, route)
|
||||
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
|
||||
DataStore.privateStore.putString(Key.method, method)
|
||||
DataStore.proxyApps = proxyApps
|
||||
DataStore.bypass = bypass
|
||||
DataStore.putBoolean(Key.udpdns, udpdns)
|
||||
DataStore.putBoolean(Key.ipv6, ipv6)
|
||||
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
|
||||
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
|
||||
DataStore.individual = individual
|
||||
DataStore.plugin = plugin ?: ""
|
||||
DataStore.remove(Key.dirty)
|
||||
DataStore.privateStore.remove(Key.dirty)
|
||||
}
|
||||
fun deserialize() {
|
||||
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
|
||||
name = DataStore.getString(Key.name) ?: ""
|
||||
host = DataStore.getString(Key.host) ?: ""
|
||||
remotePort = parsePort(DataStore.getString(Key.remotePort), 8388, 1)
|
||||
password = DataStore.getString(Key.password) ?: ""
|
||||
method = DataStore.getString(Key.method) ?: ""
|
||||
route = DataStore.getString(Key.route) ?: ""
|
||||
remoteDns = DataStore.getString(Key.remoteDns) ?: ""
|
||||
name = DataStore.privateStore.getString(Key.name) ?: ""
|
||||
host = DataStore.privateStore.getString(Key.host) ?: ""
|
||||
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
|
||||
password = DataStore.privateStore.getString(Key.password) ?: ""
|
||||
method = DataStore.privateStore.getString(Key.method) ?: ""
|
||||
route = DataStore.privateStore.getString(Key.route) ?: ""
|
||||
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
|
||||
proxyApps = DataStore.proxyApps
|
||||
bypass = DataStore.bypass
|
||||
udpdns = DataStore.getBoolean(Key.udpdns, false)
|
||||
ipv6 = DataStore.getBoolean(Key.ipv6, false)
|
||||
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
|
||||
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
|
||||
individual = DataStore.individual
|
||||
plugin = DataStore.plugin
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ package com.github.shadowsocks.database
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.ProfilesFragment
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
|
||||
object ProfileManager {
|
||||
private const val TAG = "ProfileManager"
|
||||
@@ -41,10 +43,10 @@ object ProfileManager {
|
||||
profile.individual = oldProfile.individual
|
||||
profile.udpdns = oldProfile.udpdns
|
||||
}
|
||||
val last = DBHelper.profileDao.queryRaw(DBHelper.profileDao.queryBuilder().selectRaw("MAX(userOrder)")
|
||||
.prepareStatementString()).firstResult
|
||||
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
|
||||
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
|
||||
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
|
||||
DBHelper.profileDao.createOrUpdate(profile)
|
||||
PrivateDatabase.profileDao.createOrUpdate(profile)
|
||||
ProfilesFragment.instance?.profilesAdapter?.add(profile)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "addProfile", ex)
|
||||
@@ -53,8 +55,11 @@ object ProfileManager {
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
|
||||
*/
|
||||
fun updateProfile(profile: Profile): Boolean = try {
|
||||
DBHelper.profileDao.update(profile)
|
||||
PrivateDatabase.profileDao.update(profile)
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "updateProfile", ex)
|
||||
@@ -63,7 +68,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun getProfile(id: Int): Profile? = try {
|
||||
DBHelper.profileDao.queryForId(id)
|
||||
PrivateDatabase.profileDao.queryForId(id)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getProfile", ex)
|
||||
app.track(ex)
|
||||
@@ -71,8 +76,9 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun delProfile(id: Int): Boolean = try {
|
||||
DBHelper.profileDao.deleteById(id)
|
||||
PrivateDatabase.profileDao.deleteById(id)
|
||||
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
|
||||
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "delProfile", ex)
|
||||
@@ -81,7 +87,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun getFirstProfile(): Profile? = try {
|
||||
val result = DBHelper.profileDao.query(DBHelper.profileDao.queryBuilder().limit(1L).prepare())
|
||||
val result = PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare())
|
||||
if (result.size == 1) result[0] else null
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getAllProfiles", ex)
|
||||
@@ -90,7 +96,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun getAllProfiles(): List<Profile>? = try {
|
||||
DBHelper.profileDao.query(DBHelper.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
|
||||
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getAllProfiles", ex)
|
||||
app.track(ex)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.j256.ormlite.android.AndroidDatabaseConnection
|
||||
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
|
||||
import com.j256.ormlite.dao.Dao
|
||||
import com.j256.ormlite.support.ConnectionSource
|
||||
import com.j256.ormlite.table.TableUtils
|
||||
|
||||
object PublicDatabase : OrmLiteSqliteOpenHelper(app.deviceContext, Key.DB_PUBLIC, null, 1) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
|
||||
|
||||
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
|
||||
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
|
||||
}
|
||||
|
||||
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
|
||||
oldVersion: Int, newVersion: Int) { }
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
val connection = AndroidDatabaseConnection(db, true)
|
||||
connectionSource.saveSpecialConnection(connection)
|
||||
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
|
||||
onCreate(db, connectionSource)
|
||||
connectionSource.clearSpecialConnection(connection)
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ object PluginManager {
|
||||
var initialized = false
|
||||
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
|
||||
val list = ArrayList<String>()
|
||||
val pluginDir = File(app.filesDir, "plugin")
|
||||
val pluginDir = File(app.deviceContext.filesDir, "plugin")
|
||||
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
|
||||
?: return null).use { cursor ->
|
||||
if (!cursor.moveToFirst()) entryNotFound()
|
||||
|
||||
@@ -21,115 +21,72 @@
|
||||
package com.github.shadowsocks.preference
|
||||
|
||||
import android.os.Binder
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.database.DBHelper
|
||||
import com.github.shadowsocks.database.KeyValuePair
|
||||
import com.github.shadowsocks.BootReceiver
|
||||
import com.github.shadowsocks.database.PrivateDatabase
|
||||
import com.github.shadowsocks.database.PublicDatabase
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.parsePort
|
||||
import java.util.*
|
||||
|
||||
@Suppress("MemberVisibilityCanPrivate", "unused")
|
||||
object DataStore : PreferenceDataStore() {
|
||||
fun getBoolean(key: String?) = DBHelper.kvPairDao.queryForId(key)?.boolean
|
||||
fun getFloat(key: String?) = DBHelper.kvPairDao.queryForId(key)?.float
|
||||
fun getInt(key: String?) = DBHelper.kvPairDao.queryForId(key)?.int
|
||||
fun getLong(key: String?) = DBHelper.kvPairDao.queryForId(key)?.long
|
||||
fun getString(key: String?) = DBHelper.kvPairDao.queryForId(key)?.string
|
||||
fun getStringSet(key: String?) = DBHelper.kvPairDao.queryForId(key)?.stringSet
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
|
||||
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue
|
||||
override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue
|
||||
override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue
|
||||
override fun getStringSet(key: String?, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
|
||||
|
||||
fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
|
||||
fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
|
||||
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value)
|
||||
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
fun remove(key: String?) {
|
||||
DBHelper.kvPairDao.deleteById(key)
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||
private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
|
||||
object DataStore {
|
||||
val publicStore = OrmLitePreferenceDataStore(PublicDatabase.kvPairDao)
|
||||
// privateStore will only be used as temp storage for ProfileConfigFragment
|
||||
val privateStore = OrmLitePreferenceDataStore(PrivateDatabase.kvPairDao)
|
||||
|
||||
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
|
||||
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
|
||||
private fun getLocalPort(key: String, default: Int): Int {
|
||||
val value = getInt(key)
|
||||
val value = publicStore.getInt(key)
|
||||
return if (value != null) {
|
||||
putString(key, value.toString())
|
||||
publicStore.putString(key, value.toString())
|
||||
value
|
||||
} else parsePort(getString(key), default + userIndex)
|
||||
} else parsePort(publicStore.getString(key), default + userIndex)
|
||||
}
|
||||
|
||||
var profileId: Int
|
||||
get() = getInt(Key.id) ?: 0
|
||||
set(value) = putInt(Key.id, value)
|
||||
get() = publicStore.getInt(Key.id) ?: 0
|
||||
set(value) {
|
||||
publicStore.putInt(Key.id, value)
|
||||
if (DataStore.directBootAware) DirectBoot.update()
|
||||
}
|
||||
/**
|
||||
* Setter is defined in MainActivity.onPreferenceDataStoreChanged.
|
||||
*/
|
||||
val directBootAware: Boolean get() = BootReceiver.enabled && (publicStore.getBoolean(Key.directBootAware) ?: false)
|
||||
var serviceMode: String
|
||||
get() = getString(Key.serviceMode) ?: Key.modeVpn
|
||||
set(value) = putString(Key.serviceMode, value)
|
||||
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
|
||||
set(value) = publicStore.putString(Key.serviceMode, value)
|
||||
var portProxy: Int
|
||||
get() = getLocalPort(Key.portProxy, 1080)
|
||||
set(value) = putString(Key.portProxy, value.toString())
|
||||
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
||||
var portLocalDns: Int
|
||||
get() = getLocalPort(Key.portLocalDns, 5450)
|
||||
set(value) = putString(Key.portLocalDns, value.toString())
|
||||
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
|
||||
var portTransproxy: Int
|
||||
get() = getLocalPort(Key.portTransproxy, 8200)
|
||||
set(value) = putString(Key.portTransproxy, value.toString())
|
||||
|
||||
var proxyApps: Boolean
|
||||
get() = getBoolean(Key.proxyApps) ?: false
|
||||
set(value) = putBoolean(Key.proxyApps, value)
|
||||
var bypass: Boolean
|
||||
get() = getBoolean(Key.bypass) ?: false
|
||||
set(value) = putBoolean(Key.bypass, value)
|
||||
var individual: String
|
||||
get() = getString(Key.individual) ?: ""
|
||||
set(value) = putString(Key.individual, value)
|
||||
var plugin: String
|
||||
get() = getString(Key.plugin) ?: ""
|
||||
set(value) = putString(Key.plugin, value)
|
||||
var dirty: Boolean
|
||||
get() = getBoolean(Key.dirty) ?: false
|
||||
set(value) = putBoolean(Key.dirty, value)
|
||||
set(value) = publicStore.putString(Key.portTransproxy, value.toString())
|
||||
|
||||
fun initGlobal() {
|
||||
// temporary workaround for support lib bug
|
||||
if (getString(Key.serviceMode) == null) serviceMode = serviceMode
|
||||
if (getString(Key.portProxy) == null) portProxy = portProxy
|
||||
if (getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
||||
if (getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||
if (publicStore.getString(Key.serviceMode) == null) serviceMode = serviceMode
|
||||
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
|
||||
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
||||
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||
}
|
||||
|
||||
var proxyApps: Boolean
|
||||
get() = privateStore.getBoolean(Key.proxyApps) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.proxyApps, value)
|
||||
var bypass: Boolean
|
||||
get() = privateStore.getBoolean(Key.bypass) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.bypass, value)
|
||||
var individual: String
|
||||
get() = privateStore.getString(Key.individual) ?: ""
|
||||
set(value) = privateStore.putString(Key.individual, value)
|
||||
var plugin: String
|
||||
get() = privateStore.getString(Key.plugin) ?: ""
|
||||
set(value) = privateStore.putString(Key.plugin, value)
|
||||
var dirty: Boolean
|
||||
get() = privateStore.getBoolean(Key.dirty) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.dirty, value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.preference
|
||||
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.database.KeyValuePair
|
||||
import com.j256.ormlite.dao.Dao
|
||||
import java.util.HashSet
|
||||
|
||||
@Suppress("MemberVisibilityCanPrivate", "unused")
|
||||
open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, String?>) : PreferenceDataStore() {
|
||||
fun getBoolean(key: String?) = kvPairDao.queryForId(key)?.boolean
|
||||
fun getFloat(key: String?) = kvPairDao.queryForId(key)?.float
|
||||
fun getInt(key: String?) = kvPairDao.queryForId(key)?.int
|
||||
fun getLong(key: String?) = kvPairDao.queryForId(key)?.long
|
||||
fun getString(key: String?) = kvPairDao.queryForId(key)?.string
|
||||
fun getStringSet(key: String?) = kvPairDao.queryForId(key)?.stringSet
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
|
||||
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue
|
||||
override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue
|
||||
override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue
|
||||
override fun getStringSet(key: String?, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
|
||||
|
||||
fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
|
||||
fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
|
||||
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value)
|
||||
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
fun remove(key: String?) {
|
||||
kvPairDao.deleteById(key)
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||
private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
|
||||
}
|
||||
@@ -21,7 +21,11 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
object Key {
|
||||
const val PROFILE = "profile.db"
|
||||
/**
|
||||
* Public config that doesn't need to be kept secret.
|
||||
*/
|
||||
const val DB_PUBLIC = "config.db"
|
||||
const val DB_PROFILE = "profile.db"
|
||||
|
||||
const val id = "profileId"
|
||||
const val name = "profileName"
|
||||
@@ -39,6 +43,7 @@ object Key {
|
||||
const val route = "route"
|
||||
|
||||
const val isAutoConnect = "isAutoConnect"
|
||||
const val directBootAware = "directBootAware"
|
||||
|
||||
const val proxyApps = "isProxyApps"
|
||||
const val bypass = "isBypassApps"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
|
||||
@TargetApi(24)
|
||||
class DeviceContext(context: Context) : ContextWrapper(context.createDeviceProtectedStorageContext()) {
|
||||
/**
|
||||
* Thou shalt not get the REAL underlying application context which would no longer be operating under device
|
||||
* protected storage.
|
||||
*/
|
||||
override fun getApplicationContext(): Context = this
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
|
||||
@TargetApi(24)
|
||||
object DirectBoot {
|
||||
private val file = File(app.deviceContext.noBackupFilesDir, "directBootProfile")
|
||||
|
||||
fun getDeviceProfile(): Profile? = try {
|
||||
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
|
||||
} catch (_: FileNotFoundException) { null }
|
||||
|
||||
fun clean() {
|
||||
file.delete()
|
||||
File(app.deviceContext.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
val profile = ProfileManager.getProfile(DataStore.profileId) // app.currentProfile will call this
|
||||
if (profile == null) clean() else ObjectOutputStream(file.outputStream()).use { it.writeObject(profile) }
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ object TcpFastOpen {
|
||||
|
||||
val sendEnabled: Boolean get() {
|
||||
val file = File("/proc/sys/net/ipv4/tcp_fastopen")
|
||||
// File.readText doesn't work since this special file will return length 0
|
||||
return file.canRead() && file.bufferedReader().use { it.readText() }.trim().toInt() and 1 > 0
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user