Unverified Commit 612a34a6 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1508 from Mygod/direct-boot

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