From d7a11eaa3b8feda48dadec091907275e04061c97 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 15 Dec 2017 15:30:00 -0800 Subject: [PATCH] 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. --- mobile/src/main/AndroidManifest.xml | 33 +++-- .../com/evernote/android/job/JobConstants.kt | 26 ++++ .../main/java/com/github/shadowsocks/App.kt | 46 +++--- .../java/com/github/shadowsocks/AppManager.kt | 2 + .../com/github/shadowsocks/BootReceiver.kt | 8 +- .../github/shadowsocks/ConfigBackupHelper.kt | 4 +- .../GlobalSettingsPreferenceFragment.kt | 23 ++- .../com/github/shadowsocks/MainActivity.kt | 4 +- .../shadowsocks/ProfileConfigFragment.kt | 8 +- .../java/com/github/shadowsocks/acl/Acl.kt | 5 +- .../com/github/shadowsocks/bg/BaseService.kt | 25 +++- .../com/github/shadowsocks/bg/Executable.kt | 3 +- .../github/shadowsocks/bg/GuardedProcess.kt | 2 +- .../github/shadowsocks/bg/LocalDnsService.kt | 2 +- .../shadowsocks/bg/TrafficMonitorThread.kt | 2 +- .../shadowsocks/bg/TransproxyService.kt | 10 +- .../com/github/shadowsocks/bg/VpnService.kt | 4 +- .../{DBHelper.kt => PrivateDatabase.kt} | 12 +- .../github/shadowsocks/database/Profile.kt | 41 +++--- .../shadowsocks/database/ProfileManager.kt | 22 +-- .../shadowsocks/database/PublicDatabase.kt | 50 +++++++ .../shadowsocks/plugin/PluginManager.kt | 2 +- .../shadowsocks/preference/DataStore.kt | 133 ++++++------------ .../preference/OrmLitePreferenceDataStore.kt | 82 +++++++++++ .../com/github/shadowsocks/utils/Constants.kt | 7 +- .../github/shadowsocks/utils/DeviceContext.kt | 34 +++++ .../github/shadowsocks/utils/DirectBoot.kt | 31 ++++ .../github/shadowsocks/utils/TcpFastOpen.kt | 1 + mobile/src/main/res/values/strings.xml | 3 + mobile/src/main/res/xml/backup_descriptor.xml | 4 + mobile/src/main/res/xml/pref_global.xml | 5 + 31 files changed, 457 insertions(+), 177 deletions(-) create mode 100644 mobile/src/main/java/com/evernote/android/job/JobConstants.kt rename mobile/src/main/java/com/github/shadowsocks/database/{DBHelper.kt => PrivateDatabase.kt} (93%) create mode 100644 mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt create mode 100644 mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt create mode 100644 mobile/src/main/java/com/github/shadowsocks/utils/DeviceContext.kt create mode 100644 mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index c8f7fc76..6f06ec37 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -113,6 +113,7 @@ @@ -124,17 +125,20 @@ @@ -142,13 +146,20 @@ - + + - + @@ -156,16 +167,22 @@ + android:process=":bg" + android:directBootAware="true"/> + android:process=":bg" + android:directBootAware="true"/> + android:process=":bg" + android:directBootAware="true"/> + android:process=":bg" + android:directBootAware="true"/> + android:process=":bg" + android:directBootAware="true"/> + android:process=":bg" + android:directBootAware="true"/> diff --git a/mobile/src/main/java/com/evernote/android/job/JobConstants.kt b/mobile/src/main/java/com/evernote/android/job/JobConstants.kt new file mode 100644 index 00000000..382f6060 --- /dev/null +++ b/mobile/src/main/java/com/evernote/android/job/JobConstants.kt @@ -0,0 +1,26 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.evernote.android.job + +object JobConstants { + const val DATABASE_NAME = JobStorage.DATABASE_NAME + const val PREF_FILE_NAME = JobStorage.PREF_FILE_NAME +} diff --git a/mobile/src/main/java/com/github/shadowsocks/App.kt b/mobile/src/main/java/com/github/shadowsocks/App.kt index 8156f6ee..a59306f5 100644 --- a/mobile/src/main/java/com/github/shadowsocks/App.kt +++ b/mobile/src/main/java/com/github/shadowsocks/App.kt @@ -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) { diff --git a/mobile/src/main/java/com/github/shadowsocks/AppManager.kt b/mobile/src/main/java/com/github/shadowsocks/AppManager.kt index 46f6cf8f..6a9db475 100644 --- a/mobile/src/main/java/com/github/shadowsocks/AppManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/AppManager.kt @@ -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 diff --git a/mobile/src/main/java/com/github/shadowsocks/BootReceiver.kt b/mobile/src/main/java/com/github/shadowsocks/BootReceiver.kt index 8e1c8f87..af75829c 100644 --- a/mobile/src/main/java/com/github/shadowsocks/BootReceiver.kt +++ b/mobile/src/main/java/com/github/shadowsocks/BootReceiver.kt @@ -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() } } diff --git a/mobile/src/main/java/com/github/shadowsocks/ConfigBackupHelper.kt b/mobile/src/main/java/com/github/shadowsocks/ConfigBackupHelper.kt index f7f91982..b7098ff1 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ConfigBackupHelper.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ConfigBackupHelper.kt @@ -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")) } diff --git a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt index 78cf93cf..301490a2 100644 --- a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt @@ -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 diff --git a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt index 628a48a4..873dd598 100644 --- a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt +++ b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt @@ -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) diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt index d70af73b..bac205b8 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt @@ -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() } } diff --git a/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt b/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt index 6ee3838c..314b0416 100644 --- a/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt +++ b/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt @@ -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 : SortedList.Callback() { diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt index 52668a39..11c6bec1 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt @@ -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 diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/Executable.kt b/mobile/src/main/java/com/github/shadowsocks/bg/Executable.kt index 2d001d47..37e17d43 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/Executable.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/Executable.kt @@ -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") diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/GuardedProcess.kt b/mobile/src/main/java/com/github/shadowsocks/bg/GuardedProcess.kt index a4faeb6a..0ea94155 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/GuardedProcess.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/GuardedProcess.kt @@ -62,7 +62,7 @@ class GuardedProcess(private val cmd: List) { process = ProcessBuilder(cmd) .redirectErrorStream(true) - .directory(app.filesDir) + .directory(app.deviceContext.filesDir) .start() streamLogger(process.inputStream, Log::i) diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt index 4500b838..1eb2be7f 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt @@ -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 } diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt b/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt index b0b1eae4..70d1b14d 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/TrafficMonitorThread.kt @@ -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 { diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/TransproxyService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/TransproxyService.kt index f8b1e27e..a04d39e7 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/TransproxyService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/TransproxyService.kt @@ -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") diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt index c24fcac0..8f2b610c 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt @@ -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 } } diff --git a/mobile/src/main/java/com/github/shadowsocks/database/DBHelper.kt b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt similarity index 93% rename from mobile/src/main/java/com/github/shadowsocks/database/DBHelper.kt rename to mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt index 3c7d6aa6..1b504325 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/DBHelper.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt @@ -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 by lazy { getDao(Profile::class.java) as Dao } @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) diff --git a/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt b/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt index 7202c5df..a756dedd 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt @@ -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 } diff --git a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt index aa222a1f..9ab1c473 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt @@ -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? = 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) diff --git a/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt b/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt new file mode 100644 index 00000000..d9d57106 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt @@ -0,0 +1,50 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.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 by lazy { getDao(KeyValuePair::class.java) as Dao } + + 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(connectionSource, KeyValuePair::class.java, true) + onCreate(db, connectionSource) + connectionSource.clearSpecialConnection(connection) + } +} diff --git a/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt b/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt index 2a3f0d52..74c34167 100644 --- a/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt @@ -157,7 +157,7 @@ object PluginManager { var initialized = false fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found") val list = ArrayList() - 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() diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt index ec35ec0f..87651719 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt @@ -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?) = 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?) = 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() - 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) } diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt new file mode 100644 index 00000000..19849133 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt @@ -0,0 +1,82 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.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) : 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?) = 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?) = 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() + 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) +} diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt index b8f04e22..05f8e480 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Constants.kt @@ -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" diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/DeviceContext.kt b/mobile/src/main/java/com/github/shadowsocks/utils/DeviceContext.kt new file mode 100644 index 00000000..ce791f60 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/utils/DeviceContext.kt @@ -0,0 +1,34 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.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 +} diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt b/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt new file mode 100644 index 00000000..9bf345fd --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/utils/DirectBoot.kt @@ -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) } + } +} diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt b/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt index bd38cf62..49c2781d 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/TcpFastOpen.kt @@ -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 } diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 92f3ac44..48771b24 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -57,6 +57,9 @@ Enable Shadowsocks on startup Enable Shadowsocks on startup. Recommended to use always-on VPN instead + Direct Boot Aware + Allow Shadowsocks to start before your device is unlocked (your selected + profile information will be less protected) Toggling requires ROOT permission Unsupported kernel version: %s < 3.7.1 DNS Forwarding diff --git a/mobile/src/main/res/xml/backup_descriptor.xml b/mobile/src/main/res/xml/backup_descriptor.xml index 96554ae6..32a3b82b 100644 --- a/mobile/src/main/res/xml/backup_descriptor.xml +++ b/mobile/src/main/res/xml/backup_descriptor.xml @@ -1,5 +1,9 @@ + + + + diff --git a/mobile/src/main/res/xml/pref_global.xml b/mobile/src/main/res/xml/pref_global.xml index 6fb0543d..ac03c8ab 100644 --- a/mobile/src/main/res/xml/pref_global.xml +++ b/mobile/src/main/res/xml/pref_global.xml @@ -5,6 +5,11 @@ android:persistent="false" android:summary="@string/auto_connect_summary" android:title="@string/auto_connect"/> + +