Unverified Commit 77ca235b authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1927 from Mygod/tv

Add support for Android TV
parents 9ac50d9e 6015417f
......@@ -5,10 +5,10 @@ jobs:
docker:
- image: shadowsocks/shadowsocks-android:circleci
environment:
JVM_OPTS: -Xmx3200m
JVM_OPTS: -Xmx5g
resource_class: medium+
steps:
- checkout
- run: git submodule sync
- run: git submodule update --init --recursive
- restore_cache:
key: jars-{{ checksum "build.gradle" }}
......@@ -28,7 +28,19 @@ jobs:
key: go-full-{{ checksum "~/code/.git/modules/core/src/overture/go/HEAD" }}
- store_artifacts:
path: mobile/build/outputs/apk
destination: apk
destination: apk/mobile
- store_artifacts:
path: tv/build/outputs/apk
destination: apk/tv
- store_artifacts:
path: core/build/reports
destination: reports/core
- store_artifacts:
path: mobile/build/reports
destination: reports
destination: reports/mobile
- store_artifacts:
path: plugin/build/reports
destination: reports/plugin
- store_artifacts:
path: tv/build/reports
destination: reports/tv
......@@ -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
......@@ -11,7 +11,7 @@ buildscript {
buildToolsVersion = '28.0.2'
androidxVersion = '1.0.0-rc01'
roomVersion = '2.0.0-rc01'
workVersion = '1.0.0-alpha06'
workVersion = '1.0.0-alpha07'
preferencexVersion = '1.0.0-alpha2'
junitVersion = '4.12'
androidTestVersion = '1.1.0-alpha4'
......@@ -27,7 +27,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-beta05'
classpath 'com.android.tools.build:gradle:3.2.0-rc02'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath 'com.google.gms:google-services:4.1.0'
classpath 'io.fabric.tools:gradle:1.25.4'
......
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')) {
task.dependsOn(goBuild)
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.5'
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"
}
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="ImpliedQuantity" severity="warning" />
<issue id="ExtraTranslation" severity="warning" />
<issue id="MissingDefaultResource" severity="warning" />
<issue id="MissingTranslation" severity="informational" />
</lint>
package com.github.shadowsocks;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.github.shadowsocks.core.test", appContext.getPackageName());
}
}
<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.ACCESS_NETWORK_STATE"/>
<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
......@@ -255,7 +256,7 @@ object BaseService {
val cmd = buildAdditionalArguments(arrayListOf(
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-u",
"-b", "127.0.0.1",
"-b", DataStore.listenAddress,
"-l", DataStore.portProxy.toString(),
"-t", "600",
"-c", data.buildShadowsocksConfig().absolutePath))
......@@ -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()
......@@ -394,6 +395,7 @@ object BaseService {
// Clean up
killProcesses()
// it's hard to resolve DNS on a specific interface so we'll do it here
if (!profile.host.isNumericAddress()) {
thread("BaseService-resolve") {
profile.host = InetAddress.getByName(profile.host).hostAddress ?: ""
......
......@@ -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)
......@@ -79,6 +79,7 @@ class GuardedProcessPool {
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Crashlytics.log(Log.WARN, TAG, "process exit too fast, stop guard: $cmdName")
break
}
}
} catch (_: InterruptedException) {
......
......@@ -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,8 +52,8 @@ object LocalDnsService {
}
fun buildOvertureConfig(file: String) = file.also {
File(app.deviceStorage.filesDir, it).writeText(JSONObject().run {
put("BindAddress", "127.0.0.1:" + DataStore.portLocalDns)
File(Core.deviceStorage.filesDir, it).writeText(JSONObject().run {
put("BindAddress", "${DataStore.listenAddress}:${DataStore.portLocalDns}")
put("RedirectIPv6Record", true)
put("DomainBase64Decode", false)
put("HostsFile", "hosts")
......
......@@ -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
......@@ -42,7 +42,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
private fun startDNSTunnel() {
data.processes.start(listOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
"-t", "10",
"-b", "127.0.0.1",
"-b", DataStore.listenAddress,
"-u",
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
......@@ -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;
......@@ -58,7 +58,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
redirector = iptables;
}
redsocks {
local_ip = 127.0.0.1;
local_ip = ${DataStore.listenAddress};
local_port = ${DataStore.portTransproxy};
ip = 127.0.0.1;
port = ${DataStore.portProxy};
......
......@@ -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)
......@@ -229,7 +228,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"),
"--netif-netmask", "255.255.255.0",
"--socks-server-addr", "127.0.0.1:${DataStore.portProxy}",
"--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}",
"--tunfd", fd.toString(),
"--tunmtu", VPN_MTU.toString(),
"--sock-path", "sock_path",
......@@ -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,18 +66,27 @@ 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 {
PrivateDatabase.profileDao.isNotEmpty()
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
printLog(ex)
false
fun ensureNotEmpty() {
val nonEmpty = try {
PrivateDatabase.profileDao.isNotEmpty()
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
printLog(ex)
false
}
if (!nonEmpty) DataStore.profileId = createProfile().id
}
@Throws(IOException::class)
......
......@@ -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)) {
......@@ -62,6 +70,7 @@ object DataStore {
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
val serviceMode get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
val listenAddress get() = if (publicStore.getBoolean(Key.shareOverLan, false)) "0.0.0.0" else "127.0.0.1"
var portProxy: Int
get() = getLocalPort(Key.portProxy, 1080)
set(value) = publicStore.putString(Key.portProxy, value.toString())
......@@ -76,6 +85,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
......
......@@ -42,6 +42,7 @@ object Key {
const val modeProxy = "proxy"
const val modeVpn = "vpn"
const val modeTransproxy = "transproxy"
const val shareOverLan = "shareOverLan"
const val portProxy = "portProxy"
const val portLocalDns = "portLocalDns"
const val portTransproxy = "portTransproxy"
......@@ -69,6 +70,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
......
Subproject commit d107f9ecdb76d6503bba1288ea2bc16052b62f16
Subproject commit 307088ed4af0c54ac4dd55ff3bfda5133ae67d32
<?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>
......@@ -11,6 +11,7 @@
<string name="service_mode_proxy">Proxy only</string>
<string name="service_mode_vpn">VPN</string>
<string name="service_mode_transproxy">Transproxy</string>
<string name="share_over_lan">Share over LAN</string>
<string name="port_proxy">SOCKS5 proxy port</string>
<string name="port_local_dns">Local DNS port</string>
<string name="port_transproxy">Transproxy port</string>
......@@ -19,6 +20,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>
......@@ -80,6 +82,7 @@
<!-- alert category -->
<string name="profile_empty">Please select a profile</string>
<string name="proxy_empty">Proxy/Password should not be empty</string>
<string name="file_manager_missing">Please install a file manager like MiXplorer</string>
<string name="connect">Connect</string>
<!-- menu category -->
......
......@@ -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"
}
......
......@@ -2,8 +2,4 @@
<lint>
<!-- Check is buggy: https://issuetracker.google.com/issues/112159117 -->
<issue id="AppCompatResource" severity="ignore" />
<issue id="ImpliedQuantity" severity="warning" />
<issue id="ExtraTranslation" severity="warning" />
<issue id="MissingDefaultResource" severity="warning" />
<issue id="MissingTranslation" severity="informational" />
</lint>
......@@ -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">
tools:ignore="MissingLeanbackSupport">
<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 +10,12 @@
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"/>
tools:ignore="MissingTvBanner">
<activity
android:name=".MainActivity"
......@@ -70,13 +50,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 +83,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 +93,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 +101,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,18 +80,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
true
}
val listener: (Int) -> Unit = {
when (it) {
BaseService.IDLE, 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
}
if (it == BaseService.STOPPED) {
tfo.isEnabled = true
serviceMode.isEnabled = true
portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
} else {
tfo.isEnabled = false
serviceMode.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false
portTransproxy.isEnabled = false
}
}
listener((activity as MainActivity).state)
......
......@@ -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)
}
......
......@@ -22,10 +22,7 @@ package com.github.shadowsocks
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.*
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
......@@ -42,7 +39,7 @@ 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.crashlytics.android.Crashlytics
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
......@@ -61,7 +58,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 +157,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 +199,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 +220,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 +234,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 +288,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 +321,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 +332,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 +367,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) }
......@@ -383,7 +381,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
true
}
R.id.action_import_file -> {
startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
......@@ -392,7 +390,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 -> {
......@@ -404,7 +402,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
true
}
R.id.action_export_file -> {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
startFilesForResult(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
......@@ -415,11 +413,23 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
}
private fun startFilesForResult(intent: Intent?, requestCode: Int) {
try {
startActivityForResult(intent, requestCode)
} catch (e: ActivityNotFoundException) {
Crashlytics.logException(e)
(activity as MainActivity).snackbar(getString(R.string.file_manager_missing)).show()
} catch (e: SecurityException) {
Crashlytics.logException(e)
(activity as MainActivity).snackbar(getString(R.string.file_manager_missing)).show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
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 +487,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()
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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