From 772dfdef8c01388966a2a6c8fd80c045cfc98ece Mon Sep 17 00:00:00 2001
From: Revertron <105154+Revertron@users.noreply.github.com>
Date: Sun, 30 Oct 2022 22:14:30 +0100
Subject: [PATCH] Added DNS configuration functionality. (#24)
* Changed app icon from default to Yggdrasil leaf.
* Added workaround for DNS-reslver and fix for unmetered networks.
* Added DNS configuration functionality.
* Changed DNS configuration UI. Disabled DNS config by default. Added DNS fix for Chrome-based browsers.
---
app/build.gradle | 1 +
app/src/main/AndroidManifest.xml | 6 +-
.../eu/neilalexander/yggdrasil/DnsActivity.kt | 166 ++++++++++++
.../neilalexander/yggdrasil/MainActivity.kt | 24 +-
.../yggdrasil/PacketTunnelProvider.kt | 31 ++-
.../neilalexander/yggdrasil/PeersActivity.kt | 10 +-
app/src/main/res/layout/activity_dns.xml | 245 ++++++++++++++++++
app/src/main/res/layout/activity_main.xml | 50 +++-
app/src/main/res/layout/activity_peers.xml | 4 +-
.../main/res/layout/dialog_add_dns_server.xml | 34 +++
app/src/main/res/layout/dialog_addpeer.xml | 1 +
app/src/main/res/layout/dns_server_usable.xml | 47 ++++
app/src/main/res/layout/peers_configured.xml | 3 +-
app/src/main/res/values/strings.xml | 17 ++
build.gradle | 5 +-
readme.md | 2 +-
16 files changed, 620 insertions(+), 26 deletions(-)
create mode 100644 app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt
create mode 100644 app/src/main/res/layout/activity_dns.xml
create mode 100644 app/src/main/res/layout/dialog_add_dns_server.xml
create mode 100644 app/src/main/res/layout/dns_server_usable.xml
diff --git a/app/build.gradle b/app/build.gradle
index 213ae2b..52a4d45 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -55,6 +55,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.preference:preference-ktx:1.1.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 776bbad..9a1557c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,17 +16,19 @@
android:parentActivityName=".MainActivity" />
-
+
+
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:exported="true">
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt
new file mode 100644
index 0000000..fc1c3ef
--- /dev/null
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt
@@ -0,0 +1,166 @@
+package eu.neilalexander.yggdrasil
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.content.SharedPreferences
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.*
+import com.google.android.material.textfield.TextInputEditText
+
+const val KEY_DNS_SERVERS = "dns_servers"
+const val KEY_ENABLE_CHROME_FIX = "enable_chrome_fix"
+const val DEFAULT_DNS_SERVERS = "302:7991::53,302:db60::53,300:6223::53,301:1088::53"
+
+class DnsActivity : AppCompatActivity() {
+ private lateinit var config: ConfigurationProxy
+ private lateinit var inflater: LayoutInflater
+
+ private lateinit var serversTableLayout: TableLayout
+ private lateinit var serversTableLabel: TextView
+ private lateinit var serversTableHint: TextView
+ private lateinit var addServerButton: ImageButton
+ private lateinit var enableChromeFix: Switch
+
+ private lateinit var servers: MutableList
+ private lateinit var preferences: SharedPreferences
+
+ @SuppressLint("ApplySharedPref")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_dns)
+
+ config = ConfigurationProxy(applicationContext)
+ inflater = LayoutInflater.from(this)
+
+ serversTableLayout = findViewById(R.id.configuredDnsTableLayout)
+ serversTableLabel = findViewById(R.id.configuredDnsLabel)
+ serversTableHint = findViewById(R.id.configuredDnsHint)
+ enableChromeFix = findViewById(R.id.enableChromeFix)
+
+ addServerButton = findViewById(R.id.addServerButton)
+ addServerButton.setOnClickListener {
+ val view = inflater.inflate(R.layout.dialog_add_dns_server, null)
+ val input = view.findViewById(R.id.addDnsInput)
+ val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog))
+ builder.setTitle(getString(R.string.dns_add_server_dialog_title))
+ builder.setView(view)
+ builder.setPositiveButton(getString(R.string.add)) { dialog, _ ->
+ servers.add(input.text.toString())
+ preferences.edit().apply {
+ putString(KEY_DNS_SERVERS, servers.joinToString(","))
+ commit()
+ }
+ dialog.dismiss()
+ updateConfiguredServers()
+ }
+ builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
+ dialog.cancel()
+ }
+ builder.show()
+ }
+
+ enableChromeFix.setOnCheckedChangeListener { _, isChecked ->
+ preferences.edit().apply {
+ putBoolean(KEY_ENABLE_CHROME_FIX, isChecked)
+ commit()
+ }
+ }
+
+ preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ val serverString = preferences.getString(KEY_DNS_SERVERS, "")
+ servers = if (serverString!!.isNotEmpty()) {
+ serverString.split(",").toMutableList()
+ } else {
+ mutableListOf()
+ }
+ updateUsableServers()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateConfiguredServers()
+ enableChromeFix.isChecked = preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)
+ }
+
+ @SuppressLint("ApplySharedPref")
+ private fun updateConfiguredServers() {
+ when (servers.size) {
+ 0 -> {
+ serversTableLayout.visibility = View.GONE
+ serversTableLabel.text = getString(R.string.dns_no_configured_servers)
+ serversTableHint.text = getText(R.string.dns_configured_servers_hint_empty)
+ }
+ else -> {
+ serversTableLayout.visibility = View.VISIBLE
+ serversTableLabel.text = getString(R.string.dns_configured_servers)
+ serversTableHint.text = getText(R.string.dns_configured_servers_hint)
+
+ serversTableLayout.removeAllViewsInLayout()
+ for (i in servers.indices) {
+ val server = servers[i]
+ val view = inflater.inflate(R.layout.peers_configured, null)
+ view.findViewById(R.id.addressValue).text = server
+ view.findViewById(R.id.deletePeerButton).tag = i
+
+ view.findViewById(R.id.deletePeerButton).setOnClickListener { button ->
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ builder.setTitle("Remove ${server}?")
+ builder.setPositiveButton(getString(R.string.remove)) { dialog, _ ->
+ servers.removeAt(button.tag as Int)
+ preferences.edit().apply {
+ this.putString(KEY_DNS_SERVERS, servers.joinToString(","))
+ this.commit()
+ }
+ dialog.dismiss()
+ updateConfiguredServers()
+ }
+ builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
+ dialog.cancel()
+ }
+ builder.show()
+ }
+ serversTableLayout.addView(view)
+ }
+ }
+ }
+ }
+
+ @SuppressLint("ApplySharedPref")
+ private fun updateUsableServers() {
+ val usableTableLayout: TableLayout = findViewById(R.id.usableDnsTableLayout)
+ val defaultServers = DEFAULT_DNS_SERVERS.split(",")
+
+ defaultServers.forEach {
+ val server = it
+ val view = inflater.inflate(R.layout.dns_server_usable, null)
+ view.findViewById(R.id.serverValue).text = server
+ val addButton = view.findViewById(R.id.addButton)
+ addButton.tag = server
+
+ addButton.setOnClickListener { button ->
+ servers.add(button.tag as String)
+ preferences.edit().apply {
+ this.putString(KEY_DNS_SERVERS, servers.joinToString(","))
+ this.commit()
+ }
+ updateConfiguredServers()
+ }
+ view.setOnLongClickListener {
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ builder.setTitle(getString(R.string.dns_server_info_dialog_title))
+ builder.setMessage(getText(R.string.dns_server_info_revertron))
+ builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
+ dialog.dismiss()
+ }
+ builder.show()
+ true
+ }
+
+ usableTableLayout.addView(view)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
index 769b803..ea56090 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
@@ -27,6 +27,8 @@ class MainActivity : AppCompatActivity() {
private lateinit var coordinatesLabel: TextView
private lateinit var peersLabel: TextView
private lateinit var peersRow: TableRow
+ private lateinit var dnsLabel: TextView
+ private lateinit var dnsRow: TableRow
private lateinit var settingsRow: TableRow
private fun start() {
@@ -47,13 +49,15 @@ class MainActivity : AppCompatActivity() {
findViewById(R.id.versionValue).text = Mobile.getVersion()
- enabledSwitch = findViewById(R.id.enableMulticastBeacon)
+ enabledSwitch = findViewById(R.id.enableYggdrasil)
enabledLabel = findViewById(R.id.yggdrasilStatusLabel)
ipAddressLabel = findViewById(R.id.ipAddressValue)
subnetLabel = findViewById(R.id.subnetValue)
coordinatesLabel = findViewById(R.id.coordinatesValue)
peersLabel = findViewById(R.id.peersValue)
peersRow = findViewById(R.id.peersTableRow)
+ dnsLabel = findViewById(R.id.dnsValue)
+ dnsRow = findViewById(R.id.dnsTableRow)
settingsRow = findViewById(R.id.settingsTableRow)
enabledLabel.setTextColor(Color.GRAY)
@@ -82,6 +86,12 @@ class MainActivity : AppCompatActivity() {
startActivity(intent)
}
+ dnsRow.isClickable = true
+ dnsRow.setOnClickListener {
+ val intent = Intent(this, DnsActivity::class.java)
+ startActivity(intent)
+ }
+
settingsRow.isClickable = true
settingsRow.setOnClickListener {
val intent = Intent(this, SettingsActivity::class.java)
@@ -94,6 +104,18 @@ class MainActivity : AppCompatActivity() {
LocalBroadcastManager.getInstance(this).registerReceiver(
receiver, IntentFilter(PacketTunnelState.RECEIVER_INTENT)
)
+ val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ val serverString = preferences.getString(KEY_DNS_SERVERS, "")
+ if (serverString!!.isNotEmpty()) {
+ val servers = serverString.split(",")
+ dnsLabel.text = when (servers.size) {
+ 0 -> "No servers"
+ 1 -> "1 server"
+ else -> "${servers.size} servers"
+ }
+ } else {
+ dnsLabel.text = "No servers"
+ }
}
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt
index d92ce8f..dd8afc3 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt
@@ -17,6 +17,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
+private const val TAG = "PacketTunnelProvider"
+
class PacketTunnelProvider: VpnService() {
companion object {
const val RECEIVER_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.MESSAGE"
@@ -50,16 +52,16 @@ class PacketTunnelProvider: VpnService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
- Log.d("PacketTunnelProvider", "Intent is null")
+ Log.d(TAG, "Intent is null")
return START_NOT_STICKY
}
return when (intent.action ?: ACTION_STOP) {
ACTION_STOP -> {
- Log.d("PacketTunnelProvider", "Stopping...")
+ Log.d(TAG, "Stopping...")
stop(); START_NOT_STICKY
}
else -> {
- Log.d("PacketTunnelProvider", "Starting...")
+ Log.d(TAG, "Starting...")
start(); START_STICKY
}
}
@@ -70,11 +72,11 @@ class PacketTunnelProvider: VpnService() {
return
}
- Log.d("PacketTunnelProvider", config.getJSON().toString())
+ Log.d(TAG, config.getJSON().toString())
yggdrasil.startJSON(config.getJSONByteArray())
val address = yggdrasil.addressString
- var builder = Builder()
+ val builder = Builder()
.addAddress(address, 7)
.addRoute("200::", 7)
// We do this to trick the DNS-resolver into thinking that we have "regular" IPv6,
@@ -97,6 +99,21 @@ class PacketTunnelProvider: VpnService() {
builder.setMetered(false)
}
+ val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ val serverString = preferences.getString(KEY_DNS_SERVERS, "")
+ if (serverString!!.isNotEmpty()) {
+ val servers = serverString.split(",")
+ if (servers.isNotEmpty()) {
+ servers.forEach {
+ Log.i(TAG, "Using DNS server $it")
+ builder.addDnsServer(it)
+ }
+ }
+ }
+ if (preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)) {
+ builder.addRoute("2001:4860:4860::8888", 128)
+ }
+
parcel = builder.establish()
val parcel = parcel
if (parcel == null || !parcel.fileDescriptor.valid()) {
@@ -123,7 +140,7 @@ class PacketTunnelProvider: VpnService() {
intent.putExtra("ip", yggdrasil.addressString)
intent.putExtra("subnet", yggdrasil.subnetString)
intent.putExtra("coords", yggdrasil.coordsString)
- intent.putExtra("peers", JSONArray(yggdrasil.peersJSON).length())
+ intent.putExtra("peers", yggdrasil.peersJSON)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@@ -214,7 +231,7 @@ class PacketTunnelProvider: VpnService() {
}
private fun reader() {
- var b = ByteArray(65535)
+ val b = ByteArray(65535)
reads@ while (started.get()) {
val readerStream = readerStream
val readerThread = readerThread
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt
index af76c8b..b9ec651 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt
@@ -49,14 +49,14 @@ class PeersActivity : AppCompatActivity() {
addPeerButton = findViewById(R.id.addPeerButton)
addPeerButton.setOnClickListener {
- var view = inflater.inflate(R.layout.dialog_addpeer, null)
- var input = view.findViewById(R.id.addPeerInput)
+ val view = inflater.inflate(R.layout.dialog_addpeer, null)
+ val input = view.findViewById(R.id.addPeerInput)
val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog))
builder.setTitle("Add Configured Peer")
builder.setView(view)
builder.setPositiveButton("Add") { dialog, _ ->
config.updateJSON { json ->
- json.getJSONArray("Peers").put(input.text)
+ json.getJSONArray("Peers").put(input.text.toString().trim())
}
dialog.dismiss()
updateConfiguredPeers()
@@ -90,7 +90,7 @@ class PeersActivity : AppCompatActivity() {
configuredTableLayout.removeAllViewsInLayout()
for (i in 0 until peers.length()) {
val peer = peers[i].toString()
- var view = inflater.inflate(R.layout.peers_configured, null)
+ val view = inflater.inflate(R.layout.peers_configured, null)
view.findViewById(R.id.addressValue).text = peer
view.findViewById(R.id.deletePeerButton).tag = i
@@ -130,7 +130,7 @@ class PeersActivity : AppCompatActivity() {
connectedTableLayout.removeAllViewsInLayout()
for (i in 0 until peers.length()) {
val peer = peers.getJSONObject(i)
- var view = inflater.inflate(R.layout.peers_connected, null)
+ val view = inflater.inflate(R.layout.peers_connected, null)
val ip = peer.getString("IP")
view.findViewById(R.id.addressLabel).text = ip
view.findViewById(R.id.detailsLabel).text = peer.getString("Remote")
diff --git a/app/src/main/res/layout/activity_dns.xml b/app/src/main/res/layout/activity_dns.xml
new file mode 100644
index 0000000..132f2a0
--- /dev/null
+++ b/app/src/main/res/layout/activity_dns.xml
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 9b2fcd8..2064b49 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -94,7 +94,7 @@
android:layout_weight="2" />
@@ -347,6 +347,50 @@
app:srcCompat="@drawable/ic_baseline_chevron_right_24" />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_peers.xml b/app/src/main/res/layout/activity_peers.xml
index 3001949..a9d74b0 100644
--- a/app/src/main/res/layout/activity_peers.xml
+++ b/app/src/main/res/layout/activity_peers.xml
@@ -128,7 +128,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16pt"
android:layout_marginLeft="16pt"
- android:layout_marginTop="2pt"
+ android:layout_marginTop="4pt"
android:layout_marginEnd="8pt"
android:layout_marginRight="8pt"
android:layout_marginBottom="4pt"
@@ -237,7 +237,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16pt"
android:layout_marginLeft="16pt"
- android:layout_marginTop="2pt"
+ android:layout_marginTop="4pt"
android:layout_marginEnd="8pt"
android:layout_marginRight="8pt"
android:alpha="0.7"
diff --git a/app/src/main/res/layout/dialog_add_dns_server.xml b/app/src/main/res/layout/dialog_add_dns_server.xml
new file mode 100644
index 0000000..341c502
--- /dev/null
+++ b/app/src/main/res/layout/dialog_add_dns_server.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_addpeer.xml b/app/src/main/res/layout/dialog_addpeer.xml
index c627c5c..0e334d2 100644
--- a/app/src/main/res/layout/dialog_addpeer.xml
+++ b/app/src/main/res/layout/dialog_addpeer.xml
@@ -27,6 +27,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4pt"
+ android:lines="1"
android:hint="tcp://address:port" />
diff --git a/app/src/main/res/layout/dns_server_usable.xml b/app/src/main/res/layout/dns_server_usable.xml
new file mode 100644
index 0000000..d26e788
--- /dev/null
+++ b/app/src/main/res/layout/dns_server_usable.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/peers_configured.xml b/app/src/main/res/layout/peers_configured.xml
index 82615d5..19ede91 100644
--- a/app/src/main/res/layout/peers_configured.xml
+++ b/app/src/main/res/layout/peers_configured.xml
@@ -1,7 +1,6 @@
@@ -16,7 +15,7 @@
android:layout_marginBottom="12dp"
android:ellipsize="end"
android:singleLine="true"
- android:text="TextView"
+ android:text=""
android:textColor="?attr/textDefault"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/deletePeerButton"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c2e6bce..a8469d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,20 @@
Yggdrasil
+ Yggdrasil will use these DNS servers in VPN config when service starts. All your DNS requests will be resolved by them.
+ Yggdrasil will not configure any DNS servers when service starts. All your DNS requests will be resolved by system resolver.
+ These are DNS servers run by community members. Click plus button to add them to a list above. Long-tap to see more info.
+ No servers configured
+ Configured servers
+ The server supports resolving regular ICANN domains, ALFIS domains, OpenNIC domains.\n\nAlso, it blocks ads, analytics and malware websites.\n\nThe server is run by Revertron.
+ Server info
+ OK
+ Cancel
+ Remove
+ Add
+ Add DNS server
+ DNS
+ Usable servers
+ Fix Chrome-based browsers
+ Chrome-based browsers need an additional fix to understand that you have IPv6 connectivity.
+ DNS fixes
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index b090095..0cf859d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = "1.5.0"
+ ext.kotlin_version = '1.7.20'
repositories {
google()
mavenCentral()
}
dependencies {
- classpath "com.android.tools.build:gradle:4.2.1"
+ classpath "com.android.tools.build:gradle:4.2.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@@ -18,7 +18,6 @@ allprojects {
repositories {
google()
mavenCentral()
- jcenter() // Warning: this repository is going to shut down soon
}
}
diff --git a/readme.md b/readme.md
index a4f2ec1..69fe52e 100644
--- a/readme.md
+++ b/readme.md
@@ -26,7 +26,7 @@ cp /tmp/yggdrasil-go/yggdrasil.aar /tmp/yggdrasil-android/app/libs/
```
cd /tmp/yggdrasil-android
-./gradew assembleRelease
+./gradlew assembleRelease
```
note: you will need to use jdk-11 as jdk-16 `"doesn't work" ™`