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