Commit f09f12c0 authored by Mygod's avatar Mygod

Initial design for Android TV

parent 95959639
...@@ -17,8 +17,5 @@ core/src/overture/src/github.com/Sirupsen ...@@ -17,8 +17,5 @@ core/src/overture/src/github.com/Sirupsen
core/src/overture/src/github.com/miekg core/src/overture/src/github.com/miekg
core/src/overture/src/golang.org core/src/overture/src/golang.org
# work in progress
tv/
# release apks # release apks
*.apk *.apk
import org.apache.tools.ant.taskdefs.condition.Os import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android { android {
buildToolsVersion rootProject.buildToolsVersion buildToolsVersion rootProject.buildToolsVersion
...@@ -20,6 +22,11 @@ android { ...@@ -20,6 +22,11 @@ android {
arguments "-j${Runtime.runtime.availableProcessors()}" arguments "-j${Runtime.runtime.availableProcessors()}"
} }
} }
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
buildTypes { buildTypes {
...@@ -56,15 +63,26 @@ task goClean(type: Exec) { ...@@ -56,15 +63,26 @@ task goClean(type: Exec) {
} }
tasks.whenTaskAdded { task -> tasks.whenTaskAdded { task ->
if ((task.name == 'generateJsonModelDebug' || if ((task.name == 'generateJsonModelDebug' || task.name == 'generateJsonModelRelease')) {
task.name == 'generateJsonModelRelease')) {
task.dependsOn(goBuild) task.dependsOn(goBuild)
} }
} }
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion"
api "androidx.preference:preference:$androidxVersion"
api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.4'
api 'com.google.firebase:firebase-config:16.0.0'
api 'com.google.firebase:firebase-core:16.0.0'
api 'com.squareup.okhttp3:okhttp:3.11.0'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'eu.chainfire:libsuperuser:1.0.0.201704021214'
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.room:room-testing:$roomVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "android.arch.work:work-testing:$workVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
} }
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.shadowsocks.core"> xmlns:tools="http://schemas.android.com/tools"
package="com.github.shadowsocks.core"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-feature android:name="android.software.leanback"
android:required="false"/>
<application
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:backupAgent="com.github.shadowsocks.ConfigBackupHelper"
android:fullBackupContent="@xml/backup_descriptor"
android:fullBackupOnly="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:banner="@mipmap/banner">
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<meta-data android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<meta-data android:name="firebase_crashlytics_collection_enabled"
android:value="false"/>
<service
android:name="com.github.shadowsocks.bg.VpnService"
android:process=":bg"
android:directBootAware="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>
<service
android:name="com.github.shadowsocks.bg.TransproxyService"
android:process=":bg"
android:directBootAware="true"
android:exported="false">
</service>
<service
android:name="com.github.shadowsocks.bg.ProxyService"
android:process=":bg"
android:directBootAware="true"
android:exported="false">
</service>
<activity
android:name="com.github.shadowsocks.VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
<receiver android:name="com.github.shadowsocks.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>
<!-- https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/work/workmanager/src/main/AndroidManifest.xml -->
<provider android:name="androidx.work.impl.WorkManagerInitializer"
tools:node="remove"/>
<service android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
android:process=":bg"
android:directBootAware="true"/>
<service android:name="androidx.work.impl.background.systemjob.SystemJobService"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
android:process=":bg"
android:directBootAware="true"/>
</application>
</manifest> </manifest>
...@@ -25,7 +25,7 @@ import android.content.ComponentName ...@@ -25,7 +25,7 @@ import android.content.ComponentName
import android.content.Context 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.Core.app
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
...@@ -45,6 +45,6 @@ class BootReceiver : BroadcastReceiver() { ...@@ -45,6 +45,6 @@ class BootReceiver : BroadcastReceiver() {
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
else -> return else -> return
} }
if (DataStore.directBootAware == locked) app.startService() if (DataStore.directBootAware == locked) Core.startService()
} }
} }
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.UserManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.work.WorkManager
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import io.fabric.sdk.android.Fabric
import java.io.File
import java.io.IOException
import kotlin.reflect.KClass
object Core {
const val TAG = "Core"
lateinit var app: Application
lateinit var configureIntent: (Context) -> PendingIntent
val handler by lazy { Handler(Looper.getMainLooper()) }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) }
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
}
val currentProfile: Profile? get() =
if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId)
fun switchProfile(id: Long): Profile {
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
DataStore.profileId = result.id
return result
}
fun init(app: Application, configureClass: KClass<out Any>) {
this.app = app
this.configureIntent = {
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
}
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
val old = Acl.getFile(Acl.CUSTOM_RULES, app)
if (old.canRead()) {
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
old.delete()
}
}
Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
FirebaseApp.initializeApp(deviceStorage)
remoteConfig.setDefaults(R.xml.default_configs)
remoteConfig.fetch().addOnCompleteListener {
if (it.isSuccessful) remoteConfig.activateFetched() else {
Log.e(TAG, "Failed to fetch config")
Crashlytics.logException(it.exception)
}
}
WorkManager.initialize(deviceStorage, androidx.work.Configuration.Builder().build())
// handle data restored/crash
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
if (DataStore.tcpFastOpen) TcpFastOpen.enabledAsync(true)
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
val assetManager = app.assets
for (dir in arrayOf("acl", "overture"))
try {
for (file in assetManager.list(dir)!!) assetManager.open("$dir/$file").use { input ->
File(deviceStorage.filesDir, file).outputStream().use { output -> input.copyTo(output) }
}
} catch (e: IOException) {
printLog(e)
}
DataStore.publicStore.putLong(Key.assetUpdateTime, packageInfo.lastUpdateTime)
}
updateNotificationChannels()
}
fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = app.getSystemService<NotificationManager>()!!
nm.createNotificationChannels(listOf(
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW)))
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
}
}
fun getPackageInfo(packageName: String) = app.packageManager.getPackageInfo(packageName,
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() = ContextCompat.startForegroundService(app, Intent(app, BaseService.serviceClass.java))
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit): BroadcastReceiver {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
val result = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
callback()
if (onetime) app.unregisterReceiver(this)
}
}
app.registerReceiver(result, filter)
return result
}
}
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Fragment
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
...@@ -40,7 +41,7 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection ...@@ -40,7 +41,7 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
interface Interface : IBinder.DeathRecipient { interface Interface : IBinder.DeathRecipient {
val serviceCallback: IShadowsocksServiceCallback? get() = null val serviceCallback: IShadowsocksServiceCallback? get() = null
val connection: ShadowsocksConnection val connection: ShadowsocksConnection
get() = connections.getOrPut(this, { ShadowsocksConnection(this) }) get() = connections.getOrPut(this) { ShadowsocksConnection(this) }
val listenForDeath get() = false val listenForDeath get() = false
fun onServiceConnected(service: IShadowsocksService) { } fun onServiceConnected(service: IShadowsocksService) { }
...@@ -56,6 +57,12 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection ...@@ -56,6 +57,12 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
private var connectionActive = false private var connectionActive = false
private var callbackRegistered = false private var callbackRegistered = false
private var binder: IBinder? = null private var binder: IBinder? = null
private val context by lazy {
when (instance) {
is Fragment -> instance.activity
else -> instance as Context
}
}
var listeningForBandwidth = false var listeningForBandwidth = false
set(value) { set(value) {
...@@ -98,15 +105,15 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection ...@@ -98,15 +105,15 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
fun connect() { fun connect() {
if (connectionActive) return if (connectionActive) return
connectionActive = true connectionActive = true
val intent = Intent(instance as Context, BaseService.serviceClass.java).setAction(Action.SERVICE) val intent = Intent(context, BaseService.serviceClass.java).setAction(Action.SERVICE)
instance.bindService(intent, this, Context.BIND_AUTO_CREATE) context.bindService(intent, this, Context.BIND_AUTO_CREATE)
} }
fun disconnect() { fun disconnect() {
unregisterCallback() unregisterCallback()
instance.onServiceDisconnected() instance.onServiceDisconnected()
if (connectionActive) try { if (connectionActive) try {
(instance as Context).unbindService(this) context.unbindService(this)
} catch (_: IllegalArgumentException) { } // ignore } catch (_: IllegalArgumentException) { } // ignore
connectionActive = false connectionActive = false
if (instance.listenForDeath) binder?.unlinkToDeath(instance, 0) if (instance.listenForDeath) binder?.unlinkToDeath(instance, 0)
......
...@@ -31,9 +31,9 @@ import android.widget.Toast ...@@ -31,9 +31,9 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.core.R
import com.github.shadowsocks.utils.broadcastReceiver import com.github.shadowsocks.utils.broadcastReceiver
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface { class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
...@@ -63,7 +63,7 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface ...@@ -63,7 +63,7 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == RESULT_OK) app.startService() else { if (resultCode == RESULT_OK) Core.startService() else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data") Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
} }
......
...@@ -24,7 +24,7 @@ import android.content.Context ...@@ -24,7 +24,7 @@ import android.content.Context
import android.util.Log import android.util.Log
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.asIterable import com.github.shadowsocks.utils.asIterable
...@@ -46,7 +46,7 @@ class Acl { ...@@ -46,7 +46,7 @@ class Acl {
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex() val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
fun getFile(id: String, context: Context = app.deviceStorage) = File(context.filesDir, "$id.acl") fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.filesDir, "$id.acl")
var customRules: Acl var customRules: Acl
get() { get() {
......
...@@ -33,12 +33,13 @@ import android.util.Log ...@@ -33,12 +33,13 @@ import android.util.Log
import androidx.core.os.UserManagerCompat import androidx.core.os.UserManagerCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.R import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncer import com.github.shadowsocks.acl.AclSyncer
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
...@@ -109,7 +110,7 @@ object BaseService { ...@@ -109,7 +110,7 @@ object BaseService {
t.schedule(object : TimerTask() { t.schedule(object : TimerTask() {
override fun run() { override fun run() {
val profile = profile ?: return val profile = profile ?: return
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post { if (state == CONNECTED && TrafficMonitor.updateRate()) Core.handler.post {
if (bandwidthListeners.isNotEmpty()) { if (bandwidthListeners.isNotEmpty()) {
val txRate = TrafficMonitor.txRate val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate val rxRate = TrafficMonitor.rxRate
...@@ -157,7 +158,7 @@ object BaseService { ...@@ -157,7 +158,7 @@ object BaseService {
profile.tx += tx profile.tx += tx
profile.rx += rx profile.rx += rx
ProfileManager.updateProfile(profile) ProfileManager.updateProfile(profile)
app.handler.post { Core.handler.post {
if (bandwidthListeners.isNotEmpty()) { if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast() val n = callbacks.beginBroadcast()
for (i in 0 until n) { for (i in 0 until n) {
...@@ -196,7 +197,7 @@ object BaseService { ...@@ -196,7 +197,7 @@ object BaseService {
} }
// sensitive Shadowsocks config is stored in // sensitive Shadowsocks config is stored in
return File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) { return File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
app.deviceStorage.noBackupFilesDir // only API 24+ will be in locked state Core.deviceStorage.noBackupFilesDir // only API 24+ will be in locked state
}, CONFIG_FILE).apply { }, CONFIG_FILE).apply {
shadowsocksConfigFile = this shadowsocksConfigFile = this
writeText(config.toString()) writeText(config.toString())
...@@ -210,7 +211,7 @@ object BaseService { ...@@ -210,7 +211,7 @@ object BaseService {
fun changeState(s: Int, msg: String? = null) { fun changeState(s: Int, msg: String? = null) {
if (state == s && msg == null) return if (state == s && msg == null) return
if (callbacks.registeredCallbackCount > 0) app.handler.post { if (callbacks.registeredCallbackCount > 0) Core.handler.post {
val n = callbacks.beginBroadcast() val n = callbacks.beginBroadcast()
for (i in 0 until n) try { for (i in 0 until n) try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg) callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
...@@ -233,7 +234,7 @@ object BaseService { ...@@ -233,7 +234,7 @@ object BaseService {
false false
} else true } else true
fun forceLoad() { fun forceLoad() {
val profile = app.currentProfile val profile = Core.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty)) ?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
if (!checkProfile(profile)) return if (!checkProfile(profile)) return
val s = data.state val s = data.state
...@@ -288,7 +289,7 @@ object BaseService { ...@@ -288,7 +289,7 @@ object BaseService {
val data = data val data = data
data.changeState(STOPPING) data.changeState(STOPPING)
app.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag))) Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
killProcesses() killProcesses()
...@@ -326,7 +327,7 @@ object BaseService { ...@@ -326,7 +327,7 @@ object BaseService {
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data val data = data
if (data.state != STOPPED) return Service.START_NOT_STICKY if (data.state != STOPPED) return Service.START_NOT_STICKY
val profile = app.currentProfile val profile = Core.currentProfile
this as Context this as Context
if (profile == null) { if (profile == null) {
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107 // gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
...@@ -353,7 +354,7 @@ object BaseService { ...@@ -353,7 +354,7 @@ object BaseService {
} }
data.notification = createNotification(profile.formattedName) data.notification = createNotification(profile.formattedName)
app.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag))) Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(CONNECTING) data.changeState(CONNECTING)
...@@ -366,12 +367,12 @@ object BaseService { ...@@ -366,12 +367,12 @@ object BaseService {
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.build() .build()
val mdg = MessageDigest.getInstance("SHA-1") val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signaturesCompat.first().toByteArray()) mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder() val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0))) .add("sig", String(Base64.encode(mdg.digest(), 0)))
.build() .build()
val request = Request.Builder() val request = Request.Builder()
.url(app.remoteConfig.getString("proxy_url")) .url(Core.remoteConfig.getString("proxy_url"))
.post(requestBody) .post(requestBody)
.build() .build()
......
...@@ -23,7 +23,7 @@ package com.github.shadowsocks.bg ...@@ -23,7 +23,7 @@ package com.github.shadowsocks.bg
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
......
...@@ -24,7 +24,7 @@ import android.os.Build ...@@ -24,7 +24,7 @@ import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread import com.github.shadowsocks.utils.thread
...@@ -47,7 +47,7 @@ class GuardedProcessPool { ...@@ -47,7 +47,7 @@ class GuardedProcessPool {
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = private fun streamLogger(input: InputStream, logger: (String, String) -> Int) =
thread("StreamLogger-$cmdName") { thread("StreamLogger-$cmdName") {
try { try {
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } } input.bufferedReader().forEachLine { logger(TAG, it) }
} catch (_: IOException) { } // ignore } catch (_: IOException) { } // ignore
} }
private fun pushException(ioException: IOException?) { private fun pushException(ioException: IOException?) {
...@@ -66,7 +66,7 @@ class GuardedProcessPool { ...@@ -66,7 +66,7 @@ class GuardedProcessPool {
process = ProcessBuilder(cmd) process = ProcessBuilder(cmd)
.redirectErrorStream(true) .redirectErrorStream(true)
.directory(app.deviceStorage.filesDir) .directory(Core.deviceStorage.filesDir)
.start() .start()
streamLogger(process.inputStream, Log::i) streamLogger(process.inputStream, Log::i)
......
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
...@@ -51,7 +52,7 @@ object LocalDnsService { ...@@ -51,7 +52,7 @@ object LocalDnsService {
} }
fun buildOvertureConfig(file: String) = file.also { fun buildOvertureConfig(file: String) = file.also {
File(app.deviceStorage.filesDir, it).writeText(JSONObject().run { File(Core.deviceStorage.filesDir, it).writeText(JSONObject().run {
put("BindAddress", "127.0.0.1:" + DataStore.portLocalDns) put("BindAddress", "127.0.0.1:" + DataStore.portLocalDns)
put("RedirectIPv6Record", true) put("RedirectIPv6Record", true)
put("DomainBase64Decode", false) put("DomainBase64Decode", false)
......
...@@ -33,9 +33,9 @@ import android.text.format.Formatter ...@@ -33,9 +33,9 @@ import android.text.format.Formatter
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.shadowsocks.MainActivity import com.github.shadowsocks.Core
import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.core.R
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver import com.github.shadowsocks.utils.broadcastReceiver
import java.util.* import java.util.*
...@@ -58,7 +58,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam ...@@ -58,7 +58,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, txRate)) val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, txRate))
val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, rxRate)) val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, rxRate))
builder.setContentText("$txr↑\t$rxr↓") builder.setContentText("$txr↑\t$rxr↓")
style.bigText(service.getString(R.string.stat_summary).format(Locale.ENGLISH, txr, rxr, style.bigText(service.getString(R.string.stat_summary, txr, rxr,
Formatter.formatFileSize(service, txTotal), Formatter.formatFileSize(service, rxTotal))) Formatter.formatFileSize(service, txTotal), Formatter.formatFileSize(service, rxTotal)))
show() show()
} }
...@@ -73,7 +73,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam ...@@ -73,7 +73,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
.setColor(ContextCompat.getColor(service, R.color.material_primary_500)) .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success)) .setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName) .setContentTitle(profileName)
.setContentIntent(MainActivity.pendingIntent(service)) .setContentIntent(Core.configureIntent(service))
.setSmallIcon(R.drawable.ic_service_active) .setSmallIcon(R.drawable.ic_service_active)
private val style = NotificationCompat.BigTextStyle(builder) private val style = NotificationCompat.BigTextStyle(builder)
private var isVisible = true private var isVisible = true
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.net.LocalSocket import android.net.LocalSocket
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
...@@ -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.deviceStorage.filesDir, "stat_path") override val socketFile = File(Core.deviceStorage.filesDir, "stat_path")
override fun accept(socket: LocalSocket) { override fun accept(socket: LocalSocket) {
try { try {
......
...@@ -22,7 +22,7 @@ package com.github.shadowsocks.bg ...@@ -22,7 +22,7 @@ package com.github.shadowsocks.bg
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import java.io.File import java.io.File
...@@ -50,7 +50,7 @@ class TransproxyService : Service(), LocalDnsService.Interface { ...@@ -50,7 +50,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
} }
private fun startRedsocksDaemon() { private fun startRedsocksDaemon() {
File(app.deviceStorage.filesDir, "redsocks.conf").writeText("""base { File(Core.deviceStorage.filesDir, "redsocks.conf").writeText("""base {
log_debug = off; log_debug = off;
log_info = off; log_info = off;
log = stderr; log = stderr;
......
...@@ -28,12 +28,11 @@ import android.net.* ...@@ -28,12 +28,11 @@ import android.net.*
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.VpnRequestActivity import com.github.shadowsocks.VpnRequestActivity
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
...@@ -73,7 +72,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -73,7 +72,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
} }
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") { private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
override val socketFile: File = File(app.deviceStorage.filesDir, "protect_path") override val socketFile: File = File(Core.deviceStorage.filesDir, "protect_path")
override fun accept(socket: LocalSocket) { override fun accept(socket: LocalSocket) {
try { try {
...@@ -177,7 +176,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -177,7 +176,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private fun startVpn(): Int { private fun startVpn(): Int {
val profile = data.profile!! val profile = data.profile!!
val builder = Builder() val builder = Builder()
.setConfigureIntent(MainActivity.pendingIntent(this)) .setConfigureIntent(Core.configureIntent(this))
.setSession(profile.formattedName) .setSession(profile.formattedName)
.setMtu(VPN_MTU) .setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24) .addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24)
...@@ -252,7 +251,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -252,7 +251,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
var tries = 0 var tries = 0
while (tries < 10) { while (tries < 10) {
Thread.sleep(30L shl tries) Thread.sleep(30L shl tries)
if (JniHelper.sendFd(fd, File(app.deviceStorage.filesDir, "sock_path").absolutePath) != -1) return true if (JniHelper.sendFd(fd, File(Core.deviceStorage.filesDir, "sock_path").absolutePath) != -1) return true
tries += 1 tries += 1
} }
} }
......
...@@ -24,7 +24,7 @@ import androidx.room.Database ...@@ -24,7 +24,7 @@ import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.migration.RecreateSchemaMigration import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
......
...@@ -171,6 +171,9 @@ class Profile : Serializable { ...@@ -171,6 +171,9 @@ class Profile : Serializable {
@Query("DELETE FROM `Profile` WHERE `id` = :id") @Query("DELETE FROM `Profile` WHERE `id` = :id")
fun delete(id: Long): Int fun delete(id: Long): Int
@Query("DELETE FROM `Profile`")
fun deleteAll(): Int
} }
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.database.sqlite.SQLiteCantOpenDatabaseException import android.database.sqlite.SQLiteCantOpenDatabaseException
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
...@@ -33,12 +32,18 @@ import java.sql.SQLException ...@@ -33,12 +32,18 @@ import java.sql.SQLException
* to ensure we are in a consistent state. * to ensure we are in a consistent state.
*/ */
object ProfileManager { object ProfileManager {
interface Listener {
fun onAdd(profile: Profile)
fun onRemove(profileId: Long)
}
var listener: Listener? = null
@Throws(SQLException::class) @Throws(SQLException::class)
fun createProfile(profile: Profile = Profile()): Profile { fun createProfile(profile: Profile = Profile()): Profile {
profile.id = 0 profile.id = 0
profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0 profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0
profile.id = PrivateDatabase.profileDao.create(profile) profile.id = PrivateDatabase.profileDao.create(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile) listener?.onAdd(profile)
return profile return profile
} }
...@@ -61,12 +66,19 @@ object ProfileManager { ...@@ -61,12 +66,19 @@ object ProfileManager {
@Throws(SQLException::class) @Throws(SQLException::class)
fun delProfile(id: Long) { fun delProfile(id: Long) {
check(PrivateDatabase.profileDao.delete(id) == 1) check(PrivateDatabase.profileDao.delete(id) == 1)
ProfilesFragment.instance?.profilesAdapter?.removeId(id) listener?.onRemove(id)
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean() if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
} }
@Throws(SQLException::class)
fun clear() = PrivateDatabase.profileDao.deleteAll().also {
// listener is not called since this won't be used in mobile submodule
DirectBoot.clean()
}
@Throws(IOException::class) @Throws(IOException::class)
fun isNotEmpty(): Boolean = try { fun ensureNotEmpty() {
val nonEmpty = try {
PrivateDatabase.profileDao.isNotEmpty() PrivateDatabase.profileDao.isNotEmpty()
} catch (ex: SQLiteCantOpenDatabaseException) { } catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex) throw IOException(ex)
...@@ -74,6 +86,8 @@ object ProfileManager { ...@@ -74,6 +86,8 @@ object ProfileManager {
printLog(ex) printLog(ex)
false false
} }
if (!nonEmpty) DataStore.profileId = createProfile().id
}
@Throws(IOException::class) @Throws(IOException::class)
fun getAllProfiles(): List<Profile>? = try { fun getAllProfiles(): List<Profile>? = try {
......
...@@ -23,7 +23,7 @@ package com.github.shadowsocks.database ...@@ -23,7 +23,7 @@ package com.github.shadowsocks.database
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.database.migration.RecreateSchemaMigration import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
...@@ -31,7 +31,7 @@ import com.github.shadowsocks.utils.Key ...@@ -31,7 +31,7 @@ import com.github.shadowsocks.utils.Key
abstract class PublicDatabase : RoomDatabase() { abstract class PublicDatabase : RoomDatabase() {
companion object { companion object {
private val instance by lazy { private val instance by lazy {
Room.databaseBuilder(app.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC) Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
.allowMainThreadQueries() .allowMainThreadQueries()
.addMigrations( .addMigrations(
Migration3 Migration3
......
package com.github.shadowsocks.plugin package com.github.shadowsocks.plugin
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core.app
object NoPlugin : Plugin() { object NoPlugin : Plugin() {
override val id: String get() = "" override val id: String get() = ""
override val label: CharSequence get() = app.getText(com.github.shadowsocks.R.string.plugin_disabled) override val label: CharSequence get() = app.getText(com.github.shadowsocks.core.R.string.plugin_disabled)
} }
...@@ -31,7 +31,8 @@ import android.util.Base64 ...@@ -31,7 +31,8 @@ import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.signaturesCompat import com.github.shadowsocks.utils.signaturesCompat
...@@ -50,7 +51,7 @@ object PluginManager { ...@@ -50,7 +51,7 @@ object PluginManager {
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys. * public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/ */
val trustedSignatures by lazy { val trustedSignatures by lazy {
app.info.signaturesCompat.toSet() + Core.packageInfo.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod Signature(Base64.decode( // @Mygod
""" """
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD |MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
...@@ -91,7 +92,7 @@ object PluginManager { ...@@ -91,7 +92,7 @@ object PluginManager {
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) { fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges { if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(this) { synchronized(this) {
receiver = null receiver = null
cachedPlugins = null cachedPlugins = null
...@@ -128,7 +129,7 @@ object PluginManager { ...@@ -128,7 +129,7 @@ object PluginManager {
// add other plugin types here // add other plugin types here
throw throwable throw throwable
?: FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) ?: FileNotFoundException(app.getString(com.github.shadowsocks.core.R.string.plugin_unknown, options.id))
} }
private fun initNative(options: PluginOptions): String? { private fun initNative(options: PluginOptions): String? {
...@@ -162,7 +163,7 @@ object PluginManager { ...@@ -162,7 +163,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.deviceStorage.filesDir, "plugin") val pluginDir = File(Core.deviceStorage.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()
......
...@@ -23,7 +23,8 @@ package com.github.shadowsocks.plugin ...@@ -23,7 +23,8 @@ package com.github.shadowsocks.plugin
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.utils.signaturesCompat import com.github.shadowsocks.utils.signaturesCompat
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
...@@ -35,6 +36,6 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() ...@@ -35,6 +36,6 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin()
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) } override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
override val packageName: String get() = resolveInfo.resolvePackageName override val packageName: String get() = resolveInfo.resolvePackageName
override val trusted by lazy { override val trusted by lazy {
app.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains) Core.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
} }
} }
...@@ -22,7 +22,8 @@ package com.github.shadowsocks.preference ...@@ -22,7 +22,8 @@ package com.github.shadowsocks.preference
import android.os.Binder import android.os.Binder
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.github.shadowsocks.App.Companion.app import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.PrivateDatabase import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase import com.github.shadowsocks.database.PublicDatabase
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
...@@ -30,11 +31,21 @@ import com.github.shadowsocks.utils.Key ...@@ -30,11 +31,21 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.TcpFastOpen import com.github.shadowsocks.utils.TcpFastOpen
import com.github.shadowsocks.utils.parsePort import com.github.shadowsocks.utils.parsePort
object DataStore { object DataStore : OnPreferenceDataStoreChangeListener {
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao) val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
// privateStore will only be used as temp storage for ProfileConfigFragment // privateStore will only be used as temp storage for ProfileConfigFragment
val privateStore = RoomPreferenceDataStore(PrivateDatabase.kvPairDao) val privateStore = RoomPreferenceDataStore(PrivateDatabase.kvPairDao)
init {
publicStore.registerChangeListener(this)
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) {
Key.id -> if (DataStore.directBootAware) DirectBoot.update()
}
}
// 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 {
...@@ -47,12 +58,9 @@ object DataStore { ...@@ -47,12 +58,9 @@ object DataStore {
var profileId: Long var profileId: Long
get() = publicStore.getLong(Key.id) ?: 0 get() = publicStore.getLong(Key.id) ?: 0
set(value) { set(value) = publicStore.putLong(Key.id, value)
publicStore.putLong(Key.id, value)
if (DataStore.directBootAware) DirectBoot.update()
}
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
val directBootAware: Boolean get() = app.directBootSupported && canToggleLocked val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && DataStore.publicStore.getBoolean(Key.tfo, true) val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && DataStore.publicStore.getBoolean(Key.tfo, true)
@AppCompatDelegate.NightMode @AppCompatDelegate.NightMode
val nightMode get() = when (publicStore.getString(Key.nightMode)) { val nightMode get() = when (publicStore.getString(Key.nightMode)) {
...@@ -76,6 +84,7 @@ object DataStore { ...@@ -76,6 +84,7 @@ object DataStore {
* Initialize settings that have complicated default values. * Initialize settings that have complicated default values.
*/ */
fun initGlobal() { fun initGlobal() {
if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
......
...@@ -69,6 +69,11 @@ object Key { ...@@ -69,6 +69,11 @@ object Key {
const val tfo = "tcp_fastopen" const val tfo = "tcp_fastopen"
const val assetUpdateTime = "assetUpdateTime" const val assetUpdateTime = "assetUpdateTime"
// TV specific values
const val controlStats = "control.stats"
const val controlImport = "control.import"
const val controlExport = "control.export"
} }
object Action { object Action {
......
...@@ -5,7 +5,8 @@ import android.content.BroadcastReceiver ...@@ -5,7 +5,8 @@ import android.content.BroadcastReceiver
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 com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
...@@ -17,7 +18,7 @@ import java.io.ObjectOutputStream ...@@ -17,7 +18,7 @@ import java.io.ObjectOutputStream
@TargetApi(24) @TargetApi(24)
object DirectBoot : BroadcastReceiver() { object DirectBoot : BroadcastReceiver() {
private val file = File(app.deviceStorage.noBackupFilesDir, "directBootProfile") private val file = File(Core.deviceStorage.noBackupFilesDir, "directBootProfile")
private var registered = false private var registered = false
fun getDeviceProfile(): Profile? = try { fun getDeviceProfile(): Profile? = try {
...@@ -26,7 +27,7 @@ object DirectBoot : BroadcastReceiver() { ...@@ -26,7 +27,7 @@ object DirectBoot : BroadcastReceiver() {
fun clean() { fun clean() {
file.delete() file.delete()
File(app.deviceStorage.noBackupFilesDir, BaseService.CONFIG_FILE).delete() File(Core.deviceStorage.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
} }
/** /**
......
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.os.SystemClock
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
/**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
*/
class HttpsTest(private val setStatus: (CharSequence?) -> Unit, private val errorCallback: (CharSequence) -> Unit) {
private var testCount = 0
fun testConnection() {
++testCount
setStatus(app.getText(R.string.connection_test_testing))
val id = testCount // it would change by other code
thread("ConnectionTest") {
val url = URL("https", when (Core.currentProfile!!.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (BaseService.usingVpnMode) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val (success, result) = try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L)
Pair(true, app.getString(R.string.connection_test_available, elapsed))
else throw IOException(app.getString(R.string.connection_test_error_status_code, code))
} catch (e: IOException) {
Pair(false, app.getString(R.string.connection_test_error, e.message))
} finally {
conn.disconnect()
}
if (testCount == id) Core.handler.post {
if (success) setStatus(result) else {
setStatus(app.getText(R.string.connection_test_fail))
errorCallback(result)
}
}
}
}
fun invalidate() {
testCount += 1
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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 package com.github.shadowsocks.utils
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="enc_method_entry" translatable="false">
<item>RC4-MD5</item>
<item>AES-128-CFB</item>
<item>AES-192-CFB</item>
<item>AES-256-CFB</item>
<item>AES-128-CTR</item>
<item>AES-192-CTR</item>
<item>AES-256-CTR</item>
<item>BF-CFB</item>
<item>CAMELLIA-128-CFB</item>
<item>CAMELLIA-192-CFB</item>
<item>CAMELLIA-256-CFB</item>
<item>SALSA20</item>
<item>CHACHA20</item>
<item>CHACHA20-IETF</item>
<item>AES-128-GCM</item>
<item>AES-192-GCM</item>
<item>AES-256-GCM</item>
<item>CHACHA20-IETF-POLY1305</item>
<item>XCHACHA20-IETF-POLY1305</item>
</string-array>
<string-array name="enc_method_value" translatable="false">
<item>rc4-md5</item>
<item>aes-128-cfb</item>
<item>aes-192-cfb</item>
<item>aes-256-cfb</item>
<item>aes-128-ctr</item>
<item>aes-192-ctr</item>
<item>aes-256-ctr</item>
<item>bf-cfb</item>
<item>camellia-128-cfb</item>
<item>camellia-192-cfb</item>
<item>camellia-256-cfb</item>
<item>salsa20</item>
<item>chacha20</item>
<item>chacha20-ietf</item>
<item>aes-128-gcm</item>
<item>aes-192-gcm</item>
<item>aes-256-gcm</item>
<item>chacha20-ietf-poly1305</item>
<item>xchacha20-ietf-poly1305</item>
</string-array>
<string-array name="bypass_private_route" translatable="false">
<item>1.0.0.0/8</item>
<item>2.0.0.0/7</item>
<item>4.0.0.0/6</item>
<item>8.0.0.0/7</item>
<item>11.0.0.0/8</item>
<item>12.0.0.0/6</item>
<item>16.0.0.0/4</item>
<item>32.0.0.0/3</item>
<item>64.0.0.0/3</item>
<item>96.0.0.0/6</item>
<item>100.0.0.0/10</item>
<item>100.128.0.0/9</item>
<item>101.0.0.0/8</item>
<item>102.0.0.0/7</item>
<item>104.0.0.0/5</item>
<item>112.0.0.0/10</item>
<item>112.64.0.0/11</item>
<item>112.96.0.0/12</item>
<item>112.112.0.0/13</item>
<item>112.120.0.0/14</item>
<item>112.124.0.0/19</item>
<item>112.124.32.0/21</item>
<item>112.124.40.0/22</item>
<item>112.124.44.0/23</item>
<item>112.124.46.0/24</item>
<item>112.124.48.0/20</item>
<item>112.124.64.0/18</item>
<item>112.124.128.0/17</item>
<item>112.125.0.0/16</item>
<item>112.126.0.0/15</item>
<item>112.128.0.0/9</item>
<item>113.0.0.0/8</item>
<item>114.0.0.0/10</item>
<item>114.64.0.0/11</item>
<item>114.96.0.0/12</item>
<item>114.112.0.0/15</item>
<item>114.114.0.0/18</item>
<item>114.114.64.0/19</item>
<item>114.114.96.0/20</item>
<item>114.114.112.0/23</item>
<item>114.114.115.0/24</item>
<item>114.114.116.0/22</item>
<item>114.114.120.0/21</item>
<item>114.114.128.0/17</item>
<item>114.115.0.0/16</item>
<item>114.116.0.0/14</item>
<item>114.120.0.0/13</item>
<item>114.128.0.0/9</item>
<item>115.0.0.0/8</item>
<item>116.0.0.0/6</item>
<item>120.0.0.0/6</item>
<item>124.0.0.0/7</item>
<item>126.0.0.0/8</item>
<item>128.0.0.0/3</item>
<item>160.0.0.0/5</item>
<item>168.0.0.0/8</item>
<item>169.0.0.0/9</item>
<item>169.128.0.0/10</item>
<item>169.192.0.0/11</item>
<item>169.224.0.0/12</item>
<item>169.240.0.0/13</item>
<item>169.248.0.0/14</item>
<item>169.252.0.0/15</item>
<item>169.255.0.0/16</item>
<item>170.0.0.0/7</item>
<item>172.0.0.0/12</item>
<item>172.32.0.0/11</item>
<item>172.64.0.0/10</item>
<item>172.128.0.0/9</item>
<item>173.0.0.0/8</item>
<item>174.0.0.0/7</item>
<item>176.0.0.0/4</item>
<item>192.0.0.8/29</item>
<item>192.0.0.16/28</item>
<item>192.0.0.32/27</item>
<item>192.0.0.64/26</item>
<item>192.0.0.128/25</item>
<item>192.0.1.0/24</item>
<item>192.0.3.0/24</item>
<item>192.0.4.0/22</item>
<item>192.0.8.0/21</item>
<item>192.0.16.0/20</item>
<item>192.0.32.0/19</item>
<item>192.0.64.0/18</item>
<item>192.0.128.0/17</item>
<item>192.1.0.0/16</item>
<item>192.2.0.0/15</item>
<item>192.4.0.0/14</item>
<item>192.8.0.0/13</item>
<item>192.16.0.0/12</item>
<item>192.32.0.0/11</item>
<item>192.64.0.0/12</item>
<item>192.80.0.0/13</item>
<item>192.88.0.0/18</item>
<item>192.88.64.0/19</item>
<item>192.88.96.0/23</item>
<item>192.88.98.0/24</item>
<item>192.88.100.0/22</item>
<item>192.88.104.0/21</item>
<item>192.88.112.0/20</item>
<item>192.88.128.0/17</item>
<item>192.89.0.0/16</item>
<item>192.90.0.0/15</item>
<item>192.92.0.0/14</item>
<item>192.96.0.0/11</item>
<item>192.128.0.0/11</item>
<item>192.160.0.0/13</item>
<item>192.169.0.0/16</item>
<item>192.170.0.0/15</item>
<item>192.172.0.0/14</item>
<item>192.176.0.0/12</item>
<item>192.192.0.0/10</item>
<item>193.0.0.0/8</item>
<item>194.0.0.0/7</item>
<item>196.0.0.0/7</item>
<item>198.0.0.0/12</item>
<item>198.16.0.0/15</item>
<item>198.20.0.0/14</item>
<item>198.24.0.0/13</item>
<item>198.32.0.0/12</item>
<item>198.48.0.0/15</item>
<item>198.50.0.0/16</item>
<item>198.51.0.0/18</item>
<item>198.51.64.0/19</item>
<item>198.51.96.0/22</item>
<item>198.51.101.0/24</item>
<item>198.51.102.0/23</item>
<item>198.51.104.0/21</item>
<item>198.51.112.0/20</item>
<item>198.51.128.0/17</item>
<item>198.52.0.0/14</item>
<item>198.56.0.0/13</item>
<item>198.64.0.0/10</item>
<item>198.128.0.0/9</item>
<item>199.0.0.0/8</item>
<item>200.0.0.0/7</item>
<item>202.0.0.0/8</item>
<item>203.0.0.0/18</item>
<item>203.0.64.0/19</item>
<item>203.0.96.0/20</item>
<item>203.0.112.0/24</item>
<item>203.0.114.0/23</item>
<item>203.0.116.0/22</item>
<item>203.0.120.0/21</item>
<item>203.0.128.0/17</item>
<item>203.1.0.0/16</item>
<item>203.2.0.0/15</item>
<item>203.4.0.0/14</item>
<item>203.8.0.0/13</item>
<item>203.16.0.0/12</item>
<item>203.32.0.0/11</item>
<item>203.64.0.0/10</item>
<item>203.128.0.0/9</item>
<item>204.0.0.0/6</item>
<item>208.0.0.0/4</item>
</string-array>
<string-array name="service_modes">
<item>@string/service_mode_proxy</item>
<item>@string/service_mode_vpn</item>
<item>@string/service_mode_transproxy</item>
</string-array>
<string-array name="service_mode_values" translatable="false">
<item>proxy</item>
<item>vpn</item>
<item>transproxy</item>
</string-array>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="auto_connect_summary">@string/auto_connect_summary_v24</string> <color name="ic_launcher_background">#7488A1</color>
</resources> </resources>
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
<string name="traffic">%s↑\t%s↓</string> <string name="traffic">%s↑\t%s↓</string>
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s\nReceived: \t%4$s\t↓\t%2$s</string> <string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s\nReceived: \t%4$s\t↓\t%2$s</string>
<string name="speed">%s/s</string> <string name="speed">%s/s</string>
<string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string> <string name="connection_test_testing">Testing…</string>
<string name="connection_test_available">Success: HTTPS handshake took %dms</string> <string name="connection_test_available">Success: HTTPS handshake took %dms</string>
<string name="connection_test_error">Fail to detect internet connection: %s</string> <string name="connection_test_error">Fail to detect internet connection: %s</string>
......
...@@ -5,7 +5,6 @@ import java.util.regex.Pattern ...@@ -5,7 +5,6 @@ import java.util.regex.Pattern
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
def getCurrentFlavor() { def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString() String task = getGradle().getStartParameter().getTaskRequests().toString()
...@@ -28,11 +27,6 @@ android { ...@@ -28,11 +27,6 @@ android {
testApplicationId "com.github.shadowsocks.test" testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW" resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
buildTypes { buildTypes {
debug { debug {
...@@ -61,24 +55,14 @@ android { ...@@ -61,24 +55,14 @@ android {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "android.arch.work:work-runtime-ktx:$workVersion"
implementation "androidx.browser:browser:$androidxVersion" implementation "androidx.browser:browser:$androidxVersion"
implementation "androidx.room:room-runtime:$roomVersion"
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4'
implementation 'com.google.android.gms:play-services-vision:15.0.2' implementation 'com.google.android.gms:play-services-vision:15.0.2'
implementation 'com.google.firebase:firebase-ads:15.0.1' implementation 'com.google.firebase:firebase-ads:15.0.1'
implementation 'com.google.firebase:firebase-config:16.0.0'
implementation 'com.google.firebase:firebase-core:16.0.0'
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion" implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion"
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'eu.chainfire:libsuperuser:1.0.0.201704021214'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.room:room-testing:$roomVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "android.arch.work:work-testing:$workVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
} }
......
...@@ -21,10 +21,6 @@ ...@@ -21,10 +21,6 @@
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontwarn com.j256.ormlite.**
-dontwarn com.google.android.gms.internal.** -dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**
-dontwarn org.xbill.**
-dontwarn com.evernote.android.job.gcm.**
-dontwarn com.evernote.android.job.util.GcmAvailableHelper
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.github.shadowsocks" package="com.github.shadowsocks">
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
...@@ -13,29 +9,10 @@ ...@@ -13,29 +9,10 @@
android:required="false"/> android:required="false"/>
<uses-feature android:name="android.hardware.nfc" <uses-feature android:name="android.hardware.nfc"
android:required="false"/> android:required="false"/>
<uses-feature android:name="android.software.leanback"
android:required="false"/>
<uses-feature android:name="android.hardware.camera" <uses-feature android:name="android.hardware.camera"
android:required="false"/> android:required="false"/>
<application <application android:name=".App">
android:name=".App"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:backupAgent=".ConfigBackupHelper"
android:fullBackupContent="@xml/backup_descriptor"
android:fullBackupOnly="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:banner="@mipmap/banner">
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<meta-data android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<meta-data android:name="firebase_crashlytics_collection_enabled"
android:value="false"/>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
...@@ -70,13 +47,6 @@ ...@@ -70,13 +47,6 @@
android:label="@string/profile_config" android:label="@string/profile_config"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity
android:name=".VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
<activity <activity
android:name=".AppManager" android:name=".AppManager"
android:label="@string/proxied_apps" android:label="@string/proxied_apps"
...@@ -110,32 +80,6 @@ ...@@ -110,32 +80,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<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">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>
<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" <service android:name=".bg.TileService" android:label="@string/quick_toggle"
android:process=":bg" android:process=":bg"
android:directBootAware="true" android:directBootAware="true"
...@@ -146,16 +90,6 @@ ...@@ -146,16 +90,6 @@
</intent-filter> </intent-filter>
</service> </service>
<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" <receiver android:name=".tasker.ActionListener"
android:process=":bg" android:process=":bg"
android:directBootAware="true" android:directBootAware="true"
...@@ -164,36 +98,5 @@ ...@@ -164,36 +98,5 @@
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/> <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/work/workmanager/src/main/AndroidManifest.xml -->
<provider android:name="androidx.work.impl.WorkManagerInitializer"
tools:node="remove"/>
<service android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
android:process=":bg"
android:directBootAware="true"/>
<service android:name="androidx.work.impl.background.systemjob.SystemJobService"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
android:process=":bg"
android:directBootAware="true"/>
</application> </application>
</manifest> </manifest>
...@@ -21,156 +21,25 @@ ...@@ -21,156 +21,25 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Application import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.UserManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.getSystemService
import androidx.work.WorkManager
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
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.*
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.takisoft.preferencex.PreferenceFragmentCompat import com.takisoft.preferencex.PreferenceFragmentCompat
import io.fabric.sdk.android.Fabric
import java.io.File
import java.io.IOException
class App : Application() { class App : Application() {
companion object {
lateinit var app: App
private const val TAG = "ShadowsocksApplication"
}
val handler by lazy { Handler(Looper.getMainLooper()) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceStorageApp(this) }
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) }
val info: PackageInfo by lazy { getPackageInfo(packageName) }
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
}
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName,
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() {
val intent = Intent(this, BaseService.serviceClass.java)
if (Build.VERSION.SDK_INT >= 26) startForegroundService(intent) else startService(intent)
}
fun reloadService() = sendBroadcast(Intent(Action.RELOAD))
fun stopService() = sendBroadcast(Intent(Action.CLOSE))
val currentProfile: Profile? get() =
if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId)
fun switchProfile(id: Long): Profile {
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
DataStore.profileId = result.id
return result
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
app = this Core.init(this, MainActivity::class)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java, PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java) BottomSheetPreferenceDialogFragment::class.java)
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
deviceStorage.moveDatabaseFrom(this, Key.DB_PUBLIC)
val old = Acl.getFile(Acl.CUSTOM_RULES, this)
if (old.canRead()) {
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
old.delete()
}
}
Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
FirebaseApp.initializeApp(deviceStorage)
remoteConfig.setDefaults(R.xml.default_configs)
remoteConfig.fetch().addOnCompleteListener {
if (it.isSuccessful) remoteConfig.activateFetched() else {
Log.e(TAG, "Failed to fetch config")
Crashlytics.logException(it.exception)
}
}
WorkManager.initialize(deviceStorage, androidx.work.Configuration.Builder().build())
// handle data restored/crash
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
if (DataStore.tcpFastOpen) TcpFastOpen.enabledAsync(true)
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(deviceStorage.filesDir, file).outputStream().use { output -> input.copyTo(output) }
}
} catch (e: IOException) {
printLog(e)
}
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
}
AppCompatDelegate.setDefaultNightMode(DataStore.nightMode) AppCompatDelegate.setDefaultNightMode(DataStore.nightMode)
updateNotificationChannels()
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
updateNotificationChannels() Core.updateNotificationChannels()
}
private fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = getSystemService<NotificationManager>()!!
nm.createNotificationChannels(listOf(
NotificationChannel("service-vpn", getText(R.string.service_vpn),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-proxy", getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-transproxy", getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW)))
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
}
}
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit): BroadcastReceiver {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
val result = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
callback()
if (onetime) app.unregisterReceiver(this)
}
}
app.registerReceiver(result, filter)
return result
} }
} }
...@@ -42,7 +42,7 @@ import androidx.core.content.getSystemService ...@@ -42,7 +42,7 @@ import androidx.core.content.getSystemService
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core.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.DirectBoot
...@@ -59,7 +59,7 @@ class AppManager : AppCompatActivity() { ...@@ -59,7 +59,7 @@ class AppManager : AppCompatActivity() {
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager) = synchronized(AppManager) { private fun getApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges { if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(AppManager) { synchronized(AppManager) {
receiver = null receiver = null
cachedApps = null cachedApps = null
......
...@@ -24,7 +24,6 @@ import android.os.Build ...@@ -24,7 +24,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
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.DirectBoot
...@@ -43,10 +42,11 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -43,10 +42,11 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
true true
} }
boot.isChecked = BootReceiver.enabled boot.isChecked = BootReceiver.enabled
if (Build.VERSION.SDK_INT >= 24) boot.setSummary(R.string.auto_connect_summary_v24)
val canToggleLocked = findPreference(Key.directBootAware) val canToggleLocked = findPreference(Key.directBootAware)
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue -> if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
if (app.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean() if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true true
} else canToggleLocked.parent!!.removePreference(canToggleLocked) } else canToggleLocked.parent!!.removePreference(canToggleLocked)
...@@ -80,20 +80,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -80,20 +80,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
true true
} }
val listener: (Int) -> Unit = { val listener: (Int) -> Unit = {
when (it) { if (it == BaseService.STOPPED) {
BaseService.IDLE, BaseService.STOPPED -> {
serviceMode.isEnabled = true serviceMode.isEnabled = true
portProxy.isEnabled = true portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
} } else {
else -> {
serviceMode.isEnabled = false serviceMode.isEnabled = false
portProxy.isEnabled = false portProxy.isEnabled = false
portLocalDns.isEnabled = false portLocalDns.isEnabled = false
portTransproxy.isEnabled = false portTransproxy.isEnabled = false
} }
} }
}
listener((activity as MainActivity).state) listener((activity as MainActivity).state)
MainActivity.stateListener = listener MainActivity.stateListener = listener
serviceMode.onPreferenceChangeListener = onServiceModeChange serviceMode.onPreferenceChangeListener = onServiceModeChange
......
...@@ -21,11 +21,9 @@ ...@@ -21,11 +21,9 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.app.PendingIntent
import android.app.UiModeManager import android.app.UiModeManager
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.nfc.NdefMessage import android.nfc.NdefMessage
...@@ -49,7 +47,6 @@ import androidx.core.view.updateLayoutParams ...@@ -49,7 +47,6 @@ import androidx.core.view.updateLayoutParams
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.CustomRulesFragment import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
...@@ -64,7 +61,6 @@ import com.github.shadowsocks.widget.ServiceButton ...@@ -64,7 +61,6 @@ import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPreferenceDataStoreChangeListener, class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPreferenceDataStoreChangeListener,
...@@ -73,9 +69,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -73,9 +69,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
private const val TAG = "ShadowsocksMainActivity" private const val TAG = "ShadowsocksMainActivity"
private const val REQUEST_CONNECT = 1 private const val REQUEST_CONNECT = 1
fun pendingIntent(context: Context) = PendingIntent.getActivity(context, 0,
Intent(context, MainActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
var stateListener: ((Int) -> Unit)? = null var stateListener: ((Int) -> Unit)? = null
} }
...@@ -106,10 +99,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -106,10 +99,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy { override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
object : IShadowsocksServiceCallback.Stub() { object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) { override fun stateChanged(state: Int, profileName: String?, msg: String?) {
app.handler.post { changeState(state, msg, true) } Core.handler.post { changeState(state, msg, true) }
} }
override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
app.handler.post { Core.handler.post {
stats.updateTraffic(txRate, rxRate, txTotal, rxTotal) stats.updateTraffic(txRate, rxRate, txTotal, rxTotal)
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment? val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING) if (state != BaseService.STOPPING)
...@@ -117,7 +110,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -117,7 +110,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
} }
} }
override fun trafficPersisted(profileId: Long) { override fun trafficPersisted(profileId: Long) {
app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) } Core.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) }
} }
} }
} }
...@@ -125,7 +118,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -125,7 +118,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
private fun changeState(state: Int, msg: String? = null, animate: Boolean = false) { private fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, animate) fab.changeState(state, animate)
stats.changeState(state) stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error).format(Locale.ENGLISH, msg)).show() if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
this.state = state this.state = state
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state) stateListener?.invoke(state)
...@@ -136,7 +129,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -136,7 +129,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onServiceDisconnected() = changeState(BaseService.IDLE) override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun binderDied() { override fun binderDied() {
super.binderDied() super.binderDied()
app.handler.post { Core.handler.post {
connection.disconnect() connection.disconnect()
Executable.killAll() Executable.killAll()
connection.connect() connection.connect()
...@@ -146,7 +139,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -146,7 +139,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when { when {
requestCode != REQUEST_CONNECT -> super.onActivityResult(requestCode, resultCode, data) requestCode != REQUEST_CONNECT -> super.onActivityResult(requestCode, resultCode, data)
resultCode == Activity.RESULT_OK -> app.startService() resultCode == Activity.RESULT_OK -> Core.startService()
else -> { else -> {
snackbar().setText(R.string.vpn_permission_denied).show() snackbar().setText(R.string.vpn_permission_denied).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data") Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
...@@ -170,18 +163,18 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -170,18 +163,18 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
fab = findViewById(R.id.fab) fab = findViewById(R.id.fab)
fab.setOnClickListener { fab.setOnClickListener {
when { when {
state == BaseService.CONNECTED -> app.stopService() state == BaseService.CONNECTED -> Core.stopService()
BaseService.usingVpnMode -> { BaseService.usingVpnMode -> {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT) if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null) else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
} }
else -> app.startService() else -> Core.startService()
} }
} }
changeState(BaseService.IDLE) // reset everything to init state changeState(BaseService.IDLE) // reset everything to init state
app.handler.post { connection.connect() } Core.handler.post { connection.connect() }
DataStore.publicStore.registerChangeListener(this) DataStore.publicStore.registerChangeListener(this)
val intent = this.intent val intent = this.intent
...@@ -208,7 +201,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -208,7 +201,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
else -> null else -> null
} }
if (sharedStr.isNullOrEmpty()) return if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, app.currentProfile).toList() val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile).toList()
if (profiles.isEmpty()) { if (profiles.isEmpty()) {
snackbar().setText(R.string.profile_invalid_input).show() snackbar().setText(R.string.profile_invalid_input).show()
return return
...@@ -224,7 +217,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -224,7 +217,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) { when (key) {
Key.serviceMode -> app.handler.post { Key.serviceMode -> Core.handler.post {
connection.disconnect() connection.disconnect()
connection.connect() connection.connect()
} }
...@@ -251,7 +244,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -251,7 +244,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
R.id.profiles -> displayFragment(ProfilesFragment()) R.id.profiles -> displayFragment(ProfilesFragment())
R.id.globalSettings -> displayFragment(GlobalSettingsFragment()) R.id.globalSettings -> displayFragment(GlobalSettingsFragment())
R.id.about -> { R.id.about -> {
app.analytics.logEvent("about", Bundle()) Core.analytics.logEvent("about", Bundle())
displayFragment(AboutFragment()) displayFragment(AboutFragment())
} }
R.id.faq -> { R.id.faq -> {
...@@ -268,7 +261,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -268,7 +261,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
app.remoteConfig.fetch() Core.remoteConfig.fetch()
} }
override fun onStart() { override fun onStart() {
...@@ -305,6 +298,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -305,6 +298,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
DataStore.publicStore.unregisterChangeListener(this) DataStore.publicStore.unregisterChangeListener(this)
connection.disconnect() connection.disconnect()
BackupManager(this).dataChanged() BackupManager(this).dataChanged()
app.handler.removeCallbacksAndMessages(null) Core.handler.removeCallbacksAndMessages(null)
} }
} }
...@@ -33,7 +33,7 @@ import androidx.core.os.bundleOf ...@@ -33,7 +33,7 @@ import androidx.core.os.bundleOf
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
...@@ -98,7 +98,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -98,7 +98,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
} }
pluginConfigure.onPreferenceChangeListener = this pluginConfigure.onPreferenceChangeListener = this
initPlugins() initPlugins()
receiver = app.listenForPackageChanges(false) { initPlugins() } receiver = Core.listenForPackageChanges(false) { initPlugins() }
DataStore.privateStore.registerChangeListener(this) DataStore.privateStore.registerChangeListener(this)
} }
......
...@@ -42,7 +42,6 @@ import androidx.core.content.getSystemService ...@@ -42,7 +42,6 @@ import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.*
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
...@@ -61,7 +60,7 @@ import org.json.JSONArray ...@@ -61,7 +60,7 @@ import org.json.JSONArray
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
companion object { companion object {
/** /**
* used for callback from ProfileManager and stateChanged from MainActivity * used for callback from stateChanged from MainActivity
*/ */
var instance: ProfilesFragment? = null var instance: ProfilesFragment? = null
...@@ -160,7 +159,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -160,7 +159,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
text2.text = ArrayList<String>().apply { text2.text = ArrayList<String>().apply {
if (!item.name.isNullOrEmpty()) this += item.formattedAddress if (!item.name.isNullOrEmpty()) this += item.formattedAddress
val id = PluginConfiguration(item.plugin ?: "").selected val id = PluginConfiguration(item.plugin ?: "").selected
if (id.isNotEmpty()) this += app.getString(R.string.profile_plugin, id) if (id.isNotEmpty()) this += getString(R.string.profile_plugin, id)
}.joinToString("\n") }.joinToString("\n")
val context = requireContext() val context = requireContext()
traffic.text = if (tx <= 0 && rx <= 0) null else getString(R.string.traffic, traffic.text = if (tx <= 0 && rx <= 0) null else getString(R.string.traffic,
...@@ -202,10 +201,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -202,10 +201,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
if (isEnabled) { if (isEnabled) {
val activity = activity as MainActivity val activity = activity as MainActivity
val old = DataStore.profileId val old = DataStore.profileId
app.switchProfile(item.id) Core.switchProfile(item.id)
profilesAdapter.refreshId(old) profilesAdapter.refreshId(old)
itemView.isSelected = true itemView.isSelected = true
if (activity.state == BaseService.CONNECTED) app.reloadService() if (activity.state == BaseService.CONNECTED) Core.reloadService()
} }
} }
...@@ -223,7 +222,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -223,7 +222,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
} }
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() { inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>(), ProfileManager.Listener {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf() internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
private val updated = HashSet<Profile>() private val updated = HashSet<Profile>()
...@@ -237,10 +236,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -237,10 +236,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun getItemCount(): Int = profiles.size override fun getItemCount(): Int = profiles.size
override fun getItemId(position: Int): Long = profiles[position].id override fun getItemId(position: Int): Long = profiles[position].id
fun add(item: Profile) { override fun onAdd(profile: Profile) {
undoManager.flush() undoManager.flush()
val pos = itemCount val pos = itemCount
profiles += item profiles += profile
notifyItemInserted(pos) notifyItemInserted(pos)
} }
...@@ -291,12 +290,12 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -291,12 +290,12 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profiles[index] = ProfileManager.getProfile(id)!! profiles[index] = ProfileManager.getProfile(id)!!
notifyItemChanged(index) notifyItemChanged(index)
} }
fun removeId(id: Long) { override fun onRemove(profileId: Long) {
val index = profiles.indexOfFirst { it.id == id } val index = profiles.indexOfFirst { it.id == profileId }
if (index < 0) return if (index < 0) return
profiles.removeAt(index) profiles.removeAt(index)
notifyItemRemoved(index) notifyItemRemoved(index)
if (id == DataStore.profileId) DataStore.profileId = 0 // switch to null profile if (profileId == DataStore.profileId) DataStore.profileId = 0 // switch to null profile
} }
} }
...@@ -324,7 +323,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -324,7 +323,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.profile_manager_menu) toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (!ProfileManager.isNotEmpty()) DataStore.profileId = ProfileManager.createProfile().id ProfileManager.ensureNotEmpty()
val profilesList = view.findViewById<RecyclerView>(R.id.list) val profilesList = view.findViewById<RecyclerView>(R.id.list)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
profilesList.layoutManager = layoutManager profilesList.layoutManager = layoutManager
...@@ -335,6 +334,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -335,6 +334,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profilesList.itemAnimator = animator profilesList.itemAnimator = animator
profilesList.adapter = profilesAdapter profilesList.adapter = profilesAdapter
instance = this instance = this
ProfileManager.listener = profilesAdapter
undoManager = UndoSnackbarManager(activity as MainActivity, profilesAdapter::undo, profilesAdapter::commit) undoManager = UndoSnackbarManager(activity as MainActivity, profilesAdapter::undo, profilesAdapter::commit)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START or ItemTouchHelper.END) { ItemTouchHelper.START or ItemTouchHelper.END) {
...@@ -369,7 +369,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -369,7 +369,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
R.id.action_import_clipboard -> { R.id.action_import_clipboard -> {
try { try {
val profiles = Profile.findAllUrls(clipboard.primaryClip!!.getItemAt(0).text, app.currentProfile) val profiles = Profile.findAllUrls(clipboard.primaryClip!!.getItemAt(0).text, Core.currentProfile)
.toList() .toList()
if (profiles.isNotEmpty()) { if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) } profiles.forEach { ProfileManager.createProfile(it) }
...@@ -392,7 +392,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -392,7 +392,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
R.id.action_manual_settings -> { R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile( startConfig(ProfileManager.createProfile(
Profile().also { app.currentProfile?.copyFeatureSettingsTo(it) })) Profile().also { Core.currentProfile?.copyFeatureSettingsTo(it) }))
true true
} }
R.id.action_export_clipboard -> { R.id.action_export_clipboard -> {
...@@ -419,7 +419,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -419,7 +419,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
if (resultCode != Activity.RESULT_OK) super.onActivityResult(requestCode, resultCode, data) if (resultCode != Activity.RESULT_OK) super.onActivityResult(requestCode, resultCode, data)
else when (requestCode) { else when (requestCode) {
REQUEST_IMPORT_PROFILES -> { REQUEST_IMPORT_PROFILES -> {
val feature = app.currentProfile val feature = Core.currentProfile
var success = false var success = false
val activity = activity as MainActivity val activity = activity as MainActivity
for (uri in data!!.datas) try { for (uri in data!!.datas) try {
...@@ -477,6 +477,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -477,6 +477,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun onDestroy() { override fun onDestroy() {
instance = null instance = null
ProfileManager.listener = null
super.onDestroy() super.onDestroy()
} }
} }
...@@ -29,7 +29,6 @@ import androidx.core.content.getSystemService ...@@ -29,7 +29,6 @@ import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
...@@ -52,8 +51,8 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Interface { ...@@ -52,8 +51,8 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Interface {
override fun onServiceConnected(service: IShadowsocksService) { override fun onServiceConnected(service: IShadowsocksService) {
when (service.state) { when (service.state) {
BaseService.STOPPED -> app.startService() BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> app.stopService() BaseService.CONNECTED -> Core.stopService()
} }
finish() finish()
} }
......
...@@ -36,7 +36,6 @@ import androidx.appcompat.app.AppCompatActivity ...@@ -36,7 +36,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.util.forEach import androidx.core.util.forEach
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.datas import com.github.shadowsocks.utils.datas
...@@ -97,7 +96,7 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever { ...@@ -97,7 +96,7 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
} }
override fun onRetrieved(barcode: Barcode) = runOnUiThread { override fun onRetrieved(barcode: Barcode) = runOnUiThread {
Profile.findAllUrls(barcode.rawValue, app.currentProfile).forEach { ProfileManager.createProfile(it) } Profile.findAllUrls(barcode.rawValue, Core.currentProfile).forEach { ProfileManager.createProfile(it) }
onSupportNavigateUp() onSupportNavigateUp()
} }
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false) override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false)
...@@ -128,7 +127,7 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever { ...@@ -128,7 +127,7 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) { REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
val feature = app.currentProfile val feature = Core.currentProfile
var success = false var success = false
for (uri in data!!.datas) try { for (uri in data!!.datas) try {
detector.detect(Frame.Builder().setBitmap(contentResolver.openBitmap(uri)).build()) detector.detect(Frame.Builder().setBitmap(contentResolver.openBitmap(uri)).build())
......
...@@ -37,7 +37,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator ...@@ -37,7 +37,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.MainActivity import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment import com.github.shadowsocks.ToolbarFragment
...@@ -338,7 +338,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -338,7 +338,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
} }
private val isEnabled get() = when ((activity as MainActivity).state) { private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> app.currentProfile?.route != Acl.CUSTOM_RULES BaseService.CONNECTED -> Core.currentProfile?.route != Acl.CUSTOM_RULES
BaseService.STOPPED -> true BaseService.STOPPED -> true
else -> false else -> false
} }
......
...@@ -26,7 +26,7 @@ import android.service.quicksettings.Tile ...@@ -26,7 +26,7 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService as BaseTileService import android.service.quicksettings.TileService as BaseTileService
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.ShadowsocksConnection import com.github.shadowsocks.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
...@@ -88,8 +88,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface { ...@@ -88,8 +88,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
private fun toggle() { private fun toggle() {
val service = connection.service ?: return val service = connection.service ?: return
when (service.state) { when (service.state) {
BaseService.STOPPED -> app.startService() BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> app.stopService() BaseService.CONNECTED -> Core.stopService()
} }
} }
} }
...@@ -23,7 +23,7 @@ package com.github.shadowsocks.tasker ...@@ -23,7 +23,7 @@ package com.github.shadowsocks.tasker
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.Core
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
class ActionListener : BroadcastReceiver() { class ActionListener : BroadcastReceiver() {
...@@ -31,12 +31,12 @@ class ActionListener : BroadcastReceiver() { ...@@ -31,12 +31,12 @@ class ActionListener : BroadcastReceiver() {
val settings = Settings.fromIntent(intent) val settings = Settings.fromIntent(intent)
var changed = false var changed = false
if (ProfileManager.getProfile(settings.profileId) != null) { if (ProfileManager.getProfile(settings.profileId) != null) {
app.switchProfile(settings.profileId) Core.switchProfile(settings.profileId)
changed = true changed = true
} }
if (settings.switchOn) { if (settings.switchOn) {
app.startService() Core.startService()
if (changed) app.reloadService() if (changed) Core.reloadService()
} else app.stopService() } else Core.stopService()
} }
} }
...@@ -21,36 +21,26 @@ ...@@ -21,36 +21,26 @@
package com.github.shadowsocks.widget package com.github.shadowsocks.widget
import android.content.Context import android.content.Context
import android.os.SystemClock
import android.text.format.Formatter import android.text.format.Formatter
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.MainActivity import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.utils.HttpsTest
import com.github.shadowsocks.utils.responseLength
import com.github.shadowsocks.utils.thread
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.bottomAppBarStyle) : defStyleAttr: Int = R.attr.bottomAppBarStyle) :
BottomAppBar(context, attrs, defStyleAttr) { BottomAppBar(context, attrs, defStyleAttr) {
private var testCount = 0
private lateinit var statusText: TextView private lateinit var statusText: TextView
private lateinit var txText: TextView private lateinit var txText: TextView
private lateinit var rxText: TextView private lateinit var rxText: TextView
private lateinit var txRateText: TextView private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView private lateinit var rxRateText: TextView
private val tester = HttpsTest(statusText::setText) { (context as MainActivity).snackbar(it).show() }
override fun getBehavior() = object : Behavior() { override fun getBehavior() = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold) val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
...@@ -80,7 +70,7 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? ...@@ -80,7 +70,7 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
}) })
if (state != BaseService.CONNECTED) { if (state != BaseService.CONNECTED) {
updateTraffic(0, 0, 0, 0) updateTraffic(0, 0, 0, 0)
testCount += 1 // suppress previous test messages tester.invalidate()
} }
} }
...@@ -91,44 +81,5 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? ...@@ -91,44 +81,5 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
rxRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, rxRate)) rxRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, rxRate))
} }
/** fun testConnection() = tester.testConnection()
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
*/
fun testConnection() {
++testCount
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
thread("ConnectionTest") {
val url = URL("https", when (app.currentProfile!!.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (BaseService.usingVpnMode) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS,
InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val context = context as MainActivity
val (success, result) = try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L)
Pair(true, context.getString(R.string.connection_test_available, elapsed))
else throw IOException(context.getString(R.string.connection_test_error_status_code, code))
} catch (e: IOException) {
Pair(false, context.getString(R.string.connection_test_error, e.message))
} finally {
conn.disconnect()
}
if (testCount == id) app.handler.post {
if (success) statusText.text = result else {
statusText.setText(R.string.connection_test_fail)
context.snackbar(result).show()
}
}
}
}
} }
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string-array name="enc_method_entry" translatable="false">
<item>RC4-MD5</item>
<item>AES-128-CFB</item>
<item>AES-192-CFB</item>
<item>AES-256-CFB</item>
<item>AES-128-CTR</item>
<item>AES-192-CTR</item>
<item>AES-256-CTR</item>
<item>BF-CFB</item>
<item>CAMELLIA-128-CFB</item>
<item>CAMELLIA-192-CFB</item>
<item>CAMELLIA-256-CFB</item>
<item>SALSA20</item>
<item>CHACHA20</item>
<item>CHACHA20-IETF</item>
<item>AES-128-GCM</item>
<item>AES-192-GCM</item>
<item>AES-256-GCM</item>
<item>CHACHA20-IETF-POLY1305</item>
<item>XCHACHA20-IETF-POLY1305</item>
</string-array>
<string-array name="enc_method_value" translatable="false">
<item>rc4-md5</item>
<item>aes-128-cfb</item>
<item>aes-192-cfb</item>
<item>aes-256-cfb</item>
<item>aes-128-ctr</item>
<item>aes-192-ctr</item>
<item>aes-256-ctr</item>
<item>bf-cfb</item>
<item>camellia-128-cfb</item>
<item>camellia-192-cfb</item>
<item>camellia-256-cfb</item>
<item>salsa20</item>
<item>chacha20</item>
<item>chacha20-ietf</item>
<item>aes-128-gcm</item>
<item>aes-192-gcm</item>
<item>aes-256-gcm</item>
<item>chacha20-ietf-poly1305</item>
<item>xchacha20-ietf-poly1305</item>
</string-array>
<string-array name="route_entry"> <string-array name="route_entry">
<item>@string/route_entry_all</item> <item>@string/route_entry_all</item>
<item>@string/route_entry_bypass_lan</item> <item>@string/route_entry_bypass_lan</item>
...@@ -63,180 +19,12 @@ ...@@ -63,180 +19,12 @@
<item>custom-rules</item> <item>custom-rules</item>
</string-array> </string-array>
<string-array name="bypass_private_route" translatable="false">
<item>1.0.0.0/8</item>
<item>2.0.0.0/7</item>
<item>4.0.0.0/6</item>
<item>8.0.0.0/7</item>
<item>11.0.0.0/8</item>
<item>12.0.0.0/6</item>
<item>16.0.0.0/4</item>
<item>32.0.0.0/3</item>
<item>64.0.0.0/3</item>
<item>96.0.0.0/6</item>
<item>100.0.0.0/10</item>
<item>100.128.0.0/9</item>
<item>101.0.0.0/8</item>
<item>102.0.0.0/7</item>
<item>104.0.0.0/5</item>
<item>112.0.0.0/10</item>
<item>112.64.0.0/11</item>
<item>112.96.0.0/12</item>
<item>112.112.0.0/13</item>
<item>112.120.0.0/14</item>
<item>112.124.0.0/19</item>
<item>112.124.32.0/21</item>
<item>112.124.40.0/22</item>
<item>112.124.44.0/23</item>
<item>112.124.46.0/24</item>
<item>112.124.48.0/20</item>
<item>112.124.64.0/18</item>
<item>112.124.128.0/17</item>
<item>112.125.0.0/16</item>
<item>112.126.0.0/15</item>
<item>112.128.0.0/9</item>
<item>113.0.0.0/8</item>
<item>114.0.0.0/10</item>
<item>114.64.0.0/11</item>
<item>114.96.0.0/12</item>
<item>114.112.0.0/15</item>
<item>114.114.0.0/18</item>
<item>114.114.64.0/19</item>
<item>114.114.96.0/20</item>
<item>114.114.112.0/23</item>
<item>114.114.115.0/24</item>
<item>114.114.116.0/22</item>
<item>114.114.120.0/21</item>
<item>114.114.128.0/17</item>
<item>114.115.0.0/16</item>
<item>114.116.0.0/14</item>
<item>114.120.0.0/13</item>
<item>114.128.0.0/9</item>
<item>115.0.0.0/8</item>
<item>116.0.0.0/6</item>
<item>120.0.0.0/6</item>
<item>124.0.0.0/7</item>
<item>126.0.0.0/8</item>
<item>128.0.0.0/3</item>
<item>160.0.0.0/5</item>
<item>168.0.0.0/8</item>
<item>169.0.0.0/9</item>
<item>169.128.0.0/10</item>
<item>169.192.0.0/11</item>
<item>169.224.0.0/12</item>
<item>169.240.0.0/13</item>
<item>169.248.0.0/14</item>
<item>169.252.0.0/15</item>
<item>169.255.0.0/16</item>
<item>170.0.0.0/7</item>
<item>172.0.0.0/12</item>
<item>172.32.0.0/11</item>
<item>172.64.0.0/10</item>
<item>172.128.0.0/9</item>
<item>173.0.0.0/8</item>
<item>174.0.0.0/7</item>
<item>176.0.0.0/4</item>
<item>192.0.0.8/29</item>
<item>192.0.0.16/28</item>
<item>192.0.0.32/27</item>
<item>192.0.0.64/26</item>
<item>192.0.0.128/25</item>
<item>192.0.1.0/24</item>
<item>192.0.3.0/24</item>
<item>192.0.4.0/22</item>
<item>192.0.8.0/21</item>
<item>192.0.16.0/20</item>
<item>192.0.32.0/19</item>
<item>192.0.64.0/18</item>
<item>192.0.128.0/17</item>
<item>192.1.0.0/16</item>
<item>192.2.0.0/15</item>
<item>192.4.0.0/14</item>
<item>192.8.0.0/13</item>
<item>192.16.0.0/12</item>
<item>192.32.0.0/11</item>
<item>192.64.0.0/12</item>
<item>192.80.0.0/13</item>
<item>192.88.0.0/18</item>
<item>192.88.64.0/19</item>
<item>192.88.96.0/23</item>
<item>192.88.98.0/24</item>
<item>192.88.100.0/22</item>
<item>192.88.104.0/21</item>
<item>192.88.112.0/20</item>
<item>192.88.128.0/17</item>
<item>192.89.0.0/16</item>
<item>192.90.0.0/15</item>
<item>192.92.0.0/14</item>
<item>192.96.0.0/11</item>
<item>192.128.0.0/11</item>
<item>192.160.0.0/13</item>
<item>192.169.0.0/16</item>
<item>192.170.0.0/15</item>
<item>192.172.0.0/14</item>
<item>192.176.0.0/12</item>
<item>192.192.0.0/10</item>
<item>193.0.0.0/8</item>
<item>194.0.0.0/7</item>
<item>196.0.0.0/7</item>
<item>198.0.0.0/12</item>
<item>198.16.0.0/15</item>
<item>198.20.0.0/14</item>
<item>198.24.0.0/13</item>
<item>198.32.0.0/12</item>
<item>198.48.0.0/15</item>
<item>198.50.0.0/16</item>
<item>198.51.0.0/18</item>
<item>198.51.64.0/19</item>
<item>198.51.96.0/22</item>
<item>198.51.101.0/24</item>
<item>198.51.102.0/23</item>
<item>198.51.104.0/21</item>
<item>198.51.112.0/20</item>
<item>198.51.128.0/17</item>
<item>198.52.0.0/14</item>
<item>198.56.0.0/13</item>
<item>198.64.0.0/10</item>
<item>198.128.0.0/9</item>
<item>199.0.0.0/8</item>
<item>200.0.0.0/7</item>
<item>202.0.0.0/8</item>
<item>203.0.0.0/18</item>
<item>203.0.64.0/19</item>
<item>203.0.96.0/20</item>
<item>203.0.112.0/24</item>
<item>203.0.114.0/23</item>
<item>203.0.116.0/22</item>
<item>203.0.120.0/21</item>
<item>203.0.128.0/17</item>
<item>203.1.0.0/16</item>
<item>203.2.0.0/15</item>
<item>203.4.0.0/14</item>
<item>203.8.0.0/13</item>
<item>203.16.0.0/12</item>
<item>203.32.0.0/11</item>
<item>203.64.0.0/10</item>
<item>203.128.0.0/9</item>
<item>204.0.0.0/6</item>
<item>208.0.0.0/4</item>
</string-array>
<string-array name="acl_rule_templates" translatable="false"> <string-array name="acl_rule_templates" translatable="false">
<item>@string/acl_rule_templates_generic</item> <item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item> <item>@string/acl_rule_templates_domain</item>
<item>@string/acl_rule_online_config</item> <item>@string/acl_rule_online_config</item>
</string-array> </string-array>
<string-array name="service_modes">
<item>@string/service_mode_proxy</item>
<item>@string/service_mode_vpn</item>
<item>@string/service_mode_transproxy</item>
</string-array>
<string-array name="service_mode_values" translatable="false">
<item>proxy</item>
<item>vpn</item>
<item>transproxy</item>
</string-array>
<string-array name="night_modes"> <string-array name="night_modes">
<item>@string/night_mode_system</item> <item>@string/night_mode_system</item>
<item>@string/night_mode_auto</item> <item>@string/night_mode_auto</item>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#7488A1</color>
<color name="background_selected">@color/material_primary_100</color> <color name="background_selected">@color/material_primary_100</color>
<color name="primary_drawable">@android:color/black</color> <color name="primary_drawable">@android:color/black</color>
<color name="background_stat">@color/material_primary_300</color> <color name="background_stat">@color/material_primary_300</color>
......
* 0.2.0: * 0.2.0:
* Refactor to AndroidX. * Refactor to AndroidX;
* No longer depends on preference libraries.
* 0.1.1: * 0.1.1:
* Rewritten in Kotlin; * Rewritten in Kotlin;
* Fix assert not working; * Fix assert not working;
......
...@@ -25,9 +25,7 @@ android { ...@@ -25,9 +25,7 @@ android {
dependencies { dependencies {
api "androidx.core:core-ktx:$androidxVersion" api "androidx.core:core-ktx:$androidxVersion"
api "androidx.preference:preference:$androidxVersion"
api "com.google.android.material:material:$androidxVersion" api "com.google.android.material:material:$androidxVersion"
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
......
include ':mobile', ':core', ':plugin' include ':mobile', ':core', ':plugin', ':tv'
import com.android.build.OutputFile
import java.util.regex.Matcher
import java.util.regex.Pattern
apply plugin: 'com.android.application'
apply plugin: 'io.fabric'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString()
Matcher matcher = Pattern.compile("(assemble|generate)\\w*(Release|Debug)").matcher(task)
if (matcher.find()) return matcher.group(2).toLowerCase() else {
println "Warning: No match found for $task"
return "debug"
}
}
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
applicationId "com.github.shadowsocks.tv"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 100
versionName "1.0"
testApplicationId "com.github.shadowsocks.tv.test"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
}
buildTypes {
debug {
multiDexEnabled true
pseudoLocalesEnabled true
}
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions.checkReleaseBuilds false
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86'
universalApk true
}
}
sourceSets.main.jniLibs.srcDirs +=
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "src/overture/bin")
}
dependencies {
implementation project(':core')
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.leanback:leanback-preference:$androidxVersion"
implementation 'com.github.bumptech.glide:glide:3.8.0'
}
apply plugin: 'com.google.gms.google-services'
ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3]
if (getCurrentFlavor() == 'release') android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def offset = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (offset != null) output.versionCodeOverride = variant.versionCode + offset
}
}
{
"project_info": {
"project_number": "261400168171",
"firebase_url": "https://admob-app-id-3330146721.firebaseio.com",
"project_id": "admob-app-id-3330146721",
"storage_bucket": "admob-app-id-3330146721.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:261400168171:android:dbdd6331c434162f",
"android_client_info": {
"package_name": "com.github.shadowsocks.tv"
}
},
"oauth_client": [
{
"client_id": "261400168171-sfik8o3pj7e243583olorh7s5974vab1.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.github.shadowsocks.tv",
"certificate_hash": "58a90f84cfe99d4280aec677c9a1292fae131677"
}
},
{
"client_id": "261400168171-g7aelv5bu012ojr7dod7lq09c9anjimh.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCee3fAad7nb3YsxeUO9mqqHFfAvsSCbVs"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable
-dontobfuscate
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-dontwarn okio.**
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.shadowsocks.tv">
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="true"/>
<application
android:name=".App"
android:theme="@style/Theme.Shadowsocks.TV"
tools:replace="theme">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.tv
import android.app.Application
import android.content.res.Configuration
import com.github.shadowsocks.Core
class App : Application() {
override fun onCreate() {
super.onCreate()
Core.init(this, MainActivity::class)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Core.updateNotificationChannels()
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.tv
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.tv
import androidx.core.os.bundleOf
import androidx.leanback.preference.LeanbackPreferenceDialogFragment
import androidx.leanback.preference.LeanbackSettingsFragment
import androidx.preference.*
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.tv.preference.LeanbackSingleListPreferenceDialogFragment
import com.github.shadowsocks.utils.Key
class MainFragment : LeanbackSettingsFragment() {
override fun onPreferenceStartInitialScreen() = startPreferenceFragment(MainPreferenceFragment())
override fun onPreferenceStartScreen(caller: PreferenceFragment?, pref: PreferenceScreen?): Boolean {
onPreferenceStartInitialScreen()
return true
}
override fun onPreferenceStartFragment(caller: PreferenceFragment?, pref: Preference?) = false
override fun onPreferenceDisplayDialog(caller: PreferenceFragment, pref: Preference?): Boolean {
if (pref?.key == Key.id) {
if ((childFragmentManager.findFragmentById(R.id.settings_preference_fragment_container)
as MainPreferenceFragment).state == BaseService.STOPPED) {
startPreferenceFragment(ProfilesDialogFragment().apply {
arguments = bundleOf(Pair(LeanbackPreferenceDialogFragment.ARG_KEY, Key.id))
setTargetFragment(caller, 0)
})
}
return true
}
if (pref is ListPreference && pref !is MultiSelectListPreference) {
startPreferenceFragment(LeanbackSingleListPreferenceDialogFragment().apply {
arguments = bundleOf(Pair(LeanbackPreferenceDialogFragment.ARG_KEY, pref.key))
setTargetFragment(caller, 0)
})
return true
}
return super.onPreferenceDisplayDialog(caller, pref)
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.tv
import android.app.Activity
import android.app.backup.BackupManager
import android.content.Intent
import android.net.VpnService
import android.os.Bundle
import android.text.format.Formatter
import android.util.Log
import android.widget.Toast
import androidx.leanback.preference.LeanbackPreferenceFragment
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core
import com.github.shadowsocks.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.Executable
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.*
import org.json.JSONArray
class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnection.Interface,
OnPreferenceDataStoreChangeListener {
companion object {
private const val REQUEST_CONNECT = 1
private const val REQUEST_IMPORT_PROFILES = 2
private const val REQUEST_EXPORT_PROFILES = 3
private const val TAG = "MainPreferenceFragment"
}
private lateinit var fab: ListPreference
private lateinit var stats: Preference
private lateinit var serviceMode: Preference
private lateinit var portProxy: Preference
private lateinit var portLocalDns: Preference
private lateinit var portTransproxy: Preference
private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false)
Key.modeVpn -> Pair(true, false)
Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue")
}
portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy
true
}
private val tester by lazy {
HttpsTest(stats::setTitle) { Toast.makeText(activity, it, Toast.LENGTH_LONG).show() }
}
// service
var state = BaseService.IDLE
private set
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) = changeState(state, msg)
override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
Core.handler.post {
stats.summary = getString(R.string.stat_summary,
getString(R.string.speed, Formatter.formatFileSize(activity, txRate)),
getString(R.string.speed, Formatter.formatFileSize(activity, rxRate)),
Formatter.formatFileSize(activity, txTotal), Formatter.formatFileSize(activity, rxTotal))
}
}
override fun trafficPersisted(profileId: Long) { }
}
}
private fun changeState(state: Int, msg: String? = null) {
fab.isEnabled = state == BaseService.STOPPED || state == BaseService.CONNECTED
fab.setTitle(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.CONNECTED -> R.string.stop
BaseService.STOPPING -> R.string.stopping
else -> R.string.connect
})
stats.setTitle(R.string.connection_test_pending)
stats.isVisible = state == BaseService.CONNECTED
if (state != BaseService.CONNECTED) {
serviceCallback.trafficUpdated(0, 0, 0, 0, 0)
tester.invalidate()
}
if (msg != null) Toast.makeText(activity, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state
if (state == BaseService.STOPPED) {
serviceMode.isEnabled = true
portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
} else {
serviceMode.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false
portTransproxy.isEnabled = false
}
}
override val listenForDeath: Boolean get() = true
override fun onServiceConnected(service: IShadowsocksService) = changeState(service.state)
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun binderDied() {
super.binderDied()
Core.handler.post {
connection.disconnect()
Executable.killAll()
connection.connect()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_main)
fab = findPreference(Key.id) as ListPreference
populateProfiles()
stats = findPreference(Key.controlStats)
val boot = findPreference(Key.isAutoConnect) as SwitchPreference
boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
boot.isChecked = BootReceiver.enabled
val tfo = findPreference(Key.tfo) as SwitchPreference
tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean) {
val result = TcpFastOpen.enabled(true)
if (result != null && result != "Success.") Toast.makeText(activity, result, Toast.LENGTH_LONG).show()
TcpFastOpen.sendEnabled
} else true
}
if (!TcpFastOpen.supported) {
tfo.isEnabled = false
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
}
serviceMode = findPreference(Key.serviceMode)
portProxy = findPreference(Key.portProxy)
portLocalDns = findPreference(Key.portLocalDns)
portTransproxy = findPreference(Key.portTransproxy)
serviceMode.onPreferenceChangeListener = onServiceModeChange
changeState(BaseService.IDLE) // reset everything to init state
connection.connect()
DataStore.publicStore.registerChangeListener(this)
}
override fun onStart() {
super.onStart()
connection.listeningForBandwidth = true
}
override fun onResume() {
super.onResume()
fab.value = DataStore.profileId.toString()
}
private fun populateProfiles() {
ProfileManager.ensureNotEmpty()
val profiles = ProfileManager.getAllProfiles()!!
fab.entries = profiles.map { it.formattedName }.toTypedArray()
fab.entryValues = profiles.map { it.id.toString() }.toTypedArray()
}
fun startService() {
when {
state != BaseService.STOPPED -> return
BaseService.usingVpnMode -> {
val intent = VpnService.prepare(activity)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
}
else -> Core.startService()
}
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) {
Key.serviceMode -> Core.handler.post {
connection.disconnect()
connection.connect()
}
}
}
override fun onStop() {
connection.listeningForBandwidth = false
super.onStop()
}
override fun onPreferenceTreeClick(preference: Preference?) = when (preference?.key) {
Key.id -> {
if (state == BaseService.CONNECTED) Core.stopService()
true
}
Key.controlStats -> {
tester.testConnection()
true
}
Key.controlImport -> {
startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}, REQUEST_IMPORT_PROFILES)
true
}
Key.controlExport -> {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_TITLE, "profiles.json") // optional title that can be edited
}, REQUEST_EXPORT_PROFILES)
true
}
else -> super.onPreferenceTreeClick(preference)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CONNECT -> if (resultCode == Activity.RESULT_OK) Core.startService() else {
Toast.makeText(activity, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
}
REQUEST_IMPORT_PROFILES -> {
if (resultCode != Activity.RESULT_OK) return
val profiles = ProfileManager.getAllProfiles()?.associateBy { it.formattedAddress }
val feature = profiles?.values?.singleOrNull { it.id == DataStore.profileId }
ProfileManager.clear()
for (uri in data!!.datas) try {
Profile.parseJson(activity.contentResolver.openInputStream(uri)!!.bufferedReader().readText(),
feature).forEach {
ProfileManager.createProfile(it)
// if two profiles has the same address, treat them as the same profile and copy stats over
profiles?.get(it.formattedAddress)?.apply {
it.tx = tx
it.rx = rx
}
}
} catch (e: Exception) {
printLog(e)
}
populateProfiles()
}
REQUEST_EXPORT_PROFILES -> {
if (resultCode != Activity.RESULT_OK) return
val profiles = ProfileManager.getAllProfiles()
if (profiles != null) try {
activity.contentResolver.openOutputStream(data?.data!!)!!.bufferedWriter().use {
it.write(JSONArray(profiles.map { it.toJson() }.toTypedArray()).toString(2))
}
} catch (e: Exception) {
printLog(e)
Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() {
super.onDestroy()
DataStore.publicStore.unregisterChangeListener(this)
connection.disconnect()
BackupManager(activity).dataChanged()
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.tv
import android.os.Bundle
import android.text.format.Formatter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.leanback.preference.LeanbackListPreferenceDialogFragment
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
class ProfilesDialogFragment : LeanbackListPreferenceDialogFragment() {
inner class ProfilesAdapter : RecyclerView.Adapter<ViewHolder>(), ViewHolder.OnItemClickListener {
private val profiles = ProfileManager.getAllProfiles()!!
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(LayoutInflater
.from(parent.context).inflate(R.layout.leanback_list_preference_item_single_2, parent, false), this)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val profile = profiles[position]
holder.widgetView.isChecked = profile.id == DataStore.profileId
holder.titleView.text = profile.formattedName
holder.itemView.findViewById<TextView>(android.R.id.summary).text = ArrayList<String>().apply {
if (!profile.name.isNullOrEmpty()) this += profile.formattedAddress
val id = PluginConfiguration(profile.plugin ?: "").selected
if (id.isNotEmpty()) this += getString(R.string.profile_plugin, id)
if (profile.tx > 0 || profile.rx > 0) this += getString(R.string.traffic,
Formatter.formatFileSize(activity, profile.tx), Formatter.formatFileSize(activity, profile.rx))
}.joinToString("\n")
}
override fun getItemCount() = profiles.size
override fun onItemClick(viewHolder: ViewHolder) {
val index = viewHolder.adapterPosition
if (index == RecyclerView.NO_POSITION) return
Core.switchProfile(profiles[index].id)
(targetFragment as MainPreferenceFragment).startService()
fragmentManager.popBackStack()
notifyDataSetChanged()
}
val selectedIndex = profiles.indexOfFirst { it.id == DataStore.profileId }
}
override fun onCreateAdapter() = ProfilesAdapter()
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {
return super.onCreateView(inflater, container, savedInstanceState)!!.also {
val list = it.findViewById<RecyclerView>(android.R.id.list)
list.layoutManager!!.scrollToPosition((list.adapter as ProfilesAdapter).selectedIndex)
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.tv.preference
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.leanback.preference.LeanbackListPreferenceDialogFragment
import androidx.recyclerview.widget.RecyclerView
/**
* Fix: scroll to selected item.
*/
open class LeanbackSingleListPreferenceDialogFragment : LeanbackListPreferenceDialogFragment() {
companion object {
private val mEntryValues = LeanbackListPreferenceDialogFragment::class.java
.getDeclaredField("mEntryValues").apply { isAccessible = true }
private val mInitialSelection = LeanbackListPreferenceDialogFragment::class.java
.getDeclaredField("mInitialSelection").apply { isAccessible = true }
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {
val selected = mInitialSelection.get(this) as? String
val index = (mEntryValues.get(this) as? Array<CharSequence?>)?.indexOfFirst { it == selected }
return super.onCreateView(inflater, container, savedInstanceState)!!.also {
if (index != null) it.findViewById<RecyclerView>(android.R.id.list).layoutManager!!.scrollToPosition(index)
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_browse_fragment"
android:name="com.github.shadowsocks.tv.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:deviceIds="tv"
tools:ignore="MergeRootFrame"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- Source: https://android.googlesource.com/platform/frameworks/support/+/b80f425/leanback-preference/src/main/res/layout/leanback_list_preference_item_single.xml -->
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" >
<RadioButton
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
style="?android:attr/radioButtonStyle"
android:background="@null"
android:focusable="false"
android:clickable="false"
android:layout_marginEnd="@dimen/lb_preference_item_icon_margin_end"
android:layout_gravity="center_vertical" />
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:orientation="vertical">
<Space android:layout_width="0dp" android:layout_height="@dimen/lb_preference_item_text_space_top" />
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/lb_preference_item_primary_text_margin_bottom"
android:duplicateParentState="true"
android:textAppearance="?android:attr/textAppearanceListItem"/>
<com.github.shadowsocks.widget.AutoCollapseTextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textSize="@dimen/lb_preference_item_secondary_text_size"/>
<Space android:layout_width="0dp" android:layout_height="@dimen/lb_preference_item_text_space_bottom" />
</LinearLayout>
</LinearLayout>
<resources>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Shadowsocks.TV" parent="@style/Theme.Leanback">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/color_primary</item>
<item name="colorPrimaryDark">@color/color_primary_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Leanback</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/app_name">
<!-- profileId is stored in privateStore so its value needs to be populated manually -->
<ListPreference
android:key="profileId"
android:title="@string/connect"
android:summary="%s"
android:persistent="false"/>
<Preference
android:key="control.stats"
android:title="@string/connection_test_pending"
android:summary="@string/stat_summary"/>
<Preference
android:key="control.import"
android:title="@string/action_import_file"/>
<Preference
android:key="control.export"
android:title="@string/action_export_file"/>
<PreferenceCategory
android:title="@string/settings">
<SwitchPreference
android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference
android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/advanced">
<ListPreference
android:key="serviceMode"
android:entries="@array/service_modes"
android:entryValues="@array/service_mode_values"
android:defaultValue="vpn"
android:summary="%s"
android:title="@string/service_mode"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portProxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:title="@string/port_proxy"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portLocalDns"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:title="@string/port_local_dns"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portTransproxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:title="@string/port_transproxy"/>
</PreferenceCategory>
</PreferenceScreen>
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