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
core/src/overture/src/github.com/miekg
core/src/overture/src/golang.org
# work in progress
tv/
# release apks
*.apk
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
buildToolsVersion rootProject.buildToolsVersion
......@@ -20,6 +22,11 @@ android {
arguments "-j${Runtime.runtime.availableProcessors()}"
}
}
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
......@@ -56,15 +63,26 @@ task goClean(type: Exec) {
}
tasks.whenTaskAdded { task ->
if ((task.name == 'generateJsonModelDebug' ||
task.name == 'generateJsonModelRelease')) {
if ((task.name == 'generateJsonModelDebug' || task.name == 'generateJsonModelRelease')) {
task.dependsOn(goBuild)
}
}
dependencies {
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"
androidTestImplementation "android.arch.work:work-testing:$workVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
}
<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.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>
......@@ -25,7 +25,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.preference.DataStore
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
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 @@
package com.github.shadowsocks
import android.app.Fragment
import android.content.ComponentName
import android.content.Context
import android.content.Intent
......@@ -40,7 +41,7 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
interface Interface : IBinder.DeathRecipient {
val serviceCallback: IShadowsocksServiceCallback? get() = null
val connection: ShadowsocksConnection
get() = connections.getOrPut(this, { ShadowsocksConnection(this) })
get() = connections.getOrPut(this) { ShadowsocksConnection(this) }
val listenForDeath get() = false
fun onServiceConnected(service: IShadowsocksService) { }
......@@ -56,6 +57,12 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
private var connectionActive = false
private var callbackRegistered = false
private var binder: IBinder? = null
private val context by lazy {
when (instance) {
is Fragment -> instance.activity
else -> instance as Context
}
}
var listeningForBandwidth = false
set(value) {
......@@ -98,15 +105,15 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
fun connect() {
if (connectionActive) return
connectionActive = true
val intent = Intent(instance as Context, BaseService.serviceClass.java).setAction(Action.SERVICE)
instance.bindService(intent, this, Context.BIND_AUTO_CREATE)
val intent = Intent(context, BaseService.serviceClass.java).setAction(Action.SERVICE)
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
fun disconnect() {
unregisterCallback()
instance.onServiceDisconnected()
if (connectionActive) try {
(instance as Context).unbindService(this)
context.unbindService(this)
} catch (_: IllegalArgumentException) { } // ignore
connectionActive = false
if (instance.listenForDeath) binder?.unlinkToDeath(instance, 0)
......
......@@ -31,9 +31,9 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.core.R
import com.github.shadowsocks.utils.broadcastReceiver
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
......@@ -63,7 +63,7 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface
}
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()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
}
......
......@@ -24,7 +24,7 @@ import android.content.Context
import android.util.Log
import androidx.recyclerview.widget.SortedList
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.utils.Subnet
import com.github.shadowsocks.utils.asIterable
......@@ -46,7 +46,7 @@ class Acl {
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
get() {
......
......@@ -33,12 +33,13 @@ import android.util.Log
import androidx.core.os.UserManagerCompat
import androidx.core.os.bundleOf
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncer
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
......@@ -109,7 +110,7 @@ object BaseService {
t.schedule(object : TimerTask() {
override fun run() {
val profile = profile ?: return
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post {
if (state == CONNECTED && TrafficMonitor.updateRate()) Core.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate
......@@ -157,7 +158,7 @@ object BaseService {
profile.tx += tx
profile.rx += rx
ProfileManager.updateProfile(profile)
app.handler.post {
Core.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) {
......@@ -196,7 +197,7 @@ object BaseService {
}
// sensitive Shadowsocks config is stored in
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 {
shadowsocksConfigFile = this
writeText(config.toString())
......@@ -210,7 +211,7 @@ object BaseService {
fun changeState(s: Int, msg: String? = null) {
if (state == s && msg == null) return
if (callbacks.registeredCallbackCount > 0) app.handler.post {
if (callbacks.registeredCallbackCount > 0) Core.handler.post {
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
......@@ -233,7 +234,7 @@ object BaseService {
false
} else true
fun forceLoad() {
val profile = app.currentProfile
val profile = Core.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
if (!checkProfile(profile)) return
val s = data.state
......@@ -288,7 +289,7 @@ object BaseService {
val data = data
data.changeState(STOPPING)
app.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
killProcesses()
......@@ -326,7 +327,7 @@ object BaseService {
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != STOPPED) return Service.START_NOT_STICKY
val profile = app.currentProfile
val profile = Core.currentProfile
this as Context
if (profile == null) {
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
......@@ -353,7 +354,7 @@ object BaseService {
}
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)
......@@ -366,12 +367,12 @@ object BaseService {
.readTimeout(30, TimeUnit.SECONDS)
.build()
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signaturesCompat.first().toByteArray())
mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0)))
.build()
val request = Request.Builder()
.url(app.remoteConfig.getString("proxy_url"))
.url(Core.remoteConfig.getString("proxy_url"))
.post(requestBody)
.build()
......
......@@ -23,7 +23,7 @@ package com.github.shadowsocks.bg
import android.text.TextUtils
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.JniHelper
import java.io.File
import java.io.FileNotFoundException
......
......@@ -24,7 +24,7 @@ import android.os.Build
import android.os.SystemClock
import android.util.Log
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.utils.Commandline
import com.github.shadowsocks.utils.thread
......@@ -47,7 +47,7 @@ class GuardedProcessPool {
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) =
thread("StreamLogger-$cmdName") {
try {
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } }
input.bufferedReader().forEachLine { logger(TAG, it) }
} catch (_: IOException) { } // ignore
}
private fun pushException(ioException: IOException?) {
......@@ -66,7 +66,7 @@ class GuardedProcessPool {
process = ProcessBuilder(cmd)
.redirectErrorStream(true)
.directory(app.deviceStorage.filesDir)
.directory(Core.deviceStorage.filesDir)
.start()
streamLogger(process.inputStream, Log::i)
......
......@@ -20,7 +20,8 @@
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.preference.DataStore
import com.github.shadowsocks.utils.parseNumericAddress
......@@ -51,7 +52,7 @@ object LocalDnsService {
}
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("RedirectIPv6Record", true)
put("DomainBase64Decode", false)
......
......@@ -33,9 +33,9 @@ import android.text.format.Formatter
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.Core
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.core.R
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import java.util.*
......@@ -58,7 +58,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, txRate))
val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, rxRate))
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)))
show()
}
......@@ -73,7 +73,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName)
.setContentIntent(MainActivity.pendingIntent(service))
.setContentIntent(Core.configureIntent(service))
.setSmallIcon(R.drawable.ic_service_active)
private val style = NotificationCompat.BigTextStyle(builder)
private var isVisible = true
......
......@@ -21,7 +21,7 @@
package com.github.shadowsocks.bg
import android.net.LocalSocket
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.printLog
import java.io.File
import java.io.IOException
......@@ -29,7 +29,7 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder
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) {
try {
......
......@@ -22,7 +22,7 @@ package com.github.shadowsocks.bg
import android.app.Service
import android.content.Intent
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.Core
import com.github.shadowsocks.preference.DataStore
import java.io.File
......@@ -50,7 +50,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
}
private fun startRedsocksDaemon() {
File(app.deviceStorage.filesDir, "redsocks.conf").writeText("""base {
File(Core.deviceStorage.filesDir, "redsocks.conf").writeText("""base {
log_debug = off;
log_info = off;
log = stderr;
......
......@@ -28,12 +28,11 @@ import android.net.*
import android.os.Build
import android.os.ParcelFileDescriptor
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.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.VpnRequestActivity
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
......@@ -73,7 +72,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
}
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) {
try {
......@@ -177,7 +176,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private fun startVpn(): Int {
val profile = data.profile!!
val builder = Builder()
.setConfigureIntent(MainActivity.pendingIntent(this))
.setConfigureIntent(Core.configureIntent(this))
.setSession(profile.formattedName)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24)
......@@ -252,7 +251,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
var tries = 0
while (tries < 10) {
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
}
}
......
......@@ -24,7 +24,7 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
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.utils.Key
......
......@@ -171,6 +171,9 @@ class Profile : Serializable {
@Query("DELETE FROM `Profile` WHERE `id` = :id")
fun delete(id: Long): Int
@Query("DELETE FROM `Profile`")
fun deleteAll(): Int
}
@PrimaryKey(autoGenerate = true)
......
......@@ -21,7 +21,6 @@
package com.github.shadowsocks.database
import android.database.sqlite.SQLiteCantOpenDatabaseException
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.printLog
......@@ -33,12 +32,18 @@ import java.sql.SQLException
* to ensure we are in a consistent state.
*/
object ProfileManager {
interface Listener {
fun onAdd(profile: Profile)
fun onRemove(profileId: Long)
}
var listener: Listener? = null
@Throws(SQLException::class)
fun createProfile(profile: Profile = Profile()): Profile {
profile.id = 0
profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0
profile.id = PrivateDatabase.profileDao.create(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile)
listener?.onAdd(profile)
return profile
}
......@@ -61,12 +66,19 @@ object ProfileManager {
@Throws(SQLException::class)
fun delProfile(id: Long) {
check(PrivateDatabase.profileDao.delete(id) == 1)
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
listener?.onRemove(id)
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)
fun isNotEmpty(): Boolean = try {
fun ensureNotEmpty() {
val nonEmpty = try {
PrivateDatabase.profileDao.isNotEmpty()
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
......@@ -74,6 +86,8 @@ object ProfileManager {
printLog(ex)
false
}
if (!nonEmpty) DataStore.profileId = createProfile().id
}
@Throws(IOException::class)
fun getAllProfiles(): List<Profile>? = try {
......
......@@ -23,7 +23,7 @@ package com.github.shadowsocks.database
import androidx.room.Database
import androidx.room.Room
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.utils.Key
......@@ -31,7 +31,7 @@ import com.github.shadowsocks.utils.Key
abstract class PublicDatabase : RoomDatabase() {
companion object {
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()
.addMigrations(
Migration3
......
package com.github.shadowsocks.plugin
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.Core.app
object NoPlugin : Plugin() {
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
import android.util.Log
import androidx.core.os.bundleOf
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.printLog
import com.github.shadowsocks.utils.signaturesCompat
......@@ -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.
*/
val trustedSignatures by lazy {
app.info.signaturesCompat.toSet() +
Core.packageInfo.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
......@@ -91,7 +92,7 @@ object PluginManager {
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges {
if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
......@@ -128,7 +129,7 @@ object PluginManager {
// add other plugin types here
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? {
......@@ -162,7 +163,7 @@ object PluginManager {
var initialized = false
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
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)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
......
......@@ -23,7 +23,8 @@ package com.github.shadowsocks.plugin
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
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
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 packageName: String get() = resolveInfo.resolvePackageName
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
import android.os.Binder
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.PublicDatabase
import com.github.shadowsocks.utils.DirectBoot
......@@ -30,11 +31,21 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.TcpFastOpen
import com.github.shadowsocks.utils.parsePort
object DataStore {
object DataStore : OnPreferenceDataStoreChangeListener {
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
// privateStore will only be used as temp storage for ProfileConfigFragment
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
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
private fun getLocalPort(key: String, default: Int): Int {
......@@ -47,12 +58,9 @@ object DataStore {
var profileId: Long
get() = publicStore.getLong(Key.id) ?: 0
set(value) {
publicStore.putLong(Key.id, value)
if (DataStore.directBootAware) DirectBoot.update()
}
set(value) = publicStore.putLong(Key.id, value)
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)
@AppCompatDelegate.NightMode
val nightMode get() = when (publicStore.getString(Key.nightMode)) {
......@@ -76,6 +84,7 @@ object DataStore {
* Initialize settings that have complicated default values.
*/
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.portLocalDns) == null) portLocalDns = portLocalDns
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
......
......@@ -69,6 +69,11 @@ object Key {
const val tfo = "tcp_fastopen"
const val assetUpdateTime = "assetUpdateTime"
// TV specific values
const val controlStats = "control.stats"
const val controlImport = "control.import"
const val controlExport = "control.export"
}
object Action {
......
......@@ -5,7 +5,8 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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.database.Profile
import com.github.shadowsocks.database.ProfileManager
......@@ -17,7 +18,7 @@ import java.io.ObjectOutputStream
@TargetApi(24)
object DirectBoot : BroadcastReceiver() {
private val file = File(app.deviceStorage.noBackupFilesDir, "directBootProfile")
private val file = File(Core.deviceStorage.noBackupFilesDir, "directBootProfile")
private var registered = false
fun getDeviceProfile(): Profile? = try {
......@@ -26,7 +27,7 @@ object DirectBoot : BroadcastReceiver() {
fun clean() {
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
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"?>
<resources>
<string name="auto_connect_summary">@string/auto_connect_summary_v24</string>
<color name="ic_launcher_background">#7488A1</color>
</resources>
......@@ -19,6 +19,7 @@
<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="speed">%s/s</string>
<string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string>
<string name="connection_test_available">Success: HTTPS handshake took %dms</string>
<string name="connection_test_error">Fail to detect internet connection: %s</string>
......
......@@ -5,7 +5,6 @@ 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()
......@@ -28,11 +27,6 @@ android {
testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
debug {
......@@ -61,24 +55,14 @@ android {
dependencies {
implementation project(':core')
implementation "android.arch.work:work-runtime-ktx:$workVersion"
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.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.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 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.room:room-testing:$roomVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "android.arch.work:work-testing:$workVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
}
......
......@@ -21,10 +21,6 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn com.j256.ormlite.**
-dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-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"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.shadowsocks"
android:installLocation="internalOnly">
package="com.github.shadowsocks">
<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.CAMERA" />
......@@ -13,29 +9,10 @@
android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.software.leanback"
android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
<application
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"/>
<application android:name=".App">
<activity
android:name=".MainActivity"
......@@ -70,13 +47,6 @@
android:label="@string/profile_config"
android:launchMode="singleTask"/>
<activity
android:name=".VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
......@@ -110,32 +80,6 @@
</intent-filter>
</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"
android:process=":bg"
android:directBootAware="true"
......@@ -146,16 +90,6 @@
</intent-filter>
</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"
android:process=":bg"
android:directBootAware="true"
......@@ -164,36 +98,5 @@
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</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>
......@@ -21,156 +21,25 @@
package com.github.shadowsocks
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.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.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.DataStore
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 io.fabric.sdk.android.Fabric
import java.io.File
import java.io.IOException
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() {
super.onCreate()
app = this
Core.init(this, MainActivity::class)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::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)
updateNotificationChannels()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
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
Core.updateNotificationChannels()
}
}
......@@ -42,7 +42,7 @@ import androidx.core.content.getSystemService
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
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.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
......@@ -59,7 +59,7 @@ class AppManager : AppCompatActivity() {
private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges {
if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = null
......
......@@ -24,7 +24,6 @@ import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.SwitchPreference
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
......@@ -43,10 +42,11 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
true
}
boot.isChecked = BootReceiver.enabled
if (Build.VERSION.SDK_INT >= 24) boot.setSummary(R.string.auto_connect_summary_v24)
val canToggleLocked = findPreference(Key.directBootAware)
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
} else canToggleLocked.parent!!.removePreference(canToggleLocked)
......@@ -80,20 +80,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
true
}
val listener: (Int) -> Unit = {
when (it) {
BaseService.IDLE, BaseService.STOPPED -> {
if (it == BaseService.STOPPED) {
serviceMode.isEnabled = true
portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
}
else -> {
} else {
serviceMode.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false
portTransproxy.isEnabled = false
}
}
}
listener((activity as MainActivity).state)
MainActivity.stateListener = listener
serviceMode.onPreferenceChangeListener = onServiceModeChange
......
......@@ -21,11 +21,9 @@
package com.github.shadowsocks
import android.app.Activity
import android.app.PendingIntent
import android.app.UiModeManager
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.nfc.NdefMessage
......@@ -49,7 +47,6 @@ import androidx.core.view.updateLayoutParams
import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceDataStore
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
......@@ -64,7 +61,6 @@ import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import java.util.*
import kotlin.math.roundToInt
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPreferenceDataStoreChangeListener,
......@@ -73,9 +69,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
private const val TAG = "ShadowsocksMainActivity"
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
}
......@@ -106,10 +99,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
object : IShadowsocksServiceCallback.Stub() {
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) {
app.handler.post {
Core.handler.post {
stats.updateTraffic(txRate, rxRate, txTotal, rxTotal)
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING)
......@@ -117,7 +110,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
}
}
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
private fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, animate)
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
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state)
......@@ -136,7 +129,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun binderDied() {
super.binderDied()
app.handler.post {
Core.handler.post {
connection.disconnect()
Executable.killAll()
connection.connect()
......@@ -146,7 +139,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when {
requestCode != REQUEST_CONNECT -> super.onActivityResult(requestCode, resultCode, data)
resultCode == Activity.RESULT_OK -> app.startService()
resultCode == Activity.RESULT_OK -> Core.startService()
else -> {
snackbar().setText(R.string.vpn_permission_denied).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
......@@ -170,18 +163,18 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
fab = findViewById(R.id.fab)
fab.setOnClickListener {
when {
state == BaseService.CONNECTED -> app.stopService()
state == BaseService.CONNECTED -> Core.stopService()
BaseService.usingVpnMode -> {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
}
else -> app.startService()
else -> Core.startService()
}
}
changeState(BaseService.IDLE) // reset everything to init state
app.handler.post { connection.connect() }
Core.handler.post { connection.connect() }
DataStore.publicStore.registerChangeListener(this)
val intent = this.intent
......@@ -208,7 +201,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
else -> null
}
if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, app.currentProfile).toList()
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile).toList()
if (profiles.isEmpty()) {
snackbar().setText(R.string.profile_invalid_input).show()
return
......@@ -224,7 +217,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) {
Key.serviceMode -> app.handler.post {
Key.serviceMode -> Core.handler.post {
connection.disconnect()
connection.connect()
}
......@@ -251,7 +244,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
R.id.profiles -> displayFragment(ProfilesFragment())
R.id.globalSettings -> displayFragment(GlobalSettingsFragment())
R.id.about -> {
app.analytics.logEvent("about", Bundle())
Core.analytics.logEvent("about", Bundle())
displayFragment(AboutFragment())
}
R.id.faq -> {
......@@ -268,7 +261,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onResume() {
super.onResume()
app.remoteConfig.fetch()
Core.remoteConfig.fetch()
}
override fun onStart() {
......@@ -305,6 +298,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
DataStore.publicStore.unregisterChangeListener(this)
connection.disconnect()
BackupManager(this).dataChanged()
app.handler.removeCallbacksAndMessages(null)
Core.handler.removeCallbacksAndMessages(null)
}
}
......@@ -33,7 +33,7 @@ import androidx.core.os.bundleOf
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
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.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
......@@ -98,7 +98,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
}
pluginConfigure.onPreferenceChangeListener = this
initPlugins()
receiver = app.listenForPackageChanges(false) { initPlugins() }
receiver = Core.listenForPackageChanges(false) { initPlugins() }
DataStore.privateStore.registerChangeListener(this)
}
......
......@@ -42,7 +42,6 @@ import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.*
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
......@@ -61,7 +60,7 @@ import org.json.JSONArray
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
companion object {
/**
* used for callback from ProfileManager and stateChanged from MainActivity
* used for callback from stateChanged from MainActivity
*/
var instance: ProfilesFragment? = null
......@@ -160,7 +159,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
text2.text = ArrayList<String>().apply {
if (!item.name.isNullOrEmpty()) this += item.formattedAddress
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")
val context = requireContext()
traffic.text = if (tx <= 0 && rx <= 0) null else getString(R.string.traffic,
......@@ -202,10 +201,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
if (isEnabled) {
val activity = activity as MainActivity
val old = DataStore.profileId
app.switchProfile(item.id)
Core.switchProfile(item.id)
profilesAdapter.refreshId(old)
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 {
}
}
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>(), ProfileManager.Listener {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
private val updated = HashSet<Profile>()
......@@ -237,10 +236,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun getItemCount(): Int = profiles.size
override fun getItemId(position: Int): Long = profiles[position].id
fun add(item: Profile) {
override fun onAdd(profile: Profile) {
undoManager.flush()
val pos = itemCount
profiles += item
profiles += profile
notifyItemInserted(pos)
}
......@@ -291,12 +290,12 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profiles[index] = ProfileManager.getProfile(id)!!
notifyItemChanged(index)
}
fun removeId(id: Long) {
val index = profiles.indexOfFirst { it.id == id }
override fun onRemove(profileId: Long) {
val index = profiles.indexOfFirst { it.id == profileId }
if (index < 0) return
profiles.removeAt(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 {
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!ProfileManager.isNotEmpty()) DataStore.profileId = ProfileManager.createProfile().id
ProfileManager.ensureNotEmpty()
val profilesList = view.findViewById<RecyclerView>(R.id.list)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
profilesList.layoutManager = layoutManager
......@@ -335,6 +334,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profilesList.itemAnimator = animator
profilesList.adapter = profilesAdapter
instance = this
ProfileManager.listener = profilesAdapter
undoManager = UndoSnackbarManager(activity as MainActivity, profilesAdapter::undo, profilesAdapter::commit)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START or ItemTouchHelper.END) {
......@@ -369,7 +369,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R.id.action_import_clipboard -> {
try {
val profiles = Profile.findAllUrls(clipboard.primaryClip!!.getItemAt(0).text, app.currentProfile)
val profiles = Profile.findAllUrls(clipboard.primaryClip!!.getItemAt(0).text, Core.currentProfile)
.toList()
if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) }
......@@ -392,7 +392,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile(
Profile().also { app.currentProfile?.copyFeatureSettingsTo(it) }))
Profile().also { Core.currentProfile?.copyFeatureSettingsTo(it) }))
true
}
R.id.action_export_clipboard -> {
......@@ -419,7 +419,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
if (resultCode != Activity.RESULT_OK) super.onActivityResult(requestCode, resultCode, data)
else when (requestCode) {
REQUEST_IMPORT_PROFILES -> {
val feature = app.currentProfile
val feature = Core.currentProfile
var success = false
val activity = activity as MainActivity
for (uri in data!!.datas) try {
......@@ -477,6 +477,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun onDestroy() {
instance = null
ProfileManager.listener = null
super.onDestroy()
}
}
......@@ -29,7 +29,6 @@ import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService
......@@ -52,8 +51,8 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Interface {
override fun onServiceConnected(service: IShadowsocksService) {
when (service.state) {
BaseService.STOPPED -> app.startService()
BaseService.CONNECTED -> app.stopService()
BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> Core.stopService()
}
finish()
}
......
......@@ -36,7 +36,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.core.util.forEach
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.datas
......@@ -97,7 +96,7 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
}
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()
}
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false)
......@@ -128,7 +127,7 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
val feature = app.currentProfile
val feature = Core.currentProfile
var success = false
for (uri in data!!.datas) try {
detector.detect(Frame.Builder().setBitmap(contentResolver.openBitmap(uri)).build())
......
......@@ -37,7 +37,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
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.R
import com.github.shadowsocks.ToolbarFragment
......@@ -338,7 +338,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
}
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
else -> false
}
......
......@@ -26,7 +26,7 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService as BaseTileService
import androidx.annotation.RequiresApi
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.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService
......@@ -88,8 +88,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
private fun toggle() {
val service = connection.service ?: return
when (service.state) {
BaseService.STOPPED -> app.startService()
BaseService.CONNECTED -> app.stopService()
BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> Core.stopService()
}
}
}
......@@ -23,7 +23,7 @@ package com.github.shadowsocks.tasker
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.ProfileManager
class ActionListener : BroadcastReceiver() {
......@@ -31,12 +31,12 @@ class ActionListener : BroadcastReceiver() {
val settings = Settings.fromIntent(intent)
var changed = false
if (ProfileManager.getProfile(settings.profileId) != null) {
app.switchProfile(settings.profileId)
Core.switchProfile(settings.profileId)
changed = true
}
if (settings.switchOn) {
app.startService()
if (changed) app.reloadService()
} else app.stopService()
Core.startService()
if (changed) Core.reloadService()
} else Core.stopService()
}
}
......@@ -21,36 +21,26 @@
package com.github.shadowsocks.widget
import android.content.Context
import android.os.SystemClock
import android.text.format.Formatter
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.responseLength
import com.github.shadowsocks.utils.thread
import com.github.shadowsocks.utils.HttpsTest
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,
defStyleAttr: Int = R.attr.bottomAppBarStyle) :
BottomAppBar(context, attrs, defStyleAttr) {
private var testCount = 0
private lateinit var statusText: TextView
private lateinit var txText: TextView
private lateinit var rxText: TextView
private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView
private val tester = HttpsTest(statusText::setText) { (context as MainActivity).snackbar(it).show() }
override fun getBehavior() = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
......@@ -80,7 +70,7 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
})
if (state != BaseService.CONNECTED) {
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?
rxRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, rxRate))
}
/**
* 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()
}
}
}
}
fun testConnection() = tester.testConnection()
}
<?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="route_entry">
<item>@string/route_entry_all</item>
<item>@string/route_entry_bypass_lan</item>
......@@ -63,180 +19,12 @@
<item>custom-rules</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="acl_rule_templates" translatable="false">
<item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item>
<item>@string/acl_rule_online_config</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>
<string-array name="night_modes">
<item>@string/night_mode_system</item>
<item>@string/night_mode_auto</item>
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#7488A1</color>
<color name="background_selected">@color/material_primary_100</color>
<color name="primary_drawable">@android:color/black</color>
<color name="background_stat">@color/material_primary_300</color>
......
* 0.2.0:
* Refactor to AndroidX.
* Refactor to AndroidX;
* No longer depends on preference libraries.
* 0.1.1:
* Rewritten in Kotlin;
* Fix assert not working;
......
......@@ -25,9 +25,7 @@ android {
dependencies {
api "androidx.core:core-ktx:$androidxVersion"
api "androidx.preference:preference:$androidxVersion"
api "com.google.android.material:material:$androidxVersion"
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
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