Commit 8dd24d69 authored by Mygod's avatar Mygod

Merge branch 'master' into preference-1.1

parents 576ab96c 723b2b4d
......@@ -35,6 +35,7 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
* Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
* Another Samsung: [#1712](https://github.com/shadowsocks/shadowsocks-android/issues/1712)
* Samsung with GMS: [#2138](https://github.com/shadowsocks/shadowsocks-android/issues/2138)
* Don't install this app on SD card because of permission issues: [#1124 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453)
* `INTERACT_ACROSS_USERS` permission missing: [#1184](https://github.com/shadowsocks/shadowsocks-android/issues/1184)
......
......@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.3.21'
kotlinVersion = '1.3.31'
minSdkVersion = 21
sdkVersion = 28
compileSdkVersion = 28
......@@ -12,8 +12,8 @@ buildscript {
junitVersion = '4.12'
androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1'
versionCode = 4070350
versionName = '4.7.3-nightly'
versionCode = 4070450
versionName = '4.7.4-nightly'
resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW']
}
......@@ -26,7 +26,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.android.tools.build:gradle:3.4.0'
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'
......
......@@ -45,27 +45,24 @@ androidExtensions {
def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0'
def workVersion = '1.0.0'
dependencies {
api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.1.0-alpha04'
api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.9'
api 'com.google.firebase:firebase-config:16.4.1'
api 'com.google.firebase:firebase-core:16.0.8'
api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.crashlytics.sdk.android:crashlytics:2.10.0'
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 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
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 "androidx.arch.core:core-testing:$lifecycleVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "android.arch.work:work-testing:$workVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
androidTestImplementation "androidx.test.ext:junit-ktx:1.1.0"
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
}
......@@ -50,6 +50,8 @@ import com.github.shadowsocks.utils.*
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import java.io.File
import java.io.IOException
import kotlin.reflect.KClass
......@@ -97,6 +99,8 @@ object Core {
}
}
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
FirebaseApp.initializeApp(deviceStorage)
WorkManager.initialize(deviceStorage, Configuration.Builder().build())
......
......@@ -121,8 +121,7 @@ class Acl {
val blocks = (line as java.lang.String).split("#", 2)
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
if (url != null) urls.add(URL(url))
val input = blocks[0].trim()
when (input) {
when (val input = blocks[0].trim()) {
"[outbound_block_list]" -> {
hostnames = null
subnets = null
......
......@@ -254,7 +254,7 @@ object BaseService {
if (data.state == State.Stopping) return
// channge the state
data.changeState(State.Stopping)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
GlobalScope.launch(Dispatchers.Main.immediate) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
this@Interface as Service
......
......@@ -104,8 +104,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
}
}
private val job = Job()
override val coroutineContext get() = Dispatchers.Main + job
override val coroutineContext = Dispatchers.Main.immediate + Job()
@MainThread
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
......@@ -118,7 +117,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
@MainThread
fun close(scope: CoroutineScope) {
job.cancel()
scope.launch { job.join() }
cancel()
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
}
}
......@@ -42,7 +42,7 @@ object LocalDnsService {
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
}
private val servers = WeakHashMap<LocalDnsService.Interface, LocalDnsServer>()
private val servers = WeakHashMap<Interface, LocalDnsServer>()
interface Interface : BaseService.Interface {
override suspend fun startProcesses() {
......
......@@ -37,13 +37,13 @@ object RemoteConfig {
}
fun scheduleFetch() = config.fetch().addOnCompleteListener {
if (it.isSuccessful) config.activateFetched() else it.exception?.log()
if (it.isSuccessful) config.activate() else it.exception?.log()
}
suspend fun fetch() = suspendCancellableCoroutine<Pair<FirebaseRemoteConfig, Boolean>> { cont ->
config.fetch().addOnCompleteListener {
if (it.isSuccessful) {
config.activateFetched()
config.activate()
cont.resume(config to true)
} else {
it.exception?.log()
......
......@@ -129,11 +129,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (DataStore.serviceMode == Key.modeVpn)
if (BaseVpnService.prepare(this) != null)
startActivity(Intent(this, VpnRequestActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
if (DataStore.serviceMode == Key.modeVpn) {
if (prepare(this) != null) {
startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
}
stopRunner()
return Service.START_NOT_STICKY
}
......
......@@ -244,12 +244,12 @@ data class Profile(
}
fun toUri(): Uri {
val auth = Base64.encodeToString("$method:$password".toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
val wrappedHost = if (host.contains(':')) "[$host]" else host
val builder = Uri.Builder()
.scheme("ss")
.encodedAuthority("%s@%s:%d".format(Locale.ENGLISH,
Base64.encodeToString("$method:$password".toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE),
if (host.contains(':')) "[$host]" else host, remotePort))
.encodedAuthority("$auth@$wrappedHost:$remotePort")
val configuration = PluginConfiguration(plugin ?: "")
if (configuration.selected.isNotEmpty())
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
......
......@@ -27,8 +27,7 @@ import java.io.File
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext get() = Dispatchers.IO + job + CoroutineExceptionHandler { _, t -> printLog(t) }
override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun accept(socket: LocalSocket) {
launch { super.accept(socket) }
......@@ -36,8 +35,8 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L
override fun shutdown(scope: CoroutineScope) {
running = false
job.cancel()
cancel()
super.shutdown(scope)
scope.launch { job.join() }
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
}
}
......@@ -62,7 +62,7 @@ object DefaultNetworkListener {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
if (network == null) pendingRequests += message else message.response.complete(network)
}
is NetworkMessage.Stop -> if (!listeners.isEmpty() && // was not empty
is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
listeners.remove(message.key) != null && listeners.isEmpty()) {
network = null
unregister()
......
......@@ -91,7 +91,7 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
running = conn to GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) {
try {
val start = SystemClock.elapsedRealtime()
......
......@@ -86,8 +86,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
private val monitor = ChannelMonitor()
private val job = SupervisorJob()
override val coroutineContext = job + CoroutineExceptionHandler { _, t -> printLog(t) }
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
configureBlocking(false)
......@@ -182,8 +181,8 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
}
fun shutdown(scope: CoroutineScope) {
job.cancel()
cancel()
monitor.close(scope)
scope.launch { job.join() }
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
}
}
......@@ -44,7 +44,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) {
Key.id -> if (DataStore.directBootAware) DirectBoot.update()
Key.id -> if (directBootAware) DirectBoot.update()
}
}
......
Subproject commit e8660cfae85762d3d3eaf72c47b8478f1c9f0e22
Subproject commit 6e14849aa213cbb028aeb0c75905438260fd867c
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
......@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
......
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
......@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
......
......@@ -39,7 +39,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions.checkReleaseBuilds false
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
enable true
......
......@@ -53,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.*
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext
class AppManager : AppCompatActivity() {
companion object {
......@@ -68,7 +69,7 @@ class AppManager : AppCompatActivity() {
receiver = null
cachedApps = null
}
AppManager.instance?.loadApps()
instance?.loadApps()
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
......@@ -108,7 +109,7 @@ class AppManager : AppCompatActivity() {
}
fun handlePayload(payloads: List<String>) {
if (payloads.contains(AppManager.SWITCH)) itemView.itemcheck.isChecked = isProxiedApp(item)
if (payloads.contains(SWITCH)) itemView.itemcheck.isChecked = isProxiedApp(item)
}
override fun onClick(v: View?) {
......@@ -116,7 +117,7 @@ class AppManager : AppCompatActivity() {
DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName }
DataStore.dirty = true
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH)
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
}
}
......@@ -125,7 +126,7 @@ class AppManager : AppCompatActivity() {
suspend fun reload() {
apps = getCachedApps(packageManager).map { (packageName, packageInfo) ->
yield()
coroutineContext[Job]!!.ensureActive()
ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)
}.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
}
......@@ -196,7 +197,7 @@ class AppManager : AppCompatActivity() {
@UiThread
private fun loadApps() {
loader?.cancel()
loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
loader = GlobalScope.launch(Dispatchers.Main.immediate) {
loading.crossFadeFrom(list)
val adapter = list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() }
......@@ -280,7 +281,7 @@ class AppManager : AppCompatActivity() {
DataStore.dirty = true
Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedUids(apps)
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH)
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
return true
} catch (_: IllegalArgumentException) { }
}
......
......@@ -41,7 +41,7 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(Intent(this, QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
......
......@@ -63,7 +63,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
companion object {
private const val REQUEST_CODE_ADD = 1
private const val REQUEST_CODE_EDIT = 2
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private const val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
......@@ -170,14 +169,20 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
inputLayout.error = message
}
override val ret get() = AclEditResult(editText.text.toString().let { text ->
when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> AclItem(text)
Template.Domain -> AclItem(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
Template.Url -> AclItem(text, true)
}
}, arg)
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 onClick(dialog: DialogInterface?, which: Int) {
if (which != DialogInterface.BUTTON_NEGATIVE) super.onClick(dialog, which)
}
}
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
......
......@@ -7,7 +7,7 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:duplicateParentState="false">
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
......@@ -25,7 +25,7 @@
android:iconifiedByDefault="false"
android:queryHint="@android:string/search_go"/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.MaterialToolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="fill_parent"
......
......@@ -50,8 +50,8 @@ mavenPublish {
dependencies {
api 'androidx.core:core-ktx:1.0.1'
api 'com.google.android.material:material:1.1.0-alpha05'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
api 'com.google.android.material:material:1.1.0-alpha06'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
......
......@@ -43,21 +43,21 @@ abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
protected val arg by lazy { arguments!!.getParcelable<Arg>(KEY_ARG)!! }
protected open val ret: Ret? get() = null
protected open fun ret(which: Int): Ret? = null
fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } }
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) {
targetFragment?.onActivityResult(targetRequestCode, which, ret?.let {
targetFragment?.onActivityResult(targetRequestCode, which, ret(which)?.let {
Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) })
})
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_CANCELED, null)
onClick(dialog, Activity.RESULT_CANCELED)
}
fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) {
......
......@@ -42,25 +42,22 @@ class PluginOptions : HashMap<String, String?> {
val tokenizer = StringTokenizer("$options;", "\\=;", true)
val current = StringBuilder()
var key: String? = null
while (tokenizer.hasMoreTokens()) {
val nextToken = tokenizer.nextToken()
when (nextToken) {
"\\" -> current.append(tokenizer.nextToken())
"=" -> if (key == null) {
key = current.toString()
current.setLength(0)
} else current.append(nextToken)
";" -> {
if (key != null) {
put(key, current.toString())
key = null
} else if (current.isNotEmpty())
if (parseId) id = current.toString() else put(current.toString(), null)
current.setLength(0)
parseId = false
}
else -> current.append(nextToken)
while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
"\\" -> current.append(tokenizer.nextToken())
"=" -> if (key == null) {
key = current.toString()
current.setLength(0)
} else current.append(nextToken)
";" -> {
if (key != null) {
put(key, current.toString())
key = null
} else if (current.isNotEmpty())
if (parseId) id = current.toString() else put(current.toString(), null)
current.setLength(0)
parseId = false
}
else -> current.append(nextToken)
}
}
......
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="?attr/actionBarSize"
......
......@@ -38,7 +38,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions.checkReleaseBuilds false
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
enable true
......
......@@ -94,7 +94,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private set
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) requireContext().let { context ->
if (profileId == 0L) context?.let { context ->
this.stats.summary = getString(R.string.stat_summary,
getString(R.string.speed, Formatter.formatFileSize(context, stats.txRate)),
getString(R.string.speed, Formatter.formatFileSize(context, stats.rxRate)),
......@@ -104,6 +104,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
private fun changeState(state: BaseService.State, msg: String? = null) {
val context = context ?: return
fab.isEnabled = state.canStop || state == BaseService.State.Stopped
fab.setTitle(when (state) {
BaseService.State.Connecting -> R.string.connecting
......@@ -118,9 +119,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
tester.status.removeObservers(this)
if (state != BaseService.State.Idle) tester.invalidate()
} else tester.status.observe(this, Observer {
it.retrieve(stats::setTitle) { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
it.retrieve(stats::setTitle) { Toast.makeText(context, it, Toast.LENGTH_LONG).show() }
})
if (msg != null) Toast.makeText(requireContext(), getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state
val stopped = state == BaseService.State.Stopped
controlImport.isEnabled = stopped
......
......@@ -18,6 +18,7 @@
app:key="control.export"
app:title="@string/action_export_file"/>
<PreferenceCategory
app:key="settings"
app:title="@string/settings"
app:initialExpandedChildrenCount="3">
<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