Commit ac57e915 authored by Mygod's avatar Mygod

Merge branch 'master' into q-beta

parents 590f28c8 a2c090f2
github: [Mygod]
# Submitting Issues # Submitting Issues
* Questions and general discussions should go to [Discourse forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android); * Issues can be submitted in two different places:
* Search for your issue here and at the forum and check [FAQ](https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md) before submitting new issues; 1) [The GitHub issue log](https://github.com/shadowsocks/shadowsocks-android/issues)
* Always provide information asked in the template unless you know what you're doing; 2) [The Shadowsocks.org Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android)
* Logcat would also be helpful. * Please search for your issues in both places before submitting new issues.
* If you have any questions, please check the [FAQ](https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md) and the questions submitted at the [Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android).
* General discussions should go to the [Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android).
* Always provide information asked in the template unless you know what you're doing.
* Utilizing Logcat would also be helpful.
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
[![Language: Kotlin](https://img.shields.io/github/languages/top/shadowsocks/shadowsocks-android.svg)](https://github.com/shadowsocks/shadowsocks-android/search?l=kotlin) [![Language: Kotlin](https://img.shields.io/github/languages/top/shadowsocks/shadowsocks-android.svg)](https://github.com/shadowsocks/shadowsocks-android/search?l=kotlin)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a21d48d466644cdbcb57a1889abea5b)](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a21d48d466644cdbcb57a1889abea5b)](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-orange.svg)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-orange.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android?ref=badge_shield)
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a> <a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
for Android & Chrome OS ([beta](https://play.google.com/apps/testing/com.github.shadowsocks)) for Android & Chrome OS ([beta](https://play.google.com/apps/testing/com.github.shadowsocks))
...@@ -34,6 +35,11 @@ sudo chown 3434:3434 build ...@@ -34,6 +35,11 @@ sudo chown 3434:3434 build
docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug" docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug"
``` ```
### CONTRIBUTING
If you are interested in contributing or getting involved with this project, please read the CONTRIBUTING page for more information. The page can be found [here](https://github.com/shadowsocks/shadowsocks-android/blob/master/CONTRIBUTING.md).
### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30) ### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30)
## OPEN SOURCE LICENSES ## OPEN SOURCE LICENSES
...@@ -50,6 +56,9 @@ docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd / ...@@ -50,6 +56,9 @@ docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li> <li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
</ul> </ul>
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android?ref=badge_large)
### LICENSE ### LICENSE
Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>> Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>>
...@@ -66,4 +75,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ...@@ -66,4 +75,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
\ No newline at end of file
...@@ -4,6 +4,7 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,6 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
javaVersion = JavaVersion.VERSION_1_8
kotlinVersion = '1.3.31' kotlinVersion = '1.3.31'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
...@@ -11,8 +12,8 @@ buildscript { ...@@ -11,8 +12,8 @@ buildscript {
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.1' androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1' androidEspressoVersion = '3.1.1'
versionCode = 4070450 versionCode = 4080050
versionName = '4.7.4-nightly' versionName = '4.8.0-nightly'
resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW'] resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW']
} }
...@@ -25,11 +26,11 @@ buildscript { ...@@ -25,11 +26,11 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.4.0' classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath 'io.fabric.tools:gradle:1.28.1' classpath 'io.fabric.tools:gradle:1.29.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
} }
} }
......
...@@ -27,6 +27,11 @@ android { ...@@ -27,6 +27,11 @@ android {
} }
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
externalNativeBuild { externalNativeBuild {
ndkBuild { ndkBuild {
path 'src/main/jni/Android.mk' path 'src/main/jni/Android.mk'
...@@ -46,18 +51,18 @@ def lifecycleVersion = '2.0.0' ...@@ -46,18 +51,18 @@ def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0' def roomVersion = '2.0.0'
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.1.0-alpha05' api 'androidx.preference:preference:1.1.0-alpha05'
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'androidx.work:work-runtime-ktx:2.0.1' api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.crashlytics.sdk.android:crashlytics:2.10.0' api 'com.crashlytics.sdk.android:crashlytics:2.10.1'
api 'com.google.firebase:firebase-config:17.0.0' api 'com.google.firebase:firebase-config:17.0.0'
api 'com.google.firebase:firebase-core:16.0.9' api 'com.google.firebase:firebase-core:16.0.9'
api 'dnsjava:dnsjava:2.1.8' api 'dnsjava:dnsjava:2.1.9'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
api 'org.connectbot.jsocks:jsocks:1.0.0' api 'org.connectbot.jsocks:jsocks:1.0.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.room:room-testing:$roomVersion"
......
...@@ -56,12 +56,23 @@ ...@@ -56,12 +56,23 @@
android:exported="false"> android:exported="false">
</service> </service>
<activity
android:name="com.github.shadowsocks.UrlImportActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
</activity>
<activity <activity
android:name="com.github.shadowsocks.VpnRequestActivity" android:name="com.github.shadowsocks.VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent" android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""/>
android:launchMode="singleTask"/>
<receiver android:name="com.github.shadowsocks.BootReceiver" <receiver android:name="com.github.shadowsocks.BootReceiver"
android:process=":bg" android:process=":bg"
...@@ -70,6 +81,7 @@ ...@@ -70,6 +81,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" /> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
......
...@@ -1928,61 +1928,8 @@ ...@@ -1928,61 +1928,8 @@
(^|\.)goodtv\.com\.tw$ (^|\.)goodtv\.com\.tw$
(^|\.)goodtv\.tv$ (^|\.)goodtv\.tv$
(^|\.)goofind\.com$ (^|\.)goofind\.com$
(^|\.)google\.ae$ (^|\.)google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)google\.am$ (^|\.)googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)google\.as$
(^|\.)google\.at$
(^|\.)google\.az$
(^|\.)google\.ba$
(^|\.)google\.be$
(^|\.)google\.bg$
(^|\.)google\.ca$
(^|\.)google\.calstate\.edu$
(^|\.)google\.cd$
(^|\.)google\.ci$
(^|\.)google\.co\.id$
(^|\.)google\.co\.jp$
(^|\.)google\.co\.kr$
(^|\.)google\.co\.ma$
(^|\.)google\.co\.uk$
(^|\.)google\.com$
(^|\.)google\.de$
(^|\.)google\.dj$
(^|\.)google\.dk$
(^|\.)google\.es$
(^|\.)google\.fi$
(^|\.)google\.fm$
(^|\.)google\.fr$
(^|\.)google\.gg$
(^|\.)google\.gl$
(^|\.)google\.gr$
(^|\.)google\.ie$
(^|\.)google\.is$
(^|\.)google\.it$
(^|\.)google\.jo$
(^|\.)google\.kz$
(^|\.)google\.lv$
(^|\.)google\.mn$
(^|\.)google\.ms$
(^|\.)google\.nl$
(^|\.)google\.no$
(^|\.)google\.nu$
(^|\.)google\.ro$
(^|\.)google\.ru$
(^|\.)google\.rw$
(^|\.)google\.sc$
(^|\.)google\.sh$
(^|\.)google\.sk$
(^|\.)google\.sm$
(^|\.)google\.sn$
(^|\.)google\.tk$
(^|\.)google\.tm$
(^|\.)google\.to$
(^|\.)google\.tt$
(^|\.)google\.vu$
(^|\.)google\.ws$
(^|\.)googleapis\.cn$
(^|\.)googleapis\.com$
(^|\.)googleapps\.com$ (^|\.)googleapps\.com$
(^|\.)googlearth\.com$ (^|\.)googlearth\.com$
(^|\.)googleartproject\.com$ (^|\.)googleartproject\.com$
......
...@@ -25,6 +25,9 @@ import android.content.ComponentName ...@@ -25,6 +25,9 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.UserManager
import androidx.core.content.getSystemService
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
...@@ -40,11 +43,16 @@ class BootReceiver : BroadcastReceiver() { ...@@ -40,11 +43,16 @@ class BootReceiver : BroadcastReceiver() {
} }
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val locked = when (intent.action) { if (!DataStore.persistAcrossReboot) { // sanity check
Intent.ACTION_BOOT_COMPLETED -> false enabled = false
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks return
else -> return
} }
if (DataStore.directBootAware == locked) Core.startService() val doStart = when (intent.action) {
Intent.ACTION_BOOT_COMPLETED -> !DataStore.directBootAware
Intent.ACTION_LOCKED_BOOT_COMPLETED -> DataStore.directBootAware
else -> DataStore.directBootAware ||
Build.VERSION.SDK_INT >= 24 && app.getSystemService<UserManager>()?.isUserUnlocked != false
}
if (doStart) Core.startService()
} }
} }
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 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.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import kotlinx.android.parcel.Parcelize
class UrlImportActivity : AppCompatActivity() {
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes, listener)
setNegativeButton(R.string.no, listener)
setMessage(arg.profiles.joinToString("\n"))
}
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which == DialogInterface.BUTTON_POSITIVE) arg.profiles.forEach { ProfileManager.createProfile(it) }
requireActivity().finish()
}
override fun onDismiss(dialog: DialogInterface) {
requireActivity().finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (val dialog = handleShareIntent()) {
null -> {
Toast.makeText(this, R.string.profile_invalid_input, Toast.LENGTH_SHORT).show()
finish()
}
else -> dialog.show(supportFragmentManager, null)
}
}
private fun handleShareIntent() = intent.data?.toString()?.let { sharedStr ->
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) null else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles))
}
}
...@@ -29,6 +29,7 @@ import android.util.Log ...@@ -29,6 +29,7 @@ import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
...@@ -36,6 +37,7 @@ import com.github.shadowsocks.aidl.IShadowsocksServiceCallback ...@@ -36,6 +37,7 @@ import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
...@@ -75,6 +77,7 @@ object BaseService { ...@@ -75,6 +77,7 @@ object BaseService {
var notification: ServiceNotification? = null var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent -> val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) { when (intent.action) {
Intent.ACTION_SHUTDOWN -> service.persistStats()
Action.RELOAD -> service.forceLoad() Action.RELOAD -> service.forceLoad()
else -> service.stopRunner() else -> service.stopRunner()
} }
...@@ -283,10 +286,16 @@ object BaseService { ...@@ -283,10 +286,16 @@ object BaseService {
data.changeState(State.Stopped, msg) data.changeState(State.Stopped, msg)
// stop the service if nothing has bound to it // stop the service if nothing has bound to it
if (restart) startRunner() else stopSelf() if (restart) startRunner() else {
BootReceiver.enabled = false
stopSelf()
}
} }
} }
fun persistStats() =
listOfNotNull(data.proxy, data.udpFallback).forEach { it.trafficMonitor?.persistStats(it.profile.id) }
suspend fun preInit() { } suspend fun preInit() { }
suspend fun resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host) suspend fun resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host)
suspend fun openConnection(url: URL) = url.openConnection() suspend fun openConnection(url: URL) = url.openConnection()
...@@ -308,6 +317,7 @@ object BaseService { ...@@ -308,6 +317,7 @@ object BaseService {
data.proxy = proxy data.proxy = proxy
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route) data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
BootReceiver.enabled = DataStore.persistAcrossReboot
if (!data.closeReceiverRegistered) { if (!data.closeReceiverRegistered) {
registerReceiver(data.closeReceiver, IntentFilter().apply { registerReceiver(data.closeReceiver, IntentFilter().apply {
addAction(Action.RELOAD) addAction(Action.RELOAD)
......
...@@ -28,17 +28,14 @@ import com.github.shadowsocks.Core ...@@ -28,17 +28,14 @@ import com.github.shadowsocks.Core
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncer import com.github.shadowsocks.acl.AclSyncer
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.disconnectFromMain
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.signaturesCompat import com.github.shadowsocks.utils.signaturesCompat
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.net.UnknownHostException import java.net.UnknownHostException
...@@ -65,15 +62,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -65,15 +62,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.requestMethod = "POST" conn.requestMethod = "POST"
conn.doOutput = true conn.doOutput = true
val proxies = try { val proxies = conn.useCancellable {
withContext(Dispatchers.IO) { outputStream.bufferedWriter().use {
conn.outputStream.bufferedWriter().use { it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
conn.inputStream.bufferedReader().readText()
} }
} finally { inputStream.bufferedReader().readText()
conn.disconnectFromMain()
}.split('|').toMutableList() }.split('|').toMutableList()
proxies.shuffle() proxies.shuffle()
val proxy = proxies.first().split(':') val proxy = proxies.first().split(':')
...@@ -144,22 +137,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -144,22 +137,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
fun shutdown(scope: CoroutineScope) { fun shutdown(scope: CoroutineScope) {
trafficMonitor?.apply { trafficMonitor?.apply {
thread.shutdown(scope) thread.shutdown(scope)
// Make sure update total traffic when stopping the runner persistStats(profile.id) // Make sure update total traffic when stopping the runner
try {
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile(profile.id) ?: return
profile.tx += current.txTotal
profile.rx += current.rxTotal
ProfileManager.updateProfile(profile)
} catch (e: IOException) {
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == profile.id }
profile.tx += current.txTotal
profile.rx += current.rxTotal
profile.dirty = true
DirectBoot.update(profile)
DirectBoot.listenForUnlock()
}
} }
trafficMonitor = null trafficMonitor = null
configFile?.delete() // remove old config possibly in device storage configFile?.delete() // remove old config possibly in device storage
......
...@@ -23,7 +23,10 @@ package com.github.shadowsocks.bg ...@@ -23,7 +23,10 @@ package com.github.shadowsocks.bg
import android.net.LocalSocket import android.net.LocalSocket
import android.os.SystemClock import android.os.SystemClock
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.LocalSocketListener import com.github.shadowsocks.net.LocalSocketListener
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
...@@ -52,6 +55,7 @@ class TrafficMonitor(statFile: File) { ...@@ -52,6 +55,7 @@ class TrafficMonitor(statFile: File) {
var out = TrafficStats() var out = TrafficStats()
private var timestampLast = 0L private var timestampLast = 0L
private var dirty = false private var dirty = false
private var persisted = false
fun requestUpdate(): Pair<TrafficStats, Boolean> { fun requestUpdate(): Pair<TrafficStats, Boolean> {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
...@@ -79,4 +83,24 @@ class TrafficMonitor(statFile: File) { ...@@ -79,4 +83,24 @@ class TrafficMonitor(statFile: File) {
} }
return Pair(out, updated) return Pair(out, updated)
} }
fun persistStats(id: Long) {
check(!persisted) { "Double persisting?" }
persisted = true
try {
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile(id) ?: return
profile.tx += current.txTotal
profile.rx += current.rxTotal
ProfileManager.updateProfile(profile)
} catch (e: IOException) {
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == id }
profile.tx += current.txTotal
profile.rx += current.rxTotal
profile.dirty = true
DirectBoot.update(profile)
DirectBoot.listenForUnlock()
}
}
} }
...@@ -30,8 +30,11 @@ import com.github.shadowsocks.acl.Acl ...@@ -30,8 +30,11 @@ import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.disconnectFromMain import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.Proxy import java.net.Proxy
...@@ -75,7 +78,7 @@ class HttpsTest : ViewModel() { ...@@ -75,7 +78,7 @@ class HttpsTest : ViewModel() {
} }
} }
private var running: Pair<HttpURLConnection, Job>? = null private var running: Job? = null
val status = MutableLiveData<Status>().apply { value = Status.Idle } val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() { fun testConnection() {
...@@ -91,26 +94,25 @@ class HttpsTest : ViewModel() { ...@@ -91,26 +94,25 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false
conn.useCaches = false conn.useCaches = false
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) { running = GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) { status.value = conn.useCancellable {
try { try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = conn.responseCode val code = responseCode
val elapsed = SystemClock.elapsedRealtime() - start val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed) if (code == 204 || code == 200 && responseLength == 0L) Status.Success(elapsed)
else Status.Error.UnexpectedResponseCode(code) else Status.Error.UnexpectedResponseCode(code)
} catch (e: IOException) { } catch (e: IOException) {
Status.Error.IOFailure(e) Status.Error.IOFailure(e)
} finally { } finally {
conn.disconnect() disconnect()
} }
} }
} }
} }
private fun cancelTest() = running?.let { (conn, job) -> private fun cancelTest() {
job.cancel() // ensure job is cancelled before interrupting running?.cancel()
conn.disconnectFromMain()
running = null running = null
} }
......
...@@ -22,6 +22,7 @@ package com.github.shadowsocks.preference ...@@ -22,6 +22,7 @@ package com.github.shadowsocks.preference
import android.os.Binder import android.os.Binder
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.database.PrivateDatabase import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase import com.github.shadowsocks.database.PublicDatabase
...@@ -61,6 +62,8 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -61,6 +62,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var profileId: Long var profileId: Long
get() = publicStore.getLong(Key.id) ?: 0 get() = publicStore.getLong(Key.id) ?: 0
set(value) = publicStore.putLong(Key.id, value) set(value) = publicStore.putLong(Key.id, value)
val persistAcrossReboot get() = publicStore.getBoolean(Key.persistAcrossReboot)
?: BootReceiver.enabled.also { publicStore.putBoolean(Key.persistAcrossReboot, it) }
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && DataStore.publicStore.getBoolean(Key.tfo, true) val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && DataStore.publicStore.getBoolean(Key.tfo, true)
...@@ -101,6 +104,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -101,6 +104,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
* Initialize settings that have complicated default values. * Initialize settings that have complicated default values.
*/ */
fun initGlobal() { fun initGlobal() {
persistAcrossReboot
if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen) if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
......
...@@ -43,7 +43,7 @@ object Key { ...@@ -43,7 +43,7 @@ object Key {
const val route = "route" const val route = "route"
const val isAutoConnect = "isAutoConnect" const val persistAcrossReboot = "isAutoConnect"
const val directBootAware = "directBootAware" const val directBootAware = "directBootAware"
const val proxyApps = "isProxyApps" const val proxyApps = "isProxyApps"
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 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 androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
/**
* See also: https://stackoverflow.com/a/30821062/2245107
*/
object SingleInstanceActivity : DefaultLifecycleObserver {
private val active = mutableSetOf<Class<LifecycleOwner>>()
@MainThread
fun register(activity: ComponentActivity) = if (active.add(activity.javaClass)) apply {
activity.lifecycle.addObserver(this)
} else {
activity.finish()
null
}
override fun onDestroy(owner: LifecycleOwner) {
check(active.remove(owner.javaClass)) { "Double destroy?" }
}
}
...@@ -38,11 +38,10 @@ import androidx.annotation.AttrRes ...@@ -38,11 +38,10 @@ import androidx.annotation.AttrRes
import androidx.core.os.BuildCompat import androidx.core.os.BuildCompat
import androidx.preference.Preference import androidx.preference.Preference
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetAddress import java.net.InetAddress
import kotlin.coroutines.resume
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
...@@ -64,8 +63,13 @@ fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_IN ...@@ -64,8 +63,13 @@ fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_IN
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT >= 24) fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT >= 24)
computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) } computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) }
fun HttpURLConnection.disconnectFromMain() { suspend fun <T> HttpURLConnection.useCancellable(block: HttpURLConnection.() -> T) = withContext(Dispatchers.IO) {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() } suspendCancellableCoroutine<T> { cont ->
cont.invokeOnCancellation {
if (Build.VERSION.SDK_INT >= 26) disconnect() else launch(Dispatchers.IO) { disconnect() }
}
cont.resume(block())
}
} }
fun parsePort(str: String?, default: Int, min: Int = 1025): Int { fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<string name="remote_dns">"DNS Remoto"</string> <string name="remote_dns">"DNS Remoto"</string>
<string name="stat_summary">"Enviado: \t\t\t\t\t%3$s\t↑\t%1$s <string name="stat_summary">"Enviado: \t\t\t\t\t%3$s\t↑\t%1$s
Recibido: \t%4$s\t↓\t%2$s"</string> Recibido: \t%4$s\t↓\t%2$s"</string>
<string name="connection_test_testing">"Probando..."</string> <string name="connection_test_testing">"Probando"</string>
<string name="connection_test_available">"Éxito: El establecimiento de la comunicación HTTPS tomó %dms"</string> <string name="connection_test_available">"Éxito: El establecimiento de la comunicación HTTPS tomó %dms"</string>
<string name="connection_test_error">"Fallo al detectar conexión a Internet: %s"</string> <string name="connection_test_error">"Fallo al detectar conexión a Internet: %s"</string>
<string name="connection_test_fail">"Internet no disponible"</string> <string name="connection_test_fail">"Internet no disponible"</string>
...@@ -38,7 +38,7 @@ Recibido: \t%4$s\t↓\t%2$s"</string> ...@@ -38,7 +38,7 @@ Recibido: \t%4$s\t↓\t%2$s"</string>
<string name="invalid_server">"Nombre de servidor inválido"</string> <string name="invalid_server">"Nombre de servidor inválido"</string>
<string name="service_failed">"Fallo al conectar al servidor remoto"</string> <string name="service_failed">"Fallo al conectar al servidor remoto"</string>
<string name="stop">"Detener"</string> <string name="stop">"Detener"</string>
<string name="stopping">"Apagando..."</string> <string name="stopping">"Apagando"</string>
<string name="reboot_required">"Fallo al iniciar servicio VPN. Puede que tengas que reiniciar tu dispositivo."</string> <string name="reboot_required">"Fallo al iniciar servicio VPN. Puede que tengas que reiniciar tu dispositivo."</string>
<string name="profile_invalid_input">"No se ha encontrado datos de perfil válidos."</string> <string name="profile_invalid_input">"No se ha encontrado datos de perfil válidos."</string>
...@@ -88,19 +88,19 @@ Recibido: \t%4$s\t↓\t%2$s"</string> ...@@ -88,19 +88,19 @@ Recibido: \t%4$s\t↓\t%2$s"</string>
<string name="received">"Recibido:"</string> <string name="received">"Recibido:"</string>
<!-- status --> <!-- status -->
<string name="connecting">"Conectando..."</string> <string name="connecting">"Conectando"</string>
<string name="vpn_connected">"Conectado, presiona para verificar la conexión"</string> <string name="vpn_connected">"Conectado, presiona para verificar la conexión"</string>
<string name="not_connected">"No conectado"</string> <string name="not_connected">"No conectado"</string>
<!-- acl --> <!-- acl -->
<string name="custom_rules">"Reglas personalizadas"</string> <string name="custom_rules">"Reglas personalizadas"</string>
<string name="action_add_rule">"Agregar regla(s)..."</string> <string name="action_add_rule">"Agregar regla(s)"</string>
<string name="edit_rule">"Editar regla"</string> <string name="edit_rule">"Editar regla"</string>
<string name="route_entry_all">"Todo"</string> <string name="route_entry_all">"Todo"</string>
<!-- plugin --> <!-- plugin -->
<string name="plugin">"Complemento"</string> <string name="plugin">"Complemento"</string>
<string name="plugin_configure">"Configurar..."</string> <string name="plugin_configure">"Configurar"</string>
<string name="plugin_disabled">"Deshabilitado"</string> <string name="plugin_disabled">"Deshabilitado"</string>
<string name="plugin_unknown">"Complemento desconocido %s"</string> <string name="plugin_unknown">"Complemento desconocido %s"</string>
<string name="plugin_untrusted">"Precaución: Este complemento no parece venir de una fuente confiable."</string> <string name="plugin_untrusted">"Precaución: Este complemento no parece venir de una fuente confiable."</string>
...@@ -124,17 +124,17 @@ Recibido: \t%4$s\t↓\t%2$s"</string> ...@@ -124,17 +124,17 @@ Recibido: \t%4$s\t↓\t%2$s"</string>
<string name="service_transproxy">"Servicio Transproxy"</string> <string name="service_transproxy">"Servicio Transproxy"</string>
<string name="vpn_permission_denied">"Permiso denegado al crear servicio VPN"</string> <string name="vpn_permission_denied">"Permiso denegado al crear servicio VPN"</string>
<string name="direct_boot_aware_summary">"La información del perfil seleccionado será menos protegida"</string> <string name="direct_boot_aware_summary">"La información del perfil seleccionado será menos protegida"</string>
<string name="action_import_file">"Importar desde archivo..."</string> <string name="action_import_file">"Importar desde archivo"</string>
<string name="night_mode">"Modo nocturno"</string> <string name="night_mode">"Modo nocturno"</string>
<string name="night_mode_system">"Seguir sistema"</string> <string name="night_mode_system">"Seguir sistema"</string>
<string name="night_mode_auto">"Auto"</string> <string name="night_mode_auto">"Auto"</string>
<string name="night_mode_on">"Encendido"</string> <string name="night_mode_on">"Encendido"</string>
<string name="night_mode_off">"Apagado"</string> <string name="night_mode_off">"Apagado"</string>
<string name="send_email">"Enviar email"</string> <string name="send_email">"Enviar email"</string>
<string name="action_export_more">"Exportar..."</string> <string name="action_export_more">"Exportar"</string>
<string name="action_export_file">"Exportar a archivo..."</string> <string name="action_export_file">"Exportar a archivo"</string>
<string name="share_over_lan">"Compartir por LAN"</string> <string name="share_over_lan">"Compartir por LAN"</string>
<string name="connection_test_pending">"Verificar conectividad"</string> <string name="connection_test_pending">"Verificar conectividad"</string>
<string name="file_manager_missing">"Por favor, instala un explorador de archivos como MiXplorer"</string> <string name="file_manager_missing">"Por favor, instala un explorador de archivos como MiXplorer"</string>
<string name="action_replace_file">"Reemplazar desde archivo..."</string> <string name="action_replace_file">"Reemplazar desde archivo"</string>
</resources> </resources>
\ No newline at end of file
...@@ -53,9 +53,7 @@ ...@@ -53,9 +53,7 @@
<string name="bypass_apps">Bypass</string> <string name="bypass_apps">Bypass</string>
<string name="bypass_apps_summary">Enable this option to bypass selected apps</string> <string name="bypass_apps_summary">Enable this option to bypass selected apps</string>
<string name="auto_connect">Auto Connect</string> <string name="auto_connect">Auto Connect</string>
<string name="auto_connect_summary">Enable Shadowsocks on startup</string> <string name="auto_connect_summary">Enable Shadowsocks on startup/app update if it was running before</string>
<string name="auto_connect_summary_v24">Enable Shadowsocks on startup. Recommended to use always-on VPN
instead</string>
<string name="direct_boot_aware">Allow Toggling in Lock Screen</string> <string name="direct_boot_aware">Allow Toggling in Lock Screen</string>
<string name="direct_boot_aware_summary">Your selected profile information will be less protected</string> <string name="direct_boot_aware_summary">Your selected profile information will be less protected</string>
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string> <string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
...@@ -114,7 +112,7 @@ ...@@ -114,7 +112,7 @@
<string name="profile_config">Profile config</string> <string name="profile_config">Profile config</string>
<string name="delete">Remove</string> <string name="delete">Remove</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string> <string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
<string name="share_qr_nfc">QR code/NFC</string> <string name="share_qr_nfc">QR code</string>
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string> <string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string> <string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string> <string name="add_profile_methods_manual_settings">Manual Settings</string>
......
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
...@@ -38,6 +38,10 @@ android { ...@@ -38,6 +38,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*' packagingOptions.exclude '**/*.kotlin_*'
splits { splits {
abi { abi {
...@@ -58,7 +62,7 @@ dependencies { ...@@ -58,7 +62,7 @@ dependencies {
implementation 'androidx.browser:browser:1.0.0' implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.gms:play-services-vision:17.0.2' implementation 'com.google.android.gms:play-services-vision:17.0.2'
implementation 'com.google.firebase:firebase-ads:17.2.0' implementation 'com.google.firebase:firebase-ads:17.2.1'
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0' implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
......
...@@ -3,13 +3,10 @@ ...@@ -3,13 +3,10 @@
package="com.github.shadowsocks" package="com.github.shadowsocks"
tools:ignore="MissingLeanbackSupport"> tools:ignore="MissingLeanbackSupport">
<uses-permission android:name="android.permission.NFC" />
<uses-permission-sdk-23 android:name="android.permission.CAMERA" /> <uses-permission-sdk-23 android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"
android:required="false"/> android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.hardware.camera" <uses-feature android:name="android.hardware.camera"
android:required="false"/> android:required="false"/>
...@@ -21,7 +18,7 @@ ...@@ -21,7 +18,7 @@
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.Navigation" android:theme="@style/Theme.Shadowsocks.Navigation"
android:launchMode="singleTask"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
...@@ -30,17 +27,6 @@ ...@@ -30,17 +27,6 @@
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" <meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/> android:resource="@xml/shortcuts"/>
</activity> </activity>
...@@ -48,22 +34,19 @@ ...@@ -48,22 +34,19 @@
<activity <activity
android:name=".ProfileConfigActivity" android:name=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:label="@string/profile_config" android:label="@string/profile_config"/>
android:launchMode="singleTask"/>
<activity <activity
android:name=".AppManager" android:name=".AppManager"
android:label="@string/proxied_apps" android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"/>
android:launchMode="singleTask"/>
<activity <activity
android:name=".UdpFallbackProfileActivity" android:name=".UdpFallbackProfileActivity"
android:label="@string/udp_fallback" android:label="@string/udp_fallback"
android:parentActivityName=".ProfileConfigActivity" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"/>
android:launchMode="singleTask"/>
<activity <activity
android:name=".ScannerActivity" android:name=".ScannerActivity"
...@@ -84,8 +67,7 @@ ...@@ -84,8 +67,7 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:process=":bg" android:process=":bg">
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" /> <action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter> </intent-filter>
......
...@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.* import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.* import kotlinx.android.synthetic.main.layout_apps_item.view.*
...@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() { ...@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_apps) setContentView(R.layout.layout_apps)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
......
...@@ -50,13 +50,10 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -50,13 +50,10 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
preferenceManager.preferenceDataStore = DataStore.publicStore preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal() DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global) addPreferencesFromResource(R.xml.pref_global)
val boot = findPreference<SwitchPreference>(Key.isAutoConnect)!! findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
true true
} }
boot.isChecked = BootReceiver.enabled
if (Build.VERSION.SDK_INT >= 24) boot.setSummary(R.string.auto_connect_summary_v24)
val canToggleLocked = findPreference<Preference>(Key.directBootAware)!! val canToggleLocked = findPreference<Preference>(Key.directBootAware)!!
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue -> if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
......
...@@ -23,20 +23,15 @@ package com.github.shadowsocks ...@@ -23,20 +23,15 @@ package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle import android.os.Bundle
import android.os.DeadObjectException import android.os.DeadObjectException
import android.os.Handler import android.os.Handler
import android.os.Parcelable
import android.util.Log import android.util.Log
import android.view.KeyCharacterMap import android.view.KeyCharacterMap
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
...@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService ...@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.widget.ServiceButton import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener, class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener { NavigationView.OnNavigationItemSelectedListener {
...@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
var stateListener: ((BaseService.State) -> Unit)? = null var stateListener: ((BaseService.State) -> Unit)? = null
} }
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes) { _, _ -> arg.profiles.forEach { ProfileManager.createProfile(it) } }
setNegativeButton(R.string.no, null)
setMessage(arg.profiles.joinToString("\n"))
}
}
// UI // UI
private lateinit var fab: ServiceButton private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar private lateinit var stats: StatsBar
...@@ -92,7 +72,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -92,7 +72,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
val snackbar by lazy { findViewById<CoordinatorLayout>(R.id.snackbar) } val snackbar by lazy { findViewById<CoordinatorLayout>(R.id.snackbar) }
fun snackbar(text: CharSequence = "") = Snackbar.make(snackbar, text, Snackbar.LENGTH_LONG).apply { fun snackbar(text: CharSequence = "") = Snackbar.make(snackbar, text, Snackbar.LENGTH_LONG).apply {
view.translationY += fab.top + fab.translationY - snackbar.measuredHeight anchorView = fab
} }
private val customTabsIntent by lazy { private val customTabsIntent by lazy {
...@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_main) setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats) stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() } stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
...@@ -184,29 +165,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -184,29 +165,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
changeState(BaseService.State.Idle) // reset everything to init state changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, this) connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this) DataStore.publicStore.registerChangeListener(this)
val intent = this.intent
if (intent != null) handleShareIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
private fun handleShareIntent(intent: Intent) {
val sharedStr = when (intent.action) {
Intent.ACTION_VIEW -> intent.data?.toString()
NfcAdapter.ACTION_NDEF_DISCOVERED -> {
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.isNotEmpty()) String((rawMsgs[0] as NdefMessage).records[0].payload)
else null
}
else -> null
}
if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) snackbar().setText(R.string.profile_invalid_input).show()
else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null)
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
......
...@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment ...@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
class ProfileConfigActivity : AppCompatActivity() { class ProfileConfigActivity : AppCompatActivity() {
companion object { companion object {
...@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() { ...@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_profile_config) setContentView(R.layout.layout_profile_config)
setSupportActionBar(findViewById(R.id.toolbar)) setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.apply { supportActionBar!!.apply {
......
...@@ -23,9 +23,6 @@ package com.github.shadowsocks ...@@ -23,9 +23,6 @@ package com.github.shadowsocks
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.os.Bundle import android.os.Bundle
import android.text.format.Formatter import android.text.format.Formatter
import android.util.LongSparseArray import android.util.LongSparseArray
...@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
arguments = bundleOf(Pair(KEY_URL, url)) arguments = bundleOf(Pair(KEY_URL, url))
} }
private val url get() = arguments?.getString(KEY_URL)!!
private val nfcShareItem by lazy { url.toByteArray() }
private var adapter: NfcAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val image = ImageView(context) val image = ImageView(context)
image.layoutParams = LinearLayout.LayoutParams(-1, -1) image.layoutParams = LinearLayout.LayoutParams(-1, -1)
val size = resources.getDimensionPixelSize(R.dimen.qr_code_size) val size = resources.getDimensionPixelSize(R.dimen.qr_code_size)
image.setImageBitmap((QRCode.from(url).withSize(size, size) as QRCode).bitmap()) image.setImageBitmap((QRCode.from(arguments?.getString(KEY_URL)!!).withSize(size, size) as QRCode).bitmap())
return image return image
} }
override fun onAttach(context: Context) {
super.onAttach(context)
val adapter = NfcAdapter.getDefaultAdapter(context)
adapter?.setNdefPushMessage(NdefMessage(arrayOf(
NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, byteArrayOf(), nfcShareItem))), activity)
this.adapter = adapter
}
override fun onDetach() {
super.onDetach()
val activity = activity
if (activity != null && !activity.isFinishing && !activity.isDestroyed)
adapter?.setNdefPushMessage(null, activity)
adapter = null
}
} }
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view), inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view),
...@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_qr_code_nfc -> { R.id.action_qr_code -> {
requireFragmentManager().beginTransaction().add(QRCodeDialog(this.item.toString()), "") requireFragmentManager().beginTransaction().add(QRCodeDialog(this.item.toString()), "")
.commitAllowingStateLoss() .commitAllowingStateLoss()
true true
......
...@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile ...@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
class UdpFallbackProfileActivity : AppCompatActivity() { class UdpFallbackProfileActivity : AppCompatActivity() {
...@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() { ...@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
private val profilesAdapter = ProfilesAdapter() private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (editingId == null) { if (editingId == null) {
finish() finish()
return return
} }
super.onCreate(savedInstanceState) SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_udp_fallback) setContentView(R.layout.layout_udp_fallback)
val toolbar = findViewById<Toolbar>(R.id.toolbar) val toolbar = findViewById<Toolbar>(R.id.toolbar)
......
...@@ -169,15 +169,18 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -169,15 +169,18 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
inputLayout.error = message inputLayout.error = message
} }
override fun ret(which: Int) = if (which != DialogInterface.BUTTON_POSITIVE) null else { override fun ret(which: Int) = when (which) {
AclEditResult(editText.text.toString().let { text -> DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEUTRAL -> {
when (Template.values()[templateSelector.selectedItemPosition]) { AclEditResult(editText.text.toString().let { text ->
Template.Generic -> AclItem(text) when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES) Template.Generic -> AclItem(text)
.replace(".", "\\.").let { "(^|\\.)$it\$" }) Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
Template.Url -> AclItem(text, true) .replace(".", "\\.").let { "(^|\\.)$it\$" })
} Template.Url -> AclItem(text, true)
}, arg) }
}, arg)
}
else -> null
} }
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
......
...@@ -77,14 +77,9 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute ...@@ -77,14 +77,9 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
} }
else -> changeState(iconStopped, animate) else -> changeState(iconStopped, animate)
} }
if (state == BaseService.State.Connected) { checked = state == BaseService.State.Connected
checked = true
TooltipCompat.setTooltipText(this, context.getString(R.string.stop))
} else {
checked = false
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
}
refreshDrawableState() refreshDrawableState()
TooltipCompat.setTooltipText(this, context.getString(if (state.canStop) R.string.stop else R.string.connect))
isEnabled = state.canStop || state == BaseService.State.Stopped isEnabled = state.canStop || state == BaseService.State.Stopped
} }
......
...@@ -48,10 +48,11 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? ...@@ -48,10 +48,11 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private val behavior = object : Behavior() { private val behavior = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold) val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View, override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) { dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int,
consumed: IntArray) {
val dy = dyConsumed + dyUnconsumed val dy = dyConsumed + dyUnconsumed
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, if (Math.abs(dy) >= threshold) dy else 0, super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, if (Math.abs(dy) >= threshold) dy else 0,
dxUnconsumed, 0, type) dxUnconsumed, 0, type, consumed)
} }
} }
override fun getBehavior() = behavior override fun getBehavior() = behavior
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_qr_code_nfc" <item android:id="@+id/action_qr_code"
android:title="@string/share_qr_nfc"/> android:title="@string/share_qr_nfc"/>
<item android:id="@+id/action_export_clipboard" <item android:id="@+id/action_export_clipboard"
android:title="@string/action_export"/> android:title="@string/action_export"/>
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
app:initialExpandedChildrenCount="4"> app:initialExpandedChildrenCount="4">
<SwitchPreference <SwitchPreference
app:key="isAutoConnect" app:key="isAutoConnect"
app:persistent="false"
app:summary="@string/auto_connect_summary" app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/> app:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
......
...@@ -25,6 +25,11 @@ android { ...@@ -25,6 +25,11 @@ android {
testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner"
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
...@@ -48,7 +53,7 @@ mavenPublish { ...@@ -48,7 +53,7 @@ mavenPublish {
} }
dependencies { dependencies {
api 'androidx.core:core-ktx:1.0.1' api 'androidx.core:core-ktx:1.0.2'
api 'com.google.android.material:material:1.1.0-alpha06' api 'com.google.android.material:material:1.1.0-alpha06'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Shadowsocks" parent="Theme.MaterialComponents.NoActionBar">
<item name="android:navigationBarColor">@color/color_primary_dark</item>
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
<item name="bottomSheetDialogTheme">@style/Theme.MaterialComponents.DayNight.BottomSheetDialog</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="windowActionModeOverlay">true</item>
</style>
<style name="Theme.MaterialComponents.DayNight.BottomSheetDialog"
parent="@style/Theme.MaterialComponents.BottomSheetDialog"/>
<style name="ThemeOverlay.AppCompat.DayNight" parent="ThemeOverlay.AppCompat.Dark"/> <style name="ThemeOverlay.AppCompat.DayNight" parent="ThemeOverlay.AppCompat.Dark"/>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Shadowsocks" parent="Theme.MaterialComponents.Light.NoActionBar"> <style name="Theme.Shadowsocks" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:navigationBarColor">@color/color_primary_dark</item> <item name="android:navigationBarColor">@color/color_primary_dark</item>
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item> <item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
<item name="bottomSheetDialogTheme">@style/Theme.MaterialComponents.DayNight.BottomSheetDialog</item>
<item name="colorAccent">@color/material_accent_200</item> <item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_accent_200</item> <item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/color_primary</item> <item name="colorPrimary">@color/color_primary</item>
<item name="colorPrimaryDark">@color/color_primary_dark</item> <item name="colorPrimaryDark">@color/color_primary_dark</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
</style> </style>
<style name="Theme.MaterialComponents.DayNight.BottomSheetDialog"
parent="@style/Theme.MaterialComponents.Light.BottomSheetDialog"/>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Dialog"> <style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Dialog">
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item> <item name="android:windowFrame">@null</item>
......
...@@ -37,6 +37,10 @@ android { ...@@ -37,6 +37,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*' packagingOptions.exclude '**/*.kotlin_*'
splits { splits {
abi { abi {
......
...@@ -17,8 +17,7 @@ ...@@ -17,8 +17,7 @@
tools:replace="theme"> tools:replace="theme">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name">
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
......
...@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv ...@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.github.shadowsocks.utils.SingleInstanceActivity
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
} }
} }
...@@ -157,12 +157,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -157,12 +157,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
stats = findPreference(Key.controlStats)!! stats = findPreference(Key.controlStats)!!
controlImport = findPreference(Key.controlImport)!! controlImport = findPreference(Key.controlImport)!!
findPreference<SwitchPreference>(Key.isAutoConnect)!!.apply { findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
setOnPreferenceChangeListener { _, value -> BootReceiver.enabled = value as Boolean
BootReceiver.enabled = value as Boolean true
true
}
isChecked = BootReceiver.enabled
} }
tfo = findPreference(Key.tfo)!! tfo = findPreference(Key.tfo)!!
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
app:initialExpandedChildrenCount="3"> app:initialExpandedChildrenCount="3">
<SwitchPreference <SwitchPreference
app:key="isAutoConnect" app:key="isAutoConnect"
app:persistent="false"
app:summary="@string/auto_connect_summary" app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/> app:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
......
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