Commit d7a11eaa authored by Mygod's avatar Mygod

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.
parent 14ac54c2
......@@ -113,6 +113,7 @@
<service
android:name=".bg.VpnService"
android:process=":bg"
android:directBootAware="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">
......@@ -124,17 +125,20 @@
<service
android:name=".bg.TransproxyService"
android:process=":bg"
android:directBootAware="true"
android:exported="false">
</service>
<service
android:name=".bg.ProxyService"
android:process=":bg"
android:directBootAware="true"
android:exported="false">
</service>
<service android:name=".bg.TileService" android:label="@string/quick_toggle"
android:process=":bg"
android:directBootAware="true"
android:icon="@drawable/ic_start_connected"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
......@@ -142,13 +146,20 @@
</intent-filter>
</service>
<receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
<receiver android:name=".BootReceiver"
android:process=":bg"
android:directBootAware="true"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".tasker.ActionListener" android:process=":bg" tools:ignore="ExportedReceiver">
<receiver android:name=".tasker.ActionListener"
android:process=":bg"
android:directBootAware="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter>
......@@ -156,16 +167,22 @@
<!-- android-job components -->
<service android:name="com.evernote.android.job.v21.PlatformJobService"
android:process=":bg"/>
android:process=":bg"
android:directBootAware="true"/>
<service android:name="com.evernote.android.job.v14.PlatformAlarmService"
android:process=":bg"/>
android:process=":bg"
android:directBootAware="true"/>
<service android:name="com.evernote.android.job.gcm.PlatformGcmService"
android:process=":bg"/>
android:process=":bg"
android:directBootAware="true"/>
<service android:name="com.evernote.android.job.JobRescheduleService"
android:process=":bg"/>
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="com.evernote.android.job.v14.PlatformAlarmReceiver"
android:process=":bg"/>
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="com.evernote.android.job.JobBootReceiver"
android:process=":bg"/>
android:process=":bg"
android:directBootAware="true"/>
</application>
</manifest>
/*******************************************************************************
* *
* 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.evernote.android.job
object JobConstants {
const val DATABASE_NAME = JobStorage.DATABASE_NAME
const val PREF_FILE_NAME = JobStorage.PREF_FILE_NAME
}
......@@ -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)
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(this)
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
......@@ -49,12 +50,12 @@ class TransproxyService : Service(), LocalDnsService.Interface {
"-u",
"-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
}
}
......
......@@ -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)
......
/*******************************************************************************
* *
* 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())
set(value) = publicStore.putString(Key.portTransproxy, value.toString())
fun initGlobal() {
// temporary workaround for support lib bug
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() = getBoolean(Key.proxyApps) ?: false
set(value) = putBoolean(Key.proxyApps, value)
get() = privateStore.getBoolean(Key.proxyApps) ?: false
set(value) = privateStore.putBoolean(Key.proxyApps, value)
var bypass: Boolean
get() = getBoolean(Key.bypass) ?: false
set(value) = putBoolean(Key.bypass, value)
get() = privateStore.getBoolean(Key.bypass) ?: false
set(value) = privateStore.putBoolean(Key.bypass, value)
var individual: String
get() = getString(Key.individual) ?: ""
set(value) = putString(Key.individual, value)
get() = privateStore.getString(Key.individual) ?: ""
set(value) = privateStore.putString(Key.individual, value)
var plugin: String
get() = getString(Key.plugin) ?: ""
set(value) = putString(Key.plugin, value)
get() = privateStore.getString(Key.plugin) ?: ""
set(value) = privateStore.putString(Key.plugin, value)
var dirty: Boolean
get() = getBoolean(Key.dirty) ?: false
set(value) = putBoolean(Key.dirty, value)
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
}
get() = privateStore.getBoolean(Key.dirty) ?: false
set(value) = privateStore.putBoolean(Key.dirty, value)
}
/*******************************************************************************
* *
* 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"
......
/*******************************************************************************
* *
* 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
}
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
}
......
......@@ -57,6 +57,9 @@
<string name="auto_connect_summary">Enable Shadowsocks on startup</string>
<string name="auto_connect_summary_v24">Enable Shadowsocks on startup. Recommended to use always-on VPN
instead</string>
<string name="direct_boot_aware">Direct Boot Aware</string>
<string name="direct_boot_aware_summary">Allow Shadowsocks to start before your device is unlocked (your selected
profile information will be less protected)</string>
<string name="tcp_fastopen_summary">Toggling requires ROOT permission</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string>
<string name="udp_dns">DNS Forwarding</string>
......
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="database" path="profile.db"/>
<!-- No device storage yet in Android 6.0 -->
<include domain="database" path="config.db"/>
<include domain="file" path="custom-rules.acl"/>
<include domain="device_database" path="config.db"/>
<include domain="device_file" path="custom-rules.acl"/>
</full-backup-content>
......@@ -5,6 +5,11 @@
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<!-- Direct Boot Aware alone doesn't do anything without auto connect -->
<SwitchPreference android:key="directBootAware"
android:dependency="isAutoConnect"
android:summary="@string/direct_boot_aware_summary"
android:title="@string/direct_boot_aware"/>
<SwitchPreference android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment