Commit ac57e915 authored by Mygod's avatar Mygod

Merge branch 'master' into q-beta

parents 590f28c8 a2c090f2
github: [Mygod]
# Submitting Issues
* Questions and general discussions should go to [Discourse forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android);
* 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;
* Always provide information asked in the template unless you know what you're doing;
* Logcat would also be helpful.
* Issues can be submitted in two different places:
1) [The GitHub issue log](https://github.com/shadowsocks/shadowsocks-android/issues)
2) [The Shadowsocks.org Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android)
* 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 @@
[![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)
[![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>
for Android & Chrome OS ([beta](https://play.google.com/apps/testing/com.github.shadowsocks))
......@@ -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"
```
### 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)
## OPEN SOURCE LICENSES
......@@ -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>
</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
Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>>
......@@ -66,4 +75,4 @@ 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/>.
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'
buildscript {
ext {
javaVersion = JavaVersion.VERSION_1_8
kotlinVersion = '1.3.31'
minSdkVersion = 21
sdkVersion = 28
......@@ -11,8 +12,8 @@ buildscript {
junitVersion = '4.12'
androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1'
versionCode = 4070450
versionName = '4.7.4-nightly'
versionCode = 4080050
versionName = '4.8.0-nightly'
resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW']
}
......@@ -25,11 +26,11 @@ buildscript {
}
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.google.gms:google-services:4.2.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"
}
}
......
......@@ -27,6 +27,11 @@ android {
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
......@@ -46,18 +51,18 @@ def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0'
dependencies {
api project(':plugin')
api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.1.0-alpha05'
api "androidx.room:room-runtime:$roomVersion"
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-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.connectbot.jsocks:jsocks:1.0.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
......
......@@ -56,12 +56,23 @@
android:exported="false">
</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
android:name="com.github.shadowsocks.VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
android:taskAffinity=""/>
<receiver android:name="com.github.shadowsocks.BootReceiver"
android:process=":bg"
......@@ -70,6 +81,7 @@
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
......
......@@ -1928,61 +1928,8 @@
(^|\.)goodtv\.com\.tw$
(^|\.)goodtv\.tv$
(^|\.)goofind\.com$
(^|\.)google\.ae$
(^|\.)google\.am$
(^|\.)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$
(^|\.)google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)googleapps\.com$
(^|\.)googlearth\.com$
(^|\.)googleartproject\.com$
......
......@@ -25,6 +25,9 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
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.preference.DataStore
......@@ -40,11 +43,16 @@ class BootReceiver : BroadcastReceiver() {
}
override fun onReceive(context: Context, intent: Intent) {
val locked = when (intent.action) {
Intent.ACTION_BOOT_COMPLETED -> false
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
else -> return
if (!DataStore.persistAcrossReboot) { // sanity check
enabled = false
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
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.aidl.IShadowsocksService
......@@ -36,6 +37,7 @@ import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.core.R
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.printLog
......@@ -75,6 +77,7 @@ object BaseService {
var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Intent.ACTION_SHUTDOWN -> service.persistStats()
Action.RELOAD -> service.forceLoad()
else -> service.stopRunner()
}
......@@ -283,10 +286,16 @@ object BaseService {
data.changeState(State.Stopped, msg)
// 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 resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host)
suspend fun openConnection(url: URL) = url.openConnection()
......@@ -308,6 +317,7 @@ object BaseService {
data.proxy = proxy
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
BootReceiver.enabled = DataStore.persistAcrossReboot
if (!data.closeReceiverRegistered) {
registerReceiver(data.closeReceiver, IntentFilter().apply {
addAction(Action.RELOAD)
......
......@@ -28,17 +28,14 @@ import com.github.shadowsocks.Core
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncer
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
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.signaturesCompat
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.net.UnknownHostException
......@@ -65,15 +62,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.requestMethod = "POST"
conn.doOutput = true
val proxies = try {
withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
conn.inputStream.bufferedReader().readText()
val proxies = conn.useCancellable {
outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
} finally {
conn.disconnectFromMain()
inputStream.bufferedReader().readText()
}.split('|').toMutableList()
proxies.shuffle()
val proxy = proxies.first().split(':')
......@@ -144,22 +137,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
fun shutdown(scope: CoroutineScope) {
trafficMonitor?.apply {
thread.shutdown(scope)
// 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()
}
persistStats(profile.id) // Make sure update total traffic when stopping the runner
}
trafficMonitor = null
configFile?.delete() // remove old config possibly in device storage
......
......@@ -23,7 +23,10 @@ package com.github.shadowsocks.bg
import android.net.LocalSocket
import android.os.SystemClock
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.database.ProfileManager
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.IOException
import java.nio.ByteBuffer
......@@ -52,6 +55,7 @@ class TrafficMonitor(statFile: File) {
var out = TrafficStats()
private var timestampLast = 0L
private var dirty = false
private var persisted = false
fun requestUpdate(): Pair<TrafficStats, Boolean> {
val now = SystemClock.elapsedRealtime()
......@@ -79,4 +83,24 @@ class TrafficMonitor(statFile: File) {
}
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
import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.disconnectFromMain
import kotlinx.coroutines.*
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException
import java.net.HttpURLConnection
import java.net.Proxy
......@@ -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 }
fun testConnection() {
......@@ -91,26 +94,25 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) {
running = GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = conn.useCancellable {
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val code = responseCode
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)
} catch (e: IOException) {
Status.Error.IOFailure(e)
} finally {
conn.disconnect()
disconnect()
}
}
}
}
private fun cancelTest() = running?.let { (conn, job) ->
job.cancel() // ensure job is cancelled before interrupting
conn.disconnectFromMain()
private fun cancelTest() {
running?.cancel()
running = null
}
......
......@@ -22,6 +22,7 @@ package com.github.shadowsocks.preference
import android.os.Binder
import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase
......@@ -61,6 +62,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var profileId: Long
get() = publicStore.getLong(Key.id) ?: 0
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 directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && DataStore.publicStore.getBoolean(Key.tfo, true)
......@@ -101,6 +104,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
* Initialize settings that have complicated default values.
*/
fun initGlobal() {
persistAcrossReboot
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
......
......@@ -43,7 +43,7 @@ object Key {
const val route = "route"
const val isAutoConnect = "isAutoConnect"
const val persistAcrossReboot = "isAutoConnect"
const val directBootAware = "directBootAware"
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
import androidx.core.os.BuildCompat
import androidx.preference.Preference
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import java.net.HttpURLConnection
import java.net.InetAddress
import kotlin.coroutines.resume
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
......@@ -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)
computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) }
fun HttpURLConnection.disconnectFromMain() {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
suspend fun <T> HttpURLConnection.useCancellable(block: HttpURLConnection.() -> T) = withContext(Dispatchers.IO) {
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 {
......
......@@ -4,7 +4,7 @@
<string name="remote_dns">"DNS Remoto"</string>
<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>
<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_error">"Fallo al detectar conexión a Internet: %s"</string>
<string name="connection_test_fail">"Internet no disponible"</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="service_failed">"Fallo al conectar al servidor remoto"</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="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>
<string name="received">"Recibido:"</string>
<!-- 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="not_connected">"No conectado"</string>
<!-- acl -->
<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="route_entry_all">"Todo"</string>
<!-- plugin -->
<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_unknown">"Complemento desconocido %s"</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>
<string name="service_transproxy">"Servicio Transproxy"</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="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_system">"Seguir sistema"</string>
<string name="night_mode_auto">"Auto"</string>
<string name="night_mode_on">"Encendido"</string>
<string name="night_mode_off">"Apagado"</string>
<string name="send_email">"Enviar email"</string>
<string name="action_export_more">"Exportar..."</string>
<string name="action_export_file">"Exportar a archivo..."</string>
<string name="action_export_more">"Exportar"</string>
<string name="action_export_file">"Exportar a archivo"</string>
<string name="share_over_lan">"Compartir por LAN"</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="action_replace_file">"Reemplazar desde archivo..."</string>
<string name="action_replace_file">"Reemplazar desde archivo"</string>
</resources>
\ No newline at end of file
......@@ -53,9 +53,7 @@
<string name="bypass_apps">Bypass</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_summary">Enable Shadowsocks on startup</string>
<string name="auto_connect_summary_v24">Enable Shadowsocks on startup. Recommended to use always-on VPN
instead</string>
<string name="auto_connect_summary">Enable Shadowsocks on startup/app update if it was running before</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="tcp_fastopen_summary">Toggling might require ROOT permission</string>
......@@ -114,7 +112,7 @@
<string name="profile_config">Profile config</string>
<string name="delete">Remove</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_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
......
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists
......@@ -38,6 +38,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
......@@ -58,7 +62,7 @@ dependencies {
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
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.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0'
......
......@@ -3,13 +3,10 @@
package="com.github.shadowsocks"
tools:ignore="MissingLeanbackSupport">
<uses-permission android:name="android.permission.NFC" />
<uses-permission-sdk-23 android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
......@@ -21,7 +18,7 @@
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.Navigation"
android:launchMode="singleTask">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
......@@ -30,17 +27,6 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</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"
android:resource="@xml/shortcuts"/>
</activity>
......@@ -48,22 +34,19 @@
<activity
android:name=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:label="@string/profile_config"
android:launchMode="singleTask"/>
android:label="@string/profile_config"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
android:excludeFromRecents="true"/>
<activity
android:name=".UdpFallbackProfileActivity"
android:label="@string/udp_fallback"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
android:excludeFromRecents="true"/>
<activity
android:name=".ScannerActivity"
......@@ -84,8 +67,7 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:process=":bg"
android:launchMode="singleTask">
android:process=":bg">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
......
......@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.*
......@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_apps)
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
......
......@@ -50,13 +50,10 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global)
val boot = findPreference<SwitchPreference>(Key.isAutoConnect)!!
boot.setOnPreferenceChangeListener { _, value ->
findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
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)!!
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
......
......@@ -23,20 +23,15 @@ package com.github.shadowsocks
import android.app.Activity
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler
import android.os.Parcelable
import android.util.Log
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout
......@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats
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.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
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 kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener {
......@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
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
private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar
......@@ -92,7 +72,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
val snackbar by lazy { findViewById<CoordinatorLayout>(R.id.snackbar) }
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 {
......@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
......@@ -184,29 +165,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, 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) {
......
......@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
class ProfileConfigActivity : AppCompatActivity() {
companion object {
......@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_profile_config)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.apply {
......
......@@ -23,9 +23,6 @@ package com.github.shadowsocks
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.os.Bundle
import android.text.format.Formatter
import android.util.LongSparseArray
......@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
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? {
val image = ImageView(context)
image.layoutParams = LinearLayout.LayoutParams(-1, -1)
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
}
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),
......@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
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()), "")
.commitAllowingStateLoss()
true
......
......@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.utils.resolveResourceId
class UdpFallbackProfileActivity : AppCompatActivity() {
......@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (editingId == null) {
finish()
return
}
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_udp_fallback)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
......
......@@ -169,15 +169,18 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
inputLayout.error = message
}
override fun ret(which: Int) = if (which != DialogInterface.BUTTON_POSITIVE) null else {
AclEditResult(editText.text.toString().let { text ->
when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> AclItem(text)
Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
.replace(".", "\\.").let { "(^|\\.)$it\$" })
Template.Url -> AclItem(text, true)
}
}, arg)
override fun ret(which: Int) = when (which) {
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEUTRAL -> {
AclEditResult(editText.text.toString().let { text ->
when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> AclItem(text)
Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
.replace(".", "\\.").let { "(^|\\.)$it\$" })
Template.Url -> AclItem(text, true)
}
}, arg)
}
else -> null
}
override fun onClick(dialog: DialogInterface?, which: Int) {
......
......@@ -77,14 +77,9 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
}
else -> changeState(iconStopped, animate)
}
if (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))
}
checked = state == BaseService.State.Connected
refreshDrawableState()
TooltipCompat.setTooltipText(this, context.getString(if (state.canStop) R.string.stop else R.string.connect))
isEnabled = state.canStop || state == BaseService.State.Stopped
}
......
......@@ -48,10 +48,11 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private val behavior = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
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
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
......
<?xml version="1.0" encoding="utf-8"?>
<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"/>
<item android:id="@+id/action_export_clipboard"
android:title="@string/action_export"/>
......
......@@ -3,7 +3,6 @@
app:initialExpandedChildrenCount="4">
<SwitchPreference
app:key="isAutoConnect"
app:persistent="false"
app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/>
<SwitchPreference
......
......@@ -25,6 +25,11 @@ android {
testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
buildTypes {
release {
minifyEnabled false
......@@ -48,7 +53,7 @@ mavenPublish {
}
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 "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
......
<?xml version="1.0" encoding="utf-8"?>
<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"/>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<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="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.Light.BottomSheetDialog"/>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
......
......@@ -37,6 +37,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
......
......@@ -17,8 +17,7 @@
tools:replace="theme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
......
......@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.github.shadowsocks.utils.SingleInstanceActivity
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.activity_main)
}
}
......@@ -157,12 +157,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
stats = findPreference(Key.controlStats)!!
controlImport = findPreference(Key.controlImport)!!
findPreference<SwitchPreference>(Key.isAutoConnect)!!.apply {
setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
isChecked = BootReceiver.enabled
findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
tfo = findPreference(Key.tfo)!!
......
......@@ -23,7 +23,6 @@
app:initialExpandedChildrenCount="3">
<SwitchPreference
app:key="isAutoConnect"
app:persistent="false"
app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/>
<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