implementation "androidx.activity:activity-ktx:1.13.0"
implementation 'androidx.arch.core:core-common:2.2.0'
implementation 'androidx.arch.core:core-runtime:2.2.0'
- implementation 'androidx.appcompat:appcompat:1.7.1'
+ implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
implementation 'androidx.core:core-ktx:1.19.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
implementation 'androidx.viewpager:viewpager:1.1.0'
- implementation 'androidx.webkit:webkit:1.16.0'
+ implementation 'androidx.webkit:webkit:1.17.0'
// Include the Kotlin standard library. This should be the same version number listed in project build.gradle.
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.2.10'
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2016-2018, 2020-2022, 2025-2026 Soren Stoutner <soren@stoutner.com>
- Translation 2016 Aaron Gerlach <aaron@gerlach.com>. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2019, 2022, 2026 Bernhard G. Keller. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
Translation 2018 Stefan Erhardt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
- Translation 2019, 2022 Bernhard G. Keller. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2016 Aaron Gerlach <aaron@gerlach.com>. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
<h3>Geräte in der Nähe</h3>
<p><a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">android.permission.<wbr>ACCESS_LOCAL_NETWORK</a></p>
- <p>This is a dangerous permission, meaning users must manually enable it if they desire the functionality. It was added in API 37 (Android 17).
- It is required to load web pages on the Local Area Network (LAN), like the web interface of a wireless router.</p>
+ <p>Dies ist eine gefährliche Berechtigung, sodass sie vom Benutzer manuell aktiviert werden muss, wenn sie benötigt wird.
+ Sie wurde mit API 37 (Android 17) eingeführt und wird benötigt, um Web-Seiten in einem lokalen Netzwerk (LAN) zu laden, wie z.B. das Web-Interface eines WLAN-Routers o.ä.</p>
<h3><a href="https://developer.android.com/about/versions/13/features#runtime-receivers">
com.stoutner.privacybrowser.<wbr>standard.<wbr>DYNAMIC_<wbr>RECEIVER_<wbr>NOT_<wbr>EXPORTED_<wbr>PERMISSION</a></h3>
Eine detailiertere Beschreibung, wie die Filterlisten-Einträge verarbeitet werden, ist unter <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a> verfügbar.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> Lokale Netzwerk-Anfragen</h3>
- <p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
- <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
- This is a dangerous permission that must be explicitly enabled by the user. Devices on the LAN include everything that is on the same subnet as Privacy Browser, like the web interface of a wireless router.
- Privacy Browser does not prompt for this permission, and there is no need for users to enable it if they only want to browse the general internet.
- Users who need to be able to load web pages from the LAN can grant this permission in the OS settings for Privacy Browser.</p>
+ <p>Seit Android 17 (API 37), kann Android’s WebView keine Anfragen an Geräte im LAN (Local Area Network) senden, sofern dafür nicht die Berechtigung
+ <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Geräte in der Nähe (Nearby Devices)</a> gewährt wurde.
+ Dies ist eine gefährliche Berechtigung, welche vom Benutzer explizit aktiviert werden muss.
+ Geräte im LAN beinhalten Alles im selben Subnet, in dem sich auch Privacy Browser befindet, wie z.B. das Web-Interface eines WLAN-Routers.
+ Privacy Browser fordert diese Berechtigung nicht ein und es ist nicht notwendig, sie zu aktivieren, um lediglich das allgemeinen Internet zu nutzen.
+ Benutzer, die Web-Seiten im LAN nutzen wollen, können diese Berechtigung in den Betriebssystem-Einstellungen von Privacy Browsergewähren.</p>
- <img class="center224" src="images/nearby_devices.png" alt="Nearby Devices Permission"/>
+ <img class="center224" src="images/nearby_devices.png" alt="Berechtigung für Geräte in der Nähe"/>
</body>
</html>
A more detailed description of how the filter list entries are processed is available at <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> LAN Requests</h3>
<p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
<a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2016-2018, 2020-2022, 2025-2026 Soren Stoutner <soren@stoutner.com>
- Translation 2017-2018, 2022 Jose A. León. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2017-2018, 2022, 2026 Jose A. León. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
<h3>Dispositivos cercanos</h3>
<p><a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">android.permission.<wbr>ACCESS_LOCAL_NETWORK</a></p>
- <p>This is a dangerous permission, meaning users must manually enable it if they desire the functionality. It was added in API 37 (Android 17).
- It is required to load web pages on the Local Area Network (LAN), like the web interface of a wireless router.</p>
+ <p>Este es un permiso peligroso, lo que significa que los usuarios deben activarlo manualmente si desean esta funcionalidad. Fue añadido en la API 37 (Android 17).
+ Es necesario para cargar páginas web en la red de área local (LAN), como la interfaz web de un router inalámbrico.</p>
<h3><a href="https://developer.android.com/about/versions/13/features#runtime-receivers">
com.stoutner.privacybrowser.<wbr>standard.<wbr>DYNAMIC_<wbr>RECEIVER_<wbr>NOT_<wbr>EXPORTED_<wbr>PERMISSION</a></h3>
Encontrará una descripción más detallada de cómo se procesan las entradas de la lista de filtros en <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> Solicitudes de LAN</h3>
- <p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
- <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
- This is a dangerous permission that must be explicitly enabled by the user. Devices on the LAN include everything that is on the same subnet as Privacy Browser, like the web interface of a wireless router.
- Privacy Browser does not prompt for this permission, and there is no need for users to enable it if they only want to browse the general internet.
- Users who need to be able to load web pages from the LAN can grant this permission in the OS settings for Privacy Browser.</p>
+ <p>A partir de Android 17 (API 37), WebView de Android no puede realizar solicitudes a dispositivos de la LAN (Red de Área Local) a menos que
+ <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">el permiso Dispositivos Cercanos</a> se haya concedido.
+ Este es un permiso peligroso que debe ser habilitado explícitamente por el usuario.
+ Los dispositivos de la LAN incluyen todos aquellos que están en la misma subred que Navegador Privado, como la interfaz web de un router inalámbrico.
+ Navegador Privado no solicita este permiso, y no hay necesidad de que los usuarios lo activen si solo desean navegar por internet en general.
+ Los usuarios que necesiten cargar páginas webs desde la LAN pueden conceder este permiso en la configuración del sistema operativo para Navegador Privado.</p>
- <img class="center224" src="images/nearby_devices.png" alt="Nearby Devices Permission"/>
+ <img class="center224" src="images/nearby_devices.png" alt="Permiso de Dispositivos Cercanos"/>
</body>
</html>
<a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> LAN Requests</h3>
<p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
<a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2017-2018, 2020-2022, 2025-2026 Soren Stoutner <soren@stoutner.com>
- Translation 2017-2018, 2022 Francesco Buratti. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2017-2018, 2022, 2026 Francesco Buratti. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
<h3>Dispositivi nelle vicinanze</h3>
<p><a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">android.permission.<wbr>ACCESS_LOCAL_NETWORK</a></p>
- <p>This is a dangerous permission, meaning users must manually enable it if they desire the functionality. It was added in API 37 (Android 17).
- It is required to load web pages on the Local Area Network (LAN), like the web interface of a wireless router.</p>
+ <p>Si tratta di una autorizzazione pericolosa, per cui gli utenti devono abilitarla manualmente nel caso vogliano utilizzare questa funzionalità. E' stata aggiunta con la API 37 (Android 17).
+ E' richiesta per caricare pagine web nella Local Area Network (LAN), come l'interfaccia web di un router wireless.</p>
<h3><a href="https://developer.android.com/about/versions/13/features#runtime-receivers">
com.stoutner.privacybrowser.<wbr>standard.<wbr>AUTORIZZAZIONE_<wbr>DYNAMIC_<wbr>RECEIVER_<wbr>NON_<wbr>ESPORTATA</a></h3>
Una descrizione più dettagliata di come le liste dei filtri sono processate è disponibile su <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
-
- <p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
- <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
- This is a dangerous permission that must be explicitly enabled by the user. Devices on the LAN include everything that is on the same subnet as Privacy Browser, like the web interface of a wireless router.
- Privacy Browser does not prompt for this permission, and there is no need for users to enable it if they only want to browse the general internet.
- Users who need to be able to load web pages from the LAN can grant this permission in the OS settings for Privacy Browser.</p>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> Richieste LAN</h3>
+
+ <p>A partire da Android 17 (API 37), la webview di Android non può inviare richieste a dispositivi sulla LAN (Local Area Network) a meno che l'autorizzazione
+ <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Dispositivi nelle vicinanze</a> non sia stata garantita.
+ Si tratta di una autorizzazione pericolosa che deve essere abilitata esplicitamente dall'utente.
+ I dispositivi presenti sulla LAN includono tutto ciò che è presente sulla stessa subnet di Privacy Browser, come l'interfaccia web di un router wireless.
+ Privacy Browser non chiede questa autorizzazione, e non c'è nessuna necessità per gli utenti di abilitarla se tutto ciò che fogliono fare è solo navigare su internet.
+ Gli utenti che vogliono essere in grado di caricare pagine web dalla LAN possono garantire questa autorizzazione nelle impostazioni del sistema operativo per Privacy Browser.</p>
<img class="center224" src="images/nearby_devices.png" alt="Nearby Devices Permission"/>
</body>
A more detailed description of how the filter list entries are processed is available at <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> LAN Requests</h3>
<p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
<a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
<h3>Устройства поблизости</h3>
<p><a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">android.permission.<wbr>ACCESS_LOCAL_NETWORK</a></p>
- <p>This is a dangerous permission, meaning users must manually enable it if they desire the functionality. It was added in API 37 (Android 17).
- It is required to load web pages on the Local Area Network (LAN), like the web interface of a wireless router.</p>
+ <p>Это опасное разрешение, то есть пользователи должны включить его вручную, если им нужна эта функциональность. Оно было добавлено в API 37 (Android 17).
+ Оно требуется для загрузки веб-страниц в локальной сети (LAN), например веб-интерфейса беспроводного роутера.</p>
<h3><a href="https://developer.android.com/about/versions/13/features#runtime-receivers">
com.stoutner.privacybrowser.<wbr>standard.<wbr>DYNAMIC_<wbr>RECEIVER_<wbr>NOT_<wbr>EXPORTED_<wbr>PERMISSION</a></h3>
Более подробное описание обработки записей фильтров доступно на странице <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> Запросы в локальной сети</h3>
- <p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
- <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
- This is a dangerous permission that must be explicitly enabled by the user. Devices on the LAN include everything that is on the same subnet as Privacy Browser, like the web interface of a wireless router.
- Privacy Browser does not prompt for this permission, and there is no need for users to enable it if they only want to browse the general internet.
- Users who need to be able to load web pages from the LAN can grant this permission in the OS settings for Privacy Browser.</p>
+ <p>Начиная с Android 17 (API 37), WebView в Android не может отправлять запросы к устройствам в локальной сети (LAN), если не предоставлено разрешение
+ <a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">«Устройства поблизости»</a>.
+ Это опасное разрешение, которое пользователь должен включить явным образом.
+ К устройствам в локальной сети относится все, что находится в той же подсети, что и Privacy Browser, например веб-интерфейс беспроводного роутера.
+ Privacy Browser не запрашивает это разрешение, и пользователям не нужно его включать, если они хотят просто просматривать интернет.
+ Пользователи, которым нужна возможность загружать веб-страницы из локальной сети, могут предоставить это разрешение для Privacy Browser в настройках ОС.</p>
- <img class="center224" src="images/nearby_devices.png" alt="Nearby Devices Permission"/>
+ <img class="center224" src="images/nearby_devices.png" alt="Разрешение Устройства поблизости"/>
</body>
</html>
A more detailed description of how the filter list entries are processed is available at <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> LAN Requests</h3>
<p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
<a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
A more detailed description of how the filter list entries are processed is available at <a href="https://www.stoutner.com/privacy-browser-android/filter-lists/">stoutner.com</a>.</p>
- <h3>LAN Requests</h3>
+ <h3><svg class="header"><use href="../shared_images/local_activity.svg#icon"/></svg> LAN Requests</h3>
<p>Beginning with Android 17 (API 37), Android’s WebView cannot make requests to devices on the LAN (Local Area Network) unless the
<a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_LOCAL_NETWORK">Nearby Devices</a> permission has been granted.
import android.net.Uri
import android.os.Bundle
import android.os.Handler
+import android.provider.OpenableColumns
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
-import java.lang.Exception
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.CipherOutputStream
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
+
import kotlin.system.exitProcess
// Define the encryption constants.
private const val OPENPGP_ENCRYPTION = 2
// Define the saved instance state constants.
-private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "A"
-private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "B"
-private const val SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY = "C"
-private const val SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "D"
-private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "E"
-private const val SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY = "F"
-private const val SETTINGS_FILE_NAME_TEXT = "G"
-private const val SETTINGS_IMPORT_EXPORT_BUTTON_TEXT = "H"
-private const val BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "I"
-private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY = "J"
-private const val BOOKMARKS_FILE_NAME_TEXT = "K"
-private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT = "L"
+private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_STRING = "bookmarks_import_export_button_string"
+private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY = "bookmarks_import_export_button_visibility"
+private const val BOOKMARKS_URI_LINEARLAYOUT_VISIBILITY = "bookmarks_uri_linearlayout_visibility"
+private const val BOOKMARKS_URI_STRING = "booksmarks_uri_string"
+private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "encryption_password_textinputlayout_visibility"
+private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "open_keychian_import_instructions_textview_visibility"
+private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "open_keychain_required_textview_visibilty"
+private const val SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY = "settings_file_location_cardview_visibility"
+private const val SETTINGS_IMPORT_EXPORT_BUTTON_TEXT = "settings_import_export_button_text"
+private const val SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY = "settings_import_export_button_visibility"
+private const val SETTINGS_URI_LINEARLAYOUT_VISIBILITY = "settings_uri_linearlayout_visibility"
+private const val SETTINGS_URI_STRING = "settings_uri_string"
class ImportExportActivity : AppCompatActivity() {
// Define the class views.
- private lateinit var scrollView: ScrollView
- private lateinit var encryptionSpinner: Spinner
- private lateinit var encryptionPasswordTextInputLayout: TextInputLayout
+ private lateinit var bookmarksUriEditText: EditText
+ private lateinit var bookmarksImportExportButton: Button
+ private lateinit var bookmarksImportRadioButton: RadioButton
+ private lateinit var bookmarksUriLinearLayout: LinearLayout
private lateinit var encryptionPasswordEditText: EditText
+ private lateinit var encryptionPasswordTextInputLayout: TextInputLayout
+ private lateinit var encryptionSpinner: Spinner
+ private lateinit var openKeychainImportInstructionsTextView: TextView
private lateinit var openKeychainRequiredTextView: TextView
+ private lateinit var scrollView: ScrollView
private lateinit var settingsFileLocationCardView: CardView
- private lateinit var settingsImportRadioButton: RadioButton
- private lateinit var settingsFileNameLinearLayout: LinearLayout
- private lateinit var settingsFileNameEditText: EditText
- private lateinit var openKeychainImportInstructionsTextView: TextView
private lateinit var settingsImportExportButton: Button
- private lateinit var bookmarksImportRadioButton: RadioButton
- private lateinit var bookmarksFileNameLinearLayout: LinearLayout
- private lateinit var bookmarksFileNameEditText: EditText
- private lateinit var bookmarksImportExportButton: Button
+ private lateinit var settingsImportRadioButton: RadioButton
+ private lateinit var settingsUriEditText: EditText
+ private lateinit var settingsUriLinearLayout: LinearLayout
// Define the class variables.
private lateinit var fileProviderDirectory: File
private val settingsBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
// Only do something if the user didn't press back from the file picker.
if (fileUri != null) {
- // Get the file name string from the URI.
- val fileNameString = fileUri.toString()
+ // Convert the URI to a string.
+ val fileUriString = fileUri.toString()
- // Set the settings file name text.
- settingsFileNameEditText.setText(fileNameString)
+ // Set the settings URI text.
+ settingsUriEditText.setText(fileUriString)
- // Move the cursor to the end of the file name edit text.
- settingsFileNameEditText.setSelection(fileNameString.length)
+ // Move the cursor to the end of the URI edit text.
+ settingsUriEditText.setSelection(fileUriString.length)
}
}
private val settingsBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
// Only do something if the user didn't press back from the file picker.
if (fileUri != null) {
- // Get the file name string from the URI.
- val fileNameString = fileUri.toString()
+ // Convert the URI to a string.
+ val fileUriString = fileUri.toString()
- // Set the settings file name text.
- settingsFileNameEditText.setText(fileNameString)
+ // Set the settings URI text.
+ settingsUriEditText.setText(fileUriString)
- // Move the cursor to the end of the file name edit text.
- settingsFileNameEditText.setSelection(fileNameString.length)
+ // Move the cursor to the end of the Uri edit text.
+ settingsUriEditText.setSelection(fileUriString.length)
}
}
// Get the file name string from the URI.
val fileNameString = fileUri.toString()
- // Set the bookmarks file name text.
- bookmarksFileNameEditText.setText(fileNameString)
+ // Set the bookmarks URI text.
+ bookmarksUriEditText.setText(fileNameString)
- // Move the cursor to the end of the file name edit text.
- bookmarksFileNameEditText.setSelection(fileNameString.length)
+ // Move the cursor to the end of the URI edit text.
+ bookmarksUriEditText.setSelection(fileNameString.length)
// Scroll to the bottom.
scrollView.post {
// Get the file name string from the URI.
val fileNameString = fileUri.toString()
- // Set the bookmarks file name text.
- bookmarksFileNameEditText.setText(fileNameString)
+ // Set the bookmarks URI text.
+ bookmarksUriEditText.setText(fileNameString)
// Move the cursor to the end of the file name edit text.
- bookmarksFileNameEditText.setSelection(fileNameString.length)
+ bookmarksUriEditText.setSelection(fileNameString.length)
// Scroll to the bottom.
scrollView.post {
settingsFileLocationCardView = findViewById(R.id.settings_file_location_cardview)
settingsImportRadioButton = findViewById(R.id.settings_import_radiobutton)
val settingsExportRadioButton = findViewById<RadioButton>(R.id.settings_export_radiobutton)
- settingsFileNameLinearLayout = findViewById(R.id.settings_file_name_linearlayout)
- settingsFileNameEditText = findViewById(R.id.settings_file_name_edittext)
+ settingsUriLinearLayout = findViewById(R.id.settings_uri_linearlayout)
+ settingsUriEditText = findViewById(R.id.settings_uri_edittext)
+ val settingsFileNameTextView = findViewById<TextView>(R.id.settings_file_name_textview)
openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
settingsImportExportButton = findViewById(R.id.settings_import_export_button)
bookmarksImportRadioButton = findViewById(R.id.bookmarks_import_radiobutton)
- bookmarksFileNameLinearLayout = findViewById(R.id.bookmarks_file_name_linearlayout)
- bookmarksFileNameEditText = findViewById(R.id.bookmarks_file_name_edittext)
+ bookmarksUriLinearLayout = findViewById(R.id.bookmarks_uri_linearlayout)
+ bookmarksUriEditText = findViewById(R.id.bookmarks_uri_edittext)
+ val bookmarksFileNameTextView = findViewById<TextView>(R.id.bookmarks_file_name_textview)
bookmarksImportExportButton = findViewById(R.id.bookmarks_import_export_button)
// Create an array adapter for the spinner.
// Show the file location card.
settingsFileLocationCardView.visibility = View.VISIBLE
- // Show the file name linear layout if either import or export is checked.
+ // Show the URI linear layout if either import or export is checked.
if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
- settingsFileNameLinearLayout.visibility = View.VISIBLE
+ settingsUriLinearLayout.visibility = View.VISIBLE
// Reset the text of the import button, which may have been changed to `Decrypt`.
if (settingsImportRadioButton.isChecked)
settingsImportExportButton.setText(R.string.import_button)
- // Clear the file name edit text.
- settingsFileNameEditText.text.clear()
+ // Clear the texts.
+ settingsUriEditText.text.clear()
// Disable the import/export button.
settingsImportExportButton.isEnabled = false
// Show the file location card.
settingsFileLocationCardView.visibility = View.VISIBLE
- // Show the file name linear layout if either import or export is checked.
+ // Show the URI linear layout if either import or export is checked.
if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
- settingsFileNameLinearLayout.visibility = View.VISIBLE
+ settingsUriLinearLayout.visibility = View.VISIBLE
// Reset the text of the import button, which may have been changed to `Decrypt`.
if (settingsImportRadioButton.isChecked)
settingsImportExportButton.setText(R.string.import_button)
- // Clear the file name edit text.
- settingsFileNameEditText.text.clear()
+ // Clear the texts.
+ settingsUriEditText.text.clear()
// Disable the import/export button.
settingsImportExportButton.isEnabled = false
// Update the layout based on the checked radio button.
if (settingsImportRadioButton.isChecked) {
- // Show the file name linear layout and the OpenKeychain import instructions.
- settingsFileNameLinearLayout.visibility = View.VISIBLE
+ // Show the URI linear layout and the OpenKeychain import instructions.
+ settingsUriLinearLayout.visibility = View.VISIBLE
openKeychainImportInstructionsTextView.visibility = View.VISIBLE
// Set the text of the import button to be `Decrypt`.
settingsImportExportButton.setText(R.string.decrypt)
- // Clear the file name edit text.
- settingsFileNameEditText.text.clear()
+ // Clear the texts.
+ settingsUriEditText.text.clear()
// Disable the import/export button.
settingsImportExportButton.isEnabled = false
} else if (settingsExportRadioButton.isChecked) {
- // Hide the file name linear layout and the OpenKeychain import instructions.
- settingsFileNameLinearLayout.visibility = View.GONE
+ // Hide the URI linear layout and the OpenKeychain import instructions.
+ settingsUriLinearLayout.visibility = View.GONE
openKeychainImportInstructionsTextView.visibility = View.GONE
// Enable the export button.
}
override fun afterTextChanged(s: Editable) {
- // Enable the import/export button if both the file string and the password are populated.
- settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
+ // Enable the import/export button if both the URI string and the password are populated.
+ settingsImportExportButton.isEnabled = settingsUriEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
}
})
- // Update the UI when the settings file name edit text changes.
- settingsFileNameEditText.addTextChangedListener(object : TextWatcher {
+ // Update the UI when the settings URI edit text changes.
+ settingsUriEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// Do nothing.
}
// Do nothing.
}
- override fun afterTextChanged(s: Editable) {
+ override fun afterTextChanged(editable: Editable) {
// Adjust the UI according to the encryption spinner position.
if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
- // Enable the settings import/export button if both the file name and the password are populated.
- settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
+ // Enable the settings import/export button if both the URI and the password are populated.
+ settingsImportExportButton.isEnabled = settingsUriEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
} else {
- // Enable the settings import/export button if the file name is populated.
- settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty()
+ // Enable the settings import/export button if the URI is populated.
+ settingsImportExportButton.isEnabled = settingsUriEditText.text.toString().isNotEmpty()
+ }
+
+ // Update the file name text view.
+ if (editable.isEmpty()) { // The URI is empty.
+ // Hide the settings file name text view.
+ settingsFileNameTextView.visibility = View.GONE
+ } else { // The URI is not empty.
+ // Create a file name URI.
+ val fileNameUri = editable.toString().toUri()
+
+ // Create a projection string array (the list of columns to request in the content resolver query).
+ val projectionStringArray = arrayOf(OpenableColumns.DISPLAY_NAME)
+
+ // Try to get the file name. This can fail if the URI has been edited in such a way that it doesn't match a recent API request by the browse button.
+ try {
+ // Get the file name cursor.
+ val fileNameCursor = contentResolver.query(fileNameUri, projectionStringArray, null, null)
+
+ // Initialize a file name string.
+ var fileNameString = ""
+
+ // Get the file name from the cursor.
+ if ((fileNameCursor != null) && (fileNameCursor.count != 0)) {
+ // Move to the first entry in the cursor.
+ fileNameCursor.moveToFirst()
+
+ // Populate the file name string.
+ fileNameString = fileNameCursor.getString(0)
+
+ // Close the cursor.
+ fileNameCursor.close()
+ }
+
+ // Populate the file name text view if the file name string is not empty.
+ if (fileNameString.isNotEmpty()) {
+ // Populate the settings file name text.
+ settingsFileNameTextView.text = fileNameString
+
+ // Show the settings file name text view.
+ settingsFileNameTextView.visibility = View.VISIBLE
+ }
+ } catch (_: Exception) { // The URI could not be converted into a file name.
+ // Hide the settings file name text view.
+ settingsFileNameTextView.visibility = View.GONE
+ }
}
}
})
- // Update the UI when the bookmarks file name edit text changes.
- bookmarksFileNameEditText.addTextChangedListener(object : TextWatcher {
+ // Update the UI when the bookmarks URI edit text changes.
+ bookmarksUriEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// Do nothing.
}
// Do nothing.
}
- override fun afterTextChanged(s: Editable) {
+ override fun afterTextChanged(editable: Editable) {
// Enable the bookmarks import/export button if the file name is populated.
- bookmarksImportExportButton.isEnabled = bookmarksFileNameEditText.text.toString().isNotEmpty()
+ bookmarksImportExportButton.isEnabled = bookmarksUriEditText.text.toString().isNotEmpty()
+
+ // Update the bookmarks file name text view.
+ if (editable.isEmpty()) { // The URI is empty.
+ // Hide the bookmarks file name text view.
+ bookmarksFileNameTextView.visibility = View.GONE
+ } else { // The URI is not empty.
+ // Create a file name URI.
+ val fileNameUri = editable.toString().toUri()
+
+ // Create a projection string array (the list of columns to request in the content resolver query).
+ val projectionStringArray = arrayOf(OpenableColumns.DISPLAY_NAME)
+
+ // Try to get the file name. This can fail if the URI has been edited in such a way that it doesn't match a recent API request by the browse button.
+ try {
+ // Get the file name cursor.
+ val fileNameCursor = contentResolver.query(fileNameUri, projectionStringArray, null, null)
+
+ // Initialize a file name string.
+ var fileNameString = ""
+
+ // Get the file name from the cursor.
+ if ((fileNameCursor != null) && (fileNameCursor.count != 0)) {
+ // Move to the first entry in the cursor.
+ fileNameCursor.moveToFirst()
+
+ // Populate the file name string.
+ fileNameString = fileNameCursor.getString(0)
+
+ // Close the cursor.
+ fileNameCursor.close()
+ }
+
+ // Populate the file name text view if the file name string is not empty.
+ if (fileNameString.isNotEmpty()) {
+ // Populate the bookmarks file name text.
+ bookmarksFileNameTextView.text = fileNameString
+
+ // Show the bookmarks file name text view.
+ bookmarksFileNameTextView.visibility = View.VISIBLE
+ }
+ } catch (_: Exception) { // The URI could not be converted into a file name.
+ // Hide the bookmarks file name text view.
+ bookmarksFileNameTextView.visibility = View.GONE
+ }
+ }
}
})
// Initially hide the unneeded views.
encryptionPasswordTextInputLayout.visibility = View.GONE
openKeychainRequiredTextView.visibility = View.GONE
- settingsFileNameLinearLayout.visibility = View.GONE
+ settingsUriLinearLayout.visibility = View.GONE
openKeychainImportInstructionsTextView.visibility = View.GONE
settingsImportExportButton.visibility = View.GONE
- bookmarksFileNameLinearLayout.visibility = View.GONE
+ bookmarksUriLinearLayout.visibility = View.GONE
bookmarksImportExportButton.visibility = View.GONE
} else { // The app has been restarted.
// Restore the visibility of the views.
encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
settingsFileLocationCardView.visibility = savedInstanceState.getInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY)
- settingsFileNameLinearLayout.visibility = savedInstanceState.getInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
+ settingsUriLinearLayout.visibility = savedInstanceState.getInt(SETTINGS_URI_LINEARLAYOUT_VISIBILITY)
openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
settingsImportExportButton.visibility = savedInstanceState.getInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY)
- bookmarksFileNameLinearLayout.visibility = savedInstanceState.getInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
+ bookmarksUriLinearLayout.visibility = savedInstanceState.getInt(BOOKMARKS_URI_LINEARLAYOUT_VISIBILITY)
bookmarksImportExportButton.visibility = savedInstanceState.getInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY)
// Restore the text.
- settingsFileNameEditText.post { settingsFileNameEditText.setText(savedInstanceState.getString(SETTINGS_FILE_NAME_TEXT)) }
+ settingsUriEditText.post { settingsUriEditText.setText(savedInstanceState.getString(SETTINGS_URI_STRING)) }
settingsImportExportButton.text = savedInstanceState.getString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT)
- bookmarksFileNameEditText.post { bookmarksFileNameEditText.setText(savedInstanceState.getString(BOOKMARKS_FILE_NAME_TEXT)) }
- bookmarksImportExportButton.text = savedInstanceState.getString(BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT)
+ bookmarksUriEditText.post { bookmarksUriEditText.setText(savedInstanceState.getString(BOOKMARKS_URI_STRING)) }
+ bookmarksImportExportButton.text = savedInstanceState.getString(BOOKMARKS_IMPORT_EXPORT_BUTTON_STRING)
}
}
outState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
outState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
outState.putInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY, settingsFileLocationCardView.visibility)
- outState.putInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY, settingsFileNameLinearLayout.visibility)
+ outState.putInt(SETTINGS_URI_LINEARLAYOUT_VISIBILITY, settingsUriLinearLayout.visibility)
outState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
outState.putInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY, settingsImportExportButton.visibility)
- outState.putInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY, bookmarksFileNameLinearLayout.visibility)
+ outState.putInt(BOOKMARKS_URI_LINEARLAYOUT_VISIBILITY, bookmarksUriLinearLayout.visibility)
outState.putInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.visibility)
// Save the text.
- outState.putString(SETTINGS_FILE_NAME_TEXT, settingsFileNameEditText.text.toString())
+ outState.putString(SETTINGS_URI_STRING, settingsUriEditText.text.toString())
outState.putString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT, settingsImportExportButton.text.toString())
- outState.putString(BOOKMARKS_FILE_NAME_TEXT, bookmarksFileNameEditText.text.toString())
- outState.putString(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.text.toString())
+ outState.putString(BOOKMARKS_URI_STRING, bookmarksUriEditText.text.toString())
+ outState.putString(BOOKMARKS_IMPORT_EXPORT_BUTTON_STRING, bookmarksImportExportButton.text.toString())
}
fun onClickBookmarksRadioButton(view: View) {
}
// Display the bookmarks views.
- bookmarksFileNameLinearLayout.visibility = View.VISIBLE
+ bookmarksUriLinearLayout.visibility = View.VISIBLE
bookmarksImportExportButton.visibility = View.VISIBLE
- // Clear the bookmarks file name edit text.
- bookmarksFileNameEditText.text.clear()
+ // Clear the bookmarks URI edit text.
+ bookmarksUriEditText.text.clear()
// Disable the bookmarks import/export button.
bookmarksImportExportButton.isEnabled = false
}
// Display the views.
- settingsFileNameLinearLayout.visibility = View.VISIBLE
+ settingsUriLinearLayout.visibility = View.VISIBLE
settingsImportExportButton.visibility = View.VISIBLE
- // Clear the settings file name edit text.
- settingsFileNameEditText.text.clear()
+ // Clear the settings texts.
+ settingsUriEditText.text.clear()
// Disable the settings import/export button.
settingsImportExportButton.isEnabled = false
// Check to see if OpenPGP encryption is selected.
if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption is selected.
- // Hide the settings file name views.
- settingsFileNameLinearLayout.visibility = View.GONE
+ // Hide the settings URI views.
+ settingsUriLinearLayout.visibility = View.GONE
// Enable the settings export button.
settingsImportExportButton.isEnabled = true
} else { // OpenPGP encryption is not selected.
- // Show the settings file name view.
- settingsFileNameLinearLayout.visibility = View.VISIBLE
+ // Show the settings URI views.
+ settingsUriLinearLayout.visibility = View.VISIBLE
- // Clear the settings file name edit text.
- settingsFileNameEditText.text.clear()
+ // Clear the settings texts.
+ settingsUriEditText.text.clear()
// Disable the settings import/export button.
settingsImportExportButton.isEnabled = false
// Instantiate the import/export bookmarks helper.
val importExportBookmarksHelper = ImportExportBookmarksHelper()
- // Get the file name string.
- val fileNameString = bookmarksFileNameEditText.text.toString()
+ // Get the file URI string.
+ val fileUriString = bookmarksUriEditText.text.toString()
// Check to see if import or export is selected.
if (bookmarksImportRadioButton.isChecked) { // Import is selected.
// Import the bookmarks.
- importExportBookmarksHelper.importBookmarks(fileNameString, context = this, scrollView)
+ importExportBookmarksHelper.importBookmarks(fileUriString, context = this, scrollView)
// Repopulate the bookmarks in the main WebView activity.
MainWebViewActivity.restartFromBookmarksActivity = true
} else { // Export is selected.
// Export the bookmarks.
- importExportBookmarksHelper.exportBookmarks(fileNameString, context = this, scrollView)
+ importExportBookmarksHelper.exportBookmarks(fileUriString, context = this, scrollView)
}
}
// Initialize the import status string
var importStatus = ""
- // Get the file name string.
- val fileNameString = settingsFileNameEditText.text.toString()
+ // Get the file URI string.
+ val fileUriString = settingsUriEditText.text.toString()
// Import according to the encryption type.
when (encryptionSpinner.selectedItemPosition) {
NO_ENCRYPTION -> {
try {
// Get an input stream for the file name.
- val inputStream = contentResolver.openInputStream(fileNameString.toUri())!!
+ val inputStream = contentResolver.openInputStream(fileUriString.toUri())!!
// Import the unencrypted file.
importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
// Get the encryption password.
val encryptionPasswordString = encryptionPasswordEditText.text.toString()
- // Get an input stream for the file name.
- val inputStream = contentResolver.openInputStream(fileNameString.toUri())!!
+ // Get an input stream for the file URI.
+ val inputStream = contentResolver.openInputStream(fileUriString.toUri())!!
// Initialize a salt byte array. Salt protects against rainbow table attacks.
val saltByteArray = ByteArray(32)
// Create a temporary PGP encrypted import file output stream.
val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
- // Get an input stream for the file name.
- val inputStream = contentResolver.openInputStream(fileNameString.toUri())!!
+ // Get an input stream for the file URI.
+ val inputStream = contentResolver.openInputStream(fileUriString.toUri())!!
// Create a transfer byte array.
val transferByteArray = ByteArray(1024)
// Display a snack bar with the import error if it was unsuccessful.
if (importStatus != IMPORT_SUCCESSFUL)
- Snackbar.make(settingsFileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
} else { // Export is selected.
// Export according to the encryption type.
when (encryptionSpinner.selectedItemPosition) {
NO_ENCRYPTION -> {
- // Get the file name string.
- val noEncryptionFileNameString = settingsFileNameEditText.text.toString()
+ // Get the file URI string.
+ val noEncryptionFileNameString = settingsUriEditText.text.toString()
try {
// Get the export file output stream, truncating any existing content.
// Display an export disposition snackbar.
if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
else
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
} catch (fileNotFoundException: FileNotFoundException) {
// Display a snackbar with the exception.
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
}
}
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
// Get the file name string.
- val passwordEncryptionFileNameString = settingsFileNameEditText.text.toString()
+ val passwordEncryptionFileNameString = settingsUriEditText.text.toString()
// Get the export file output stream, truncating any existing content.
val exportFileOutputStream = contentResolver.openOutputStream(passwordEncryptionFileNameString.toUri(), "wt")!!
// Display an export disposition snackbar.
if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
else
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
} catch (exception: Exception) {
// Display a snackbar with the exception.
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
}
}
// Display an export error snackbar if the temporary pre-encrypted export failed.
if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL)
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
// Create an encryption intent for OpenKeychain.
val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
} catch (exception: Exception) {
// Display a snackbar with the exception.
- Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
+ Snackbar.make(settingsUriEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
}
}
}
val dialog = dialogFragment.dialog!!
// Get handles for the views.
- val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
+ val fileNameEditText = dialog.findViewById<EditText>(R.id.file_uri_edittext)
val mhtCheckBox = dialog.findViewById<CheckBox>(R.id.mht_checkbox)
// Get the file path string.
/* SPDX-License-Identifier: GPL-3.0-or-later
- * SPDX-FileCopyrightText: 2016-2025 Soren Stoutner <soren@stoutner.com>
+ * SPDX-FileCopyrightText: 2016-2026 Soren Stoutner <soren@stoutner.com>
*
* This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
*
/* SPDX-License-Identifier: GPL-3.0-or-later
- * SPDX-FileCopyrightText: 2019-2023, 2025 Soren Stoutner <soren@stoutner.com>
+ * SPDX-FileCopyrightText: 2019-2023, 2025-2026 Soren Stoutner <soren@stoutner.com>
*
* This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
*
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.provider.OpenableColumns
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
+import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
private lateinit var openListener: OpenListener
// Declare the class views.
- private lateinit var fileNameEditText: EditText
+ private lateinit var fileUriEditText: EditText
private lateinit var mhtExplanationTextView: TextView
// The public interface is used to send information back to the parent activity.
private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
// Only do something if the user didn't press back from the file picker.
if (fileUri != null) {
- // Get the file name string from the URI.
- val fileNameString = fileUri.toString()
+ // Convert the file URI to a string.
+ val fileUriString = fileUri.toString()
- // Set the file name text.
- fileNameEditText.setText(fileNameString)
+ // Populate the file URI edit text.
+ fileUriEditText.setText(fileUriString)
- // Move the cursor to the end of the file name edit text.
- fileNameEditText.setSelection(fileNameString.length)
+ // Move the cursor to the end of the file URI edit text.
+ fileUriEditText.setSelection(fileUriString.length)
}
}
alertDialog.show()
// Get handles for the layout items.
- fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext)!!
+ fileUriEditText = alertDialog.findViewById(R.id.file_uri_edittext)!!
val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
+ val fileNameTextView = alertDialog.findViewById<TextView>(R.id.file_name_textview)!!
val mhtCheckBox = alertDialog.findViewById<CheckBox>(R.id.mht_checkbox)!!
mhtExplanationTextView = alertDialog.findViewById(R.id.mht_explanation_textview)!!
val openButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
else
mhtCheckBox.visibility = View.GONE
- // Update the status of the open button when the file name changes.
- fileNameEditText.addTextChangedListener(object : TextWatcher {
+ // Update the status of the open button and the file name text view when the file URI changes.
+ fileUriEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
// Do nothing.
}
}
override fun afterTextChanged(editable: Editable) {
- // Get the current file name.
- val fileNameString = fileNameEditText.text.toString()
-
// Enable the open button if the file name is populated.
- openButton.isEnabled = fileNameString.isNotEmpty()
+ openButton.isEnabled = editable.isNotEmpty()
+
+ // Update the UI.
+ if (editable.isEmpty()) { // The URI is empty.
+ // Hide the file name text view.
+ fileNameTextView.visibility = View.GONE
+ } else { // The URI is not empty.
+ // Create a file name URI.
+ val fileNameUri = editable.toString().toUri()
+
+ // Create a projection string array (the list of columns to request in the content resolver query).
+ val projectionStringArray = arrayOf(OpenableColumns.DISPLAY_NAME)
+
+ // Try to get the file name. This can fail if the URI has been edited in such a way that it doesn't match a recent API request by the browse button.
+ try {
+ // Get the file name cursor.
+ val fileNameCursor = requireContext().contentResolver.query(fileNameUri, projectionStringArray, null, null)
+
+ // Initialize a file name string.
+ var fileNameString = ""
+
+ // Get the file name from the cursor.
+ if ((fileNameCursor != null) && (fileNameCursor.count != 0)) {
+ // Move to the first entry in the cursor.
+ fileNameCursor.moveToFirst()
+
+ // Populate the file name string.
+ fileNameString = fileNameCursor.getString(0)
+
+ // Close the cursor.
+ fileNameCursor.close()
+ }
+
+ // Populate the file name text view if the file name string is not empty.
+ if (fileNameString.isNotEmpty()) {
+ // Populate the file name text.
+ fileNameTextView.text = fileNameString
+
+ // Show the file name text view.
+ fileNameTextView.visibility = View.VISIBLE
+ }
+ } catch (_: Exception) { // The URI could not be converted into a file name.
+ // Hide the settings file name text view.
+ fileNameTextView.visibility = View.GONE
+ }
+ }
}
})
<!--
SPDX-License-Identifier: GPL-3.0-or-later
- SPDX-FileCopyrightText: 2018-2024 Soren Stoutner <soren@stoutner.com>
+ SPDX-FileCopyrightText: 2018-2024, 2026 Soren Stoutner <soren@stoutner.com>
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
android:onClick="onClickSettingsRadioButton" />
</RadioGroup>
- <!-- Align the edit text and the select file button horizontally. -->
+ <!-- Align the URI edit text and the select file button horizontally. -->
<LinearLayout
- android:id="@+id/settings_file_name_linearlayout"
+ android:id="@+id/settings_uri_linearlayout"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
- android:layout_marginTop="10dp">
+ android:layout_marginTop="10dp" >
<!-- The text input layout makes the hint float above the edit text. -->
<com.google.android.material.textfield.TextInputLayout
<!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
<com.google.android.material.textfield.TextInputEditText
- android:id="@+id/settings_file_name_edittext"
+ android:id="@+id/settings_uri_edittext"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:hint="@string/file_name"
android:onClick="settingsBrowse" />
</LinearLayout>
+ <!-- Initially hide the file name text view. -->
+ <TextView
+ android:id="@+id/settings_file_name_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:gravity="center_horizontal"
+ android:visibility="gone" />
+
<!-- OpenKeychain import instructions -->
<TextView
android:id="@+id/openkeychain_import_instructions_textview"
android:onClick="onClickBookmarksRadioButton" />
</RadioGroup>
- <!-- Align the edit text and the select file button horizontally. -->
+ <!-- Align the URI edit text and the select file button horizontally. -->
<LinearLayout
- android:id="@+id/bookmarks_file_name_linearlayout"
+ android:id="@+id/bookmarks_uri_linearlayout"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
- android:layout_marginTop="10dp">
+ android:layout_marginTop="10dp" >
<!-- The text input layout makes the hint float above the edit text. -->
<com.google.android.material.textfield.TextInputLayout
<!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
<com.google.android.material.textfield.TextInputEditText
- android:id="@+id/bookmarks_file_name_edittext"
+ android:id="@+id/bookmarks_uri_edittext"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:hint="@string/file_name"
android:onClick="bookmarksBrowse" />
</LinearLayout>
+ <!-- Initially hide the file name text view. -->
+ <TextView
+ android:id="@+id/bookmarks_file_name_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:gravity="center_horizontal"
+ android:visibility="gone" />
+
<Button
android:id="@+id/bookmarks_import_export_button"
android:layout_height="wrap_content"
<!--
SPDX-License-Identifier: GPL-3.0-or-later
- SPDX-FileCopyrightText: 2018-2024 Soren Stoutner <soren@stoutner.com>
+ SPDX-FileCopyrightText: 2018-2024, 2026 Soren Stoutner <soren@stoutner.com>
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
android:onClick="onClickSettingsRadioButton" />
</RadioGroup>
- <!-- Align the edit text and the select file button horizontally. -->
+ <!-- Align the URI edit text and the select file button horizontally. -->
<LinearLayout
- android:id="@+id/settings_file_name_linearlayout"
+ android:id="@+id/settings_uri_linearlayout"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
- android:layout_marginTop="10dp">
+ android:layout_marginTop="10dp" >
<!-- The text input layout makes the hint float above the edit text. -->
<com.google.android.material.textfield.TextInputLayout
<!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
<com.google.android.material.textfield.TextInputEditText
- android:id="@+id/settings_file_name_edittext"
+ android:id="@+id/settings_uri_edittext"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:hint="@string/file_name"
android:onClick="settingsBrowse" />
</LinearLayout>
+ <!-- Initially hide the file name text view. -->
+ <TextView
+ android:id="@+id/settings_file_name_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:gravity="center_horizontal"
+ android:visibility="gone" />
+
<!-- OpenKeychain import instructions -->
<TextView
android:id="@+id/openkeychain_import_instructions_textview"
android:onClick="onClickBookmarksRadioButton" />
</RadioGroup>
- <!-- Align the edit text and the select file button horizontally. -->
+ <!-- Align the URI edit text and the select file button horizontally. -->
<LinearLayout
- android:id="@+id/bookmarks_file_name_linearlayout"
+ android:id="@+id/bookmarks_uri_linearlayout"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
- android:layout_marginTop="10dp">
+ android:layout_marginTop="10dp" >
<!-- The text input layout makes the hint float above the edit text. -->
<com.google.android.material.textfield.TextInputLayout
<!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
<com.google.android.material.textfield.TextInputEditText
- android:id="@+id/bookmarks_file_name_edittext"
+ android:id="@+id/bookmarks_uri_edittext"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:hint="@string/file_name"
android:onClick="bookmarksBrowse" />
</LinearLayout>
+ <!-- Initially hide the file name text view. -->
+ <TextView
+ android:id="@+id/bookmarks_file_name_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:gravity="center_horizontal"
+ android:visibility="gone" />
+
<Button
android:id="@+id/bookmarks_import_export_button"
android:layout_height="wrap_content"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016-2017,2022-2023 Soren Stoutner <soren@stoutner.com>.
+ SPDX-License-Identifier: GPL-3.0-or-later
+ SPDX-FileCopyrightText: 2016-2017, 2022-2023 Soren Stoutner <soren@stoutner.com>
- This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
- Privacy Browser Android is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
- Privacy Browser Android is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
- You should have received a copy of the GNU General Public License
- along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>. -->
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>. -->
<!-- `android:dividerHeight` must be specified with `android:divider` or the height will be `0dp`. In our case the correct divider height is `0dp`. -->
<ListView
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016-2017, 2022-2024 Soren Stoutner <soren@stoutner.com>.
+ SPDX-License-Identifier: GPL-3.0-or-later
+ SPDX-FileCopyrightText: 2016-2017, 2022-2024 Soren Stoutner <soren@stoutner.com>
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
- Privacy Browser Android is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
- Privacy Browser Android is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
- You should have received a copy of the GNU General Public License
- along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>. -->
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2019-2022 Soren Stoutner <soren@stoutner.com>.
+ SPDX-License-Identifier: GPL-3.0-or-later
+ SPDX-FileCopyrightText: 2019-2022, 2026 Soren Stoutner <soren@stoutner.com>
- This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
- Privacy Browser Android is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
- Privacy Browser Android is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
- You should have received a copy of the GNU General Public License
- along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>. -->
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>. -->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical" >
- <!-- Align the edit text and the select file button horizontally. -->
+ <!-- Align the URI edit text and the select file button horizontally. -->
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
<!-- `android:inputType="textUri"` disables spell check and places an `/` on the main keyboard. -->
<com.google.android.material.textfield.TextInputEditText
- android:id="@+id/file_name_edittext"
+ android:id="@+id/file_uri_edittext"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:hint="@string/file_name"
android:text="@string/browse" />
</LinearLayout>
+ <!-- Initially hide the file name text view. -->
+ <TextView
+ android:id="@+id/file_name_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:gravity="center_horizontal"
+ android:visibility="gone" />
+
<CheckBox
android:id="@+id/mht_checkbox"
android:layout_height="wrap_content"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2017-2020,2022 Soren Stoutner <soren@stoutner.com>.
+ SPDX-License-Identifier: GPL-3.0-or-later
+ SPDX-FileCopyrightText: 2017-2020, 2022 Soren Stoutner <soren@stoutner.com>
- This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
- Privacy Browser Android is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
- Privacy Browser Android is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
- You should have received a copy of the GNU General Public License
- along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>. -->
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright © 2017-2022 Soren Stoutner <soren@stoutner.com>.
+ SPDX-License-Identifier: GPL-3.0-or-later
+ SPDX-FileCopyrightText: 2017-2022 Soren Stoutner <soren@stoutner.com>
- This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
- Privacy Browser Android is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
- Privacy Browser Android is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
- You should have received a copy of the GNU General Public License
- along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>. -->
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2019-2022, 2024 Soren Stoutner <soren@stoutner.com>.
+ SPDX-License-Identifier: GPL-3.0-or-later
+ SPDX-FileCopyrightText: 2019-2022, 2024 Soren Stoutner <soren@stoutner.com>
- This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
- Privacy Browser Android is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
- Privacy Browser Android is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ details.
- You should have received a copy of the GNU General Public License
- along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>. -->
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>. -->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
}
dependencies {
- classpath 'com.android.tools.build:gradle:9.2.1'
+ classpath 'com.android.tools.build:gradle:9.3.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10'
// NOTE: Do not place your application dependencies here; they belong