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"/> + +