[
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Build\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n  workflow_dispatch:\n  release:\n    types: [published]\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Check out\n      uses: actions/checkout@v6\n\n    - name: Check out Yggdrasil\n      uses: actions/checkout@v6\n      with:\n        repository: yggdrasil-network/yggdrasil-go\n        path: yggdrasil-go\n        ref: master\n        fetch-depth: 0\n    \n    - name: Setup Go environment\n      uses: actions/setup-go@v6\n      with:\n        go-version: stable\n    \n    - name: Install gomobile\n      run: |\n        go install golang.org/x/mobile/cmd/gomobile@latest\n        ~/go/bin/gomobile init\n    \n    - name: Set up JDK 17\n      uses: actions/setup-java@v5\n      with:\n        java-version: '17'\n        distribution: 'temurin'\n        cache: gradle\n        \n    - name: Install NDK\n      uses: nttld/setup-ndk@v1\n      id: setup-ndk\n      with:\n        ndk-version: r21e\n        add-to-path: false\n      \n    - name: Build Yggdrasil\n      run: |\n        mkdir app/libs\n        cd yggdrasil-go\n        PATH=$PATH:~/go/bin/ ./contrib/mobile/build -a\n        cp {yggdrasil.aar,yggdrasil-sources.jar} ../app/libs\n      env:\n        ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}\n\n    - name: Gradle build  \n      if: github.event_name != 'release' && github.ref_name != 'main'\n      run: |\n        chmod +x gradlew\n        ./gradlew buildRelease\n\n    - name: Gradle signed build\n      if: github.event_name == 'release' || github.ref_name == 'main'\n      run: |\n        echo \"${{ secrets.RELEASE_KEYSTORE }}\" > app/gha.keystore.asc\n        gpg -d --passphrase \"${{ secrets.RELEASE_KEYSTORE_PASSWORD }}\" --batch app/gha.keystore.asc > app/gha.jks\n        chmod +x gradlew\n        ./gradlew assembleYggdrasil\n\n    - name: Upload build artifact\n      if: github.event_name == 'release' || github.ref_name == 'main'\n      uses: actions/upload-artifact@v7\n      with:\n        name: yggdrasil-android\n        path: app/build/outputs/apk/yggdrasil/app-yggdrasil.apk\n        \n    - name: Upload release artifact\n      if: github.event_name == 'release'\n      uses: actions/upload-release-asset@v1\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        upload_url: ${{ github.event.release.upload_url }}\n        asset_path: app/build/outputs/apk/yggdrasil/app-yggdrasil.apk\n        asset_name: yggdrasil-android.apk\n        asset_content_type: application/vnd.android.package-archive\n"
  },
  {
    "path": ".gitignore",
    "content": "*.apk\n*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n/app/libs/yggdrasil.aar\n/app/libs/yggdrasil-sources.jar\n/app/release/*\n/app/release\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"libs/yggdrasil-go\"]\n\tpath = libs/yggdrasil-go\n\turl = https://github.com/yggdrasil-network/yggdrasil-go\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Yggdrasil Developers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/build.gradle",
    "content": "plugins {\n    id 'com.android.application'\n    id 'kotlin-android'\n}\n\nandroid {\n    compileSdkVersion 34\n\n    defaultConfig {\n        applicationId \"eu.neilalexander.yggdrasil\"\n        minSdkVersion 21\n        targetSdkVersion 34\n        versionCode 21\n        versionName \"0.1-021\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    signingConfigs {\n        create(\"yggdrasil\") {\n            // You need to specify either an absolute path or include the\n            // keystore file in the same directory as the build.gradle file.\n            storeFile = file(\"gha.jks\")\n            storePassword = \"g1thub4ct10n34yggdr4s1l4ndr01d\"\n            keyAlias = \"yggdrasil-android\"\n            keyPassword = \"g1thub4ct10n34yggdr4s1l4ndr01d\"\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n        yggdrasil {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n            signingConfig = signingConfigs.getByName(\"yggdrasil\")\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n    namespace 'eu.neilalexander.yggdrasil'\n}\n\ndependencies {\n    implementation fileTree(include: ['*.aar'], dir: 'libs')\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version\"\n    implementation 'androidx.core:core-ktx:1.13.1'\n    implementation 'androidx.appcompat:appcompat:1.7.0'\n    implementation 'com.google.android.material:material:1.12.0'\n    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'\n    implementation 'androidx.preference:preference-ktx:1.2.1'\n    testImplementation 'junit:junit:4.13.2'\n    androidTestImplementation 'androidx.test.ext:junit:1.2.1'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "app/src/androidTest/java/eu/neilalexander/yggdrasil/ExampleInstrumentedTest.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"eu.neilalexander.yggdrasil\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.CHANGE_WIFI_MULTICAST_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED\" />\n\n    <application\n        android:name=\".GlobalApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@drawable/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@drawable/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.Yggdrasil\"\n        android:largeHeap=\"true\">\n        <activity android:name=\".MainActivity\" android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n        <activity android:name=\".SettingsActivity\" android:parentActivityName=\".MainActivity\" android:exported=\"false\" />\n        <activity android:name=\".PeersActivity\" android:parentActivityName=\".MainActivity\" android:exported=\"false\" />\n        <activity android:name=\".DnsActivity\" android:exported=\"false\" />\n        <activity android:name=\".TileServiceActivity\" android:theme=\"@android:style/Theme.NoDisplay\"\n            android:allowTaskReparenting=\"true\"\n            android:alwaysRetainTaskState=\"false\"\n            android:clearTaskOnLaunch=\"true\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            android:excludeFromRecents=\"true\"\n            android:finishOnTaskLaunch=\"true\">\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE_PREFERENCES\" />\n            </intent-filter>\n        </activity>\n\n        <service\n            android:name=\".PacketTunnelProvider\"\n            android:permission=\"android.permission.BIND_VPN_SERVICE\"\n            android:foregroundServiceType=\"systemExempted\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.net.VpnService\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".YggTileService\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\"\n            android:icon=\"@drawable/ic_tile_icon\"\n            android:label=\"@string/app_name\"\n            android:exported=\"true\">\n            <meta-data android:name=\"android.service.quicksettings.ACTIVE_TILE\" android:value=\"true\" />\n            <meta-data android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\" android:value=\"true\" />\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n\n        <receiver android:name=\".BootUpReceiver\" android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n            </intent-filter>\n        </receiver>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/BootUpReceiver.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.app.NotificationManager\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.net.VpnService\nimport android.util.Log\nimport androidx.preference.PreferenceManager\n\nclass BootUpReceiver : BroadcastReceiver() {\n\n    companion object {\n        const val TAG = \"BootUpReceiver\"\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        if (intent.action != Intent.ACTION_BOOT_COMPLETED) {\n            Log.w(TAG, \"Wrong action: ${intent.action}\")\n        }\n        val preferences = PreferenceManager.getDefaultSharedPreferences(context)\n        if (!preferences.getBoolean(PREF_KEY_ENABLED, false)) {\n            Log.i(TAG, \"Yggdrasil disabled, not starting service\")\n            return\n        }\n        Log.i(TAG, \"Yggdrasil enabled, starting service\")\n        val serviceIntent = Intent(context, PacketTunnelProvider::class.java)\n        serviceIntent.action = PacketTunnelProvider.ACTION_START\n\n        val vpnIntent = VpnService.prepare(context)\n        if (vpnIntent != null) {\n            Log.i(TAG, \"Need to ask for VPN permission\")\n            val notification = createPermissionMissingNotification(context)\n            val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n            manager.notify(444, notification)\n        } else {\n            context.startService(serviceIntent)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/ConfigurationProxy.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.content.Context\nimport mobile.Mobile\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.io.File\n\nobject ConfigurationProxy {\n    private lateinit var json: JSONObject\n    private lateinit var file: File\n\n    operator fun invoke(applicationContext: Context): ConfigurationProxy {\n        file = File(applicationContext.filesDir, \"yggdrasil.conf\")\n        if (!file.exists()) {\n            val conf = Mobile.generateConfigJSON()\n            if (file.createNewFile()) {\n                file.writeBytes(conf)\n            }\n        }\n        fix()\n        return this\n    }\n\n    fun resetJSON() {\n        val conf = Mobile.generateConfigJSON()\n        file.writeBytes(conf)\n        fix()\n    }\n\n    fun resetKeys() {\n        val newJson = JSONObject(String(Mobile.generateConfigJSON()))\n        updateJSON { json ->\n            json.put(\"PrivateKey\", newJson.getString(\"PrivateKey\"))\n        }\n    }\n\n    fun setKeys(privateKey: String) {\n        updateJSON { json ->\n            json.put(\"PrivateKey\", privateKey)\n        }\n    }\n\n    fun updateJSON(fn: (JSONObject) -> Unit) {\n        json = JSONObject(file.readText(Charsets.UTF_8))\n        fn(json)\n        val str = json.toString()\n        file.writeText(str, Charsets.UTF_8)\n    }\n\n    private fun fix() {\n        updateJSON { json ->\n            json.put(\"AdminListen\", \"none\")\n            json.put(\"IfName\", \"none\")\n            json.put(\"IfMTU\", 65535)\n\n            if (json.getJSONArray(\"MulticastInterfaces\").get(0) is String) {\n                val ar = JSONArray()\n                ar.put(0, JSONObject(\"\"\"\n                    {\n                        \"Regex\": \".*\",\n                        \"Beacon\": true,\n                        \"Listen\": true,\n                        \"Password\": \"\"\n                    }\n                \"\"\".trimIndent()))\n                json.put(\"MulticastInterfaces\", ar)\n            }\n        }\n    }\n\n    fun getJSON(): JSONObject {\n        fix()\n        return json\n    }\n\n    fun getJSONByteArray(): ByteArray {\n        return json.toString().toByteArray(Charsets.UTF_8)\n    }\n\n    var multicastListen: Boolean\n        get() = (json.getJSONArray(\"MulticastInterfaces\").get(0) as JSONObject).getBoolean(\"Listen\")\n        set(value) {\n            updateJSON { json ->\n                (json.getJSONArray(\"MulticastInterfaces\").get(0) as JSONObject).put(\"Listen\", value)\n            }\n        }\n\n    var multicastBeacon: Boolean\n        get() = (json.getJSONArray(\"MulticastInterfaces\").get(0) as JSONObject).getBoolean(\"Beacon\")\n        set(value) {\n            updateJSON { json ->\n                (json.getJSONArray(\"MulticastInterfaces\").get(0) as JSONObject).put(\"Beacon\", value)\n            }\n        }\n\n    var multicastPassword: String\n        get() = (json.getJSONArray(\"MulticastInterfaces\").get(0) as JSONObject).optString(\"Password\")\n        set(value) {\n            updateJSON { json ->\n                (json.getJSONArray(\"MulticastInterfaces\").get(0) as JSONObject).put(\"Password\", value)\n            }\n        }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.annotation.SuppressLint\nimport android.app.AlertDialog\nimport android.content.SharedPreferences\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport android.view.ContextThemeWrapper\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.*\nimport androidx.preference.PreferenceManager\nimport com.google.android.material.textfield.TextInputEditText\n\nconst val KEY_DNS_SERVERS = \"dns_servers\"\nconst val KEY_DNS_VERSION = \"dns_version\"\nconst val KEY_ENABLE_CHROME_FIX = \"enable_chrome_fix\"\n\nclass DnsActivity : AppCompatActivity() {\n    private lateinit var config: ConfigurationProxy\n    private lateinit var inflater: LayoutInflater\n\n    private lateinit var serversTableLayout: TableLayout\n    private lateinit var serversTableLabel: TextView\n    private lateinit var serversTableHint: TextView\n    private lateinit var addServerButton: ImageButton\n    private lateinit var enableChromeFix: Switch\n\n    private lateinit var servers: MutableList<String>\n    private lateinit var preferences: SharedPreferences\n\n    private lateinit var defaultDnsServers: HashMap<String, Pair<String, String>>\n\n    @SuppressLint(\"ApplySharedPref\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_dns)\n\n        config = ConfigurationProxy(applicationContext)\n        inflater = LayoutInflater.from(this)\n\n        val descriptionRevertron = getString(R.string.dns_server_info_revertron)\n        // Here we can add some other DNS servers in a future\n        defaultDnsServers = hashMapOf(\n            \"308:62:45:62::\" to Pair(getString(R.string.location_amsterdam), descriptionRevertron),\n            \"308:84:68:55::\" to Pair(getString(R.string.location_frankfurt), descriptionRevertron),\n            \"308:25:40:bd::\" to Pair(getString(R.string.location_bratislava), descriptionRevertron),\n            \"308:c8:48:45::\" to Pair(getString(R.string.location_buffalo), descriptionRevertron),\n        )\n\n        serversTableLayout = findViewById(R.id.configuredDnsTableLayout)\n        serversTableLabel = findViewById(R.id.configuredDnsLabel)\n        serversTableHint = findViewById(R.id.configuredDnsHint)\n        enableChromeFix = findViewById(R.id.enableChromeFix)\n\n        addServerButton = findViewById(R.id.addServerButton)\n        addServerButton.setOnClickListener {\n            val view = inflater.inflate(R.layout.dialog_add_dns_server, null)\n            val input = view.findViewById<TextInputEditText>(R.id.addDnsInput)\n            val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n            builder.setTitle(getString(R.string.dns_add_server_dialog_title))\n            builder.setView(view)\n            builder.setPositiveButton(getString(R.string.add)) { _, _ ->\n                val server = input.text.toString()\n                if (!servers.contains(server)) {\n                    servers.add(server)\n                    preferences.edit().apply {\n                        putString(KEY_DNS_SERVERS, servers.joinToString(\",\"))\n                        commit()\n                    }\n                    updateConfiguredServers()\n                } else {\n                    Toast.makeText(this, R.string.dns_already_added_server, Toast.LENGTH_SHORT).show()\n                }\n            }\n            builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->\n                dialog.cancel()\n            }\n            builder.show()\n        }\n\n        enableChromeFix.setOnCheckedChangeListener { _, isChecked ->\n            preferences.edit().apply {\n                putBoolean(KEY_ENABLE_CHROME_FIX, isChecked)\n                commit()\n            }\n        }\n\n        val enableChromeFixPanel = findViewById<TableRow>(R.id.enableChromeFixPanel)\n        enableChromeFixPanel.setOnClickListener {\n            enableChromeFix.toggle()\n        }\n\n        preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n        val serverString = preferences.getString(KEY_DNS_SERVERS, \"\")\n        servers = if (serverString!!.isNotEmpty()) {\n            serverString.split(\",\").toMutableList()\n        } else {\n            mutableListOf()\n        }\n        updateUsableServers()\n    }\n\n    override fun onResume() {\n        super.onResume()\n        updateConfiguredServers()\n        enableChromeFix.isChecked = preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)\n    }\n\n    @SuppressLint(\"ApplySharedPref\")\n    private fun updateConfiguredServers() {\n        when (servers.size) {\n            0 -> {\n                serversTableLayout.visibility = View.GONE\n                serversTableLabel.text = getString(R.string.dns_no_configured_servers)\n                serversTableHint.text = getText(R.string.dns_configured_servers_hint_empty)\n            }\n            else -> {\n                serversTableLayout.visibility = View.VISIBLE\n                serversTableLabel.text = getString(R.string.dns_configured_servers)\n                serversTableHint.text = getText(R.string.dns_configured_servers_hint)\n\n                serversTableLayout.removeAllViewsInLayout()\n                for (i in servers.indices) {\n                    val server = servers[i]\n                    val view = inflater.inflate(R.layout.peers_configured, null)\n                    view.findViewById<TextView>(R.id.addressValue).text = server\n                    view.findViewById<ImageButton>(R.id.deletePeerButton).tag = i\n\n                    view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button ->\n                        val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n                        builder.setTitle(getString(R.string.dns_remove_title, server))\n                        builder.setPositiveButton(getString(R.string.remove)) { dialog, _ ->\n                            servers.removeAt(button.tag as Int)\n                            preferences.edit().apply {\n                                this.putString(KEY_DNS_SERVERS, servers.joinToString(\",\"))\n                                this.commit()\n                            }\n                            dialog.dismiss()\n                            updateConfiguredServers()\n                        }\n                        builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->\n                            dialog.cancel()\n                        }\n                        builder.show()\n                    }\n                    serversTableLayout.addView(view)\n                }\n            }\n        }\n    }\n\n    @SuppressLint(\"ApplySharedPref\")\n    private fun updateUsableServers() {\n        val usableTableLayout: TableLayout = findViewById(R.id.usableDnsTableLayout)\n\n        defaultDnsServers.forEach {\n            val server = it.key\n            val infoPair = it.value\n            val view = inflater.inflate(R.layout.dns_server_usable, null)\n            view.findViewById<TextView>(R.id.serverValue).text = server\n            val addButton = view.findViewById<ImageButton>(R.id.addButton)\n            addButton.tag = server\n\n            addButton.setOnClickListener { button ->\n                val serverString = button.tag as String\n                if (!servers.contains(serverString)) {\n                    servers.add(serverString)\n                    preferences.edit().apply {\n                        this.putString(KEY_DNS_SERVERS, servers.joinToString(\",\"))\n                        this.commit()\n                    }\n                    updateConfiguredServers()\n                } else {\n                    Toast.makeText(this, R.string.dns_already_added_server, Toast.LENGTH_SHORT).show()\n                }\n            }\n            view.setOnLongClickListener {\n                val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n                builder.setTitle(getString(R.string.dns_server_info_dialog_title))\n                builder.setMessage(\"${infoPair.first}\\n\\n${infoPair.second}\")\n                builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->\n                    dialog.dismiss()\n                }\n                builder.show()\n                true\n            }\n\n            usableTableLayout.addView(view)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.app.*\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.service.quicksettings.TileService\nimport androidx.annotation.RequiresApi\nimport androidx.core.app.NotificationCompat\nimport androidx.preference.PreferenceManager\n\nconst val PREF_KEY_ENABLED = \"enabled\"\nconst val PREF_KEY_PEERS_NOTE = \"peers_note\"\nconst val MAIN_CHANNEL_ID = \"Yggdrasil Service\"\n\nclass GlobalApplication: Application(), YggStateReceiver.StateReceiver {\n    private lateinit var config: ConfigurationProxy\n    private var currentState: State = State.Disabled\n    private var updaterConnections: Int = 0\n\n    override fun onCreate() {\n        super.onCreate()\n        config = ConfigurationProxy(applicationContext)\n        val callback = NetworkStateCallback(this)\n        callback.register()\n        val receiver = YggStateReceiver(this)\n        receiver.register(this)\n        migrateDnsServers(this)\n    }\n\n    fun subscribe() {\n        updaterConnections++\n    }\n\n    fun unsubscribe() {\n        if (updaterConnections > 0) {\n            updaterConnections--\n        }\n    }\n\n    fun needUiUpdates(): Boolean {\n        return updaterConnections > 0\n    }\n\n    fun getCurrentState(): State {\n        return currentState\n    }\n\n    @RequiresApi(Build.VERSION_CODES.N)\n    override fun onStateChange(state: State) {\n        if (state != currentState) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n                val componentName = ComponentName(this, YggTileService::class.java)\n                TileService.requestListeningState(this, componentName)\n            }\n\n            if (state != State.Disabled) {\n                val notification = createServiceNotification(this, state)\n                val notificationManager: NotificationManager =\n                    this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n                notificationManager.notify(SERVICE_NOTIFICATION_ID, notification)\n            }\n\n            currentState = state\n        }\n    }\n}\n\nfun migrateDnsServers(context: Context) {\n    val preferences = PreferenceManager.getDefaultSharedPreferences(context)\n    if (preferences.getInt(KEY_DNS_VERSION, 0) >= 1) {\n        return\n    }\n    val serverString = preferences.getString(KEY_DNS_SERVERS, \"\")\n    if (serverString!!.isNotEmpty()) {\n        // Replacing old Revertron's servers by new ones\n        val newServers = serverString\n            .replace(\"300:6223::53\", \"308:25:40:bd::\")\n            .replace(\"302:7991::53\", \"308:62:45:62::\")\n            .replace(\"302:db60::53\", \"308:84:68:55::\")\n            .replace(\"301:1088::53\", \"308:c8:48:45::\")\n        val editor = preferences.edit()\n        editor.putInt(KEY_DNS_VERSION, 1)\n        if (newServers != serverString) {\n            editor.putString(KEY_DNS_SERVERS, newServers)\n        }\n        editor.apply()\n    }\n}\n\nfun createServiceNotification(context: Context, state: State): Notification {\n    createNotificationChannels(context)\n\n    val intent = Intent(context, MainActivity::class.java).apply {\n        this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n    }\n    var flags = PendingIntent.FLAG_UPDATE_CURRENT\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n        flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n    }\n    val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, flags)\n\n    val text = when (state) {\n        State.Disabled -> context.getText(R.string.tile_disabled)\n        State.Enabled -> context.getText(R.string.tile_enabled)\n        State.Connected -> context.getText(R.string.tile_connected)\n        else -> context.getText(R.string.tile_disabled)\n    }\n\n    return NotificationCompat.Builder(context, MAIN_CHANNEL_ID)\n        .setShowWhen(false)\n        .setContentTitle(text)\n        .setSmallIcon(R.drawable.ic_tile_icon)\n        .setContentIntent(pendingIntent)\n        .setPriority(NotificationCompat.PRIORITY_MIN)\n        .build()\n}\n\nfun createPermissionMissingNotification(context: Context): Notification {\n    createNotificationChannels(context)\n    val intent = Intent(context, MainActivity::class.java).apply {\n        this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n    }\n    var flags = PendingIntent.FLAG_UPDATE_CURRENT\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n        flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n    }\n    val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, flags)\n\n    return NotificationCompat.Builder(context, MAIN_CHANNEL_ID)\n        .setShowWhen(false)\n        .setContentTitle(context.getText(R.string.app_name))\n        .setContentText(context.getText(R.string.permission_notification_text))\n        .setSmallIcon(R.drawable.ic_tile_icon)\n        .setContentIntent(pendingIntent)\n        .setAutoCancel(true)\n        .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n        .build()\n}\n\nprivate fun createNotificationChannels(context: Context) {\n    // Create the NotificationChannel, but only on API 26+ because\n    // the NotificationChannel class is new and not in the support library\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n        val name = context.getString(R.string.channel_name)\n        val descriptionText = context.getString(R.string.channel_description)\n        val importance = NotificationManager.IMPORTANCE_MIN\n        val channel = NotificationChannel(MAIN_CHANNEL_ID, name, importance).apply {\n            description = descriptionText\n        }\n        // Register the channel with the system\n        val notificationManager: NotificationManager =\n            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n        notificationManager.createNotificationChannel(channel)\n    }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.content.*\nimport android.graphics.Color\nimport android.net.Uri\nimport android.net.VpnService\nimport android.os.Bundle\nimport android.view.ContextThemeWrapper\nimport android.widget.Switch\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.LinearLayoutCompat\nimport androidx.core.content.edit\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager\nimport androidx.preference.PreferenceManager\nimport eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT\nimport mobile.Mobile\nimport org.json.JSONArray\n\nconst val APP_WEB_URL = \"https://github.com/yggdrasil-network/yggdrasil-android\"\n\nclass MainActivity : AppCompatActivity() {\n    private lateinit var enabledSwitch: Switch\n    private lateinit var enabledLabel: TextView\n    private lateinit var ipAddressLabel: TextView\n    private lateinit var subnetLabel: TextView\n    private lateinit var peersLabel: TextView\n    private lateinit var peersRow: LinearLayoutCompat\n    private lateinit var dnsLabel: TextView\n    private lateinit var dnsRow: LinearLayoutCompat\n    private lateinit var settingsRow: LinearLayoutCompat\n    private lateinit var versionRow: LinearLayoutCompat\n\n    private fun start() {\n        val intent = Intent(this, PacketTunnelProvider::class.java)\n        intent.action = PacketTunnelProvider.ACTION_START\n        startService(intent)\n    }\n\n    private var startVpnActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->\n        if (result.resultCode == Activity.RESULT_OK) {\n           start()\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        findViewById<TextView>(R.id.versionValue).text = Mobile.getVersion()\n\n        enabledSwitch = findViewById(R.id.enableYggdrasil)\n        enabledLabel = findViewById(R.id.yggdrasilStatusLabel)\n        ipAddressLabel = findViewById(R.id.ipAddressValue)\n        subnetLabel = findViewById(R.id.subnetValue)\n        peersLabel = findViewById(R.id.peersValue)\n        peersRow = findViewById(R.id.peersTableRow)\n        dnsLabel = findViewById(R.id.dnsValue)\n        dnsRow = findViewById(R.id.dnsTableRow)\n        settingsRow = findViewById(R.id.settingsTableRow)\n        versionRow = findViewById(R.id.versionTableRow)\n\n        enabledLabel.setTextColor(Color.GRAY)\n\n        enabledSwitch.setOnCheckedChangeListener { _, isChecked ->\n            when (isChecked) {\n                true -> {\n                    val vpnIntent = VpnService.prepare(this)\n                    if (vpnIntent != null) {\n                        startVpnActivity.launch(vpnIntent)\n                    } else {\n                        start()\n                        enabledSwitch.isEnabled = false\n                    }\n                }\n                false -> {\n                    val intent = Intent(this, PacketTunnelProvider::class.java)\n                    intent.action = PacketTunnelProvider.ACTION_STOP\n                    startService(intent)\n                }\n            }\n            val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n            preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, isChecked) }\n        }\n\n        val enableYggdrasilPanel = findViewById<LinearLayoutCompat>(R.id.enableYggdrasilPanel)\n        enableYggdrasilPanel.setOnClickListener {\n            enabledSwitch.toggle()\n        }\n\n        peersRow.isClickable = true\n        peersRow.setOnClickListener {\n            val intent = Intent(this, PeersActivity::class.java)\n            startActivity(intent)\n        }\n\n        dnsRow.isClickable = true\n        dnsRow.setOnClickListener {\n            val intent = Intent(this, DnsActivity::class.java)\n            startActivity(intent)\n        }\n\n        settingsRow.isClickable = true\n        settingsRow.setOnClickListener {\n            val intent = Intent(this, SettingsActivity::class.java)\n            startActivity(intent)\n        }\n\n        versionRow.isClickable = true\n        versionRow.setOnClickListener {\n            openUrlInBrowser(APP_WEB_URL)\n        }\n\n        ipAddressLabel.setOnLongClickListener {\n            val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager\n            val clip = ClipData.newPlainText(\"ip\", ipAddressLabel.text)\n            clipboard.setPrimaryClip(clip)\n            Toast.makeText(applicationContext,R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n            true\n        }\n\n        subnetLabel.setOnLongClickListener {\n            val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager\n            val clip = ClipData.newPlainText(\"subnet\", subnetLabel.text)\n            clipboard.setPrimaryClip(clip)\n            Toast.makeText(applicationContext,R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n            true\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        LocalBroadcastManager.getInstance(this).registerReceiver(\n            receiver, IntentFilter(STATE_INTENT)\n        )\n        val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n        enabledSwitch.isChecked = preferences.getBoolean(PREF_KEY_ENABLED, false)\n        val serverString = preferences.getString(KEY_DNS_SERVERS, \"\")\n        if (serverString!!.isNotEmpty()) {\n            val servers = serverString.split(\",\")\n            dnsLabel.text = when (servers.size) {\n                0 -> getString(R.string.dns_no_servers)\n                1 -> getString(R.string.dns_one_server)\n                else -> getString(R.string.dns_many_servers, servers.size)\n            }\n        } else {\n            dnsLabel.text = getString(R.string.dns_no_servers)\n        }\n        (application as GlobalApplication).subscribe()\n    }\n\n    override fun onPause() {\n        super.onPause()\n        (application as GlobalApplication).unsubscribe()\n        LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)\n    }\n\n    private val receiver: BroadcastReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context?, intent: Intent) {\n            when (intent.getStringExtra(\"type\")) {\n                \"state\" -> {\n                    val peerState = JSONArray(intent.getStringExtra(\"peers\") ?: \"[]\")\n                    var count = 0\n                    for (i in 0..<peerState.length()) {\n                        val peer = peerState.getJSONObject(i)\n                        if (peer.getString(\"IP\").isNotEmpty()) {\n                            count += 1\n                        }\n                    }\n                    enabledLabel.text = if (intent.getBooleanExtra(\"started\", false)) {\n                        showPeersNoteIfNeeded(peerState.length())\n                        if (count == 0) {\n                            enabledLabel.setTextColor(Color.RED)\n                            getString(R.string.main_no_connectivity)\n                        } else {\n                            enabledLabel.setTextColor(Color.GREEN)\n                            getString(R.string.main_enabled)\n                        }\n                    } else {\n                        enabledLabel.setTextColor(Color.GRAY)\n                        getString(R.string.main_disabled)\n                    }\n                    ipAddressLabel.text = intent.getStringExtra(\"ip\") ?: \"N/A\"\n                    subnetLabel.text = intent.getStringExtra(\"subnet\") ?: \"N/A\"\n                    if (intent.hasExtra(\"peers\")) {\n                        peersLabel.text = when (count) {\n                            0 -> getString(R.string.main_no_peers)\n                            1 -> getString(R.string.main_one_peer)\n                            else -> getString(R.string.main_many_peers, count)\n                        }\n                    }\n                    if (!enabledSwitch.isEnabled) {\n                        enabledSwitch.isEnabled = true\n                    }\n                }\n            }\n        }\n    }\n\n    private fun showPeersNoteIfNeeded(peerCount: Int) {\n        if (peerCount > 0) return\n        val preferences = PreferenceManager.getDefaultSharedPreferences(this@MainActivity.baseContext)\n        if (!preferences.getBoolean(PREF_KEY_PEERS_NOTE, false)) {\n            this@MainActivity.runOnUiThread {\n                val builder: AlertDialog.Builder =\n                    AlertDialog.Builder(ContextThemeWrapper(this@MainActivity, R.style.YggdrasilDialogs))\n                builder.setTitle(getString(R.string.main_add_some_peers_title))\n                builder.setMessage(getString(R.string.main_add_some_peers_message))\n                builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->\n                    dialog.dismiss()\n                }\n                builder.show()\n            }\n            // Mark this note as shown\n            preferences.edit().apply {\n                putBoolean(PREF_KEY_PEERS_NOTE, true)\n                commit()\n            }\n        }\n    }\n\n    fun openUrlInBrowser(url: String) {\n        val intent = Intent(Intent.ACTION_VIEW).apply {\n            data = Uri.parse(url)\n        }\n        try {\n            startActivity(intent)\n        } catch (e: ActivityNotFoundException) {\n            // Handle the exception if no browser is found\n            Toast.makeText(this, getText(R.string.no_browser_found_toast), Toast.LENGTH_SHORT).show()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/NetworkStateCallback.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.*\nimport android.os.Build\nimport android.util.Log\nimport androidx.preference.PreferenceManager\n\n\nprivate const val TAG = \"Network\"\n\nclass NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCallback() {\n\n    override fun onAvailable(network: Network) {\n        super.onAvailable(network)\n        Log.d(TAG, \"onAvailable\")\n\n        val preferences = PreferenceManager.getDefaultSharedPreferences(context)\n        if (preferences.getBoolean(PREF_KEY_ENABLED, false)) {\n            Thread {\n                // The message often arrives before the connection is fully established\n                Thread.sleep(1000)\n                val intent = Intent(context, PacketTunnelProvider::class.java)\n                intent.action = PacketTunnelProvider.ACTION_CONNECT\n                try {\n                    context.startService(intent)\n                } catch (e: IllegalStateException) {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                        context.startForegroundService(intent)\n                    }\n                }\n            }.start()\n        }\n    }\n\n    override fun onLost(network: Network) {\n        super.onLost(network)\n        Log.d(TAG, \"onLost\")\n    }\n\n    fun register() {\n        val request = NetworkRequest.Builder()\n            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)\n            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)\n            .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)\n            .build()\n\n        val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n        manager.registerNetworkCallback(request, this)\n    }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.content.Intent\nimport android.net.VpnService\nimport android.net.wifi.WifiManager\nimport android.os.Build\nimport android.os.ParcelFileDescriptor\nimport android.system.OsConstants\nimport android.util.Log\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager\nimport androidx.preference.PreferenceManager\nimport eu.neilalexander.yggdrasil.YggStateReceiver.Companion.YGG_STATE_INTENT\nimport mobile.Yggdrasil\nimport org.json.JSONArray\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.util.concurrent.atomic.AtomicBoolean\nimport kotlin.concurrent.thread\n\n\nprivate const val TAG = \"PacketTunnelProvider\"\nconst val SERVICE_NOTIFICATION_ID = 1000\n\nopen class PacketTunnelProvider: VpnService() {\n    companion object {\n        const val STATE_INTENT = \"eu.neilalexander.yggdrasil.PacketTunnelProvider.STATE_MESSAGE\"\n\n        const val ACTION_START = \"eu.neilalexander.yggdrasil.PacketTunnelProvider.START\"\n        const val ACTION_STOP = \"eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP\"\n        const val ACTION_TOGGLE = \"eu.neilalexander.yggdrasil.PacketTunnelProvider.TOGGLE\"\n        const val ACTION_CONNECT = \"eu.neilalexander.yggdrasil.PacketTunnelProvider.CONNECT\"\n    }\n\n    private var yggdrasil = Yggdrasil()\n    private var started = AtomicBoolean()\n\n    private lateinit var config: ConfigurationProxy\n\n    private var readerThread: Thread? = null\n    private var writerThread: Thread? = null\n    private var updateThread: Thread? = null\n\n    private var parcel: ParcelFileDescriptor? = null\n    private var readerStream: FileInputStream? = null\n    private var writerStream: FileOutputStream? = null\n    private var multicastLock: WifiManager.MulticastLock? = null\n\n    override fun onCreate() {\n        super.onCreate()\n        config = ConfigurationProxy(applicationContext)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        stop()\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        if (intent == null) {\n            Log.d(TAG, \"Intent is null\")\n            return START_NOT_STICKY\n        }\n        val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n        val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)\n        return when (intent.action ?: ACTION_STOP) {\n            ACTION_STOP -> {\n                Log.d(TAG, \"Stopping...\")\n                stop(); START_NOT_STICKY\n            }\n            ACTION_CONNECT -> {\n                Log.d(TAG, \"Connecting...\")\n                if (started.get()) {\n                    connect()\n                } else {\n                    start()\n                }\n                START_STICKY\n            }\n            ACTION_TOGGLE -> {\n                Log.d(TAG, \"Toggling...\")\n                if (started.get()) {\n                    stop(); START_NOT_STICKY\n                } else {\n                    start(); START_STICKY\n                }\n            }\n            else -> {\n                if (!enabled) {\n                    Log.d(TAG, \"Service is disabled\")\n                    return START_NOT_STICKY\n                }\n                Log.d(TAG, \"Starting...\")\n                start(); START_STICKY\n            }\n        }\n    }\n\n    private fun start() {\n        if (!started.compareAndSet(false, true)) {\n            return\n        }\n\n        val notification = createServiceNotification(this, State.Enabled)\n        startForeground(SERVICE_NOTIFICATION_ID, notification)\n\n        // Acquire multicast lock\n        val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager\n        multicastLock = wifi.createMulticastLock(\"Yggdrasil\").apply {\n            setReferenceCounted(false)\n            acquire()\n        }\n\n        Log.d(TAG, config.getJSON().toString())\n        yggdrasil.startJSON(config.getJSONByteArray())\n\n        val address = yggdrasil.addressString\n        val builder = Builder()\n            .addAddress(address, 7)\n            .addRoute(\"200::\", 7)\n            // We do this to trick the DNS-resolver into thinking that we have \"regular\" IPv6,\n            // and therefore we need to resolve AAAA DNS-records.\n            // See: https://android.googlesource.com/platform/bionic/+/refs/heads/master/libc/dns/net/getaddrinfo.c#1935\n            // and: https://android.googlesource.com/platform/bionic/+/refs/heads/master/libc/dns/net/getaddrinfo.c#365\n            // If we don't do this the DNS-resolver just doesn't do DNS-requests with record type AAAA,\n            // and we can't use DNS with Yggdrasil addresses.\n            .addRoute(\"2000::\", 128)\n            .allowFamily(OsConstants.AF_INET)\n            .allowBypass()\n            .setBlocking(true)\n            .setMtu(yggdrasil.mtu.toInt())\n            .setSession(\"Yggdrasil\")\n        // On Android API 29+ apps can opt-in/out to using metered networks.\n        // If we don't set metered status of VPN it is considered as metered.\n        // If we set it to false, then it will inherit this status from underlying network.\n        // See: https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            builder.setMetered(false)\n        }\n\n        val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n        val serverString = preferences.getString(KEY_DNS_SERVERS, \"\")\n        if (serverString!!.isNotEmpty()) {\n            val servers = serverString.split(\",\")\n            if (servers.isNotEmpty()) {\n                servers.forEach {\n                    Log.i(TAG, \"Using DNS server $it\")\n                    builder.addDnsServer(it)\n                }\n            }\n        }\n        if (preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)) {\n            builder.addRoute(\"2001:4860:4860::8888\", 128)\n        }\n\n        parcel = builder.establish()\n        val parcel = parcel\n        if (parcel == null || !parcel.fileDescriptor.valid()) {\n            stop()\n            return\n        }\n\n        readerStream = FileInputStream(parcel.fileDescriptor)\n        writerStream = FileOutputStream(parcel.fileDescriptor)\n\n        readerThread = thread {\n            reader()\n        }\n        writerThread = thread {\n            writer()\n        }\n        updateThread = thread {\n            updater()\n        }\n\n        var intent = Intent(YGG_STATE_INTENT)\n        intent.putExtra(\"state\", STATE_ENABLED)\n        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)\n    }\n\n    private fun stop() {\n        if (!started.compareAndSet(true, false)) {\n            return\n        }\n\n        yggdrasil.stop()\n\n        readerStream?.let {\n            it.close()\n            readerStream = null\n        }\n        writerStream?.let {\n            it.close()\n            writerStream = null\n        }\n        parcel?.let {\n            it.close()\n            parcel = null\n        }\n\n        readerThread?.let {\n            it.interrupt()\n            readerThread = null\n        }\n        writerThread?.let {\n            it.interrupt()\n            writerThread = null\n        }\n        updateThread?.let {\n            it.interrupt()\n            updateThread = null\n        }\n\n        var intent = Intent(STATE_INTENT)\n        intent.putExtra(\"type\", \"state\")\n        intent.putExtra(\"started\", false)\n        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)\n\n        intent = Intent(YGG_STATE_INTENT)\n        intent.putExtra(\"state\", STATE_DISABLED)\n        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)\n\n        stopForeground(true)\n        stopSelf()\n        multicastLock?.release()\n    }\n\n    private fun connect() {\n        if (!started.get()) {\n            return\n        }\n        yggdrasil.retryPeersNow()\n    }\n\n    private fun updater() {\n        try {\n            Thread.sleep(500)\n        } catch (_: InterruptedException) {\n            return\n        }\n        var lastStateUpdate = System.currentTimeMillis()\n        updates@ while (started.get()) {\n            val treeJSON = yggdrasil.treeJSON\n            if ((application as  GlobalApplication).needUiUpdates()) {\n                val intent = Intent(STATE_INTENT)\n                intent.putExtra(\"type\", \"state\")\n                intent.putExtra(\"started\", true)\n                intent.putExtra(\"ip\", yggdrasil.addressString)\n                intent.putExtra(\"subnet\", yggdrasil.subnetString)\n                intent.putExtra(\"pubkey\", yggdrasil.publicKeyString)\n                intent.putExtra(\"peers\", yggdrasil.peersJSON)\n                LocalBroadcastManager.getInstance(this).sendBroadcast(intent)\n            }\n            val curTime = System.currentTimeMillis()\n            if (lastStateUpdate + 10000 < curTime) {\n                val intent = Intent(YGG_STATE_INTENT)\n                var state = STATE_ENABLED\n                if (yggdrasil.routingEntries > 0) {\n                    state = STATE_CONNECTED\n                }\n                if (treeJSON != null && treeJSON != \"null\") {\n                    val treeState = JSONArray(treeJSON)\n                    val count = treeState.length()\n                    if (count > 1)\n                        state = STATE_CONNECTED\n                }\n                intent.putExtra(\"state\", state)\n                LocalBroadcastManager.getInstance(this).sendBroadcast(intent)\n                lastStateUpdate = curTime\n            }\n\n            if (Thread.currentThread().isInterrupted) {\n                break@updates\n            }\n            if (sleep()) return\n        }\n    }\n\n    private fun sleep(): Boolean {\n        try {\n            Thread.sleep(1000)\n        } catch (e: InterruptedException) {\n            return true\n        }\n        return false\n    }\n\n    private fun writer() {\n        val buf = ByteArray(65535)\n        writes@ while (started.get()) {\n            val writerStream = writerStream\n            val writerThread = writerThread\n            if (writerThread == null || writerStream == null) {\n                Log.i(TAG, \"Write thread or stream is null\")\n                break@writes\n            }\n            if (Thread.currentThread().isInterrupted || !writerStream.fd.valid()) {\n                Log.i(TAG, \"Write thread interrupted or file descriptor is invalid\")\n                break@writes\n            }\n            try {\n                val len = yggdrasil.recvBuffer(buf)\n                if (len > 0) {\n                    writerStream.write(buf, 0, len.toInt())\n                }\n            } catch (e: Exception) {\n                Log.i(TAG, \"Error in write: $e\")\n                if (e.toString().contains(\"ENOBUFS\")) {\n                    //TODO Check this by some error code\n                    //More info about this: https://github.com/AdguardTeam/AdguardForAndroid/issues/724\n                    continue\n                }\n                break@writes\n            }\n        }\n        writerStream?.let {\n            it.close()\n            writerStream = null\n        }\n    }\n\n    private fun reader() {\n        val b = ByteArray(65535)\n        reads@ while (started.get()) {\n            val readerStream = readerStream\n            val readerThread = readerThread\n            if (readerThread == null || readerStream == null) {\n                Log.i(TAG, \"Read thread or stream is null\")\n                break@reads\n            }\n            if (Thread.currentThread().isInterrupted ||!readerStream.fd.valid()) {\n                Log.i(TAG, \"Read thread interrupted or file descriptor is invalid\")\n                break@reads\n            }\n            try {\n                val n = readerStream.read(b)\n                yggdrasil.sendBuffer(b, n.toLong())\n            } catch (e: Exception) {\n                Log.i(TAG, \"Error in sendBuffer: $e\")\n                break@reads\n            }\n        }\n        readerStream?.let {\n            it.close()\n            readerStream = null\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.app.AlertDialog\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.os.Bundle\nimport android.text.method.LinkMovementMethod\nimport android.util.Log\nimport android.view.ContextThemeWrapper\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.EditText\nimport android.widget.ImageButton\nimport android.widget.Switch\nimport android.widget.TableLayout\nimport android.widget.TableRow\nimport android.widget.TextView\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.widget.doOnTextChanged\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager\nimport com.google.android.material.textfield.TextInputEditText\nimport com.google.android.material.textfield.TextInputLayout\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.net.URI\n\n\nclass PeersActivity : AppCompatActivity() {\n    private lateinit var config: ConfigurationProxy\n    private lateinit var inflater: LayoutInflater\n    private lateinit var peers: Array<JSONObject>\n\n    private lateinit var connectedTableLayout: TableLayout\n    private lateinit var connectedTableLabel: TextView\n    private lateinit var configuredTableLayout: TableLayout\n    private lateinit var configuredTableLabel: TextView\n    private lateinit var multicastListenSwitch: Switch\n    private lateinit var multicastBeaconSwitch: Switch\n    private lateinit var passwordEdit: EditText\n    private lateinit var addPeerButton: ImageButton\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_peers)\n\n        config = ConfigurationProxy(applicationContext)\n        inflater = LayoutInflater.from(this)\n        peers = emptyArray()\n\n        connectedTableLayout = findViewById(R.id.connectedPeersTableLayout)\n        connectedTableLabel = findViewById(R.id.connectedPeersLabel)\n\n        configuredTableLayout = findViewById(R.id.configuredPeersTableLayout)\n        configuredTableLabel = findViewById(R.id.configuredPeersLabel)\n\n        val discoveryLink = findViewById<TextView>(R.id.peers_discovery_link)\n        discoveryLink.movementMethod = LinkMovementMethod.getInstance()\n\n        multicastListenSwitch = findViewById(R.id.enableMulticastListen)\n        multicastListenSwitch.setOnCheckedChangeListener { button, _ ->\n            config.multicastListen = button.isChecked\n        }\n        multicastBeaconSwitch = findViewById(R.id.enableMulticastBeacon)\n        multicastBeaconSwitch.setOnCheckedChangeListener { button, _ ->\n            config.multicastBeacon = button.isChecked\n        }\n        multicastListenSwitch.isChecked = config.multicastListen\n        multicastBeaconSwitch.isChecked = config.multicastBeacon\n\n        val multicastBeaconPanel = findViewById<TableRow>(R.id.enableMulticastBeaconPanel)\n        multicastBeaconPanel.setOnClickListener {\n            multicastBeaconSwitch.toggle()\n        }\n        val multicastListenPanel = findViewById<TableRow>(R.id.enableMulticastListenPanel)\n        multicastListenPanel.setOnClickListener {\n            multicastListenSwitch.toggle()\n        }\n        passwordEdit = findViewById(R.id.passwordEdit)\n        passwordEdit.setText(config.multicastPassword)\n\n        passwordEdit.doOnTextChanged { text, _, _, _ ->\n            config.multicastPassword = text.toString()\n        }\n\n        passwordEdit.setOnKeyListener { _, keyCode, _ ->\n            (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER)\n        }\n\n        findViewById<View>(R.id.passwordTableRow).setOnKeyListener { _, keyCode, event ->\n            Log.i(\"Key\", keyCode.toString())\n            if (event.action == KeyEvent.ACTION_DOWN) {\n                if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {\n                    passwordEdit.requestFocus()\n                    true\n                } else {\n                    false\n                }\n            } else {\n                false\n            }\n        }\n\n        addPeerButton = findViewById(R.id.addPeerButton)\n        addPeerButton.setOnClickListener {\n            val view = inflater.inflate(R.layout.dialog_addpeer, null)\n            val input = view.findViewById<TextInputEditText>(R.id.addPeerInput)\n            val inputLayout = view.findViewById<TextInputLayout>(R.id.addPeerInputLayout)\n            val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n            builder.setTitle(getString(R.string.peers_add_peer))\n            builder.setView(view)\n            builder.setPositiveButton(getString(R.string.peers_add)) { dialog, _ ->\n                config.updateJSON { json ->\n                    json.getJSONArray(\"Peers\").put(input.text.toString().trim())\n                }\n                dialog.dismiss()\n                updateConfiguredPeers()\n            }\n            builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->\n                dialog.cancel()\n            }\n            val dialog = builder.create()\n            dialog.show()\n            val addButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)\n            addButton.isEnabled = false\n            input.doOnTextChanged { text, _, _, _ ->\n                val error = validatePeerUri(text.toString().trim())\n                inputLayout?.error = error\n                addButton.isEnabled = error == null && !text.isNullOrBlank()\n            }\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        LocalBroadcastManager.getInstance(this).registerReceiver(\n            receiver, IntentFilter(PacketTunnelProvider.STATE_INTENT)\n        )\n        (application as GlobalApplication).subscribe()\n\n        updateConfiguredPeers()\n        updateConnectedPeers()\n    }\n\n    override fun onPause() {\n        super.onPause()\n        (application as GlobalApplication).unsubscribe()\n        LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)\n    }\n\n    private fun updateConfiguredPeers() {\n        val peers = config.getJSON().getJSONArray(\"Peers\")\n\n        when (peers.length()) {\n            0 -> {\n                configuredTableLayout.visibility = View.GONE\n                configuredTableLabel.text = getString(R.string.peers_no_configured_title)\n            }\n            else -> {\n                configuredTableLayout.visibility = View.VISIBLE\n                configuredTableLabel.text = getString(R.string.peers_configured_title)\n\n                configuredTableLayout.removeAllViewsInLayout()\n                for (i in 0 until peers.length()) {\n                    val peer = peers[i].toString()\n                    val view = inflater.inflate(R.layout.peers_configured, null)\n                    view.findViewById<TextView>(R.id.addressValue).text = peer\n                    view.findViewById<ImageButton>(R.id.deletePeerButton).tag = i\n\n                    view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button ->\n                        val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n                        builder.setTitle(getString(R.string.peers_remove_title, peer))\n                        builder.setPositiveButton(getString(R.string.peers_remove)) { dialog, _ ->\n                            config.updateJSON { json ->\n                                json.getJSONArray(\"Peers\").remove(button.tag as Int)\n                            }\n                            dialog.dismiss()\n                            updateConfiguredPeers()\n                        }\n                        builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->\n                            dialog.cancel()\n                        }\n                        builder.show()\n                    }\n                    configuredTableLayout.addView(view)\n                }\n            }\n        }\n    }\n\n    private fun updateConnectedPeers() {\n        when (peers.size) {\n            0 -> {\n                connectedTableLayout.visibility = View.GONE\n                connectedTableLabel.text = getString(R.string.peers_no_connected_title)\n            }\n            else -> {\n                var connected = false\n                connectedTableLayout.removeAllViewsInLayout()\n                for (peer in peers) {\n                    val view = inflater.inflate(R.layout.peers_connected, null)\n                    val ip = peer.getString(\"IP\")\n                    // Only connected peers have IPs\n                    if (ip.isNotEmpty()) {\n                        view.findViewById<TextView>(R.id.addressLabel).text = ip\n                        view.findViewById<TextView>(R.id.detailsLabel).text = peer.getString(\"URI\")\n                        connectedTableLayout.addView(view)\n                        connected = true\n                    }\n                }\n                if (connected) {\n                    connectedTableLayout.visibility = View.VISIBLE\n                    connectedTableLabel.text = getString(R.string.peers_connected_title)\n                } else {\n                    connectedTableLayout.visibility = View.GONE\n                    connectedTableLabel.text = getString(R.string.peers_no_connected_title)\n                }\n            }\n        }\n    }\n\n    private val validSchemes = setOf(\"tcp\", \"tls\", \"quic\", \"ws\", \"wss\", \"socks\")\n\n    private fun validatePeerUri(input: String): String? {\n        if (input.isEmpty()) return null\n        val uri = try {\n            URI(input)\n        } catch (e: Exception) {\n            return getString(R.string.peer_invalid_uri)\n        }\n        val scheme = uri.scheme?.lowercase()\n        if (scheme == null || scheme !in validSchemes) {\n            return getString(R.string.peer_invalid_scheme, validSchemes.joinToString(\", \"))\n        }\n        if (scheme != \"socks\") {\n            if (uri.host.isNullOrEmpty()) {\n                return getString(R.string.peer_missing_host)\n            }\n            if (uri.port == -1) {\n                return getString(R.string.peer_missing_port)\n            }\n        }\n        return null\n    }\n\n    private val receiver: BroadcastReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context?, intent: Intent) {\n            when (intent.getStringExtra(\"type\")) {\n                \"state\" -> {\n                    if (intent.hasExtra(\"peers\")) {\n                        val peers1 = intent.getStringExtra(\"peers\")\n                        //Log.i(\"PeersActivity\", \"Peers json: $peers1\")\n                        val peersArray = JSONArray(peers1 ?: \"[]\")\n                        val array = Array(peersArray.length()) { i ->\n                            peersArray.getJSONObject(i)\n                        }\n                        array.sortWith(compareBy { it.getString(\"IP\") })\n                        peers = array\n\n                        updateConnectedPeers()\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/SettingsActivity.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.app.AlertDialog\nimport android.content.BroadcastReceiver\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport android.util.Log\nimport android.view.ContextThemeWrapper\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.*\nimport androidx.appcompat.widget.LinearLayoutCompat\nimport androidx.core.widget.doOnTextChanged\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager\nimport org.json.JSONObject\n\nclass SettingsActivity : AppCompatActivity() {\n    private lateinit var config: ConfigurationProxy\n    private lateinit var inflater: LayoutInflater\n\n    private lateinit var deviceNameEntry: EditText\n    private lateinit var publicKeyLabel: TextView\n    private lateinit var resetConfigurationRow: LinearLayoutCompat\n    private var publicKeyReset = false\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_settings)\n\n        config = ConfigurationProxy(applicationContext)\n        inflater = LayoutInflater.from(this)\n\n        deviceNameEntry = findViewById(R.id.deviceNameEntry)\n        publicKeyLabel = findViewById(R.id.publicKeyLabel)\n        resetConfigurationRow = findViewById(R.id.resetConfigurationRow)\n\n        deviceNameEntry.doOnTextChanged { text, _, _, _ ->\n            config.updateJSON { cfg ->\n                val nodeInfo = cfg.optJSONObject(\"NodeInfo\")\n                if (nodeInfo == null) {\n                    cfg.put(\"NodeInfo\", JSONObject(\"{}\"))\n                }\n                cfg.getJSONObject(\"NodeInfo\").put(\"name\", text)\n            }\n        }\n\n        deviceNameEntry.setOnKeyListener { view, keyCode, event ->\n            (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER)\n        }\n\n        findViewById<View>(R.id.deviceNameTableRow).setOnKeyListener { view, keyCode, event ->\n            Log.i(\"Key\", keyCode.toString())\n            if (event.action == KeyEvent.ACTION_DOWN) {\n                if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {\n                    deviceNameEntry.requestFocus()\n                    true\n                } else {\n                    false\n                }\n            } else {\n                false\n            }\n        }\n\n        resetConfigurationRow.setOnClickListener {\n            val view = inflater.inflate(R.layout.dialog_resetconfig, null)\n            val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n            builder.setTitle(getString(R.string.settings_warning_title))\n            builder.setView(view)\n            builder.setPositiveButton(getString(R.string.settings_reset)) { dialog, _ ->\n                config.resetJSON()\n                updateView()\n                dialog.dismiss()\n            }\n            builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->\n                dialog.cancel()\n            }\n            builder.show()\n        }\n\n        findViewById<View>(R.id.resetKeysRow).setOnClickListener {\n            config.resetKeys()\n            publicKeyReset = true\n            updateView()\n        }\n\n        findViewById<View>(R.id.setKeysRow).setOnClickListener {\n            val view = inflater.inflate(R.layout.dialog_set_keys, null)\n            val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs))\n            val privateKey = view.findViewById<EditText>(R.id.private_key)\n            builder.setTitle(getString(R.string.set_keys))\n            builder.setView(view)\n            builder.setPositiveButton(getString(R.string.save)) { dialog, _ ->\n                config.setKeys(privateKey.text.toString())\n                updateView()\n                dialog.dismiss()\n            }\n            builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->\n                dialog.cancel()\n            }\n            builder.show()\n        }\n\n        publicKeyLabel.setOnLongClickListener {\n            val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager\n            val clip = ClipData.newPlainText(\"public key\", publicKeyLabel.text)\n            clipboard.setPrimaryClip(clip)\n            Toast.makeText(applicationContext,R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n            true\n        }\n\n        updateView()\n    }\n\n    private fun updateView() {\n        val json = config.getJSON()\n        val nodeinfo = json.optJSONObject(\"NodeInfo\")\n        if (nodeinfo != null) {\n            deviceNameEntry.setText(nodeinfo.getString(\"name\"), TextView.BufferType.EDITABLE)\n        } else {\n            deviceNameEntry.setText(\"\", TextView.BufferType.EDITABLE)\n        }\n\n        var key = json.optString(\"PrivateKey\")\n        if (key.isNotEmpty()) {\n            key = key.substring(key.length / 2)\n        }\n        publicKeyLabel.text = key\n    }\n\n    override fun onResume() {\n        super.onResume()\n        LocalBroadcastManager.getInstance(this).registerReceiver(\n            receiver, IntentFilter(PacketTunnelProvider.STATE_INTENT)\n        )\n        (application as GlobalApplication).subscribe()\n    }\n\n    override fun onPause() {\n        super.onPause()\n        (application as GlobalApplication).unsubscribe()\n        LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)\n    }\n\n    // To be able to get public key from running Yggdrasil we use this receiver, as we don't have this field in config\n    private val receiver: BroadcastReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context?, intent: Intent) {\n            if (intent.hasExtra(\"pubkey\") && !publicKeyReset) {\n                val tree = intent.getStringExtra(\"pubkey\")\n                if (tree != null && tree != \"null\") {\n                    publicKeyLabel.text = intent.getStringExtra(\"pubkey\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/TileServiceActivity.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os.Bundle\n\nclass TileServiceActivity : Activity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        // Just starting MainActivity\n        val intent = Intent(this, MainActivity::class.java)\n        startService(intent)\n        finish()\n    }\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/YggStateReceiver.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager\n\nconst val STATE_ENABLED = \"enabled\"\nconst val STATE_DISABLED = \"disabled\"\nconst val STATE_CONNECTED = \"connected\"\nconst val STATE_RECONNECTING = \"reconnecting\"\n\nclass YggStateReceiver(var receiver: StateReceiver): BroadcastReceiver() {\n\n    companion object {\n        const val YGG_STATE_INTENT = \"eu.neilalexander.yggdrasil.YggStateReceiver.STATE\"\n    }\n\n    override fun onReceive(context: Context?, intent: Intent?) {\n        if (context == null) return\n\n        val state = when (intent?.getStringExtra(\"state\")) {\n            STATE_ENABLED -> State.Enabled\n            STATE_DISABLED -> State.Disabled\n            STATE_CONNECTED -> State.Connected\n            STATE_RECONNECTING -> State.Reconnecting\n            else -> State.Unknown\n        }\n        receiver.onStateChange(state)\n    }\n\n    fun register(context: Context) {\n        LocalBroadcastManager.getInstance(context).registerReceiver(\n            this, IntentFilter(YGG_STATE_INTENT)\n        )\n    }\n\n    fun unregister(context: Context) {\n        LocalBroadcastManager.getInstance(context).unregisterReceiver(this)\n    }\n\n    interface StateReceiver {\n        fun onStateChange(state: State)\n    }\n}\n\n/**\n * A class-supporter with an Yggdrasil state\n */\nenum class State {\n    Unknown, Disabled, Enabled, Connected, Reconnecting;\n}"
  },
  {
    "path": "app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport android.content.Intent\nimport android.graphics.drawable.Icon\nimport android.os.Build\nimport android.os.IBinder\nimport android.service.quicksettings.Tile\nimport android.service.quicksettings.TileService\nimport android.util.Log\nimport androidx.annotation.RequiresApi\nimport androidx.core.content.edit\nimport androidx.preference.PreferenceManager\n\nprivate const val TAG = \"TileService\"\n\n@RequiresApi(Build.VERSION_CODES.N)\nclass YggTileService: TileService(), YggStateReceiver.StateReceiver {\n\n    private lateinit var receiver: YggStateReceiver\n\n    override fun onCreate() {\n        super.onCreate()\n        receiver = YggStateReceiver(this)\n    }\n\n    /**\n     * We need to override the method onBind to avoid crashes that were detected on Android 8\n     *\n     * The possible reason of crashes is described here:\n     * https://github.com/aosp-mirror/platform_frameworks_base/commit/ee68fd889c2dfcd895b8e73fc39d7b97826dc3d8\n     */\n    override fun onBind(intent: Intent?): IBinder? {\n        return try {\n            super.onBind(intent)\n        } catch (th: Throwable) {\n            null\n        }\n    }\n\n    override fun onTileAdded() {\n        super.onTileAdded()\n        updateTileState((application as GlobalApplication).getCurrentState())\n    }\n\n    override fun onTileRemoved() {\n        super.onTileRemoved()\n        updateTileState((application as GlobalApplication).getCurrentState())\n    }\n\n    override fun onStartListening() {\n        super.onStartListening()\n        receiver.register(this)\n        updateTileState((application as GlobalApplication).getCurrentState())\n    }\n\n    override fun onStopListening() {\n        super.onStopListening()\n        receiver.unregister(this)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        receiver.unregister(this)\n    }\n\n    override fun onClick() {\n        super.onClick()\n        // Saving new state\n        val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n        val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)\n        preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, !enabled) }\n        // Starting or stopping VPN service\n        val intent = Intent(this, PacketTunnelProvider::class.java)\n        intent.action = PacketTunnelProvider.ACTION_TOGGLE\n        startService(intent)\n    }\n\n    private fun updateTileState(state: State) {\n        val tile = qsTile ?: return\n        val oldState = tile.state\n        val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)\n        val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)\n        tile.state = when (enabled) {\n            false -> Tile.STATE_INACTIVE\n            true -> Tile.STATE_ACTIVE\n        }\n        var changed = oldState != tile.state\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            val oldText = tile.subtitle\n            tile.subtitle = when (state) {\n                State.Enabled -> getText(R.string.tile_enabled)\n                State.Connected -> getText(R.string.tile_connected)\n                else -> getText(R.string.tile_disabled)\n            }\n            changed = changed || (oldText != tile.subtitle)\n        }\n\n        // Update tile if changed state\n        if (changed) {\n            Log.i(TAG, \"Updating tile, old state: $oldState, new state: ${tile.state}\")\n            /*\n              Force set the icon in the tile, because there is a problem on icon tint in the Android Oreo.\n              Issue: https://github.com/AdguardTeam/AdguardForAndroid/issues/1996\n             */\n            tile.icon = Icon.createWithResource(applicationContext, R.drawable.ic_tile_icon)\n            tile.updateTile()\n        }\n    }\n\n    override fun onStateChange(state: State) {\n        updateTileState(state)\n    }\n}"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_add_circle_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_chevron_right_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_remove_circle_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_tile_icon.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"480dp\"\n    android:height=\"480dp\"\n    android:viewportWidth=\"480\"\n    android:viewportHeight=\"480\">\n  <path\n      android:pathData=\"m58.51,474.95c1.37,-5.62 17.69,-45.43 32.05,-78.2c14.7,-33.54 14.42,-32.23 10.32,-49.01c-5.11,-20.95 -4.8,-55.06 0.67,-73.17c19.73,-65.38 70.97,-109.69 182.24,-157.59c36.24,-15.6 56.14,-25.61 71.24,-35.83c26.61,-18.01 54.3,-49.27 63.15,-71.29c1.87,-4.65 3.96,-8.4 4.66,-8.35c2.18,0.16 1.1,66.01 -1.46,88.95c-15.82,142.09 -64.01,234.52 -143.35,274.93c-45.79,23.32 -117.97,31.97 -151.59,18.15c-4.75,-1.95 -9.76,-3.55 -11.12,-3.55c-5.12,0 -23.48,49.02 -28.75,76.76c-1.64,8.61 -4.12,20.42 -4.48,22.23c-8.2,-0.06 -0.53,-0.02 -12.19,-0.02l-12.36,0z\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"1.49\"\n      android:fillColor=\"#FFFFFF\"\n      android:fillType=\"nonZero\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/rounded.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <solid android:color=\"?attr/tableBackgroundColor\"/>\n    <corners android:radius=\"10dp\"/>\n    <padding android:left=\"0dp\" android:top=\"0dp\" android:right=\"0dp\" android:bottom=\"0dp\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/layout/activity_dns.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".DnsActivity\">\n\n    <FrameLayout\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:dividerPadding=\"4pt\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/pageTitle\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"8pt\"\n                    android:text=\"@string/dns_activity_title\"\n                    android:textColor=\"?attr/textDefault\"\n                    android:textSize=\"24sp\"\n                    android:textStyle=\"bold\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\" />\n\n                <ImageButton\n                    android:id=\"@+id/addServerButton\"\n                    android:layout_width=\"16pt\"\n                    android:layout_height=\"16pt\"\n                    android:layout_marginTop=\"6pt\"\n                    android:layout_marginRight=\"8pt\"\n                    android:background=\"@android:color/transparent\"\n                    app:srcCompat=\"@drawable/ic_baseline_add_circle_24\" />\n            </LinearLayout>\n\n            <ScrollView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\" >\n\n                    <TextView\n                        android:id=\"@+id/configuredDnsLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/configuredDnsTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\" />\n\n                    <TextView\n                        android:id=\"@+id/configuredDnsHint\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/dns_configured_servers_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TextView\n                        android:id=\"@+id/usableDnsLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/dns_usable_servers\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/usableDnsTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\" />\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/dns_usable_servers_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <Space\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"32px\" />\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/dns_fixes\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/fixesTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <TableRow\n                            android:id=\"@+id/enableChromeFixPanel\"\n                            style=\"@style/SelectableSwitchItemStyle\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/dns_fix_chrome_based_browsers\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <Switch\n                                android:id=\"@+id/enableChromeFix\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\" />\n\n                        </TableRow>\n\n                    </TableLayout>\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/dns_fix_chrome_based_browsers_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                </LinearLayout>\n            </ScrollView>\n\n        </LinearLayout>\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <FrameLayout\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:dividerPadding=\"4pt\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/pageTitle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"8pt\"\n                android:text=\"@string/app_name\"\n                android:textColor=\"?attr/textDefault\"\n                android:textSize=\"24sp\"\n                android:textStyle=\"bold\" />\n\n            <ScrollView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\" >\n\n                    <TextView\n                        android:id=\"@+id/statusSectionLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:text=\"@string/main_status\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <androidx.appcompat.widget.LinearLayoutCompat\n                        android:id=\"@+id/connectedPeersTableLayout\"\n                        android:orientation=\"vertical\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/enableYggdrasilPanel\"\n                            style=\"@style/SelectableSwitchItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/enableYggdrasilLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/main_enable_yggdrasil\"\n                                android:textColor=\"?attr/textDefault\"\n                                android:textSize=\"14sp\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <Switch\n                                android:id=\"@+id/enableYggdrasil\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\" />\n\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/yggdrasilStatusLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:paddingTop=\"2dp\"\n                                android:paddingBottom=\"2dp\"\n                                android:text=\"@string/main_disabled\"\n                                android:textSize=\"14sp\"\n                                android:textStyle=\"bold\" />\n\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    <Space\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"32px\" />\n\n                    <TextView\n                        android:id=\"@+id/networkInfoSectionLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/main_network_info\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <androidx.appcompat.widget.LinearLayoutCompat\n                        android:id=\"@+id/configuredPeersTableLayout\"\n                        android:orientation=\"vertical\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <androidx.appcompat.widget.LinearLayoutCompat style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/ipAddressLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"0\"\n                                android:text=\"@string/main_ip\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <TextView\n                                android:id=\"@+id/ipAddressValue\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:alpha=\"0.5\"\n                                android:ellipsize=\"none\"\n                                android:scrollHorizontally=\"true\"\n                                android:selectAllOnFocus=\"true\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/main_not_available\"\n                                android:textAlignment=\"viewEnd\"\n                                android:textIsSelectable=\"true\"\n                                android:textSize=\"14sp\" />\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/subnetLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"0\"\n                                android:text=\"@string/main_subnet\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <TextView\n                                android:id=\"@+id/subnetValue\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:alpha=\"0.5\"\n                                android:ellipsize=\"none\"\n                                android:scrollHorizontally=\"true\"\n                                android:selectAllOnFocus=\"true\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/main_not_available\"\n                                android:textAlignment=\"viewEnd\"\n                                android:textIsSelectable=\"true\"\n                                android:textSize=\"14sp\" />\n\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    <Space\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"32px\" />\n\n                    <TextView\n                        android:id=\"@+id/configurationSectionLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/main_configuration\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <androidx.appcompat.widget.LinearLayoutCompat\n                        android:id=\"@+id/configurationTableLayout\"\n                        android:orientation=\"vertical\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/peersTableRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/multicastLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/main_peers\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <TextView\n                                android:id=\"@+id/peersValue\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:alpha=\"0.5\"\n                                android:text=\"@string/main_no_peers\"\n                                android:textAlignment=\"textEnd\" />\n\n                            <ImageView\n                                android:id=\"@+id/peersChevron\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"match_parent\"\n                                android:layout_marginLeft=\"2pt\"\n                                android:alpha=\"0.4\"\n                                android:cropToPadding=\"false\"\n                                android:scaleType=\"fitEnd\"\n                                android:scaleX=\"1.2\"\n                                android:scaleY=\"1.2\"\n                                app:srcCompat=\"@drawable/ic_baseline_chevron_right_24\" />\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/dnsTableRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/dnsLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/main_dns_servers\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <TextView\n                                android:id=\"@+id/dnsValue\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:alpha=\"0.5\"\n                                android:text=\"@string/dns_no_servers\"\n                                android:textAlignment=\"textEnd\" />\n\n                            <ImageView\n                                android:id=\"@+id/dnsChevron\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"match_parent\"\n                                android:layout_marginLeft=\"2pt\"\n                                android:alpha=\"0.4\"\n                                android:cropToPadding=\"false\"\n                                android:scaleType=\"fitEnd\"\n                                android:scaleX=\"1.2\"\n                                android:scaleY=\"1.2\"\n                                app:srcCompat=\"@drawable/ic_baseline_chevron_right_24\" />\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/settingsTableRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/settingsLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/main_settings\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"320dp\"\n                                android:layout_height=\"match_parent\"\n                                android:layout_weight=\"2\" />\n\n                            <TextView\n                                android:id=\"@+id/textView\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\" />\n\n                            <ImageView\n                                android:id=\"@+id/settingsChevron\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"match_parent\"\n                                android:layout_marginLeft=\"2pt\"\n                                android:alpha=\"0.4\"\n                                android:scaleType=\"fitEnd\"\n                                android:scaleX=\"1.2\"\n                                android:scaleY=\"1.2\"\n                                app:srcCompat=\"@drawable/ic_baseline_chevron_right_24\" />\n\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/versionTableRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:id=\"@+id/versionLabel\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/main_version\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <TextView\n                                android:id=\"@+id/versionValue\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:alpha=\"0.5\"\n                                android:text=\"@string/main_unknown\"\n                                android:textAlignment=\"textEnd\" />\n\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/main_bottom_warning\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                </LinearLayout>\n            </ScrollView>\n\n        </LinearLayout>\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_peers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".PeersActivity\">\n\n    <FrameLayout\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:dividerPadding=\"4pt\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/pageTitle\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"8pt\"\n                    android:text=\"@string/main_peers\"\n                    android:textColor=\"?attr/textDefault\"\n                    android:textSize=\"24sp\"\n                    android:textStyle=\"bold\" />\n\n                <Space\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\" />\n\n                <ImageButton\n                    android:id=\"@+id/addPeerButton\"\n                    android:layout_width=\"16pt\"\n                    android:layout_height=\"16pt\"\n                    android:layout_marginTop=\"6pt\"\n                    android:layout_marginRight=\"8pt\"\n                    android:background=\"@android:color/transparent\"\n                    app:srcCompat=\"@drawable/ic_baseline_add_circle_24\" />\n            </LinearLayout>\n\n            <ScrollView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\" >\n\n                    <TextView\n                        android:id=\"@+id/connectedPeersLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:text=\"@string/peers_connected_title\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/connectedPeersTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\" />\n\n                    <TextView\n                        android:id=\"@+id/configuredPeersLabel\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/peers_configured_title\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/configuredPeersTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\" />\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/configured_peers_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TextView\n                        android:id=\"@+id/peers_discovery_link\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/peers_discovery_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <Space\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"32px\" />\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/peer_connectivity_title\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/configurationTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <TableRow\n                            android:id=\"@+id/enableMulticastBeaconPanel\"\n                            style=\"@style/SelectableSwitchItemStyle\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/discoverable_over_multicast\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <Switch\n                                android:id=\"@+id/enableMulticastBeacon\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\" />\n\n                        </TableRow>\n\n                        <TableRow\n                            android:id=\"@+id/enableMulticastListenPanel\"\n                            style=\"@style/SelectableSwitchItemStyle\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/search_for_multicast_peers\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <Space\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"2\" />\n\n                            <Switch\n                                android:id=\"@+id/enableMulticastListen\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\" />\n\n                        </TableRow>\n\n                        <TableRow\n                            android:id=\"@+id/passwordTableRow\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:clickable=\"true\"\n                            android:paddingStart=\"4pt\"\n                            android:paddingTop=\"4pt\"\n                            android:paddingEnd=\"4pt\"\n                            android:paddingBottom=\"4pt\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/peers_multicast_password_hint\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <EditText\n                                android:id=\"@+id/passwordEdit\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:background=\"@null\"\n                                android:ems=\"10\"\n                                android:hint=\"@string/tap_to_edit\"\n                                android:inputType=\"textVisiblePassword\"\n                                android:padding=\"0pt\"\n                                android:textAlignment=\"textEnd\"\n                                android:textSize=\"14sp\" />\n\n                        </TableRow>\n\n                    </TableLayout>\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/peer_connectivity_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n                </LinearLayout>\n            </ScrollView>\n\n        </LinearLayout>\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".PeersActivity\">\n\n    <FrameLayout\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:dividerPadding=\"4pt\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"8pt\"\n                    android:text=\"@string/main_settings\"\n                    android:textColor=\"?attr/textDefault\"\n                    android:textSize=\"24sp\"\n                    android:textStyle=\"bold\" />\n\n                <Space\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\" />\n\n            </LinearLayout>\n\n            <ScrollView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\" >\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:text=\"@string/node_info\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/nodeInfoTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <TableRow\n                            android:id=\"@+id/deviceNameTableRow\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:clickable=\"true\"\n                            android:paddingStart=\"4pt\"\n                            android:paddingTop=\"4pt\"\n                            android:paddingEnd=\"4pt\"\n                            android:paddingBottom=\"4pt\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:gravity=\"center\"\n                                android:text=\"@string/device_name\"\n                                android:textColor=\"?attr/textDefault\" />\n\n                            <EditText\n                                android:id=\"@+id/deviceNameEntry\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:background=\"@null\"\n                                android:ems=\"10\"\n                                android:hint=\"@string/tap_to_edit\"\n                                android:inputType=\"textPersonName\"\n                                android:padding=\"0pt\"\n                                android:textAlignment=\"textEnd\"\n                                android:textSize=\"14sp\" />\n\n                        </TableRow>\n                    </TableLayout>\n\n                    <TextView\n                        android:id=\"@+id/textView6\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"2pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/node_info_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/public_key\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <TableLayout\n                        android:id=\"@+id/publicKeyTableLayout\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <TableRow style=\"@style/SelectableItemStyle\">\n\n                            <LinearLayout\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"match_parent\"\n                                android:orientation=\"horizontal\">\n\n                                <TextView\n                                    android:id=\"@+id/publicKeyLabel\"\n                                    android:layout_width=\"wrap_content\"\n                                    android:layout_height=\"wrap_content\"\n                                    android:focusable=\"true\"\n                                    android:fontFamily=\"monospace\"\n                                    android:text=\"@string/public_key\"\n                                    android:textSize=\"14sp\" />\n\n                            </LinearLayout>\n                        </TableRow>\n                    </TableLayout>\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"2pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/public_key_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <Space\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"32px\" />\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:layout_marginBottom=\"2pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/settings_config\"\n                        android:textAllCaps=\"true\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                    <androidx.appcompat.widget.LinearLayoutCompat\n                        android:id=\"@+id/backupTableLayout\"\n                        android:orientation=\"vertical\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"8pt\"\n                        android:layout_marginLeft=\"8pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:background=\"@drawable/rounded\"\n                        android:divider=\"#46878787\"\n                        android:dividerPadding=\"4pt\"\n                        android:paddingLeft=\"4pt\"\n                        android:paddingTop=\"2pt\"\n                        android:paddingRight=\"4pt\"\n                        android:paddingBottom=\"2pt\"\n                        android:showDividers=\"middle\">\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/resetKeysRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/regenerate_keys\" />\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/setKeysRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/set_keys\" />\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n\n                        <androidx.appcompat.widget.LinearLayoutCompat\n                            android:id=\"@+id/resetConfigurationRow\"\n                            style=\"@style/SelectableItemStyle\">\n\n                            <TextView\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/reset_configuration\"\n                                android:textColor=\"@android:color/holo_red_dark\" />\n                        </androidx.appcompat.widget.LinearLayoutCompat>\n                    </androidx.appcompat.widget.LinearLayoutCompat>\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"16pt\"\n                        android:layout_marginLeft=\"16pt\"\n                        android:layout_marginTop=\"4pt\"\n                        android:layout_marginBottom=\"4pt\"\n                        android:layout_marginEnd=\"8pt\"\n                        android:layout_marginRight=\"8pt\"\n                        android:alpha=\"0.7\"\n                        android:paddingRight=\"8pt\"\n                        android:text=\"@string/reset_configuration_hint\"\n                        android:textAllCaps=\"false\"\n                        android:textAppearance=\"@style/TextAppearance.AppCompat.Small\"\n                        android:textSize=\"12sp\" />\n\n                </LinearLayout>\n            </ScrollView>\n\n        </LinearLayout>\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_add_dns_server.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingLeft=\"10pt\"\n        android:paddingTop=\"4pt\"\n        android:paddingRight=\"10pt\"\n        android:paddingBottom=\"4pt\">\n\n        <TextView\n            android:id=\"@+id/textView2\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Enter the IP address of the DNS server to add. Note that all DNS requests, including for non-Yggdrasil internet hostnames, will be sent to these servers.\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@+id/addDnsInput\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"4pt\"\n                android:lines=\"1\"\n                android:hint=\"8.8.8.8 or 302:7991::53\" />\n        </com.google.android.material.textfield.TextInputLayout>\n    </LinearLayout>\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_addpeer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingLeft=\"10pt\"\n        android:paddingTop=\"4pt\"\n        android:paddingRight=\"10pt\"\n        android:paddingBottom=\"4pt\">\n\n        <TextView\n            android:id=\"@+id/textView2\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/add_peer_help\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:id=\"@+id/addPeerInputLayout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@+id/addPeerInput\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"4pt\"\n                android:lines=\"1\"\n                android:hint=\"tcp://address:port\" />\n        </com.google.android.material.textfield.TextInputLayout>\n    </LinearLayout>\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_resetconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingLeft=\"10pt\"\n        android:paddingTop=\"4pt\"\n        android:paddingRight=\"10pt\"\n        android:paddingBottom=\"4pt\">\n\n        <TextView\n            android:id=\"@+id/textView2\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"This operation will reset your configuration and generate new keys. This is not reversible. Changes will not take effect until the next time Yggdrasil is started.\" />\n\n    </LinearLayout>\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_set_keys.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingLeft=\"10pt\"\n        android:paddingRight=\"10pt\"\n        android:paddingTop=\"4pt\"\n        android:paddingBottom=\"4pt\">\n\n        <TextView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"4dp\"\n            android:text=\"@string/private_key_label\" />\n        <EditText\n            android:id=\"@+id/private_key\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n    </LinearLayout>\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/dns_server_usable.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/tableRow\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <TextView\n        android:id=\"@+id/serverValue\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"12dp\"\n        android:layout_marginTop=\"4dp\"\n        android:layout_marginEnd=\"16dp\"\n        android:layout_marginBottom=\"4dp\"\n        android:gravity=\"center_vertical\"\n        android:ellipsize=\"end\"\n        android:singleLine=\"true\"\n        android:text=\"\"\n        android:textColor=\"?attr/textDefault\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@+id/addButton\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <ImageButton\n        android:id=\"@+id/addButton\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"8dp\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_marginBottom=\"8dp\"\n        android:layout_gravity=\"center_vertical\"\n        android:background=\"@android:color/transparent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_baseline_add_circle_24\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/peers_configured.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/tableRow\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <TextView\n        android:id=\"@+id/addressValue\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"12dp\"\n        android:layout_marginTop=\"4dp\"\n        android:layout_marginEnd=\"16dp\"\n        android:layout_marginBottom=\"4dp\"\n        android:gravity=\"center_vertical\"\n        android:ellipsize=\"end\"\n        android:singleLine=\"true\"\n        android:text=\"\"\n        android:textColor=\"?attr/textDefault\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@+id/deletePeerButton\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <ImageButton\n        android:id=\"@+id/deletePeerButton\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"8dp\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_marginBottom=\"8dp\"\n        android:layout_gravity=\"center_vertical\"\n        android:background=\"@android:color/transparent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_baseline_remove_circle_24\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/peers_connected.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TableRow xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\"\n        android:paddingLeft=\"4pt\"\n        android:paddingTop=\"2pt\"\n        android:paddingEnd=\"4pt\"\n        android:paddingBottom=\"2pt\">\n\n        <TextView\n            android:id=\"@+id/addressLabel\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:ellipsize=\"end\"\n            android:singleLine=\"true\"\n            android:text=\"(unknown)\"\n            android:textColor=\"?attr/textDefault\" />\n\n        <TextView\n            android:id=\"@+id/detailsLabel\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:alpha=\"0.5\"\n            android:ellipsize=\"end\"\n            android:singleLine=\"true\"\n            android:text=\"TCP peer on unknown\"\n            android:textColor=\"?attr/textDefault\"\n            android:textSize=\"12sp\" />\n    </LinearLayout>\n</TableRow>"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <attr name=\"tableBackgroundColor\" format=\"reference\" />\n    <attr name=\"textDefault\" format=\"reference\" />\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"green\">#5FBF9F</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n    <color name=\"hintlight\">#F2F1F5</color>\n    <color name=\"hintdark\">#1C1C1E</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Yggdrasil</string>\n    <string name=\"copied_to_clipboard\">Copied to clipboard</string>\n    <string name=\"dns_configured_servers_hint\">Use these DNS servers while Yggdrasil is running. Note that all DNS requests, including for non-Yggdrasil internet hostnames, will be sent to these servers.</string>\n    <string name=\"dns_configured_servers_hint_empty\">Yggdrasil will not configure any DNS servers when the service starts. All DNS requests will be resolved by the default resolver.</string>\n    <string name=\"dns_usable_servers_hint\">These DNS servers are provided by community members. Click the + button to add them to the list above. Long-tap to see more info.</string>\n    <string name=\"dns_no_configured_servers\">No servers configured</string>\n    <string name=\"dns_configured_servers\">Configured servers</string>\n    <string name=\"dns_server_info_revertron\">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.</string>\n    <string name=\"dns_server_info_dialog_title\">Server info</string>\n    <string name=\"ok\">OK</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"remove\">Remove</string>\n    <string name=\"add\">Add</string>\n    <string name=\"dns_add_server_dialog_title\">Add DNS server</string>\n    <string name=\"dns_activity_title\">DNS</string>\n    <string name=\"dns_usable_servers\">Usable servers</string>\n    <string name=\"dns_fix_chrome_based_browsers\">Fix Chrome-based browsers</string>\n    <string name=\"dns_fix_chrome_based_browsers_hint\">If you do not have IPv6 internet connectivity, this option should help Chrome-based browsers to resolve Yggdrasil domain names correctly.</string>\n    <string name=\"dns_fixes\">DNS fixes</string>\n    <string name=\"dns_no_servers\">No servers</string>\n    <string name=\"dns_one_server\">1 server</string>\n    <string name=\"dns_many_servers\">%d servers</string>\n    <string name=\"dns_remove_title\">Remove %s?</string>\n    <string name=\"dns_already_added_server\">This server is already added.</string>\n    <string name=\"main_no_connectivity\">Enabled (No connectivity)</string>\n    <string name=\"main_enabled\">Connected</string>\n    <string name=\"main_disabled\">Not enabled</string>\n    <string name=\"main_no_peers\">No peers</string>\n    <string name=\"main_one_peer\">1 peer</string>\n    <string name=\"main_many_peers\">%d peers</string>\n    <string name=\"main_add_some_peers_title\">Note</string>\n    <string name=\"main_add_some_peers_message\">No peers are configured. If there are no multicast peers nearby, you will need to manually configure peers in order for Yggdrasil to connect and work properly.</string>\n    <string name=\"peers_add_peer\">Add Configured Peer</string>\n    <string name=\"peers_add\">Add</string>\n    <string name=\"peers_remove_title\">Remove %s?</string>\n    <string name=\"peers_remove\">Remove</string>\n    <string name=\"peers_no_configured_title\">No peers currently configured</string>\n    <string name=\"peers_configured_title\">Configured Peers</string>\n    <string name=\"peers_no_connected_title\">No peers currently connected</string>\n    <string name=\"peers_connected_title\">Connected Peers</string>\n    <string name=\"settings_warning_title\">Warning</string>\n    <string name=\"settings_config\">Config</string>\n    <string name=\"settings_reset\">Reset</string>\n    <string name=\"main_status\">Status</string>\n    <string name=\"main_enable_yggdrasil\">Enable Yggdrasil</string>\n    <string name=\"main_network_info\">Network info</string>\n    <string name=\"main_not_available\">N/A</string>\n    <string name=\"main_ip\">IP</string>\n    <string name=\"main_subnet\">Subnet</string>\n    <string name=\"main_configuration\">Configuration</string>\n    <string name=\"main_peers\">Peers</string>\n    <string name=\"main_dns_servers\">DNS servers</string>\n    <string name=\"main_settings\">Settings</string>\n    <string name=\"main_version\">Version</string>\n    <string name=\"main_unknown\">Unknown</string>\n    <string name=\"main_bottom_warning\">You must re-enable Yggdrasil after modifying Peers, DNS servers or Settings to make any changes effective.</string>\n    <string name=\"peer_connectivity_title\">Peer Connectivity</string>\n    <string name=\"discoverable_over_multicast\">Discoverable over multicast</string>\n    <string name=\"search_for_multicast_peers\">Search for multicast peers</string>\n    <string name=\"configured_peers_hint\">Yggdrasil will automatically attempt to connect to configured peers when started. If you configure more than one peer, your device may carry traffic on behalf of other network nodes. Avoid this by configuring only a single peer.</string>\n    <string name=\"peers_discovery_hint\">You can find public peers by opening <a href=\"https://publicpeers.neilalexander.dev/\">this link</a>.</string>\n    <string name=\"peer_connectivity_hint\">Multicast peers will be discovered on the same Wi-Fi network or via USB. They must have the same password. Data charges may apply when using mobile data. You can prevent data usage in the device settings.</string>\n    <string name=\"peers_multicast_password_hint\">Password</string>\n    <string name=\"node_info\">Node Info</string>\n    <string name=\"device_name\">Device Name</string>\n    <string name=\"tap_to_edit\">Tap to edit</string>\n    <string name=\"node_info_hint\">Information entered here is public and may be shown on network maps.</string>\n    <string name=\"public_key\">Public Key</string>\n    <string name=\"public_key_hint\">Your public key forms your identity on the network. It is safe to be shared.</string>\n    <string name=\"regenerate_keys\">Regenerate keys (and IPv6-address)</string>\n    <string name=\"reset_configuration\">Reset configuration</string>\n    <string name=\"reset_configuration_hint\">Resetting will overwrite with newly generated configuration. Your public keys and IP address on the network will change.</string>\n    <string name=\"tile_disabled\">Disabled</string>\n    <string name=\"tile_enabled\">Enabled (No connectivity)</string>\n    <string name=\"tile_connected\">Connected</string>\n    <string name=\"location_amsterdam\">Amsterdam, NL</string>\n    <string name=\"location_frankfurt\">Frankfurt, DE</string>\n    <string name=\"location_bratislava\">Bratislava, SK</string>\n    <string name=\"location_buffalo\">Buffalo, US</string>\n    <string name=\"channel_name\">VPN Service</string>\n    <string name=\"channel_description\">Main channel for foreground notification</string>\n    <string name=\"permission_notification_text\">Tap here to enable Yggdrasil.</string>\n    <string name=\"add_peer_help\">Enter the full URI of the peer to add. Yggdrasil will automatically connect to this peer when started.</string>\n    <string name=\"peer_invalid_uri\">Invalid URI format</string>\n    <string name=\"peer_invalid_scheme\">Scheme must be one of: %s</string>\n    <string name=\"peer_missing_host\">Host is required</string>\n    <string name=\"peer_missing_port\">Port is required</string>\n    <string name=\"private_key_label\">Private key:</string>\n    <string name=\"set_keys\">Set your own key</string>\n    <string name=\"save\">Save</string>\n    <string name=\"no_browser_found_toast\">No browser found to open the URL!</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"SelectableItemStyle\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:background\">?android:attr/selectableItemBackground</item>\n        <item name=\"android:paddingStart\">8dp</item>\n        <item name=\"android:paddingEnd\">8dp</item>\n        <item name=\"android:paddingTop\">10dp</item>\n        <item name=\"android:paddingBottom\">10dp</item>\n    </style>\n\n    <style name=\"SelectableSwitchItemStyle\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:background\">?android:attr/selectableItemBackground</item>\n        <item name=\"android:paddingStart\">8dp</item>\n        <item name=\"android:paddingEnd\">8dp</item>\n        <item name=\"android:paddingTop\">8dp</item>\n        <item name=\"android:paddingBottom\">8dp</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.Yggdrasil\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <!-- Primary brand color. -->\n        <item name=\"colorPrimary\">@color/white</item>\n        <item name=\"colorPrimaryDark\">@color/white</item>\n        <item name=\"colorPrimaryVariant\">@color/black</item>\n        <item name=\"colorOnPrimary\">@color/black</item>\n        <!-- Secondary brand color. -->\n        <item name=\"colorSecondary\">@color/teal_200</item>\n        <item name=\"colorSecondaryVariant\">@color/teal_700</item>\n        <item name=\"colorOnSecondary\">@color/black</item>\n        <!-- Customize your theme here. -->\n        <item name=\"android:statusBarColor\">?android:windowBackground</item>\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true</item>\n\n        <item name=\"android:windowBackground\">@color/hintlight</item>\n        <item name=\"tableBackgroundColor\">@color/white</item>\n        <item name=\"textDefault\">@color/black</item>\n    </style>\n\n    <style name=\"YggdrasilDialogs\" parent=\"@style/Theme.MaterialComponents.DayNight.Dialog\">\n        <item name=\"colorPrimary\">@color/green</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.Yggdrasil\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <!-- Primary brand color. -->\n        <item name=\"colorPrimary\">@color/white</item>\n        <item name=\"colorPrimaryVariant\">@color/white</item>\n        <item name=\"colorOnPrimary\">@color/black</item>\n        <!-- Secondary brand color. -->\n        <item name=\"colorSecondary\">@color/teal_200</item>\n        <item name=\"colorSecondaryVariant\">@color/teal_200</item>\n        <item name=\"colorOnSecondary\">@color/black</item>\n        <!-- Customize your theme here. -->\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">false</item>\n\n        <item name=\"android:windowBackground\">@color/black</item>\n        <item name=\"tableBackgroundColor\">@color/hintdark</item>\n        <item name=\"textDefault\">@color/white</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-ru/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Yggdrasil</string>\n    <string name=\"copied_to_clipboard\">Скопировано в буфер</string>\n    <string name=\"dns_configured_servers_hint\">Эти серверы DNS будут использоваться при включении Yggdrasil. Заметьте, что все запросы в DNS, даже о доменах не в Yggdrasil, будут отправляться на эти серверы.</string>\n    <string name=\"dns_configured_servers_hint_empty\">Yggdrasil не будет переопределять серверы DNS при старте. Все запросы DNS будут разрешаться серверами по умолчанию.</string>\n    <string name=\"dns_usable_servers_hint\">Эти серверы DNS предоставляются членами коммюнити. Нажмите кнопку + чтобы добавить их в список выше. Долгое нажатие чтобы посмотреть информацию.</string>\n    <string name=\"dns_no_configured_servers\">Серверы не настроены</string>\n    <string name=\"dns_configured_servers\">Настроенные серверы</string>\n    <string name=\"dns_server_info_revertron\">Этот сервер поддерживает работу с обычными доменами ICANN, системой ALFIS, доменами OpenNIC.\\n\\nКроме того, он блокирует рекламу, системы слежения и зловредные домены.\\n\\nАдминистратор сервера Revertron.</string>\n    <string name=\"dns_server_info_dialog_title\">О сервере</string>\n    <string name=\"ok\">Ок</string>\n    <string name=\"cancel\">Отмена</string>\n    <string name=\"remove\">Убрать</string>\n    <string name=\"add\">Добавить</string>\n    <string name=\"dns_add_server_dialog_title\">Добавить сервер DNS</string>\n    <string name=\"dns_activity_title\">DNS</string>\n    <string name=\"dns_usable_servers\">Рекомендуемые серверы</string>\n    <string name=\"dns_fix_chrome_based_browsers\">Обхитрить браузеры на основе Chrome</string>\n    <string name=\"dns_fix_chrome_based_browsers_hint\">Если у вас нет обычного подключения по IPv6, эта опция должна заставить браузеры на движке Chrome всё равно запрашивать записи IPv6.</string>\n    <string name=\"dns_fixes\">DNS трюки</string>\n    <string name=\"dns_no_servers\">Нет серверов</string>\n    <string name=\"dns_one_server\">1 сервер</string>\n    <string name=\"dns_many_servers\">%d сервера/серверов</string>\n    <string name=\"dns_remove_title\">Убрать %s?</string>\n    <string name=\"dns_already_added_server\">Этот сервер уже добавлен.</string>\n    <string name=\"main_no_connectivity\">Включено (Нет подключения)</string>\n    <string name=\"main_enabled\">Подключено</string>\n    <string name=\"main_disabled\">Выключено</string>\n    <string name=\"main_no_peers\">Нет пиров</string>\n    <string name=\"main_one_peer\">1 пир</string>\n    <string name=\"main_many_peers\">%d пира/пиров</string>\n    <string name=\"main_add_some_peers_title\">Внимание</string>\n    <string name=\"main_add_some_peers_message\">Не настроено ни одного пира. Если не будет обнаруживаемых пиров в этой сети, то вам надо будет добавить пир вручную, чтобы подключение к Yggdrasil работало как положено.</string>\n    <string name=\"peers_add_peer\">Добавить пира в конфиг</string>\n    <string name=\"peers_add\">Добавить</string>\n    <string name=\"peers_remove_title\">Убрать %s?</string>\n    <string name=\"peers_remove\">Убрать</string>\n    <string name=\"peers_no_configured_title\">Пиры не добавлены</string>\n    <string name=\"peers_configured_title\">Добавленные пиры</string>\n    <string name=\"peers_no_connected_title\">Нет подключенных пиров</string>\n    <string name=\"peers_connected_title\">Подключенные пиры</string>\n    <string name=\"settings_warning_title\">Внимание</string>\n    <string name=\"settings_config\">Конфигурация</string>\n    <string name=\"settings_reset\">Сброс</string>\n    <string name=\"main_status\">Состояние</string>\n    <string name=\"main_enable_yggdrasil\">Включить Yggdrasil</string>\n    <string name=\"main_network_info\">Адрес и сеть</string>\n    <string name=\"main_not_available\">Н/Д</string>\n    <string name=\"main_ip\">Адрес</string>\n    <string name=\"main_subnet\">Подсеть</string>\n    <string name=\"main_configuration\">Конфигурация</string>\n    <string name=\"main_peers\">Пиры</string>\n    <string name=\"main_dns_servers\">Серверы DNS</string>\n    <string name=\"main_settings\">Настройки</string>\n    <string name=\"main_version\">Версия</string>\n    <string name=\"main_unknown\">Не известно</string>\n    <string name=\"main_bottom_warning\">Вы должны перезапустить Yggdrasil после изменения пиров, серверов DNS или настроек, чтобы изменения вступили в силу.</string>\n    <string name=\"peer_connectivity_title\">Подключения пиров</string>\n    <string name=\"discoverable_over_multicast\">Находимый через multicast</string>\n    <string name=\"search_for_multicast_peers\">Искать пиров через multicast</string>\n    <string name=\"configured_peers_hint\">Yggdrasil будет пытаться подключаться к этим пирам автоматически. Если вы добавите несколько пиров, ваше устройство может быть использовано для переноса данных между другими узлами сети. Чтобы этого избежать настройте только один пир.</string>\n    <string name=\"peers_discovery_hint\">Вы можете найти публичные пиры <a href=\"https://publicpeers.neilalexander.dev/\">по этой ссылке</a>.</string>\n    <string name=\"peer_connectivity_hint\">Пиры могут быть найдены с помощью Multicast если они находятся в той же Wi-Fi сети, либо через USB. У них должен быть одинаковый пароль. Трафик в мобильной сети может быть платным. Вы можете отключить мобильные данные в настройках устройства.</string>\n    <string name=\"peers_multicast_password_hint\">Пароль</string>\n    <string name=\"node_info\">Об узле</string>\n    <string name=\"device_name\">Название устройства</string>\n    <string name=\"tap_to_edit\">Нажмите для изменения</string>\n    <string name=\"node_info_hint\">Эта информация публична и может появиться на картах сети.</string>\n    <string name=\"public_key\">Публичный ключ</string>\n    <string name=\"public_key_hint\">Ваш публичный ключ идентифицирует вас в сети. Его распространение безопасно.</string>\n    <string name=\"regenerate_keys\">Сбросить ключи (и адрес IPv6)</string>\n    <string name=\"reset_configuration\">Сбросить настройки</string>\n    <string name=\"reset_configuration_hint\">Сброс создаст полностью новые настройки. Это изменит ваш публичный ключ и адрес IP.</string>\n    <string name=\"tile_disabled\">Выключено</string>\n    <string name=\"tile_enabled\">Включено (Нет подключения)</string>\n    <string name=\"tile_connected\">Подключено</string>\n    <string name=\"location_amsterdam\">Амстердам, Нидерланды</string>\n    <string name=\"location_frankfurt\">Франкфурт, Германия</string>\n    <string name=\"location_bratislava\">Братислава, Словакия</string>\n    <string name=\"location_buffalo\">Буффало, США</string>\n    <string name=\"channel_name\">Сервис VPN</string>\n    <string name=\"channel_description\">Главный канал нотификаций сервиса</string>\n    <string name=\"permission_notification_text\">Нажмите здесь чтобы включить Yggdrasil.</string>\n    <string name=\"add_peer_help\">Введите полный URI пира для добавления. Yggdrasil будет автоматически подключаться к нему при запуске.</string>\n    <string name=\"peer_invalid_uri\">Неверный формат URI</string>\n    <string name=\"peer_invalid_scheme\">Протокол должен быть одним из: %s</string>\n    <string name=\"peer_missing_host\">Необходимо указать хост</string>\n    <string name=\"peer_missing_port\">Необходимо указать порт</string>\n    <string name=\"private_key_label\">Приватный ключ:</string>\n    <string name=\"set_keys\">Установить свой ключ</string>\n    <string name=\"save\">Сохранить</string>\n    <string name=\"no_browser_found_toast\">Не найден браузер для открытия ссылки!</string>\n</resources>"
  },
  {
    "path": "app/src/test/java/eu/neilalexander/yggdrasil/ExampleUnitTest.kt",
    "content": "package eu.neilalexander.yggdrasil\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n    ext.kotlin_version = '1.9.20'\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.1.4'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/11.txt",
    "content": "* Added quick-settings tile for fast on/off switching\n* Added notification with the connection status (it is needed for quick-settings tile)\n* Fixed small UI bugs\n* Updated Yggdrasil library to 0.4.7"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/15.txt",
    "content": "* Updated core Yggdrasil library to 0.5.1\n* Updated UI to reflect changes in new version\n* Fixed small bugs in UI"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/16.txt",
    "content": "* Updated core Yggdrasil library to 0.5.4"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/17.txt",
    "content": "* Updated core Yggdrasil library to 0.5.6\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/18.txt",
    "content": "Updated core library to 0.5.7, presenting these changes:\n\nAdded\nWebSocket support for peerings, by using the new ws:// scheme in Listen and Peers\nAdditionally, the wss:// scheme can be used to connect to a WebSocket peer behind a HTTPS reverse proxy\n\nChanged\nOn Linux, the TUN adapter now uses vectorised reads/writes where possible, which should reduce the amount of CPU time spent on syscalls and potentially improve throughput\nLink error handling has been improved and various link error messages have been rewritten to be clearer\nUpgrade dependencies\n\nFixed\nMultiple multicast connections to the same remote machine should now work correctly\nYou may get two connections in some cases, one inbound and one outbound, this is known and will not cause problems\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/19.txt",
    "content": "Updated core library to 0.5.9, presenting these changes:\n\nChanged\nThe routing algorithm has been updated with RTT-aware link costing, which should prefer lower latency links over higher latency links where possible\nThe calculated cost is an average of the link RTT, but newly established links are costed higher to begin with, such that unstable peerings can be avoided\nLink costs are only used where multiple next-hops are available and will be ignored if there is only one loop-free path to the destination\nThis is protocol-compatible with existing v0.5.x nodes but will have the best results when peering with nodes that are also running the latest version\nThe getPeers endpoint will now report the calculated link cost for each given peer\nUpgrade dependencies\n\nFixed\nMulticast discovery should now work again when building Yggdrasil as an Android framework\nMulticast discovery will now correctly ignore interfaces that are not marked as running\nEphemeral links, such as those added by multicast, will no longer try to reconnect in a fast loop, fixing a high CPU issue\nThe TUN interface will no longer stop working when hitting a segment read error from vectorised reads\nThe AllowedPublicKeys option will once again no longer apply to multicast peerings, as was originally intended\nA potential panic when shutting down peering links has been fixed\nA redundant system call for setting MTU on OpenBSD has been removed\n\nFixes in Android app\nFixed occasional crash on start/stop\nUpdated some dependencies\nUpdated Android API to 34"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/20.txt",
    "content": "Updated core library to 0.5.12, presenting these changes:\n\nFixed\nA timing regression which causes a higher level of idle protocol traffic on each peering has been fixed\n\nFixes in Android app\nSome UI fixes and improvements\nUpdated some dependencies\n\nUpdates from previous versions:\n\nChanged\nThe parent selection algorithm now only chooses a new parent if there is a larger cost benefit to doing so, which should help to stabilise the tree\nThe bloom filters are now repropagated periodically, to avoid nodes getting stuck with bad state\n\nFixed\nA memory leak caused by missed cleanup of the peer response map has been fixed\nOther bug fixes with bloom filter propagation for off-tree filters and zero vs one bits\nTLS-based peering connections now support TLS 1.2 again\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/changelogs/21.txt",
    "content": "Updated core library to 0.5.13.\nAdded peer validation on input."
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6 network.\nIt is lightweight, self-arranging, supported on multiple platforms and allows pretty much any IPv6-capable application to communicate securely with other Yggdrasil nodes.\nYggdrasil does not require you to have IPv6 Internet connectivity - it also works over IPv4.\n\nThis app allows you to connect to Yggdrasil Network and use any service located in this network. It works as VPN service, but all your usual traffic will go trough your provider, not through Yggdrasil Network.\n\nAlso, it is not a goal of the Yggdrasil project to provide anonymity. Direct peers over the Internet will be able to see your IP address and may be able to use this information to determine your location or identity. Multicast-discovered peerings on the same network will typically expose your device MAC address. Other nodes on the network may be able to discern some information about which nodes you are peered with.\n\nAll traffic sent across the Yggdrasil network is encrypted end-to-end. Assuming that our crypto is solid, it cannot be decrypted or read by any intermediate nodes, and can only be decrypted by the recipient for which it was intended. However, please note that Yggdrasil has not been officially externally audited."
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Official implementation for connecting to the Yggdrasil Network from Android"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "Yggdrasil"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/11.txt",
    "content": "* Добавлена кнопка в шторку для быстрого включения/выключения\n* Добавлена компактная нотификация со статусом подключения (требуется для работы кнопки в шторке)\n* Исправлены некоторые баги в интерфейсе\n* Обновлена библиотека Yggdrasil до 0.4.7"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/15.txt",
    "content": "* Обновлена библиотека Yggdrasil до 0.5.1\n* Обновлен интерфейс в соответствии с новой версией библиотеки\n* Сделаны небольшие исправления в интерфейсе"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/16.txt",
    "content": "* Обновлена библиотека Yggdrasil до 0.5.4"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/17.txt",
    "content": "* Обновлена библиотека Yggdrasil до 0.5.6\n"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/18.txt",
    "content": "Обновлена основная библиотека до версии 0.5.7, в которой представлены следующие изменения:\n\nДобавлено\nПоддержка WebSocket для пиринга с использованием новой схемы ws:// в Listen и Peers\nКроме того, схему wss:// можно использовать для подключения к узлам WebSocket за обратным прокси-сервером HTTPS вроде Nginx\n\nИзменено\nВ Linux адаптер TUN теперь использует векторизованную чтение/запись, где это возможно, что должно сократить количество времени ЦП, затрачиваемого на системные вызовы, и потенциально повысить пропускную способность\nУлучшена обработка ошибок соединения, а различные сообщения об ошибках соединения были переписаны для большей ясности\nОбновление зависимостей\n\nИсправлено\nНесколько мультикаст подключений к одной и той же удаленной машине теперь должны работать правильно\nВ некоторых случаях вы можете получить два подключения, одно входящее и одно исходящее, это известное поведение и не вызовет проблем"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/19.txt",
    "content": "Обновлена основная библиотека до версии 0.5.9, в которой представлены следующие изменения:\n\nИзменено\nАлгоритм маршрутизации был обновлен с учетом стоимости соединения с RTT, что должно отдавать предпочтения соединениям с меньшей задержкой соединениям с большей задержкой, когда это возможно\nРасчетная стоимость представляет собой среднее значение RTT соединения, но новые соединения изначально оцениваются выше, так что можно избежать проблем с нестабильными узлами\nСтоимость соединения используется только при наличии нескольких следующих переходов и будет игнорироваться, если есть только один путь без петель к месту назначения\nЭта версия совместима с существующими узлами v0.5.x, но будет иметь наилучшие результаты при пиринге с узлами, которые также работают под управлением последней версии\nКоманда getPeers теперь будет сообщать рассчитанную стоимость соединения для каждого заданного пира\nОбновлены зависимости\n\nИсправлено\nОбнаружение локальных пиров теперь должно снова работать при сборке Yggdrasil как фреймворка Android\nОбнаружение локальных пиров теперь будет правильно игнорировать интерфейсы, которые не помечены как работающие\nЭфемерные соединения, такие как добавленные мультикастом, больше не будут пытаться быстро переподключаться в цикле, устранена проблема высокой загрузки ЦП\nИнтерфейс TUN больше не будет прекращать работу при срабатывании ошибки чтения пакета с помощью векторизованного чтения\nОпция AllowedPublicKeys снова больше не будет применяться к локальным пирам, как изначально предполагалось\nПотенциальный краш при отключении пиринговых соединений был исправлен\nИзбыточный системный вызов для установки MTU в OpenBSD был удален\n\nИсправления в приложении для Android\nИсправлен случайный сбой при запуске/остановке\nОбновлены некоторые зависимости\nОбновлен Android API до 34"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/20.txt",
    "content": "Обновлена основная библиотека до версии 0.5.12, в которой представлены следующие изменения:\n\nИсправлено\nИсправлена регрессия синхронизации, которая приводит к более высокому уровню служебного трафика в простое\n\nИсправления в приложении Android\nИсправления и улучшения пользовательского интерфейса\nОбновлены некоторые зависимости\n\nОбновления с предыдущих версий:\n\nИзменено\nАлгоритм выбора родителя теперь выбирает нового родителя только в том случае, если это дает большую экономическую выгоду, что должно помочь стабилизировать дерево\nФильтры Блума теперь периодически распространяются повторно, чтобы избежать застревания узлов в плохом состоянии\n\nИсправлено\nУтечка памяти, вызванная пропущенной очисткой карты ответов пиров\nДругие исправления ошибок с распространением фильтра Блума для фильтров вне дерева\nПиринг с использованием TLS теперь снова поддерживают TLS 1.2"
  },
  {
    "path": "fastlane/metadata/android/ru/changelogs/21.txt",
    "content": "Обновлена основная библиотека до версии 0.5.13.\nДобавлена валидация пира при вводе."
  },
  {
    "path": "fastlane/metadata/android/ru/full_description.txt",
    "content": "Yggdrasil это рабочая реализация полностью зашифрованной сети IPv6.\nОна лёгкая, само-организующаяся, поддерживающая множество платформ, и позволяющая любому ПО, способному работать с IPv6, свободно коммуницировать с другими узлами сети Yggdrasil.\nYggdrasil не требует Интернет-подключения по IPv6 - она работает и поверх IPv4.\n\nЭто приложение позволяет вам подключиться к Yggdrasil Network и пользоваться любыми сервисами, расположенными в этой сети. Оно работает как сервис VPN, но весь ваш обычный Интернет-трафик будет идти через вашего провайдера, не через сеть Yggdrasil.\n\nЗаметьте, что анонимность не является целью Yggdrasil. Прямые пиры через Интернет смогут видеть ваш настоящий адрес IP и смогут использовать эту информацию для определения вашего местоположения или идентификации. Пиры, подключающиеся с помощью multicast, скорее всего смогут узнать ваш MAC-адрес. Некоторые узлы в сети могут вычислить через какие узлы вы входите в сеть Yggdrasil.\n\nВесь трафик, пересылаемый через сеть Yggdrasil зашифрован от точки к точке. Предполагая, что наше шифрование  реализовано правильно, трафик не может быть расшифрован или прочитан промежуточными узлами, иможет быть расшифрован только адресатом. Однако, просим заметить, что Yggdrasil не подвергался официальному внешнему аудиту."
  },
  {
    "path": "fastlane/metadata/android/ru/short_description.txt",
    "content": "Официальный клиент для подключения к Yggdrasil Network с устройств Android"
  },
  {
    "path": "fastlane/metadata/android/ru/title.txt",
    "content": "Yggdrasil"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Mon Nov 27 01:27:23 CET 2023\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.4-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app\"s APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n#android.enableR8.fullMode=false\nandroid.defaults.buildfeatures.buildconfig=true\nandroid.nonTransitiveRClass=false\nandroid.nonFinalResIds=false"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "readme.md",
    "content": "Yggdrasil Android\n-----------------\n\nYggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6 network. It is lightweight, self-arranging, supported on multiple platforms and allows pretty much any IPv6-capable application to communicate securely with other Yggdrasil nodes. Yggdrasil does not require you to have IPv6 Internet connectivity - it also works over IPv4.\n\nThis app allows you to connect to Yggdrasil Network and use any service located in this network. It works as VPN service, but all your usual traffic will go trough your provider, not through Yggdrasil Network.\n\nAlso, it is not a goal of the Yggdrasil project to provide anonymity. Direct peers over the Internet will be able to see your IP address and may be able to use this information to determine your location or identity. Multicast-discovered peerings on the same network will typically expose your device MAC address. Other nodes on the network may be able to discern some information about which nodes you are peered with.\n\nAll traffic sent across the Yggdrasil network is encrypted end-to-end. Assuming that our crypto is solid, it cannot be decrypted or read by any intermediate nodes, and can only be decrypted by the recipient for which it was intended. However, please note that Yggdrasil has not been officially externally audited. \n\n## Download\n\n[<img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on.png\"\n     alt=\"Get it on F-Droid\"\n     height=\"80\">](https://f-droid.org/packages/eu.neilalexander.yggdrasil/)\n\nOr get the APK from the [Releases Section](https://github.com/yggdrasil-network/yggdrasil-android/releases/latest).\n\n## Build Instructions\n\n* install gomobile\n\n```bash\ngo install golang.org/x/mobile/cmd/gomobile@latest\n```\n\n* clone yggdrasil-android and initialize the yggdrasil-go submodule\n\n```bash\ngit clone https://github.com/yggdrasil-network/yggdrasil-android /tmp/yggdrasil-android\ncd /tmp/yggdrasil-android\ngit submodule update --init\n```\n\n* build yggdrasil-go for android and copy over the built library\n\n```bash\ncd libs/yggdrasil-go\n./contrib/mobile/build -a\ncp yggdrasil.aar ../../app/libs/\ncd ../..\n```\n\n* build yggdrasil-android\n\n```bash\n./gradlew assembleRelease\n```\n\n## Updating yggdrasil-go to the Latest Release\n\nThe yggdrasil-go library is pinned as a git submodule in `libs/yggdrasil-go`. To update it to the latest release and rebuild:\n\n```bash\ncd libs/yggdrasil-go\ngit fetch --tags\ngit checkout $(git describe --tags $(git rev-list --tags --max-count=1))\n```\n\nThen rebuild the library and copy it:\n\n```bash\n./contrib/mobile/build -a\ncp yggdrasil.aar ../../app/libs/\ncd ../..\n```\n\nFinally, commit the submodule update:\n\n```bash\ngit add libs/yggdrasil-go\ngit commit -m \"Update yggdrasil-go to $(cd libs/yggdrasil-go && git describe --tags)\"\n```\n\nnote: you will need to use jdk-11 as jdk-16 `\"doesn't work\" ™`\n\non debian/ubuntu you can set which jdk used with the `JAVA_HOME` env var:\n```\nexport JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/\n```\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = \"Yggdrasil\"\ninclude ':app'\n"
  }
]