Commit 08ec7ccb authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Merge branch 'master' of https://git.frostnerd.com/PublicAndroidApps/smokescreen into translations

parents 056fcf84 f2956249
Pipeline #7388 canceled with stages
......@@ -25,32 +25,24 @@
</formatting-settings>
</DBN-SQL>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<DBN-PSQL>
<case-options enabled="false">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false" />
</DBN-PSQL>
<DBN-SQL>
<case-options enabled="false">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false">
<option name="STATEMENT_SPACING" value="one_line" />
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
</formatting-settings>
</DBN-SQL>
<codeStyleSettings language="XML">
<arrangement>
<rules>
......
All dates in dd.mm.yyy.
**30.08.2020**<br>
1.4.1, Build 63:
- This is a stability update which fixes a few bugs and crashes.
1.4.1.1, Build 65:
- Hotfix for 1.4.1
**29.07.2020**<br>
1.4.0, Build 62:
- When starting Nebulo from a server shortcut that server is now persisted
- Added support for iptables in non-vpn mode. This requires a rooted device
- DNS rule sources can now be updated individuall
- The internal DNS server can now be configured
**07.07.2020**<br>
1.3.0, Build 61:
- There are now new translations
......
# FAQ
A selection of common questions and a collection of technical aspects of Nebulo. Feel free to ask a question in the issues and I'll add it here as well.
## Non-VPN mode
(Go [here](docs/NONVPNMODE.md) for documentation)
Since 1.4.0 Nebulo can run without requiring the dummy VPN. In this mode Nebulo hosts a DNS server locally, which forwards all DNS queries it receives according to the settings you configured in Nebulo.<br>
In this mode you manually have to forward all the DNS queries your device creates to Nebulos local DNS server (normally this is what the dummy VPN is used for).<br><br>
If your device is rooted Nebulo has an inbuilt solution using iptables. If it isn't rooted you have to use third-party apps which are able to forward the DNS queries to Nebulo.<br>
Known third-party apps this works with are NetGuard and V2Ray (although there might be others). You can find instructions on how to configure these apps to work together with Nebulo in the settings or by clicking [here](docs/NONVPNMODE.md).<br>
Please note that the App exclusion setting inside the general category won't have any effect in non-VPN mode. You have to configure excluded apps inside the third-party app you are using.
## Query logging
Query logging is feature inside Nebulo. If enabled (Settings -> Query logging), it can be found in the navigation drawer.
### What does it do?
When Query logging is enabled Nebulo saves all DNS queries it receives while it is running.
This allows you to see the DNS queries your device has made in the past.
The list updates in realtime when Nebulo is active.\n
You can search for hosts in the query log.
### What do the icons mean in the query log?
There are 4 icons in the query log:
- Database/server icon: cache is enabled and the DNS response came from cache
- Flag: The response came from the DNS rules OR the upstream DNS server replied with 0.0.0.0 or ::1
- Left pointing arrow: The answer was forwarded to a DNS server
- Questionmark: Unknown what happened with the query (normally you shouldn't see that)
## ESNI
### Is ESNI supported?
Currently no. The Android platform and the libraries I am using lack support for ESNI (https://git.frostnerd.com/PublicAndroidApps/smokescreen/-/issues/237)
### Do I need ESNI?
Most likely no. It would make it harder for government/ISPs to block access to a DNS server though.
## DNS rules
DNS rules are a feature inside Nebulo. You can find the page for them in the navigation drawer.
### What are DNS rules?
You can use DNS rules to either redirect a host to some IP addresses of your choice, or to block that host completely.\n
By blocking the host it isn't reachable from your phone anymore, which you can use to block ads, tracking (even inside apps).
You can even block websites you don't want to be able to visit anymore;
for example, you could set up your childs phone to block porn pages.
### How do I use DNS rules?
There are two options on how to use the DNS rules:<br>
You can either define your own rules - for example to block single hosts - or import *rule sources* which contain a list of rules.<br><br>
There are multiple lists maintained by independent people you can use in Nebulo.
They cover different topics, from blocking ads, tracking to blocking porn or social media.<br>
I recommend checking out [Energized](energized.pro).
It has multiple types of lists, covering use cases mentioned above.
If you have the F-Droid version the Energized lists are already added as sources.
#### Own (custom) rules
To create custom DNS rules scroll the list until you see this entry (without the numbers in red of course):<br><br>
<img width=500px height=136px src="material/faq/custom_dns_rule.png" alt="Project logo"></a>
<br>
Click on the switch (1) to disable or enable custom rules.<br>
Click on the trash bin (2) to delete all custom rules<br>
Click on the plus (3) to add a new rule<br>
Click on the chevron (4) to expand the list of custom rules
#### Rule sources
If you use the F-Droid version of Nebulo there are already a number of rule sources added by default.<br>
If not, click on the plus button in the round circle to add a new source. You can either input an URL or choose a file from your phone.<br>
If you define a source as whitelist source by selecting the checkbox the hosts in this source will never be blocked by other DNS rules.<br>
Newly added sources are enabled by default and you can toggle them by clicking on the switch of each row.
To delete a source click on the trashcan.<br>
Any of those actions requires a refresh (explained in the next text).
#### Refreshing the sources
After changing the rule sources (enabling, deleting, adding or disabling a rule source) you have to refresh the DNS rules.
This is required because Nebulo has to download and parse the rule sources.<br>
When Nebulo is importing the sources a notification appears in which you can see the current progress.<br><br>
To refresh the sources click on the round refresh icon located in the center at the bottom.
A dialog will appear where you can either schedule a recurring refresh or click on 'Refresh now' to process the changes instantly.<br>
If you want to schedule a recurring refresh select the checkbox, choose your desired time between refreshes and click ok to save (clicking 'refresh now' or cancel won't save it!)
<br>
Alternativly to process every source you also can just refresh a single source.
To do that click on the refresh icon of a single row instead of the one at the bottom in the circle.<br>
You do not have to refresh the sources if you only changed custom rules.
You might have to restart the app though if Nebulo is running (by clicking stop and then start) because the rule might be cached (either by Nebulo or the system)
#### Can I edit sources/custom rules after creating them?
Sure! Just click on them once.
### How do I check whether a host is blocked?
To check whether a host is blocked click on the magnifying glass in the top bar.<br>
A dialog will appear where you can input a host.
After a few moments a text will appear telling you whether this host is part of a rule and which source it is a part of (if any)
### Can I export my combined rules?
Yes, you can!<br>
To do that click on the arrow-up icon in the circle at the bottom of the page.<br>
A dialog will appear asking you what you want to export.
Whitelist and normal DNS rules cannot be combined and have to be exported separately.
### What are whitelists for?
By whitelisting a custom rule (or setting a source as whitelist source), the host(s) are never blocked by any other DNS rule.
It allows you to unblock hosts, which might have been blocked by accident.
### Can I block multiple hosts at once with a single DNS rule? (Wildcards)
Yes, you can!<br>
Nebulo supports wildcards which allows you to specify a pattern of hosts you want to block or whitelist.
With this you can for example block/whitelist every host which contains a certain word or ends in a certain host.<br>
To see some examples of what you have to input for the host when creating a custom rule, click [here](docs/DNSRULE_WILDCARDS.md).<br>
It's a really powerful method of blocking hosts which have a common pattern (for example ad-servers which most of the time end in the same host, but do have random subdomains)
\ No newline at end of file
......@@ -30,12 +30,7 @@ The second topic, tracking, is nearly as important as the topic of censorship. M
Nebulo uses the VPN API of the Android system to create a dummy VPN which intercepts all packets for the dns servers of your device. This dummy VPN is __not__ a real VPN and does not tunnel your packets - it only handles dns packets. As only one VPN can be activate at any given time you have to decice between using Nebulo or a real VPN.
## Non-VPN mode
Since 1.4.0 Nebulo can run without requiring the dummy VPN. In this mode Nebulo hosts a DNS server locally, which forwards all DNS queries it receives according to the settings you configured in Nebulo.<br>
In this mode you manually have to forward all the DNS queries your decice creates to Nebulos local DNS server (normally this is what the dummy VPN is used for).<br>
If your device is rooted Nebulo has an inbuilt solution using `iptables`. If it isn't rooted you have to use third-party apps which are able to forward the DNS queries to Nebulo.
Known third-party apps this works with are NetGuard and V2Ray (although there might be others). You can find instructions on how to configure these apps to work together with Nebulo in the settings.<br>
Please note that the App exclusion setting inside the general category won't have any effect in non-VPN mode. You have to configure excluded apps inside the third-party app you are using.
Look in the [FAQ](FAQ.md).
## What this is based on
Nebulo is a completely original piece of software. It doesn't use any other dependency under the hood for the dns capabilities. Check the [dependencies](https://git.frostnerd.com/PublicAndroidApps/smokescreen/blob/master/app/build.gradle#L100) to see what is used for everything build around DoH/DoT.
......@@ -57,7 +52,7 @@ The app consists of a few core features:
* Query logging
* DNS rules, where you can specify your own IP addresses for hosts
* Rules can be imported from files and URLs (supports 4 different formats)
* You can block hosts by using 0.0.0.0 and ::1 as targets
* You can block hosts by using 0.0.0.0 and ::1 as targets (If you check the box "Block this host" those targets are automatically used. You don't have to do anything else.)
* The DNS rules prevent CNAME cloaking
* Highly customizable settings
* Disable IPv4/IPv6
......@@ -65,6 +60,9 @@ The app consists of a few core features:
* Allow search domains on the current network
* ... And more
# FAQ
For a growing collection of frequenty asked questions (FAQ), take a look [here](FAQ.md).
# Help wanted
Requesting your support: the app is getting closer to a proper release but it's still missing an important aspect: translations.
Translations are important to reach as broad of an audience as possible and for non-english speakers to be able to use the app to it's full extent.
......
......@@ -11,8 +11,8 @@ android {
applicationId "com.frostnerd.smokescreen"
minSdkVersion 21
targetSdkVersion 29
versionCode 62
versionName "1.4.0"
versionCode 65
versionName "1.4.1.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("Boolean", "FROM_CI", String.valueOf(getSystemVariableOrDefault("CI_COMMIT_SHORT_SHA", "") != ""))
......@@ -128,7 +128,7 @@ dependencies {
implementation 'com.frostnerd.utilskt:preferences:1.5.29' // https://git.frostnerd.com/AndroidUtils/preferenceskt
implementation 'com.frostnerd.utilskt:navigationdraweractivity:1.4.0' // https://git.frostnerd.com/AndroidUtils/navigationdraweractivity
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.6.0' // https://git.frostnerd.com/AndroidUtils/encrypteddnstunnelproxy
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.6.3' // https://git.frostnerd.com/AndroidUtils/encrypteddnstunnelproxy
implementation 'com.frostnerd.utilskt:general:1.0.25' // https://git.frostnerd.com/AndroidUtils/generalkt
implementation 'com.frostnerd.utilskt:adapters:1.2.0' // https://git.frostnerd.com/AndroidUtils/Adapters
......
......@@ -4,10 +4,7 @@ import android.app.Activity
import android.app.AlarmManager
import android.app.KeyguardManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.*
import android.hardware.fingerprint.FingerprintManager
import android.net.ConnectivityManager
import android.net.Network
......@@ -15,6 +12,7 @@ import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
......@@ -423,4 +421,17 @@ val Context.isPrivateDnsActive: Boolean
if (it.activeNetwork == null) false
else it.getLinkProperties(it.activeNetwork)?.isPrivateDnsActive ?: false
}
}
\ No newline at end of file
}
fun Context.tryOpenBrowser(withLink:String) {
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(withLink)
)
)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, R.string.error_no_webbrowser_installed, Toast.LENGTH_LONG).show()
}
}
\ No newline at end of file
......@@ -48,6 +48,13 @@ fun showPrivacyPolicyDialog(context: Context) {
dialog.show()
}
fun showInfoTextDialogWithClose(
context: Context,
title: String,
text: String,
withDialog: (AlertDialog.() -> Unit)? = null,
) = showInfoTextDialog(context, title, text, neutralButton = null, positiveButton = context.getString(R.string.all_close) to null, withDialog = withDialog)
fun showInfoTextDialog(context:Context,
title:String,
text:String,
......
......@@ -104,7 +104,7 @@ class MainActivity : NavigationDrawerActivity() {
view.dns2.text = secondaryAddress
}
val latency = DnsSpeedTest(server, log = {}).runTest(1)
val latency = DnsSpeedTest(server, log = {}).runTest(3, DnsSpeedTest.Strategy.WEIGHTED_AVERAGE)
runOnUiThread {
view.latency.text = if (latency != null && latency > 0) {
"$latency ms"
......@@ -125,6 +125,11 @@ class MainActivity : NavigationDrawerActivity() {
update()
}
}
view.infoButton.setOnClickListener {
showInfoTextDialogWithClose(this,
getString(R.string.dialog_latency_sidebar_title),
getString(R.string.dialog_latency_sidebar_message))
}
networkManager.registerNetworkCallback(NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
}.build(), cardNetworkCallback!!)
......
......@@ -175,9 +175,9 @@ class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: (
}
}
isBlockHost = dnsRule.target == "0.0.0.0" && dnsRule.ipv6Target == "::1"
if(!isBlockHost) {
view.blockHost.isChecked = false
}
}
if(!isBlockHost) {
view.blockHost.isChecked = false
}
}
}
......
package com.frostnerd.smokescreen.dialog
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.DnsRule
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.getPreferences
import kotlinx.android.synthetic.main.dialog_dnsrule_search.view.*
import kotlinx.coroutines.*
import org.minidns.record.Record
import kotlin.coroutines.CoroutineContext
/*
* Copyright (C) 2020 Daniel Wolf (Ch4t4r)
*
* 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/>.
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class DnsRuleSearchDialog(
context: Context
):AlertDialog(context, context.getPreferences().theme.dialogStyle), CoroutineScope {
val supervisor = SupervisorJob()
override val coroutineContext: CoroutineContext = supervisor + Dispatchers.IO
var currentSearchJob:Job? = null
private val watcher = object :TextWatcher{
private var previousSearch = ""
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val search = s.toString().trim()
if (search == previousSearch)
return
previousSearch = search
currentSearchJob?.cancel()
if(search.isNotBlank()) currentSearchJob = this@DnsRuleSearchDialog.launch {
delay(500) // debounce
if (search != previousSearch)
return@launch
val rule = findRuleForSearch(search)
val hostSource = rule?.importedFrom?.let { getHostSourceForId(it) }
if(isActive) launch(Dispatchers.Main) {
if(rule == null) displayRuleSource(wasFound = false, isUserRule = false, null)
else displayRuleSource(wasFound = true, isUserRule = rule.importedFrom == null, hostSource)
}
}
else clearSearchResultText()
}
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
}
val view: View
init {
view = layoutInflater.inflate(R.layout.dialog_dnsrule_search, null, false)
setTitle(R.string.dialog_dnsrules_search_title)
setView(view)
setButton(BUTTON_POSITIVE, context.getText(R.string.all_close)) {_,_ ->
supervisor.cancel()
}
view.searchTerm.addTextChangedListener(watcher)
setOnDismissListener {
supervisor.cancel()
}
}
private fun findRuleForSearch(searchTerm: String): DnsRule? {
var source: DnsRule? = null
if (searchTerm.startsWith("www", ignoreCase = true)) {
source = context.getDatabase().dnsRuleDao().findRuleTargetEntity(
searchTerm.replaceFirst("www.", "", ignoreCase = true),
Record.TYPE.ANY,
true
)
}
source = source ?: context.getDatabase().dnsRuleDao()
.findRuleTargetEntity(searchTerm, Record.TYPE.ANY, true)
return source ?: context.getDatabase().dnsRuleDao().findPossibleWildcardRuleTarget(
searchTerm, type = Record.TYPE.ANY,
useUserRules = true,
includeWhitelistEntries = false,
includeNonWhitelistEntries = true
).firstOrNull {
DnsRuleDialog.databaseHostToMatcher(it.host).reset(searchTerm).matches()
}
}
private fun getHostSourceForId(id:Long):HostSource? {
return context.getDatabase().hostSourceDao().findById(id)
}
private fun displayRuleSource(wasFound:Boolean, isUserRule:Boolean, hostSource: HostSource?) {
val text = if(wasFound) {
if(isUserRule) {
context.getString(R.string.dialog_dnsrules_status_userrule)
} else {
if(hostSource == null) {
context.getString(R.string.dialog_dnsrules_status_sourcenotfound)
} else {
context.getString(R.string.dialog_dnsrules_status_fromsource, hostSource.name)
}
}
} else {
context.getString(R.string.dialog_dnsrules_status_not_found)
}
view.searchResult.text = text
}
private fun clearSearchResultText() {
view.searchResult.text = ""
}
}
\ No newline at end of file
......@@ -80,7 +80,7 @@ class NewServerDialog(
}?.takeIf {
it.pathSegments.size == 1 && it.pathSegments[0] == "" // But isn't an URL (no path in string)
}?.takeIf {
it.host.contains(Regex("[.:]")) // It should contain at least one period (or colon for IPv6), either because it's an IP Address or a domain (like test.com). This explicitly prevents local-only domains (like localhost or raspberry)
it.host.contains(Regex("[.:]")) // It should contain at least one period (or colon for IPv6), either because it's an IP Address or a domain (like test.com). This explicitly prevents local-only domains (like localhost or raspberry)
}?.let { url ->
val port = if(s.indexOf(":").let {
it == -1 || s.toCharArray().getOrNull(it + 1)?.isDigit() != true
......@@ -95,9 +95,15 @@ class NewServerDialog(
}
}.firstOrNull()?.servers?.firstOrNull {
it.address.host == url.host
}?.address ?: TLSUpstreamAddress(url.host, port)
}?.address ?: TLSUpstreamAddress(applyCase(s.split(":").first(), url.host), port)
}
}
private fun applyCase(from:String, to:String):String {
val index = to.indexOf(from, ignoreCase = true).takeIf {
it >= 0
} ?: return to
return to.replaceRange(index, index + from.length, from)
}
}
init {
......@@ -301,7 +307,12 @@ class NewServerDialog(
context.log("Creating HttpsUpstreamAddress for `$url`")
val parsedUrl = url.toHttpUrl()
val host = parsedUrl.host
val hostFromInput = url.indexOf(parsedUrl.host, ignoreCase = true).takeIf {
it > 0
}?.let {
url.subSequence(it, it + parsedUrl.host.length).toString()
}
val host = hostFromInput ?: parsedUrl.host
val port = parsedUrl.port
val path = parsedUrl.pathSegments.takeIf {
it.isNotEmpty() && (it.size > 1 || !it.contains("")) // Non-empty AND contains something other than "" (empty string used when there is no path)
......
package com.frostnerd.smokescreen.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
......@@ -7,6 +8,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.database.AppDatabase
......@@ -115,5 +117,8 @@ class AboutFragment : Fragment() {
if(queryGenStepOne) QueryGeneratorDialog(requireContext())
true
}
view.faq.setOnClickListener {
requireContext().tryOpenBrowser("https://nebulo.app/faq")
}
}
}
\ No newline at end of file
......@@ -533,13 +533,20 @@ class DnsRuleFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_dnsrule, menu)
val switch = menu.getItem(0)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
val switch = menu.findItem(R.id.rulesEnabled)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
val search = menu.findItem(R.id.search)
switch?.isChecked = getPreferences().dnsRulesEnabled.also {
overlay.visibility = if(it) View.GONE else View.VISIBLE
search.isEnabled = it
}
switch?.setOnCheckedChangeListener { _, isChecked ->
getPreferences().dnsRulesEnabled = isChecked
overlay.visibility = if(isChecked) View.GONE else View.VISIBLE
search.isEnabled = isChecked
}
search?.setOnMenuItemClickListener {
DnsRuleSearchDialog(requireContext()).show()
true
}
}
......@@ -659,7 +666,7 @@ class DnsRuleFragment : Fragment() {
}
companion object {
const val latestSourcesVersion = 3
const val latestSourcesVersion = 4
private val defaultHostSources:Map<Int, List<HostSource>> by lazy(LazyThreadSafetyMode.NONE) {
mutableMapOf<Int, List<HostSource>>().apply {
put(1, mutableListOf(
......@@ -684,11 +691,17 @@ class DnsRuleFragment : Fragment() {
).apply {
forEach { it.enabled = false }
})
put(4, mutableListOf(
HostSource("Energized Unified", "https://block.energized.pro/unified/formats/domains.txt", false).apply {
enabled = false
}
))
}
}
private val updatedHostSources:Map<Int, List<HostSource>> by lazy {
mutableMapOf<Int, List<HostSource>>().apply {
put(3, (defaultHostSources[1] ?: error("")).subList(0, 4))
put(4, mutableListOf(defaultHostSources.getValue(1)[4]))
}
}
......
......@@ -230,6 +230,7 @@ class MainFragment : Fragment() {
private fun updateVpnIndicators() {
val privateDnsActive = requireContext().isPrivateDnsActive
var startButtonVisibility = View.VISIBLE
var privacyTextVisibility = View.VISIBLE
when(proxyState) {
ProxyState.RUNNING -> {
privateDnsInfo.visibility = View.INVISIBLE
......@@ -252,6 +253,7 @@ class MainFragment : Fragment() {
if (privateDnsActive) {
statusImage.setImageResource(R.drawable.ic_lock)
statusImage.clearAnimation()
privacyTextVisibility = View.INVISIBLE
startButtonVisibility = View.INVISIBLE
privateDnsInfo.visibility = View.VISIBLE
} else {
......@@ -262,6 +264,7 @@ class MainFragment : Fragment() {
}
}
startButton.visibility = startButtonVisibility
privacyTextWrap.visibility = privacyTextVisibility