Commit 98ed1f78 authored by Mygod's avatar Mygod

Merge branch 'master' into q-beta

parents ebda36ee 5e4e82b3
......@@ -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)
......
......@@ -29,4 +29,4 @@
branch = stable
[submodule "core/src/main/jni/libev"]
path = core/src/main/jni/libev
url = https://github.com/shadowsocks/libev.git
url = https://git.lighttpd.net/libev.git
......@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.3.21'
kotlinVersion = '1.3.30'
minSdkVersion = 21
sdkVersion = 28
compileSdkVersion = 'android-Q'
......@@ -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']
}
......
......@@ -44,28 +44,25 @@ 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.0.0'
api "androidx.room:room-runtime:$roomVersion"
api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.crashlytics.sdk.android:crashlytics:2.9.9'
api 'com.google.firebase:firebase-config:16.4.1'
api 'com.google.firebase:firebase-config:16.5.0'
api 'com.google.firebase:firebase-core:16.0.8'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
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.0'
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'
}
......@@ -51,6 +51,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
......@@ -99,6 +101,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
......
......@@ -255,7 +255,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() } }
}
}
......@@ -40,7 +40,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()
......
......@@ -131,11 +131,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() } }
}
}
......@@ -61,7 +61,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()
......
......@@ -73,8 +73,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)
......@@ -170,8 +169,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()
}
}
......
......@@ -114,7 +114,7 @@ include $(CLEAR_VARS)
LIBEVENT_SOURCES := \
buffer.c bufferevent.c event.c \
bufferevent_sock.c bufferevent_ratelim.c \
evthread.c log.c evutil.c evutil_time.c evmap.c epoll.c poll.c signal.c select.c
evthread.c log.c evutil.c evutil_rand.c evutil_time.c evmap.c epoll.c poll.c signal.c select.c
LOCAL_MODULE := event
LOCAL_SRC_FILES := $(addprefix libevent/, $(LIBEVENT_SOURCES))
......
Subproject commit e8660cfae85762d3d3eaf72c47b8478f1c9f0e22
Subproject commit 6e14849aa213cbb028aeb0c75905438260fd867c
libev @ 31ca40b7
Subproject commit 5213419011ec5de302f8bc1716f348de1fb53b12
Subproject commit 31ca40b7a18ed424213a4ecba0d57706ed3e56a0
Subproject commit fec4c92d81189e0394ad94e14f238a837c81385e
Subproject commit b732443c442239c2e0184820e9b23cca0de0828c
Subproject commit 4f0929189ab43e020b0d441919abf6bab02baf11
Subproject commit 9f4f8eec93dd1f32d78e0bcceddbef0ca570e66f
Subproject commit 274334f14839431ae003774d99c3d1de337afff4
Subproject commit d9954892ba96627748efab2901f38d7e20d6d0a2
Subproject commit b430124ed82973d4edf345dfadbe556f93377484
Subproject commit b0c6ba494509473e269f7ae5b5e769e09bb9b6ee
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
......@@ -28,7 +28,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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
......
......@@ -14,7 +14,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"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
......
......@@ -38,7 +38,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))
......
......@@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.resolveResourceId
......@@ -61,7 +62,7 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = (ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf())
.filter { it.id != editingId && it.plugin.isNullOrEmpty() }
.filter { it.id != editingId && PluginConfiguration(it.plugin ?: "").selected.isEmpty() }
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
holder.bind(if (position == 0) null else profiles[position - 1])
......
......@@ -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),
......
......@@ -49,7 +49,7 @@ mavenPublish {
dependencies {
api 'androidx.core:core-ktx:1.0.1'
api 'com.google.android.material:material:1.1.0-alpha04'
api 'com.google.android.material:material:1.1.0-alpha05'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
......
......@@ -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)
}
}
......
......@@ -37,7 +37,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions.checkReleaseBuilds false
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
enable true
......
......@@ -92,7 +92,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)),
......@@ -102,6 +102,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
......@@ -116,9 +117,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
if (state == BaseService.State.Stopped) {
controlImport.isEnabled = true
......
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