]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/commitdiff
Migrate the rest of the dialogs to Kotlin. https://redmine.stoutner.com/issues/683
authorSoren Stoutner <soren@stoutner.com>
Wed, 31 Mar 2021 18:13:09 +0000 (11:13 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 31 Mar 2021 18:13:09 +0000 (11:13 -0700)
45 files changed:
app/build.gradle
app/src/main/assets/de/about_changelog.html
app/src/main/assets/en/about_changelog.html
app/src/main/assets/es/about_changelog.html
app/src/main/assets/fr/about_changelog.html
app/src/main/assets/it/about_changelog.html
app/src/main/assets/pt-rBR/about_changelog.html
app/src/main/assets/ru/about_changelog.html
app/src/main/assets/tr/about_changelog.html
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/RequestsActivity.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/AddDomainDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateBookmarkDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateBookmarkFolderDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateHomeScreenShortcutDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDatabaseViewDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDatabaseViewDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/FontSizeDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/HttpAuthenticationDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/MoveToFolderDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/PinnedMismatchDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/ProxyNotInstalledDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewRequestDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewRequestDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/WaitingForProxyDialog.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/WaitingForProxyDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/fragments/AboutWebViewFragment.kt
app/src/main/res/menu/webview_options_menu.xml
build.gradle
fastlane/metadata/android/de-DE/changelogs/52.txt
fastlane/metadata/android/de-DE/changelogs/54.txt

index 2dff0f45afda2f962a7c8c2dcb83bf89ed645e8f..4eb4f1ec189e96778de0753d89e4a1b2298a206e 100644 (file)
@@ -88,7 +88,7 @@ dependencies {
     implementation 'androidx.webkit:webkit:1.4.0'
 
     // Include the Kotlin standard libraries.  This should be the same version number listed in project build.gradle.
     implementation 'androidx.webkit:webkit:1.4.0'
 
     // Include the Kotlin standard libraries.  This should be the same version number listed in project build.gradle.
-    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.31'
+    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32'
 
     // Include the Google material library.
     implementation 'com.google.android.material:material:1.3.0'
 
     // Include the Google material library.
     implementation 'com.google.android.material:material:1.3.0'
index 1fafac453abe50ed4fb4407860c647559bd0918a..a1d639b8459e6412e7b621a4b0fecdcf75e2b4a7 100644 (file)
@@ -33,8 +33,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (version code 54)</h3>
-        <p>29. März 2021 - Mindest-API 19, Ziel-API 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (version code 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29. März 2021</a> - Mindest-API 19, Ziel-API 30</p>
         <ul>
             <li>Datei-Zugriff geändert, um <a href="https://redmine.stoutner.com/issues/546">"Scoped Storage" und das "Storage Access Framework"</a> nutzen zu können.
                 Dies erlaubt, die Ziel-API auf 30 zu erhöhen und ermöglicht, die gefährlichen Berechtigungen READ_EXTERNAL_STORAGE und WRITE_EXTERNAL_STORAGE zu vermeiden.
         <ul>
             <li>Datei-Zugriff geändert, um <a href="https://redmine.stoutner.com/issues/546">"Scoped Storage" und das "Storage Access Framework"</a> nutzen zu können.
                 Dies erlaubt, die Ziel-API auf 30 zu erhöhen und ermöglicht, die gefährlichen Berechtigungen READ_EXTERNAL_STORAGE und WRITE_EXTERNAL_STORAGE zu vermeiden.
@@ -51,7 +51,7 @@
             <li>Fehler behoben, durch den das <a href="https://redmine.stoutner.com/issues/616">Hamburger-Menü zu einem Pfeil</a> wurde, wenn das Navigations-Menü beim Neustart der App geöffnet war.</li>
             <li><a href="https://redmine.stoutner.com/issues/650">Öffnen des Options-Menüs</a> beschleunigt.</li>
             <li>Aktualisierte deutsche Übersetzung von Bernhard G. Keller.</li>
             <li>Fehler behoben, durch den das <a href="https://redmine.stoutner.com/issues/616">Hamburger-Menü zu einem Pfeil</a> wurde, wenn das Navigations-Menü beim Neustart der App geöffnet war.</li>
             <li><a href="https://redmine.stoutner.com/issues/650">Öffnen des Options-Menüs</a> beschleunigt.</li>
             <li>Aktualisierte deutsche Übersetzung von Bernhard G. Keller.</li>
-            <li>Aktualisierte Brasilianisch Portugiesische Übersetzung von <a href="mailto:mochileiro2006-trilhas@yahoo.com.br">Thiago Nazareno Conceição Silva de Jesus</a>.</li>
+            <li>Aktualisierte brasilianisch-portugiesische Übersetzung von <a href="mailto:mochileiro2006-trilhas@yahoo.com.br">Thiago Nazareno Conceição Silva de Jesus</a>.</li>
             <li>Aktualisierte französische Übersetzung von <a href="mailto:kevinliste@framalistes.org">Kévin L</a>.</li>
             <li>Aktualisierte italienische Übersetzung von Francesco Buratti.</li>
             <li>Aktualisierte russische Übersetzung.</li>
             <li>Aktualisierte französische Übersetzung von <a href="mailto:kevinliste@framalistes.org">Kévin L</a>.</li>
             <li>Aktualisierte italienische Übersetzung von Francesco Buratti.</li>
             <li>Aktualisierte russische Übersetzung.</li>
@@ -89,7 +89,7 @@
                 <a href="https://redmine.stoutner.com/issues/359">für</a> <a href="https://redmine.stoutner.com/issues/551">das</a> <a href="https://redmine.stoutner.com/issues/610">Nutzer</a>
                 <a href="https://redmine.stoutner.com/issues/618">Erlebnis</a> <a href="https://redmine.stoutner.com/issues/609">und</a> <a href="https://redmine.stoutner.com/issues/592">der grafischen</a>
                 <a href="https://redmine.stoutner.com/issues/567">Oberfläche</a> <a href="https://redmine.stoutner.com/issues/554">umgesetzt</a>.</li>
                 <a href="https://redmine.stoutner.com/issues/359">für</a> <a href="https://redmine.stoutner.com/issues/551">das</a> <a href="https://redmine.stoutner.com/issues/610">Nutzer</a>
                 <a href="https://redmine.stoutner.com/issues/618">Erlebnis</a> <a href="https://redmine.stoutner.com/issues/609">und</a> <a href="https://redmine.stoutner.com/issues/592">der grafischen</a>
                 <a href="https://redmine.stoutner.com/issues/567">Oberfläche</a> <a href="https://redmine.stoutner.com/issues/554">umgesetzt</a>.</li>
-            <li>Teilweise Übersetzung in brasilianisches Portugiesisch von <a href="mailto:mochileiro2006-trilhas@yahoo.com.br">Thiago Nazareno Conceição Silva de Jesus</a>.</li>
+            <li>Teilweise Übersetzung in brasilianisch-portugiesische von <a href="mailto:mochileiro2006-trilhas@yahoo.com.br">Thiago Nazareno Conceição Silva de Jesus</a>.</li>
             <li>Aktualisierte deutsche Übersetzung von Bernhard G. Keller.</li>
             <li>Aktualisierte französische Übersetzung von <a href="mailto:kevinliste@framalistes.org">Kévin L</a>.</li>
             <li>Aktualisierte italienische Übersetzung von Francesco Buratti.</li>
             <li>Aktualisierte deutsche Übersetzung von Bernhard G. Keller.</li>
             <li>Aktualisierte französische Übersetzung von <a href="mailto:kevinliste@framalistes.org">Kévin L</a>.</li>
             <li>Aktualisierte italienische Übersetzung von Francesco Buratti.</li>
index 3d2b3e31e5375be1073cb028fe1830af831c5320..c9049d056c6cfc9bf91aef023e059988ca66dd91 100644 (file)
@@ -27,8 +27,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (version code 54)</h3>
-        <p>29 March 2021 - minimum API 19, target API 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (version code 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 March 2021</a> - minimum API 19, target API 30</p>
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
index d0c8cd6ea30a9ce0d60139899543a98e2ebdd631..364b7bbeacfab34142771ee37cc1b789c86fb77a 100644 (file)
@@ -29,8 +29,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (código de versión 54)</h3>
-        <p>29 de marzo de 2021 - API mínimo 19, API dirigido 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (código de versión 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 de marzo de 2021</a> - API mínimo 19, API dirigido 30</p>
         <ul>
             <li>Rediseñar el acceso a los archivos para que funcione con <a href="https://redmine.stoutner.com/issues/546">el almacenamiento de alcance y el marco de acceso al almacenamiento</a>.
                 Esto permite que la API de destino se eleve a 30 y elimina la necesidad de los peligrosos permisos READ_EXTERNAL_STORAGE y WRITE_EXTERNAL_STORAGE.
         <ul>
             <li>Rediseñar el acceso a los archivos para que funcione con <a href="https://redmine.stoutner.com/issues/546">el almacenamiento de alcance y el marco de acceso al almacenamiento</a>.
                 Esto permite que la API de destino se eleve a 30 y elimina la necesidad de los peligrosos permisos READ_EXTERNAL_STORAGE y WRITE_EXTERNAL_STORAGE.
index d6c292c0925bc4860499584686410928c77e9a12..9a9638795cef3d3db1e4e0a78ea0b7c14cb7be7d 100644 (file)
@@ -29,8 +29,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (version du code 54)</h3>
-        <p>29 Mars 2021 - API minimale : 19, API optimale : 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (version du code 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 Mars 2021</a> - API minimale : 19, API optimale : 30</p>
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
index 4f3a9a19ddb9a281d4e155ff91f18afbb264feff..7f950afe1d6165a626c20039c6f5edd977293042 100644 (file)
@@ -29,8 +29,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (versione codice 54)</h3>
-        <p>29 Marzo 2021 - minima API 19, target API 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (versione codice 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 Marzo 2021</a> - minima API 19, target API 30</p>
         <ul>
             <li>Re-design dell'accesso ai file per funzionare con la <a href="https://redmine.stoutner.com/issues/546">scoped storage e lo Storage Access Framework</a>.
                 Questo permette alla target API di essere aggiornata alla 30 ed elimina la necessità di avere le autorizzazioni molto pericolose READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE.
         <ul>
             <li>Re-design dell'accesso ai file per funzionare con la <a href="https://redmine.stoutner.com/issues/546">scoped storage e lo Storage Access Framework</a>.
                 Questo permette alla target API di essere aggiornata alla 30 ed elimina la necessità di avere le autorizzazioni molto pericolose READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE.
index 11b056b3193e70152dbc86a0165dff1f0f9151b1..f5a610f26adb91f3747e353b917864a3870d2b00 100644 (file)
@@ -29,8 +29,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (código da versão 54)</h3>
-        <p>29 March 2021 - minimum API 19, target API 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (código da versão 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 March 2021</a> - minimum API 19, target API 30</p>
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
index 2686e98ae2a29bcdf2a9202c3fa2aab3bc4ec84a..229a592d76ae85f8e5ea5f6f887065bde6f321ca 100644 (file)
@@ -27,8 +27,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (код версии 54)</h3>
-        <p>29 марта 2021 года - минимальный API 19, целевой API 29</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (код версии 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 марта 2021 года</a> - минимальный API 19, целевой API 29</p>
         <ul>
             <li>Выполнен редизайн доступа к файлам для работы с <a href="https://redmine.stoutner.com/issues/546">ограниченными по масштабу хранилищем и фреймворком доступа к хранилищу</a>.
                 Это позволило увеличить целевой API до 30 и исключить необходимость в опасных разрешениях READ_EXTERNAL_STORAGE и WRITE_EXTERNAL_STORAGE.
         <ul>
             <li>Выполнен редизайн доступа к файлам для работы с <a href="https://redmine.stoutner.com/issues/546">ограниченными по масштабу хранилищем и фреймворком доступа к хранилищу</a>.
                 Это позволило увеличить целевой API до 30 и исключить необходимость в опасных разрешениях READ_EXTERNAL_STORAGE и WRITE_EXTERNAL_STORAGE.
index e42b3f062653caa81e1b05a08cd2287bad88e0c8..37db04359f2b6d09a3f3eee004957d891352563a 100644 (file)
@@ -27,8 +27,8 @@
     </head>
 
     <body>
     </head>
 
     <body>
-        <h3>3.7 (version code 54)</h3>
-        <p>29 Mart 2021 - minimum API 19, target API 30</p>
+        <h3><a href="https://www.stoutner.com/privacy-browser-3-7/">3.7</a> (version code 54)</h3>
+        <p><a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=commitdiff;h=f3b9172adedd74f705ddc0beac80798ae84f2920">29 Mart 2021</a> - minimum API 19, target API 30</p>
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
         <ul>
             <li>Redesign file access to work with <a href="https://redmine.stoutner.com/issues/546">scoped storage and the Storage Access Framework</a>.
                 This allows the target API to be bumped to 30 and removes the need for the dangerous READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions.
index 88d418677a12c3e2b4299a0433837451a5fe8e0b..6b2d7eb0d8965ff055bc69c128a2a1aad12c8c65 100644 (file)
@@ -94,6 +94,7 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.ActionBarDrawerToggle;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.ActionBarDrawerToggle;
 import androidx.appcompat.app.AppCompatActivity;
@@ -1764,6 +1765,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
 
             // Consume the event.
             saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
 
             // Consume the event.
+            return true;
+        } else if (menuItemId == R.id.save_archive) {
+            /*  TODO.
+            try {
+                // Create an MHT file.
+                File mhtFile = File.createTempFile("mht_file", ".mht", getCacheDir());
+
+                // Populate the MHT file.
+                currentWebView.saveWebArchive(mhtFile.toString());
+
+                // Check the file length.
+                Log.i("MHT", "MHT file size:  " + mhtFile.length());
+            } catch (Exception exception){
+                Log.i("MHT", "MHT exception:  " + exception.toString());
+            }
+
+             */
+
             return true;
         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
             // Instantiate the create home screen shortcut dialog.
             return true;
         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
             // Instantiate the create home screen shortcut dialog.
@@ -3008,35 +3027,37 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
     }
 
     @Override
-    public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) {
+    public void onSaveWebpage(int saveType, @Nullable String originalUrlString, DialogFragment dialogFragment) {
         // Get the dialog.
         Dialog dialog = dialogFragment.getDialog();
 
         // Remove the incorrect lint warning below that the dialog might be null.
         assert dialog != null;
 
         // Get the dialog.
         Dialog dialog = dialogFragment.getDialog();
 
         // Remove the incorrect lint warning below that the dialog might be null.
         assert dialog != null;
 
-        // Get a handle for the edit texts.
-        EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
+        // Get a handle for the file name edit text.
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
-        // Define the save webpage URL.
-        String saveWebpageUrl;
-
-        // Store the URL.
-        if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
-            // Save the original URL.
-            saveWebpageUrl = originalUrlString;
-        } else {
-            // Get the URL from the edit text, which may have been modified.
-            saveWebpageUrl = dialogUrlEditText.getText().toString();
-        }
-
         // Get the file path from the edit text.
         String saveWebpageFilePath = fileNameEditText.getText().toString();
 
         //Save the webpage according to the save type.
         switch (saveType) {
             case SaveWebpageDialog.SAVE_URL:
         // Get the file path from the edit text.
         String saveWebpageFilePath = fileNameEditText.getText().toString();
 
         //Save the webpage according to the save type.
         switch (saveType) {
             case SaveWebpageDialog.SAVE_URL:
+                // Get a handle for the dialog URL edit text.
+                EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
+
+                // Define the save webpage URL.
+                String saveWebpageUrl;
+
+                // Store the URL.
+                if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
+                    // Save the original URL.
+                    saveWebpageUrl = originalUrlString;
+                } else {
+                    // Get the URL from the edit text, which may have been modified.
+                    saveWebpageUrl = dialogUrlEditText.getText().toString();
+                }
+
                 // Save the URL.
                 new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
                 break;
                 // Save the URL.
                 new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
                 break;
@@ -3607,7 +3628,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
     }
 
     @Override
-    public void navigateHistory(String url, int steps) {
+    public void navigateHistory(@NonNull String url, int steps) {
         // Apply the domain settings.
         applyDomainSettings(currentWebView, url, false, false, false);
 
         // Apply the domain settings.
         applyDomainSettings(currentWebView, url, false, false, false);
 
index 0b9be790da9b05abe8e66b06877988cc8811eb8d..dcb9d66c9e432eaefa89642cafe85ba3957ad70a 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright © 2018-2020 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2018-2021 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -263,15 +263,15 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
     }
 
     @Override
     }
 
     @Override
-    public void onPrevious(int id) {
+    public void onPrevious(int currentId) {
         // Show the previous dialog.
         // Show the previous dialog.
-        launchViewRequestDialog(id -1);
+        launchViewRequestDialog(currentId -1);
     }
 
     @Override
     }
 
     @Override
-    public void onNext(int id) {
+    public void onNext(int currentId) {
         // Show the next dialog.
         // Show the next dialog.
-        launchViewRequestDialog(id + 1);
+        launchViewRequestDialog(currentId + 1);
     }
 
     private void launchViewRequestDialog(int id) {
     }
 
     private void launchViewRequestDialog(int id) {
index ed762a1484c247e433734cc023108c430f8f44e9..377ce7be249e6dd0a5e5daba6b02cd8d91f25d3c 100644 (file)
@@ -112,9 +112,6 @@ public class SaveAboutVersionImage extends AsyncTask<Void, Void, String> {
             // Write the webpage image to the image file.
             aboutVersionByteArrayOutputStream.writeTo(outputStream);
 
             // Write the webpage image to the image file.
             aboutVersionByteArrayOutputStream.writeTo(outputStream);
 
-            // Flush the output stream.
-            outputStream.flush();
-
             // Close the output stream.
             outputStream.close();
         } catch (Exception exception) {
             // Close the output stream.
             outputStream.close();
         } catch (Exception exception) {
index 7d981c0f324a554cded0809515aca0c578a8fdbd..03ab37464b21782137fad85e0f533c51b4a6c5bf 100644 (file)
@@ -198,9 +198,6 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
                 }
             }
 
                 }
             }
 
-            // Flush the output stream.
-            outputStream.flush();
-
             // Close the output stream.
             outputStream.close();
         } catch (Exception exception) {
             // Close the output stream.
             outputStream.close();
         } catch (Exception exception) {
index 464a24ab068d5c428c4c9c42b51dbf8a9ea7fa0e..8a602ef5359b02b280dc15d3442a4e4732d826b4 100644 (file)
@@ -40,7 +40,7 @@ import androidx.preference.PreferenceManager
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
 
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
 
-// Declare the class constants.
+// Define the class constants.
 private const val URL_STRING = "url_string"
 
 class AddDomainDialog : DialogFragment() {
 private const val URL_STRING = "url_string"
 
 class AddDomainDialog : DialogFragment() {
@@ -100,7 +100,7 @@ class AddDomainDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.add_domain)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.add_domain)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.add_domain_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.add_domain_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 0283213752ebaba68ace3cb302d678a8719de1c8..e787e1e315451b5b68aaa65132043ddc732e5173 100644 (file)
@@ -41,7 +41,7 @@ import com.stoutner.privacybrowser.R
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val URL_STRING = "url_string"
 private const val TITLE = "title"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 private const val URL_STRING = "url_string"
 private const val TITLE = "title"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
@@ -122,7 +122,7 @@ class CreateBookmarkDialog : DialogFragment() {
         dialogBuilder.setIcon(favoriteIconDrawable)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setIcon(favoriteIconDrawable)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.create_bookmark_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.create_bookmark_dialog, null))
 
         // Set a listener on the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set a listener on the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index c2b4f5f6bbd17750939a7b8388b15199aec15274..42f9a16779a59891760ee2de72a9f2293b84701e 100644 (file)
@@ -45,7 +45,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 class CreateBookmarkFolderDialog : DialogFragment() {
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 class CreateBookmarkFolderDialog : DialogFragment() {
@@ -114,7 +114,7 @@ class CreateBookmarkFolderDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.create_folder)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.create_folder)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.create_bookmark_folder_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.create_bookmark_folder_dialog, null))
 
         // Set a listener on the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set a listener on the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index b0df47fa174c009b7b745835051d0ededaf83976..32c4e51a20bf7545c146abc73bd0eef930799579 100644 (file)
@@ -50,7 +50,7 @@ import com.stoutner.privacybrowser.R
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val SHORTCUT_NAME = "shortcut_name"
 private const val URL_STRING = "url_string"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 private const val SHORTCUT_NAME = "shortcut_name"
 private const val URL_STRING = "url_string"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
@@ -118,7 +118,7 @@ class CreateHomeScreenShortcutDialog : DialogFragment() {
         dialogBuilder.setIcon(favoriteIconDrawable)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
         dialogBuilder.setIcon(favoriteIconDrawable)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.create_home_screen_shortcut_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.create_home_screen_shortcut_dialog, null))
 
         // Set a listener on the close button.  Using null closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set a listener on the close button.  Using null closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 95d256600fa6d22ef07e4d533b9e61c9d904dbc0..542c463277db0b29cc9b5b54a004c4f4b8a14f14 100644 (file)
@@ -56,7 +56,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
@@ -145,7 +145,7 @@ class EditBookmarkDatabaseViewDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.edit_bookmark)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.edit_bookmark)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_databaseview_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.edit_bookmark_databaseview_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 8fb3568ad73d7ebcda3e3dec2dee6f2dbc36b5d0..752f5b1c9eefa3ce538c9bf2918864f417be5473 100644 (file)
@@ -46,7 +46,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
@@ -133,7 +133,7 @@ class EditBookmarkDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.edit_bookmark)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.edit_bookmark)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.edit_bookmark_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 63ba8a3cd3a0226874ffb7e5dcf4fcf40a4105de..e15e29ef6eba698e4a767abd3233bbce90755912 100644 (file)
@@ -50,7 +50,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
@@ -138,7 +138,7 @@ class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.edit_folder)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.edit_folder)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_folder_databaseview_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.edit_bookmark_folder_databaseview_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 46082b89809bd94d46abef3a56ece29a18c2d844..1c70b82d0d96b4a6e8478749168d1f656639ac23 100644 (file)
@@ -46,7 +46,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 
 import java.io.ByteArrayOutputStream
 
 
 import java.io.ByteArrayOutputStream
 
-// Declare the class constants.
+// Define the class constants.
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
 private const val DATABASE_ID = "database_id"
 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
 
@@ -134,7 +134,7 @@ class EditBookmarkFolderDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.edit_folder)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.edit_folder)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_folder_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.edit_bookmark_folder_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 80593fd915425af5bfcee7a63c258e1b21667479..b1c456738af3d0122a5681bb44762867c13005c1 100644 (file)
@@ -36,7 +36,7 @@ import androidx.preference.PreferenceManager
 
 import com.stoutner.privacybrowser.R
 
 
 import com.stoutner.privacybrowser.R
 
-// Declare the class constants.
+// Define the class constants.
 private const val FONT_SIZE = "font_size"
 
 class FontSizeDialog : DialogFragment() {
 private const val FONT_SIZE = "font_size"
 
 class FontSizeDialog : DialogFragment() {
@@ -93,7 +93,7 @@ class FontSizeDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.font_size)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.font_size)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.font_size_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.font_size_dialog, null))
 
         // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.close, null)
 
         // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.close, null)
index b3cf70742e4dd48751668fca47352899be355489..42e43d91a4cfe1b00e618a8a28b6ae12d717ced8 100644 (file)
@@ -42,7 +42,7 @@ import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.activities.MainWebViewActivity
 import com.stoutner.privacybrowser.views.NestedScrollWebView
 
 import com.stoutner.privacybrowser.activities.MainWebViewActivity
 import com.stoutner.privacybrowser.views.NestedScrollWebView
 
-// Declare the class constants.
+// Define the class constants.
 private const val HOST = "host"
 private const val REALM = "realm"
 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
 private const val HOST = "host"
 private const val REALM = "realm"
 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
@@ -51,7 +51,7 @@ class HttpAuthenticationDialog : DialogFragment() {
     // Define the class variables.
     private var dismissDialog: Boolean = false
 
     // Define the class variables.
     private var dismissDialog: Boolean = false
 
-    // Define the class views.
+    // Declare the class views.
     private lateinit var usernameEditText: EditText
     private lateinit var passwordEditText: EditText
 
     private lateinit var usernameEditText: EditText
     private lateinit var passwordEditText: EditText
 
@@ -115,10 +115,7 @@ class HttpAuthenticationDialog : DialogFragment() {
             // Set the title.
             dialogBuilder.setTitle(R.string.http_authentication)
 
             // Set the title.
             dialogBuilder.setTitle(R.string.http_authentication)
 
-            // Get the activity's layout inflater.
-            val layoutInflater = requireActivity().layoutInflater
-
-            // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
+            // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
             dialogBuilder.setView(layoutInflater.inflate(R.layout.http_authentication_dialog, null))
 
             // Set the close button listener.
             dialogBuilder.setView(layoutInflater.inflate(R.layout.http_authentication_dialog, null))
 
             // Set the close button listener.
index 84bb747875607dae040e5f92bb92943030974502..c7e7494e471dfe4755bacacedaa5a57d89d23baf 100644 (file)
@@ -53,7 +53,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
 import java.io.ByteArrayOutputStream
 import java.lang.StringBuilder
 
 import java.io.ByteArrayOutputStream
 import java.lang.StringBuilder
 
-// Declare the class constants.
+// Define the class constants.
 private const val CURRENT_FOLDER = "current_folder"
 private const val SELECTED_BOOKMARKS_LONG_ARRAY = "selected_bookmarks_long_array"
 
 private const val CURRENT_FOLDER = "current_folder"
 private const val SELECTED_BOOKMARKS_LONG_ARRAY = "selected_bookmarks_long_array"
 
@@ -125,7 +125,7 @@ class MoveToFolderDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.move_to_folder)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.move_to_folder)
 
         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.move_to_folder_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.move_to_folder_dialog, null))
 
         // Set the listener for the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the listener for the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 5bdf65acba0caa4ad4205f459fd667d727a735c1..e62b6fa6624b4f0f0c3c5bd4842307c13f3eeccc 100644 (file)
@@ -76,7 +76,7 @@ class OpenDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.open)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.open)
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.open_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.open_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
index 0dd2ce57a9b82f8ba43b4c9aaf66fcff841bb907..c3a5c7fed3034a741b36014982cf270aaa9fed18 100644 (file)
@@ -43,7 +43,7 @@ import com.stoutner.privacybrowser.adapters.PinnedMismatchPagerAdapter
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
 import com.stoutner.privacybrowser.views.NestedScrollWebView
 
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
 import com.stoutner.privacybrowser.views.NestedScrollWebView
 
-// Declare the class constants.
+// Define the class constants.
 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
 
 class PinnedMismatchDialog : DialogFragment() {
 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
 
 class PinnedMismatchDialog : DialogFragment() {
@@ -140,7 +140,7 @@ class PinnedMismatchDialog : DialogFragment() {
         dialogBuilder.setTitle(R.string.pinned_mismatch)
 
         // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
         dialogBuilder.setTitle(R.string.pinned_mismatch)
 
         // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.pinned_mismatch_linearlayout, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.pinned_mismatch_linearlayout, null))
 
         // Set the update button listener.
         dialogBuilder.setNeutralButton(R.string.update) { _: DialogInterface?, _: Int ->
 
         // Set the update button listener.
         dialogBuilder.setNeutralButton(R.string.update) { _: DialogInterface?, _: Int ->
index d8ed9f196595aba364d0e07c4bce0fdc469baf5e..ee22df72b2b84e8cd5925e6064a5033e0eb76433 100644 (file)
@@ -31,7 +31,7 @@ import androidx.preference.PreferenceManager
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.helpers.ProxyHelper
 
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.helpers.ProxyHelper
 
-// Declare the class constants.
+// Define the class constants.
 private const val PROXY_MODE = "proxy_mode"
 
 class ProxyNotInstalledDialog : DialogFragment() {
 private const val PROXY_MODE = "proxy_mode"
 
 class ProxyNotInstalledDialog : DialogFragment() {
index 35eb2e0db537786fabb4416fdb2d96e9be66f0c1..312ebd05936fa52f50124f7fc075ae58cb3017a8 100644 (file)
@@ -31,12 +31,14 @@ import android.text.TextWatcher
 import android.view.WindowManager
 import android.widget.Button
 import android.widget.EditText
 import android.view.WindowManager
 import android.widget.Button
 import android.widget.EditText
+
 import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
 import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.DialogFragment
 import androidx.preference.PreferenceManager
+
 import com.stoutner.privacybrowser.R
 
 import com.stoutner.privacybrowser.R
 
-// Declare the class constants.
+// Define the class constants.
 private const val SAVE_TYPE = "save_type"
 
 class SaveDialog : DialogFragment() {
 private const val SAVE_TYPE = "save_type"
 
 class SaveDialog : DialogFragment() {
@@ -58,7 +60,7 @@ class SaveDialog : DialogFragment() {
     }
 
     companion object {
     }
 
     companion object {
-        // Declare the companion object constants.  These can be moved to class constants once all of the code has transitioned to Kotlin.
+        // Define the companion object constants.  These can be moved to class constants once all of the code has transitioned to Kotlin.
         const val SAVE_LOGCAT = 0
         const val SAVE_ABOUT_VERSION_TEXT = 1
         const val SAVE_ABOUT_VERSION_IMAGE = 2
         const val SAVE_LOGCAT = 0
         const val SAVE_ABOUT_VERSION_TEXT = 1
         const val SAVE_ABOUT_VERSION_IMAGE = 2
@@ -75,7 +77,7 @@ class SaveDialog : DialogFragment() {
             // Create a new instance of the save dialog.
             val saveDialog = SaveDialog()
 
             // Create a new instance of the save dialog.
             val saveDialog = SaveDialog()
 
-            // Add the arguments bundle to the dialog.
+            // Add the arguments bundle to the new dialog.
             saveDialog.arguments = argumentsBundle
 
             // Return the new dialog.
             saveDialog.arguments = argumentsBundle
 
             // Return the new dialog.
@@ -135,7 +137,7 @@ class SaveDialog : DialogFragment() {
         }
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
         }
 
         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.save_dialog, null))
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.save_dialog, null))
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
 
         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
         dialogBuilder.setNegativeButton(R.string.cancel, null)
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.java
deleted file mode 100644 (file)
index 40fdbda..0000000
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
- *
- * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextWatcher;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.preference.PreferenceManager;
-
-import com.google.android.material.textfield.TextInputLayout;
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.activities.MainWebViewActivity;
-import com.stoutner.privacybrowser.asynctasks.GetUrlSize;
-
-public class SaveWebpageDialog extends DialogFragment {
-    public static final int SAVE_URL = 0;
-    public static final int SAVE_IMAGE = 1;
-
-    // Define the save webpage listener.
-    private SaveWebpageListener saveWebpageListener;
-
-    // The public interface is used to send information back to the parent activity.
-    public interface SaveWebpageListener {
-        void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment);
-    }
-
-    // Define the get URL size AsyncTask.  This allows previous instances of the task to be cancelled if a new one is run.
-    @SuppressWarnings("rawtypes")
-    private AsyncTask getUrlSize;
-
-    @Override
-    public void onAttach(@NonNull Context context) {
-        // Run the default commands.
-        super.onAttach(context);
-
-        // Get a handle for the save webpage listener from the launching context.
-        saveWebpageListener = (SaveWebpageListener) context;
-    }
-
-    public static SaveWebpageDialog saveWebpage(int saveType, String urlString, String fileSizeString, String contentDispositionFileNameString, String userAgentString, boolean cookiesEnabled) {
-        // Create an arguments bundle.
-        Bundle argumentsBundle = new Bundle();
-
-        // Store the arguments in the bundle.
-        argumentsBundle.putInt("save_type", saveType);
-        argumentsBundle.putString("url_string", urlString);
-        argumentsBundle.putString("file_size_string", fileSizeString);
-        argumentsBundle.putString("content_disposition_file_name_string", contentDispositionFileNameString);
-        argumentsBundle.putString("user_agent_string", userAgentString);
-        argumentsBundle.putBoolean("cookies_enabled", cookiesEnabled);
-
-        // Create a new instance of the save webpage dialog.
-        SaveWebpageDialog saveWebpageDialog = new SaveWebpageDialog();
-
-        // Add the arguments bundle to the new dialog.
-        saveWebpageDialog.setArguments(argumentsBundle);
-
-        // Return the new dialog.
-        return saveWebpageDialog;
-    }
-
-    // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
-    @SuppressLint("InflateParams")
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Get a handle for the arguments.
-        Bundle arguments = getArguments();
-
-        // Remove the incorrect lint warning that the arguments might be null.
-        assert arguments != null;
-
-        // Get the arguments from the bundle.
-        int saveType = arguments.getInt("save_type");
-        String originalUrlString = arguments.getString("url_string");
-        String fileSizeString = arguments.getString("file_size_string");
-        String contentDispositionFileNameString = arguments.getString("content_disposition_file_name_string");
-        String userAgentString = arguments.getString("user_agent_string");
-        boolean cookiesEnabled = arguments.getBoolean("cookies_enabled");
-
-        // Get handles for the context and the activity.
-        Context context = requireContext();
-        Activity activity = requireActivity();
-
-        // Use an alert dialog builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog);
-
-        // Get the current theme status.
-        int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-        // Set the title and icon according to the save type.
-        switch (saveType) {
-            case SAVE_URL:
-                // Set the title.
-                dialogBuilder.setTitle(R.string.save_url);
-
-                // Set the icon according to the theme.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-                    dialogBuilder.setIcon(R.drawable.copy_enabled_day);
-                } else {
-                    dialogBuilder.setIcon(R.drawable.copy_enabled_night);
-                }
-                break;
-
-            case SAVE_IMAGE:
-                // Set the title.
-                dialogBuilder.setTitle(R.string.save_image);
-
-                // Set the icon according to the theme.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-                    dialogBuilder.setIcon(R.drawable.images_enabled_day);
-                } else {
-
-                    dialogBuilder.setIcon(R.drawable.images_enabled_night);
-                }
-                break;
-        }
-
-        // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(activity.getLayoutInflater().inflate(R.layout.save_webpage_dialog, null));
-
-        // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
-        dialogBuilder.setNegativeButton(R.string.cancel, null);
-
-        // Set the save button listener.
-        dialogBuilder.setPositiveButton(R.string.save, (DialogInterface dialog, int which) -> {
-            // Return the dialog fragment to the parent activity.
-            saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this);
-        });
-
-        // Create an alert dialog from the builder.
-        AlertDialog alertDialog = dialogBuilder.create();
-
-        // Remove the incorrect lint warning below that the window might be null.
-        assert alertDialog.getWindow() != null;
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
-
-        // The alert dialog must be shown before items in the layout can be modified.
-        alertDialog.show();
-
-        // Get handles for the layout items.
-        TextInputLayout urlTextInputLayout = alertDialog.findViewById(R.id.url_textinputlayout);
-        EditText urlEditText = alertDialog.findViewById(R.id.url_edittext);
-        EditText fileNameEditText = alertDialog.findViewById(R.id.file_name_edittext);
-        Button browseButton = alertDialog.findViewById(R.id.browse_button);
-        TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview);
-        Button saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
-
-        // Remove the incorrect warnings that the views might be null.
-        assert urlTextInputLayout != null;
-        assert urlEditText != null;
-        assert fileNameEditText != null;
-        assert browseButton != null;
-        assert fileSizeTextView != null;
-
-        // Set the file size text view.
-        fileSizeTextView.setText(fileSizeString);
-
-        // Modify the layout based on the save type.
-        if (saveType == SAVE_URL) {  // A URL is being saved.
-            // Remove the incorrect lint error below that the URL string might be null.
-            assert originalUrlString != null;
-
-            // Populate the URL edit text according to the type.  This must be done before the text change listener is created below so that the file size isn't requested again.
-            if (originalUrlString.startsWith("data:")) {  // The URL contains the entire data of an image.
-                // Get a substring of the data URL with the first 100 characters.  Otherwise, the user interface will freeze while trying to layout the edit text.
-                String urlSubstring = originalUrlString.substring(0, 100) + "…";
-
-                // Populate the URL edit text with the truncated URL.
-                urlEditText.setText(urlSubstring);
-
-                // Disable the editing of the URL edit text.
-                urlEditText.setInputType(InputType.TYPE_NULL);
-            } else {  // The URL contains a reference to the location of the data.
-                // Populate the URL edit text with the full URL.
-                urlEditText.setText(originalUrlString);
-            }
-
-            // Update the file size and the status of the save button when the URL changes.
-            urlEditText.addTextChangedListener(new TextWatcher() {
-                @Override
-                public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-                    // Do nothing.
-                }
-
-                @Override
-                public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-                    // Do nothing.
-                }
-
-                @Override
-                public void afterTextChanged(Editable editable) {
-                    // Cancel the get URL size AsyncTask if it is running.
-                    if ((getUrlSize != null)) {
-                        getUrlSize.cancel(true);
-                    }
-
-                    // Get the current URL to save.
-                    String urlToSave = urlEditText.getText().toString();
-
-                    // Wipe the file size text view.
-                    fileSizeTextView.setText("");
-
-                    // Get the file size for the current URL.
-                    getUrlSize = new GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave);
-
-                    // Enable the save button if the URL and file name are populated.
-                    saveButton.setEnabled(!urlToSave.isEmpty() && !fileNameEditText.getText().toString().isEmpty());
-                }
-            });
-        } else {  // An archive or an image is being saved.
-            // Hide the URL edit text and the file size text view.
-            urlTextInputLayout.setVisibility(View.GONE);
-            fileSizeTextView.setVisibility(View.GONE);
-        }
-
-        // Initially disable the save button.
-        saveButton.setEnabled(false);
-
-        // Update the status of the save button when the file name changes.
-        fileNameEditText.addTextChangedListener(new TextWatcher() {
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-                // Do nothing.
-            }
-
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-                // Do nothing.
-            }
-
-            @Override
-            public void afterTextChanged(Editable s) {
-                // Get the current file name.
-                String fileNameString = fileNameEditText.getText().toString();
-
-                // Enable the save button based on the save type.
-                if (saveType == SAVE_URL) {  // A URL is being saved.
-                    // Enable the save button if the file name and the URL is populated.
-                    saveButton.setEnabled(!fileNameString.isEmpty() && !urlEditText.getText().toString().isEmpty());
-                } else {  // An archive or an image is being saved.
-                    // Enable the save button if the file name is populated.
-                    saveButton.setEnabled(!fileNameString.isEmpty());
-                }
-            }
-        });
-
-        // Create a file name string.
-        String fileName = "";
-
-        // Set the file name according to the type.
-        switch (saveType) {
-            case SAVE_URL:
-                // Use the file name from the content disposition.
-                fileName = contentDispositionFileNameString;
-                break;
-
-            case SAVE_IMAGE:
-                // Use a file name ending in `.png`.
-                fileName = getString(R.string.webpage_png);
-                break;
-        }
-
-        // Save the file name as the default file name.  This must be final to be used in the lambda below.
-        final String defaultFileName = fileName;
-
-        // Handle clicks on the browse button.
-        browseButton.setOnClickListener((View view) -> {
-            // Create the file picker intent.
-            Intent browseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
-
-            // Set the intent MIME type to include all files so that everything is visible.
-            browseIntent.setType("*/*");
-
-            // Set the initial file name according to the type.
-            browseIntent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
-
-            // Request a file that can be opened.
-            browseIntent.addCategory(Intent.CATEGORY_OPENABLE);
-
-            // Start the file picker.  This must be started under `activity` so that the request code is returned correctly.
-            activity.startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE);
-        });
-
-        // Return the alert dialog.
-        return alertDialog;
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.kt
new file mode 100644 (file)
index 0000000..b27e170
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.AsyncTask
+import android.os.Bundle
+import android.text.Editable
+import android.text.InputType
+import android.text.TextWatcher
+import android.view.View
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.EditText
+import android.widget.TextView
+
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.google.android.material.textfield.TextInputLayout
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.MainWebViewActivity
+import com.stoutner.privacybrowser.asynctasks.GetUrlSize
+
+// Define the class constants.
+private const val SAVE_TYPE = "save_type"
+private const val URL_STRING = "url_string"
+private const val FILE_SIZE_STRING = "file_size_string"
+private const val FILE_NAME_STRING = "file_name_string"
+private const val USER_AGENT_STRING = "user_agent_string"
+private const val COOKIES_ENABLED = "cookies_enabled"
+
+class SaveWebpageDialog : DialogFragment() {
+    // Declare the class variables.
+    private lateinit var saveWebpageListener: SaveWebpageListener
+
+    // Define the class variables.
+    private var getUrlSize: AsyncTask<*, *, *>? = null
+
+    // The public interface is used to send information back to the parent activity.
+    interface SaveWebpageListener {
+        fun onSaveWebpage(saveType: Int, originalUrlString: String?, dialogFragment: DialogFragment)
+    }
+
+    override fun onAttach(context: Context) {
+        // Run the default commands.
+        super.onAttach(context)
+
+        // Get a handle for the save webpage listener from the launching context.
+        saveWebpageListener = context as SaveWebpageListener
+    }
+
+    companion object {
+        // Define the companion object constants.  These can be moved to class constants once all of the code has transitioned to Kotlin.
+        const val SAVE_URL = 0
+        const val SAVE_IMAGE = 1
+
+        // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
+        @JvmStatic
+        fun saveWebpage(saveType: Int, urlString: String?, fileSizeString: String?, fileNameString: String, userAgentString: String?, cookiesEnabled: Boolean): SaveWebpageDialog {
+            // Create an arguments bundle.
+            val argumentsBundle = Bundle()
+
+            // Store the arguments in the bundle.
+            argumentsBundle.putInt(SAVE_TYPE, saveType)
+            argumentsBundle.putString(URL_STRING, urlString)
+            argumentsBundle.putString(FILE_SIZE_STRING, fileSizeString)
+            argumentsBundle.putString(FILE_NAME_STRING, fileNameString)
+            argumentsBundle.putString(USER_AGENT_STRING, userAgentString)
+            argumentsBundle.putBoolean(COOKIES_ENABLED, cookiesEnabled)
+
+            // Create a new instance of the save webpage dialog.
+            val saveWebpageDialog = SaveWebpageDialog()
+
+            // Add the arguments bundle to the new dialog.
+            saveWebpageDialog.arguments = argumentsBundle
+
+            // Return the new dialog.
+            return saveWebpageDialog
+        }
+    }
+
+    // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Get the arguments from the bundle.
+        val saveType = requireArguments().getInt(SAVE_TYPE)
+        val originalUrlString = requireArguments().getString(URL_STRING)
+        val fileSizeString = requireArguments().getString(FILE_SIZE_STRING)
+        val fileNameString = requireArguments().getString(FILE_NAME_STRING)!!
+        val userAgentString = requireArguments().getString(USER_AGENT_STRING)
+        val cookiesEnabled = requireArguments().getBoolean(COOKIES_ENABLED)
+
+        // Use an alert dialog builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Get the current theme status.
+        val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+
+        // Set the title and icon according to the save type.
+        when (saveType) {
+            SAVE_URL -> {
+                // Set the title.
+                dialogBuilder.setTitle(R.string.save_url)
+
+                // Set the icon according to the theme.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    dialogBuilder.setIcon(R.drawable.copy_enabled_day)
+                } else {
+                    dialogBuilder.setIcon(R.drawable.copy_enabled_night)
+                }
+            }
+
+            SAVE_IMAGE -> {
+                // Set the title.
+                dialogBuilder.setTitle(R.string.save_image)
+
+                // Set the icon according to the theme.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    dialogBuilder.setIcon(R.drawable.images_enabled_day)
+                } else {
+                    dialogBuilder.setIcon(R.drawable.images_enabled_night)
+                }
+            }
+        }
+
+        // Set the view.  The parent view is null because it will be assigned by the alert dialog.
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.save_webpage_dialog, null))
+
+        // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
+        dialogBuilder.setNegativeButton(R.string.cancel, null)
+
+        // Set the save button listener.
+        dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
+            // Return the dialog fragment to the parent activity.
+            saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this)
+        }
+
+        // Create an alert dialog from the builder.
+        val alertDialog = dialogBuilder.create()
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        // The alert dialog must be shown before items in the layout can be modified.
+        alertDialog.show()
+
+        // Get handles for the layout items.
+        val urlTextInputLayout = alertDialog.findViewById<TextInputLayout>(R.id.url_textinputlayout)!!
+        val urlEditText = alertDialog.findViewById<EditText>(R.id.url_edittext)!!
+        val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
+        val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
+        val fileSizeTextView = alertDialog.findViewById<TextView>(R.id.file_size_textview)!!
+        val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
+
+        // Set the file size text view.
+        fileSizeTextView.text = fileSizeString
+
+        // Modify the layout based on the save type.
+        if (saveType == SAVE_URL) {  // A URL is being saved.
+            // Populate the URL edit text according to the type.  This must be done before the text change listener is created below so that the file size isn't requested again.
+            if (originalUrlString!!.startsWith("data:")) {  // The URL contains the entire data of an image.
+                // Get a substring of the data URL with the first 100 characters.  Otherwise, the user interface will freeze while trying to layout the edit text.
+                val urlSubstring = originalUrlString.substring(0, 100) + "…"
+
+                // Populate the URL edit text with the truncated URL.
+                urlEditText.setText(urlSubstring)
+
+                // Disable the editing of the URL edit text.
+                urlEditText.inputType = InputType.TYPE_NULL
+            } else {  // The URL contains a reference to the location of the data.
+                // Populate the URL edit text with the full URL.
+                urlEditText.setText(originalUrlString)
+            }
+
+            // Update the file size and the status of the save button when the URL changes.
+            urlEditText.addTextChangedListener(object : TextWatcher {
+                override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
+                    // Do nothing.
+                }
+
+                override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
+                    // Do nothing.
+                }
+
+                override fun afterTextChanged(editable: Editable) {
+                    // Cancel the get URL size AsyncTask if it is running.
+                    if (getUrlSize != null) {
+                        getUrlSize!!.cancel(true)
+                    }
+
+                    // Get the current URL to save.
+                    val urlToSave = urlEditText.text.toString()
+
+                    // Wipe the file size text view.
+                    fileSizeTextView.text = ""
+
+                    // Get the file size for the current URL.
+                    getUrlSize = GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave)
+
+                    // Enable the save button if the URL and file name are populated.
+                    saveButton.isEnabled = urlToSave.isNotEmpty() && fileNameEditText.text.toString().isNotEmpty()
+                }
+            })
+        } else {  // An archive or an image is being saved.
+            // Hide the URL edit text and the file size text view.
+            urlTextInputLayout.visibility = View.GONE
+            fileSizeTextView.visibility = View.GONE
+        }
+
+        // Initially disable the save button.
+        saveButton.isEnabled = false
+
+        // Update the status of the save button when the file name changes.
+        fileNameEditText.addTextChangedListener(object : TextWatcher {
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // Do nothing.
+            }
+
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                // Do nothing.
+            }
+
+            override fun afterTextChanged(s: Editable) {
+                // Enable the save button based on the save type.
+                if (saveType == SAVE_URL) {  // A URL is being saved.
+                    // Enable the save button if the file name and the URL are populated.
+                    saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && urlEditText.text.toString().isNotEmpty()
+                } else {  // An archive or an image is being saved.
+                    // Enable the save button if the file name is populated.
+                    saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
+                }
+            }
+        })
+
+        // Handle clicks on the browse button.
+        browseButton.setOnClickListener {
+            // Create the file picker intent.
+            val browseIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+
+            // Set the intent MIME type to include all files so that everything is visible.
+            browseIntent.type = "*/*"
+
+            // Set the initial file name according to the type.
+            browseIntent.putExtra(Intent.EXTRA_TITLE, fileNameString)
+
+            // Request a file that can be opened.
+            browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
+
+            // Start the file picker.  This must be started under `activity` so that the request code is returned correctly.
+            requireActivity().startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE)
+        }
+
+        // Return the alert dialog.
+        return alertDialog
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.java
deleted file mode 100644 (file)
index c34faaa..0000000
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
- *
- * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.net.Uri;
-import android.net.http.SslCertificate;
-import android.net.http.SslError;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.ForegroundColorSpan;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.WindowManager;
-import android.webkit.SslErrorHandler;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.activities.MainWebViewActivity;
-import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
-import com.stoutner.privacybrowser.views.NestedScrollWebView;
-
-import java.lang.ref.WeakReference;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.text.DateFormat;
-import java.util.Date;
-
-public class SslCertificateErrorDialog extends DialogFragment {
-    public static SslCertificateErrorDialog displayDialog(SslError error, long webViewFragmentId) {
-        // Get the various components of the SSL error message.
-        int primaryErrorIntForBundle = error.getPrimaryError();
-        String urlWithErrorForBundle = error.getUrl();
-        SslCertificate sslCertificate = error.getCertificate();
-        String issuedToCNameForBundle = sslCertificate.getIssuedTo().getCName();
-        String issuedToONameForBundle = sslCertificate.getIssuedTo().getOName();
-        String issuedToUNameForBundle = sslCertificate.getIssuedTo().getUName();
-        String issuedByCNameForBundle = sslCertificate.getIssuedBy().getCName();
-        String issuedByONameForBundle = sslCertificate.getIssuedBy().getOName();
-        String issuedByUNameForBundle = sslCertificate.getIssuedBy().getUName();
-        Date startDateForBundle = sslCertificate.getValidNotBeforeDate();
-        Date endDateForBundle = sslCertificate.getValidNotAfterDate();
-
-        // Create an arguments bundle.
-        Bundle argumentsBundle = new Bundle();
-
-        // Store the SSL error message components in a `Bundle`.
-        argumentsBundle.putInt("primary_error_int", primaryErrorIntForBundle);
-        argumentsBundle.putString("url_with_error", urlWithErrorForBundle);
-        argumentsBundle.putString("issued_to_cname", issuedToCNameForBundle);
-        argumentsBundle.putString("issued_to_oname", issuedToONameForBundle);
-        argumentsBundle.putString("issued_to_uname", issuedToUNameForBundle);
-        argumentsBundle.putString("issued_by_cname", issuedByCNameForBundle);
-        argumentsBundle.putString("issued_by_oname", issuedByONameForBundle);
-        argumentsBundle.putString("issued_by_uname", issuedByUNameForBundle);
-        argumentsBundle.putString("start_date", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDateForBundle));
-        argumentsBundle.putString("end_date", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDateForBundle));
-        argumentsBundle.putLong("webview_fragment_id", webViewFragmentId);
-
-        // Create a new instance of the SSL certificate error dialog.
-        SslCertificateErrorDialog thisSslCertificateErrorDialog = new SslCertificateErrorDialog();
-
-        // Add the arguments bundle to the new dialog.
-        thisSslCertificateErrorDialog.setArguments(argumentsBundle);
-
-        // Return the new dialog.
-        return thisSslCertificateErrorDialog;
-    }
-
-    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
-    @SuppressLint("InflateParams")
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Get a handle for the arguments.
-        Bundle arguments = getArguments();
-
-        // Remove the incorrect lint warning that the arguments might be null.
-        assert arguments != null;
-
-        // Get the variables from the bundle.
-        int primaryErrorInt = arguments.getInt("primary_error_int");
-        String urlWithErrors = arguments.getString("url_with_error");
-        String issuedToCName = arguments.getString("issued_to_cname");
-        String issuedToOName = arguments.getString("issued_to_oname");
-        String issuedToUName = arguments.getString("issued_to_uname");
-        String issuedByCName = arguments.getString("issued_by_cname");
-        String issuedByOName = arguments.getString("issued_by_oname");
-        String issuedByUName = arguments.getString("issued_by_uname");
-        String startDate = arguments.getString("start_date");
-        String endDate = arguments.getString("end_date");
-        long webViewFragmentId = arguments.getLong("webview_fragment_id");
-
-        // Get the current position of this WebView fragment.
-        int webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId);
-
-        // Get the WebView tab fragment.
-        WebViewTabFragment webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition);
-
-        // Get the fragment view.
-        View fragmentView = webViewTabFragment.getView();
-
-        // Remove the incorrect lint warning below that the fragment view might be null.
-        assert fragmentView != null;
-
-        // Get a handle for the current WebView.
-        NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
-
-        // Get a handle for the SSL error handler.
-        SslErrorHandler sslErrorHandler = nestedScrollWebView.getSslErrorHandler();
-
-        // Get the activity's layout inflater.
-        LayoutInflater layoutInflater = requireActivity().getLayoutInflater();
-
-        // Use an alert dialog builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog);
-
-        // Get the current theme status.
-        int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-        // Set the icon according to the theme.
-        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-            dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_night);
-        } else {
-            dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_day);
-        }
-
-        // Set the title.
-        dialogBuilder.setTitle(R.string.ssl_certificate_error);
-
-        // Set the view.  The parent view is `null` because it will be assigned by `AlertDialog`.
-        dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null));
-
-        // Set a listener on the cancel button.
-        dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
-            // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
-            if (sslErrorHandler != null) {
-                // Cancel the request.
-                sslErrorHandler.cancel();
-
-                // Reset the SSL error handler.
-                nestedScrollWebView.resetSslErrorHandler();
-            }
-        });
-
-        // Set a listener on the proceed button.
-        dialogBuilder.setPositiveButton(R.string.proceed, (DialogInterface dialog, int which) -> {
-            // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
-            if (sslErrorHandler != null) {
-                // Cancel the request.
-                sslErrorHandler.proceed();
-
-                // Reset the SSL error handler.
-                nestedScrollWebView.resetSslErrorHandler();
-            }
-        });
-
-
-        // Create an alert dialog from the alert dialog builder.
-        AlertDialog alertDialog = dialogBuilder.create();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            // Remove the warning below that `getWindow()` might be null.
-            assert alertDialog.getWindow() != null;
-
-            // Disable screenshots.
-            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
-
-        // Get a URI for the URL with errors.
-        Uri uriWithErrors = Uri.parse(urlWithErrors);
-
-        // Get the IP addresses for the URI.
-        new GetIpAddresses(getActivity(), alertDialog).execute(uriWithErrors.getHost());
-
-        // The alert dialog must be shown before the contents can be modified.
-        alertDialog.show();
-
-        // Get handles for the `TextViews`
-        TextView primaryErrorTextView = alertDialog.findViewById(R.id.primary_error);
-        TextView urlTextView = alertDialog.findViewById(R.id.url);
-        TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
-        TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
-        TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
-        TextView issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview);
-        TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
-        TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
-        TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
-        TextView validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview);
-        TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
-        TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
-
-        // Remove the incorrect lint warnings below that the views might be null.
-        assert primaryErrorTextView != null;
-        assert urlTextView != null;
-        assert issuedToCNameTextView != null;
-        assert issuedToONameTextView != null;
-        assert issuedToUNameTextView != null;
-        assert issuedByTextView != null;
-        assert issuedByCNameTextView != null;
-        assert issuedByONameTextView != null;
-        assert issuedByUNameTextView != null;
-        assert validDatesTextView != null;
-        assert startDateTextView != null;
-        assert endDateTextView != null;
-
-        // Setup the common strings.
-        String urlLabel = getString(R.string.url_label) + "  ";
-        String cNameLabel = getString(R.string.common_name) + "  ";
-        String oNameLabel = getString(R.string.organization) + "  ";
-        String uNameLabel = getString(R.string.organizational_unit) + "  ";
-        String startDateLabel = getString(R.string.start_date) + "  ";
-        String endDateLabel = getString(R.string.end_date) + "  ";
-
-        // Create a spannable string builder for each text view that needs multiple colors of text.
-        SpannableStringBuilder urlStringBuilder = new SpannableStringBuilder(urlLabel + urlWithErrors);
-        SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
-        SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
-        SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
-        SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
-        SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
-        SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
-        SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + startDate);
-        SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder((endDateLabel + endDate));
-
-        // Define the color spans.
-        ForegroundColorSpan blueColorSpan;
-        ForegroundColorSpan redColorSpan;
-
-        // Set the color spans according to the theme.  The deprecated `getResources()` must be used until the minimum API >= 23.
-        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-            blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
-            redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
-        } else {
-            blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.violet_700));
-            redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
-        }
-
-        // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
-        urlStringBuilder.setSpan(blueColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-        endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-        // Initialize `primaryErrorString`.
-        String primaryErrorString = "";
-
-        // Highlight the primary error in red and store the primary error string in `primaryErrorString`.
-        switch (primaryErrorInt) {
-            case SslError.SSL_IDMISMATCH:
-                // Change the URL span colors to red.
-                urlStringBuilder.setSpan(redColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-                // Store the primary error string.
-                primaryErrorString = getString(R.string.cn_mismatch);
-                break;
-
-            case SslError.SSL_UNTRUSTED:
-                // Change the issued by text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                    issuedByTextView.setTextColor(getResources().getColor(R.color.red_900));
-                } else {
-                    issuedByTextView.setTextColor(getResources().getColor(R.color.red_a700));
-                }
-
-                // Change the issued by span color to red.
-                issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-                // Store the primary error string.
-                primaryErrorString = getString(R.string.untrusted);
-                break;
-
-            case SslError.SSL_DATE_INVALID:
-                // Change the valid dates text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                    validDatesTextView.setTextColor(getResources().getColor(R.color.red_900));
-                } else {
-                    validDatesTextView.setTextColor(getResources().getColor(R.color.red_a700));
-                }
-
-                // Change the date span colors to red.
-                startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-                // Store the primary error string.
-                primaryErrorString = getString(R.string.invalid_date);
-                break;
-
-            case SslError.SSL_NOTYETVALID:
-                // Change the start date span color to red.
-                startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-                // Store the primary error string.
-                primaryErrorString = getString(R.string.future_certificate);
-                break;
-
-            case SslError.SSL_EXPIRED:
-                // Change the end date span color to red.
-                endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-                // Store the primary error string.
-                primaryErrorString = getString(R.string.expired_certificate);
-                break;
-
-            case SslError.SSL_INVALID:
-                // Store the primary error string.
-                primaryErrorString = getString(R.string.invalid_certificate);
-                break;
-        }
-
-
-        // Display the strings.
-        primaryErrorTextView.setText(primaryErrorString);
-        urlTextView.setText(urlStringBuilder);
-        issuedToCNameTextView.setText(issuedToCNameStringBuilder);
-        issuedToONameTextView.setText(issuedToONameStringBuilder);
-        issuedToUNameTextView.setText(issuedToUNameStringBuilder);
-        issuedByCNameTextView.setText(issuedByCNameStringBuilder);
-        issuedByONameTextView.setText(issuedByONameStringBuilder);
-        issuedByUNameTextView.setText(issuedByUNameStringBuilder);
-        startDateTextView.setText(startDateStringBuilder);
-        endDateTextView.setText(endDateStringBuilder);
-
-        // `onCreateDialog` requires the return of an alert dialog.
-        return alertDialog;
-    }
-
-
-    // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
-    private static class GetIpAddresses extends AsyncTask<String, Void, SpannableStringBuilder> {
-        // The weak references are used to determine if the activity or the alert dialog have disappeared while the AsyncTask is running.
-        private final WeakReference<Activity> activityWeakReference;
-        private final WeakReference<AlertDialog> alertDialogWeakReference;
-
-        GetIpAddresses(Activity activity, AlertDialog alertDialog) {
-            // Populate the weak references.
-            activityWeakReference = new WeakReference<>(activity);
-            alertDialogWeakReference = new WeakReference<>(alertDialog);
-        }
-
-        @Override
-        protected SpannableStringBuilder doInBackground(String... domainName) {
-            // Get handles for the activity and the alert dialog.
-            Activity activity = activityWeakReference.get();
-            AlertDialog alertDialog = alertDialogWeakReference.get();
-
-            // Abort if the activity or the dialog is gone.
-            if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
-                return new SpannableStringBuilder();
-            }
-
-            // Initialize an IP address string builder.
-            StringBuilder ipAddresses = new StringBuilder();
-
-            // Get an array with the IP addresses for the host.
-            try {
-                // Get an array with all the IP addresses for the domain.
-                InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
-
-                // Add each IP address to the string builder.
-                for (InetAddress inetAddress : inetAddressesArray) {
-                    // Check to see if this is not the first IP address.
-                    if (ipAddresses.length() > 0) {
-                        // Add a line break to the string builder first.
-                        ipAddresses.append("\n");
-                    }
-
-                    // Add the IP Address to the string builder.
-                    ipAddresses.append(inetAddress.getHostAddress());
-                }
-            } catch (UnknownHostException exception) {
-                // Do nothing.
-            }
-
-            // Set the label.
-            String ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  ";
-
-            // Create a spannable string builder.
-            SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + ipAddresses);
-
-            // Create a blue foreground color span.
-            ForegroundColorSpan blueColorSpan;
-
-            // Get the current theme status.
-            int currentThemeStatus = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-            // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
-            if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.violet_500));
-            } else {
-                blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_700));
-            }
-
-            // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
-            ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-            // Return the formatted string.
-            return ipAddressesStringBuilder;
-        }
-
-        // `onPostExecute()` operates on the UI thread.
-        @Override
-        protected void onPostExecute(SpannableStringBuilder ipAddresses) {
-            // Get handles for the activity and the alert dialog.
-            Activity activity = activityWeakReference.get();
-            AlertDialog alertDialog = alertDialogWeakReference.get();
-
-            // Abort if the activity or the alert dialog is gone.
-            if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
-                return;
-            }
-
-            // Get a handle for the IP addresses text view.
-            TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
-
-            // Remove the incorrect lint warning below that the view might be null.
-            assert ipAddressesTextView != null;
-
-            // Populate the IP addresses text view.
-            ipAddressesTextView.setText(ipAddresses);
-        }
-    }
-}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt
new file mode 100644 (file)
index 0000000..139e657
--- /dev/null
@@ -0,0 +1,441 @@
+/*
+ * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.res.Configuration
+import android.net.Uri
+import android.net.http.SslError
+import android.os.AsyncTask
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import android.view.WindowManager
+import android.widget.TextView
+
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.MainWebViewActivity
+import com.stoutner.privacybrowser.views.NestedScrollWebView
+
+import java.lang.ref.WeakReference
+import java.net.InetAddress
+import java.net.UnknownHostException
+import java.text.DateFormat
+
+// Define the class constants.
+private const val PRIMARY_ERROR_INT = "primary_error_int"
+private const val URL_WITH_ERRORS = "url_with_errors"
+private const val ISSUED_TO_CNAME = "issued_to_cname"
+private const val ISSUED_TO_ONAME = "issued_to_oname"
+private const val ISSUED_TO_UNAME = "issued_to_uname"
+private const val ISSUED_BY_CNAME = "issued_by_cname"
+private const val ISSUED_BY_ONAME = "issued_by_oname"
+private const val ISSUED_BY_UNAME = "issued_by_uname"
+private const val START_DATE = "start_date"
+private const val END_DATE = "end_date"
+private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
+
+class SslCertificateErrorDialog : DialogFragment() {
+    companion object {
+        // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
+        @JvmStatic
+        fun displayDialog(sslError: SslError, webViewFragmentId: Long): SslCertificateErrorDialog {
+            // Get the various components of the SSL error message.
+            val primaryErrorInt = sslError.primaryError
+            val urlWithErrors = sslError.url
+            val sslCertificate = sslError.certificate
+            val issuedToCName = sslCertificate.issuedTo.cName
+            val issuedToOName = sslCertificate.issuedTo.oName
+            val issuedToUName = sslCertificate.issuedTo.uName
+            val issuedByCName = sslCertificate.issuedBy.cName
+            val issuedByOName = sslCertificate.issuedBy.oName
+            val issuedByUName = sslCertificate.issuedBy.uName
+            val startDate = sslCertificate.validNotBeforeDate
+            val endDate = sslCertificate.validNotAfterDate
+
+            // Create an arguments bundle.
+            val argumentsBundle = Bundle()
+
+            // Store the SSL error message components in the bundle.
+            argumentsBundle.putInt(PRIMARY_ERROR_INT, primaryErrorInt)
+            argumentsBundle.putString(URL_WITH_ERRORS, urlWithErrors)
+            argumentsBundle.putString(ISSUED_TO_CNAME, issuedToCName)
+            argumentsBundle.putString(ISSUED_TO_ONAME, issuedToOName)
+            argumentsBundle.putString(ISSUED_TO_UNAME, issuedToUName)
+            argumentsBundle.putString(ISSUED_BY_CNAME, issuedByCName)
+            argumentsBundle.putString(ISSUED_BY_ONAME, issuedByOName)
+            argumentsBundle.putString(ISSUED_BY_UNAME, issuedByUName)
+            argumentsBundle.putString(START_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
+            argumentsBundle.putString(END_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
+            argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
+
+            // Create a new instance of the SSL certificate error dialog.
+            val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
+
+            // Add the arguments bundle to the new dialog.
+            thisSslCertificateErrorDialog.arguments = argumentsBundle
+
+            // Return the new dialog.
+            return thisSslCertificateErrorDialog
+        }
+    }
+
+    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Get the variables from the bundle.
+        val primaryErrorInt = requireArguments().getInt(PRIMARY_ERROR_INT)
+        val urlWithErrors = requireArguments().getString(URL_WITH_ERRORS)
+        val issuedToCName = requireArguments().getString(ISSUED_TO_CNAME)
+        val issuedToOName = requireArguments().getString(ISSUED_TO_ONAME)
+        val issuedToUName = requireArguments().getString(ISSUED_TO_UNAME)
+        val issuedByCName = requireArguments().getString(ISSUED_BY_CNAME)
+        val issuedByOName = requireArguments().getString(ISSUED_BY_ONAME)
+        val issuedByUName = requireArguments().getString(ISSUED_BY_UNAME)
+        val startDate = requireArguments().getString(START_DATE)
+        val endDate = requireArguments().getString(END_DATE)
+        val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
+
+        // Get the current position of this WebView fragment.
+        val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
+
+        // Get the WebView tab fragment.
+        val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
+
+        // Get the fragment view.
+        val fragmentView = webViewTabFragment.requireView()
+
+        // Get a handle for the current WebView.
+        val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
+
+        // Get a handle for the SSL error handler.
+        val sslErrorHandler = nestedScrollWebView.sslErrorHandler
+
+        // Use an alert dialog builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Get the current theme status.
+        val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+
+        // Set the icon according to the theme.
+        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+            dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_day)
+        } else {
+            dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_night)
+        }
+
+        // Set the title.
+        dialogBuilder.setTitle(R.string.ssl_certificate_error)
+
+        // Set the view.  The parent view is null because it will be assigned by the alert dialog.
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null))
+
+        // Set a listener on the cancel button.
+        dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
+            // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
+            if (sslErrorHandler != null) {
+                // Cancel the request.
+                sslErrorHandler.cancel()
+
+                // Reset the SSL error handler.
+                nestedScrollWebView.resetSslErrorHandler()
+            }
+        }
+
+        // Set a listener on the proceed button.
+        dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int ->
+            // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
+            if (sslErrorHandler != null) {
+                // Proceed to the website.
+                sslErrorHandler.proceed()
+
+                // Reset the SSL error handler.
+                nestedScrollWebView.resetSslErrorHandler()
+            }
+        }
+
+        // Create an alert dialog from the builder.
+        val alertDialog = dialogBuilder.create()
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            // Disable screenshots.
+            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        // Get a URI for the URL with errors.
+        val uriWithErrors = Uri.parse(urlWithErrors)
+
+        // Get the IP addresses for the URI.
+        GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
+
+        // The alert dialog must be shown before the contents can be modified.
+        alertDialog.show()
+
+        // Get handles for the views.
+        val primaryErrorTextView = alertDialog.findViewById<TextView>(R.id.primary_error)!!
+        val urlTextView = alertDialog.findViewById<TextView>(R.id.url)!!
+        val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
+        val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
+        val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
+        val issuedByTextView = alertDialog.findViewById<TextView>(R.id.issued_by_textview)!!
+        val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
+        val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
+        val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
+        val validDatesTextView = alertDialog.findViewById<TextView>(R.id.valid_dates_textview)!!
+        val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
+        val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
+
+        // Setup the common strings.
+        val urlLabel = getString(R.string.url_label) + "  "
+        val cNameLabel = getString(R.string.common_name) + "  "
+        val oNameLabel = getString(R.string.organization) + "  "
+        val uNameLabel = getString(R.string.organizational_unit) + "  "
+        val startDateLabel = getString(R.string.start_date) + "  "
+        val endDateLabel = getString(R.string.end_date) + "  "
+
+        // Create a spannable string builder for each text view that needs multiple colors of text.
+        val urlStringBuilder = SpannableStringBuilder(urlLabel + urlWithErrors)
+        val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
+        val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
+        val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
+        val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
+        val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
+        val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
+        val startDateStringBuilder = SpannableStringBuilder(startDateLabel + startDate)
+        val endDateStringBuilder = SpannableStringBuilder(endDateLabel + endDate)
+
+        // Define the color spans.
+        val blueColorSpan: ForegroundColorSpan
+        val redColorSpan: ForegroundColorSpan
+
+        // Set the color spans according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
+        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+            @Suppress("DEPRECATION")
+            blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
+            @Suppress("DEPRECATION")
+            redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
+        } else {
+            @Suppress("DEPRECATION")
+            blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
+            @Suppress("DEPRECATION")
+            redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
+        }
+
+        // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
+        urlStringBuilder.setSpan(blueColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+        endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+        // Define the primary error string.
+        var primaryErrorString = ""
+
+        // Highlight the primary error in red and store it in the primary error string.
+        when (primaryErrorInt) {
+            SslError.SSL_IDMISMATCH -> {
+                // Change the URL span colors to red.
+                urlStringBuilder.setSpan(redColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+                // Store the primary error string.
+                primaryErrorString = getString(R.string.cn_mismatch)
+            }
+
+            SslError.SSL_UNTRUSTED -> {
+                // Change the issued by text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    @Suppress("DEPRECATION")
+                    issuedByTextView.setTextColor(resources.getColor(R.color.red_a700))
+                } else {
+                    @Suppress("DEPRECATION")
+                    issuedByTextView.setTextColor(resources.getColor(R.color.red_900))
+                }
+
+                // Change the issued by span color to red.
+                issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+                // Store the primary error string.
+                primaryErrorString = getString(R.string.untrusted)
+            }
+
+            SslError.SSL_DATE_INVALID -> {
+                // Change the valid dates text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    @Suppress("DEPRECATION")
+                    validDatesTextView.setTextColor(resources.getColor(R.color.red_a700))
+                } else {
+                    @Suppress("DEPRECATION")
+                    validDatesTextView.setTextColor(resources.getColor(R.color.red_900))
+                }
+
+                // Change the date span colors to red.
+                startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+                // Store the primary error string.
+                primaryErrorString = getString(R.string.invalid_date)
+            }
+
+            SslError.SSL_NOTYETVALID -> {
+                // Change the start date span color to red.
+                startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+                // Store the primary error string.
+                primaryErrorString = getString(R.string.future_certificate)
+            }
+
+            SslError.SSL_EXPIRED -> {
+                // Change the end date span color to red.
+                endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+                // Store the primary error string.
+                primaryErrorString = getString(R.string.expired_certificate)
+            }
+
+            SslError.SSL_INVALID ->
+                // Store the primary error string.
+                primaryErrorString = getString(R.string.invalid_certificate)
+        }
+
+
+        // Display the strings.
+        primaryErrorTextView.text = primaryErrorString
+        urlTextView.text = urlStringBuilder
+        issuedToCNameTextView.text = issuedToCNameStringBuilder
+        issuedToONameTextView.text = issuedToONameStringBuilder
+        issuedToUNameTextView.text = issuedToUNameStringBuilder
+        issuedByCNameTextView.text = issuedByCNameStringBuilder
+        issuedByONameTextView.text = issuedByONameStringBuilder
+        issuedByUNameTextView.text = issuedByUNameStringBuilder
+        startDateTextView.text = startDateStringBuilder
+        endDateTextView.text = endDateStringBuilder
+
+        // Return the alert dialog.
+        return alertDialog
+    }
+
+    // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
+    private class GetIpAddresses constructor(activity: Activity, alertDialog: AlertDialog) : AsyncTask<String, Void?, SpannableStringBuilder>() {
+        // Define the weak references.
+        private val activityWeakReference: WeakReference<Activity> = WeakReference(activity)
+        private val alertDialogWeakReference: WeakReference<AlertDialog> = WeakReference(alertDialog)
+
+        override fun doInBackground(vararg domainName: String): SpannableStringBuilder {
+            // Get handles for the activity and the alert dialog.
+            val activity = activityWeakReference.get()
+            val alertDialog = alertDialogWeakReference.get()
+
+            // Abort if the activity or the dialog is gone.
+            if (activity == null || activity.isFinishing || alertDialog == null) {
+                return SpannableStringBuilder()
+            }
+
+            // Initialize an IP address string builder.
+            val ipAddresses = StringBuilder()
+
+            // Get an array with the IP addresses for the host.
+            try {
+                // Get an array with all the IP addresses for the domain.
+                val inetAddressesArray = InetAddress.getAllByName(domainName[0])
+
+                // Add each IP address to the string builder.
+                for (inetAddress in inetAddressesArray) {
+                    // Check to see if this is not the first IP address.
+                    if (ipAddresses.isNotEmpty()) {
+                        // Add a line break to the string builder first.
+                        ipAddresses.append("\n")
+                    }
+
+                    // Add the IP Address to the string builder.
+                    ipAddresses.append(inetAddress.hostAddress)
+                }
+            } catch (exception: UnknownHostException) {
+                // Do nothing.
+            }
+
+            // Set the label.
+            val ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  "
+
+            // Create a spannable string builder.
+            val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
+
+            // Create a blue foreground color span.
+            val blueColorSpan: ForegroundColorSpan
+
+            // Get the current theme status.
+            val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+
+            // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
+            blueColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                @Suppress("DEPRECATION")
+                ForegroundColorSpan(activity.resources.getColor(R.color.blue_700))
+            } else {
+                @Suppress("DEPRECATION")
+                ForegroundColorSpan(activity.resources.getColor(R.color.violet_500))
+            }
+
+            // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
+            ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+            // Return the formatted string.
+            return ipAddressesStringBuilder
+        }
+
+        // `onPostExecute()` operates on the UI thread.
+        override fun onPostExecute(ipAddresses: SpannableStringBuilder) {
+            // Get handles for the activity and the alert dialog.
+            val activity = activityWeakReference.get()
+            val alertDialog = alertDialogWeakReference.get()
+
+            // Abort if the activity or the alert dialog is gone.
+            if (activity == null || activity.isFinishing || alertDialog == null) {
+                return
+            }
+
+            // Get a handle for the IP addresses text view.
+            val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
+
+            // Populate the IP addresses text view.
+            ipAddressesTextView.text = ipAddresses
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.java
deleted file mode 100644 (file)
index 3f8035e..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
- *
- * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.WindowManager;
-import android.webkit.WebBackForwardList;
-import android.widget.AdapterView;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.DialogFragment;  // The AndroidX dialog fragment must be used or an error is produced on API <=22.
-
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.activities.MainWebViewActivity;
-import com.stoutner.privacybrowser.adapters.HistoryArrayAdapter;
-import com.stoutner.privacybrowser.definitions.History;
-import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
-import com.stoutner.privacybrowser.views.NestedScrollWebView;
-
-import java.util.ArrayList;
-
-public class UrlHistoryDialog extends DialogFragment{
-    // The public interface is used to send information back to the parent activity.
-    public interface NavigateHistoryListener {
-        void navigateHistory(String url, int steps);
-    }
-
-    // The navigate history listener is used in `onAttach()` and `onCreateDialog()`.
-    private NavigateHistoryListener navigateHistoryListener;
-
-    @Override
-    public void onAttach(@NonNull Context context) {
-        // Run the default commands.
-        super.onAttach(context);
-
-        // Get a handle for the listener from the launching context.
-        navigateHistoryListener = (NavigateHistoryListener) context;
-    }
-
-    public static UrlHistoryDialog loadBackForwardList(long webViewFragmentId) {
-        // Create an arguments bundle.
-        Bundle argumentsBundle = new Bundle();
-
-        // Store the WebView fragment ID in the bundle.
-        argumentsBundle.putLong("webview_fragment_id", webViewFragmentId);
-
-        // Create a new instance of the URL history dialog.
-        UrlHistoryDialog urlHistoryDialog = new UrlHistoryDialog();
-
-        // Add the arguments bundle to this instance.
-        urlHistoryDialog.setArguments(argumentsBundle);
-
-        // Return the new URL history dialog.
-        return urlHistoryDialog;
-    }
-
-    @Override
-    @NonNull
-    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
-    @SuppressLint("InflateParams")
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Get the activity's layout inflater.
-        LayoutInflater layoutInflater = requireActivity().getLayoutInflater();
-
-        // Get the arguments.
-        Bundle arguments = getArguments();
-
-        // Remove the incorrect lint error that arguments might be null.
-        assert arguments != null;
-
-        // Get the WebView fragment ID from the arguments.
-        long webViewFragmentId = arguments.getLong("webview_fragment_id");
-
-        // Get the current position of this WebView fragment.
-        int webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId);
-
-        // Get the WebView tab fragment.
-        WebViewTabFragment webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition);
-
-        // Get the fragment view.
-        View fragmentView = webViewTabFragment.getView();
-
-        // Remove the incorrect lint warning below that the fragment view might be null.
-        assert fragmentView != null;
-
-        // Get a handle for the current WebView.
-        NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
-
-        // Get the web back forward list from the WebView.
-        WebBackForwardList webBackForwardList = nestedScrollWebView.copyBackForwardList();
-
-        // Store the current page index.
-        int currentPageIndex = webBackForwardList.getCurrentIndex();
-
-        // Remove the lint warning below that `getContext()` might be null.
-        assert getContext() != null;
-
-        // Get the default favorite icon drawable.  `ContextCompat` must be used until the minimum API >= 21.
-        Drawable defaultFavoriteIconDrawable = ContextCompat.getDrawable(getContext(), R.drawable.world);
-
-        // Convert the default favorite icon drawable to a `BitmapDrawable`.
-        BitmapDrawable defaultFavoriteIconBitmapDrawable = (BitmapDrawable) defaultFavoriteIconDrawable;
-
-        // Remove the incorrect lint error that `getBitmap()` might be null.
-        assert defaultFavoriteIconBitmapDrawable != null;
-
-        // Extract a bitmap from the default favorite icon bitmap drawable.
-        Bitmap defaultFavoriteIcon = defaultFavoriteIconBitmapDrawable.getBitmap();
-
-        // Create a history array list.
-        ArrayList<History> historyArrayList = new ArrayList<>();
-
-        // Populate the history array list, descending from the end of the list so that the newest entries are at the top.  `-1` is needed because the history array list is zero-based.
-        for (int i=webBackForwardList.getSize() -1; i >= 0; i--) {
-            // Create a variable to store the favorite icon bitmap.
-            Bitmap favoriteIconBitmap;
-
-            // Determine the favorite icon bitmap
-            if (webBackForwardList.getItemAtIndex(i).getFavicon() == null) {
-                // If the web back forward list does not have a favorite icon, use Privacy Browser's default world icon.
-                favoriteIconBitmap = defaultFavoriteIcon;
-            } else {  // Use the icon from the web back forward list.
-                favoriteIconBitmap = webBackForwardList.getItemAtIndex(i).getFavicon();
-            }
-
-            // Store the favorite icon and the URL in history entry.
-            History historyEntry = new History(favoriteIconBitmap, webBackForwardList.getItemAtIndex(i).getUrl());
-
-            // Add this history entry to the history array list.
-            historyArrayList.add(historyEntry);
-        }
-
-        // Subtract the original current page ID from the array size because the order of the array is reversed so that the newest entries are at the top.  `-1` is needed because the array is zero-based.
-        int currentPageId = webBackForwardList.getSize() - 1 - currentPageIndex;
-
-        // Use an alert dialog builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog);
-
-        // Set the title.
-        dialogBuilder.setTitle(R.string.history);
-
-        // Set the view.  The parent view is `null` because it will be assigned by `AlertDialog`.
-        dialogBuilder.setView(layoutInflater.inflate(R.layout.url_history_dialog, null));
-
-        // Setup the clear history button.
-        dialogBuilder.setNegativeButton(R.string.clear_history, (DialogInterface dialog, int which) -> {
-            // Clear the history.
-            nestedScrollWebView.clearHistory();
-        });
-
-        // Set an `onClick()` listener on the positive button.
-        dialogBuilder.setPositiveButton(R.string.close, (DialogInterface dialog, int which) -> {
-            // Do nothing if `Close` is clicked.  The `Dialog` will automatically close.
-        });
-
-        // Create an alert dialog from the alert dialog builder.
-        final AlertDialog alertDialog = dialogBuilder.create();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            // Remove the warning below that `getWindow()` might be null.
-            assert alertDialog.getWindow() != null;
-
-            // Disable screenshots.
-            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
-
-        //The alert dialog must be shown before the contents can be modified.
-        alertDialog.show();
-
-        // Instantiate a history array adapter.
-        HistoryArrayAdapter historyArrayAdapter = new HistoryArrayAdapter(getContext(), historyArrayList, currentPageId);
-
-        // Get a handle for the list view.
-        ListView listView = alertDialog.findViewById(R.id.history_listview);
-
-        // Remove the incorrect lint warning below that the view might be null.
-        assert listView != null;
-
-        // Set the list view adapter.
-        listView.setAdapter(historyArrayAdapter);
-
-        // Listen for clicks on entries in the list view.
-        listView.setOnItemClickListener((AdapterView<?> parent, View view, int position, long id) -> {
-            // Convert the long ID to an int.
-            int itemId = (int) id;
-
-            // Only consume the click if it is not on the `currentPageId`.
-            if (itemId != currentPageId) {
-                // Get a handle for the URL text view.
-                TextView urlTextView = view.findViewById(R.id.history_url_textview);
-
-                // Get the URL.
-                String url = urlTextView.getText().toString();
-
-                // Invoke the navigate history listener in the calling activity.  These commands cannot be run here because they need access to `applyDomainSettings()`.
-                navigateHistoryListener.navigateHistory(url, currentPageId - itemId);
-
-                // Dismiss the alert dialog.
-                alertDialog.dismiss();
-            }
-        });
-
-        // Return the alert dialog.
-        return alertDialog;
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt
new file mode 100644 (file)
index 0000000..9778954
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.graphics.drawable.BitmapDrawable
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemClickListener
+import android.widget.ListView
+import android.widget.TextView
+
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.MainWebViewActivity
+import com.stoutner.privacybrowser.adapters.HistoryArrayAdapter
+import com.stoutner.privacybrowser.definitions.History
+import com.stoutner.privacybrowser.views.NestedScrollWebView
+
+// Define the class constants.
+private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
+
+class UrlHistoryDialog : DialogFragment() {
+    // Declare the class variables.
+    private lateinit var navigateHistoryListener: NavigateHistoryListener
+
+    // The public interface is used to send information back to the parent activity.
+    interface NavigateHistoryListener {
+        fun navigateHistory(url: String, steps: Int)
+    }
+
+    override fun onAttach(context: Context) {
+        // Run the default commands.
+        super.onAttach(context)
+
+        // Get a handle for the listener from the launching context.
+        navigateHistoryListener = context as NavigateHistoryListener
+    }
+
+    companion object {
+        // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
+        @JvmStatic
+        fun loadBackForwardList(webViewFragmentId: Long): UrlHistoryDialog {
+            // Create an arguments bundle.
+            val argumentsBundle = Bundle()
+
+            // Store the WebView fragment ID in the bundle.
+            argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
+
+            // Create a new instance of the URL history dialog.
+            val urlHistoryDialog = UrlHistoryDialog()
+
+            // Add the arguments bundle to the new dialog.
+            urlHistoryDialog.arguments = argumentsBundle
+
+            // Return the new dialog.
+            return urlHistoryDialog
+        }
+    }
+
+    // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Get the WebView fragment ID from the arguments.
+        val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
+
+        // Get the current position of this WebView fragment.
+        val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
+
+        // Get the WebView tab fragment.
+        val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
+
+        // Get the fragment view.
+        val fragmentView = webViewTabFragment.requireView()
+
+        // Get a handle for the current nested scroll WebView.
+        val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
+
+        // Get the web back forward list from the nested scroll WebView.
+        val webBackForwardList = nestedScrollWebView.copyBackForwardList()
+
+        // Store the current page index.
+        val currentPageIndex = webBackForwardList.currentIndex
+
+        // Get the default favorite icon drawable.  `ContextCompat` must be used until the minimum API >= 21.
+        val defaultFavoriteIconDrawable = ContextCompat.getDrawable(requireContext(), R.drawable.world)
+
+        // Convert the default favorite icon drawable to a bitmap drawable.
+        val defaultFavoriteIconBitmapDrawable = (defaultFavoriteIconDrawable as BitmapDrawable)
+
+        // Extract a bitmap from the default favorite icon bitmap drawable.
+        val defaultFavoriteIcon = defaultFavoriteIconBitmapDrawable.bitmap
+
+        // Create a history array list.
+        val historyArrayList = ArrayList<History>()
+
+        // Populate the history array list, descending from the end of the list so that the newest entries are at the top.  `-1` is needed because the history array list is zero-based.
+        for (i in webBackForwardList.size - 1 downTo 0) {
+            // Store the favorite icon bitmap.
+            val favoriteIconBitmap = if (webBackForwardList.getItemAtIndex(i).favicon == null) {
+                // If the web back forward list does not have a favorite icon, use Privacy Browser's default world icon.
+                defaultFavoriteIcon
+            } else {  // Use the icon from the web back forward list.
+                webBackForwardList.getItemAtIndex(i).favicon
+            }
+
+            // Store the favorite icon and the URL in history entry.
+            val historyEntry = History(favoriteIconBitmap, webBackForwardList.getItemAtIndex(i).url)
+
+            // Add this history entry to the history array list.
+            historyArrayList.add(historyEntry)
+        }
+
+        // Subtract the original current page ID from the array size because the order of the array is reversed so that the newest entries are at the top.  `-1` is needed because the array is zero-based.
+        val currentPageId = webBackForwardList.size - 1 - currentPageIndex
+
+        // Use an alert dialog builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Set the title.
+        dialogBuilder.setTitle(R.string.history)
+
+        // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.url_history_dialog, null))
+
+        // Setup the clear history button listener.
+        dialogBuilder.setNegativeButton(R.string.clear_history) { _: DialogInterface, _: Int ->
+            // Clear the history.
+            nestedScrollWebView.clearHistory()
+        }
+
+        // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
+        dialogBuilder.setPositiveButton(R.string.close, null)
+
+        // Create an alert dialog from the alert dialog builder.
+        val alertDialog = dialogBuilder.create()
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            // Disable screenshots.
+            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        //The alert dialog must be shown before the contents can be modified.
+        alertDialog.show()
+
+        // Instantiate a history array adapter.
+        val historyArrayAdapter = HistoryArrayAdapter(context, historyArrayList, currentPageId)
+
+        // Get a handle for the list view.
+        val listView = alertDialog.findViewById<ListView>(R.id.history_listview)!!
+
+        // Set the list view adapter.
+        listView.adapter = historyArrayAdapter
+
+        // Listen for clicks on entries in the list view.
+        listView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, view: View, _: Int, id: Long ->
+            // Convert the long ID to an int.
+            val itemId = id.toInt()
+
+            // Only consume the click if it is not on the current page ID.
+            if (itemId != currentPageId) {
+                // Get a handle for the URL text view.
+                val urlTextView = view.findViewById<TextView>(R.id.history_url_textview)
+
+                // Get the URL.
+                val url = urlTextView.text.toString()
+
+                // Invoke the navigate history listener in the calling activity.  These commands cannot be run here because they need access to `applyDomainSettings()`.
+                navigateHistoryListener.navigateHistory(url, currentPageId - itemId)
+
+                // Dismiss the alert dialog.
+                alertDialog.dismiss()
+            }
+        }
+
+        // Return the alert dialog.
+        return alertDialog
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewRequestDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewRequestDialog.java
deleted file mode 100644 (file)
index c862159..0000000
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * Copyright © 2018-2020 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
- *
- * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.helpers.BlocklistHelper;
-
-public class ViewRequestDialog extends DialogFragment {
-    // The public interface is used to send information back to the parent activity.
-    public interface ViewRequestListener {
-        void onPrevious(int id);
-
-        void onNext(int id);
-    }
-
-    // The view request listener is used in `onAttach()` and `onCreateDialog()`.
-    private ViewRequestListener viewRequestListener;
-
-    public void onAttach(@NonNull Context context) {
-        // Run the default commands.
-        super.onAttach(context);
-
-        // Get a handle for the listener from the launching context.
-        viewRequestListener = (ViewRequestListener) context;
-    }
-
-    public static ViewRequestDialog request(int id, boolean isLastRequest, String[] requestDetails) {
-        // Create a bundle.
-        Bundle bundle = new Bundle();
-
-        // Store the request details.
-        bundle.putInt("id", id);
-        bundle.putBoolean("is_last_request", isLastRequest);
-        bundle.putStringArray("request_details", requestDetails);
-
-        // Add the bundle to the dialog.
-        ViewRequestDialog viewRequestDialog = new ViewRequestDialog();
-        viewRequestDialog.setArguments(bundle);
-
-        // Return the new dialog.
-        return viewRequestDialog;
-    }
-
-    @Override
-    @NonNull
-    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
-    @SuppressLint("InflateParams")
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Remove the incorrect lint warning that `getInt()` might be null.
-        assert getArguments() != null;
-
-        // Get the info from the bundle.
-        int id = getArguments().getInt("id");
-        boolean isLastRequest = getArguments().getBoolean("is_last_request");
-        String[] requestDetails = getArguments().getStringArray("request_details");
-
-        // Use an alert dialog builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog);
-
-        // Get the current theme status.
-        int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-        // Set the icon according to the theme.
-        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-            dialogBuilder.setIcon(R.drawable.block_ads_enabled_night);
-        } else {
-            dialogBuilder.setIcon(R.drawable.block_ads_enabled_day);
-        }
-
-        // Create the dialog title.
-        String title = getResources().getString(R.string.request_details) + " - " + id;
-
-        // Set the title.
-        dialogBuilder.setTitle(title);
-
-        // Remove the incorrect lint warnings about items being null.
-        assert requestDetails != null;
-        assert getActivity() != null;
-
-        // Set the view.  The parent view is null because it will be assigned by the alert dialog.
-        dialogBuilder.setView(getActivity().getLayoutInflater().inflate(R.layout.view_request_dialog, null));
-
-        // Set the close button.
-        dialogBuilder.setNeutralButton(R.string.close, (DialogInterface dialog, int which) -> {
-            // Do nothing.  The dialog will close automatically.
-        });
-
-        // Set the previous button.
-        dialogBuilder.setNegativeButton(R.string.previous, (DialogInterface dialog, int which) -> {
-            // Load the previous request.
-            viewRequestListener.onPrevious(id);
-        });
-
-        // Set the next button.
-        dialogBuilder.setPositiveButton(R.string.next, (DialogInterface dialog, int which) -> {
-            // Load the next request.
-            viewRequestListener.onNext(id);
-        });
-
-        // Create an alert dialog from the alert dialog builder.
-        final AlertDialog alertDialog = dialogBuilder.create();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            // Remove the warning below that `getWindow()` might be null.
-            assert alertDialog.getWindow() != null;
-
-            // Disable screenshots.
-            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
-
-        //The alert dialog must be shown before the contents can be modified.
-        alertDialog.show();
-
-        // Get handles for the dialog views.
-        TextView requestDisposition = alertDialog.findViewById(R.id.request_disposition);
-        TextView requestUrl = alertDialog.findViewById(R.id.request_url);
-        TextView requestBlockListLabel = alertDialog.findViewById(R.id.request_blocklist_label);
-        TextView requestBlockList = alertDialog.findViewById(R.id.request_blocklist);
-        TextView requestSubListLabel = alertDialog.findViewById(R.id.request_sublist_label);
-        TextView requestSubList = alertDialog.findViewById(R.id.request_sublist);
-        TextView requestBlockListEntriesLabel = alertDialog.findViewById(R.id.request_blocklist_entries_label);
-        TextView requestBlockListEntries = alertDialog.findViewById(R.id.request_blocklist_entries);
-        TextView requestBlockListOriginalEntryLabel = alertDialog.findViewById(R.id.request_blocklist_original_entry_label);
-        TextView requestBlockListOriginalEntry = alertDialog.findViewById(R.id.request_blocklist_original_entry);
-        Button previousButton = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
-        Button nextButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
-
-        // Remove the incorrect lint warnings below that the views might be null.
-        assert requestDisposition != null;
-        assert requestUrl != null;
-        assert requestBlockListLabel != null;
-        assert requestBlockList != null;
-        assert requestSubListLabel != null;
-        assert requestSubList != null;
-        assert requestBlockListEntriesLabel != null;
-        assert requestBlockListEntries != null;
-        assert requestBlockListOriginalEntryLabel != null;
-        assert requestBlockListOriginalEntry != null;
-
-        // Disable the previous button if the first resource request is displayed.
-        previousButton.setEnabled(!(id == 1));
-
-        // Disable the next button if the last resource request is displayed.
-        nextButton.setEnabled(!isLastRequest);
-
-        // Set the request action text.
-        switch (requestDetails[BlocklistHelper.REQUEST_DISPOSITION]) {
-            case BlocklistHelper.REQUEST_DEFAULT:
-                // Set the text.
-                requestDisposition.setText(R.string.default_allowed);
-
-                // Set the background color.
-                requestDisposition.setBackgroundColor(getResources().getColor(R.color.transparent));
-                break;
-
-            case BlocklistHelper.REQUEST_ALLOWED:
-                // Set the text.
-                requestDisposition.setText(R.string.allowed);
-
-                // Set the background color.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                    requestDisposition.setBackgroundColor(getResources().getColor(R.color.blue_700_50));
-                } else {
-                    requestDisposition.setBackgroundColor(getResources().getColor(R.color.blue_100));
-                }
-                break;
-
-            case BlocklistHelper.REQUEST_THIRD_PARTY:
-                // Set the text.
-                requestDisposition.setText(R.string.third_party_blocked);
-
-                // Set the background color.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                    requestDisposition.setBackgroundColor(getResources().getColor(R.color.yellow_700_50));
-                } else {
-                    requestDisposition.setBackgroundColor(getResources().getColor(R.color.yellow_100));
-                }
-                break;
-
-            case BlocklistHelper.REQUEST_BLOCKED:
-                // Set the text.
-                requestDisposition.setText(R.string.blocked);
-
-                // Set the background color.
-                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
-                    requestDisposition.setBackgroundColor(getResources().getColor(R.color.red_700_40));
-                } else {
-                    requestDisposition.setBackgroundColor(getResources().getColor(R.color.red_100));
-                }
-                break;
-        }
-
-        // Display the request URL.
-        requestUrl.setText(requestDetails[BlocklistHelper.REQUEST_URL]);
-
-        // Modify the dialog based on the request action.
-        if (requestDetails.length == 2) {  // A default request.
-            // Hide the unused views.
-            requestBlockListLabel.setVisibility(View.GONE);
-            requestBlockList.setVisibility(View.GONE);
-            requestSubListLabel.setVisibility(View.GONE);
-            requestSubList.setVisibility(View.GONE);
-            requestBlockListEntriesLabel.setVisibility(View.GONE);
-            requestBlockListEntries.setVisibility(View.GONE);
-            requestBlockListOriginalEntryLabel.setVisibility(View.GONE);
-            requestBlockListOriginalEntry.setVisibility(View.GONE);
-        } else {  // A blocked or allowed request.
-            // Set the text on the text views.
-            requestBlockList.setText(requestDetails[BlocklistHelper.REQUEST_BLOCKLIST]);
-            requestBlockListEntries.setText(requestDetails[BlocklistHelper.REQUEST_BLOCKLIST_ENTRIES]);
-            requestBlockListOriginalEntry.setText(requestDetails[BlocklistHelper.REQUEST_BLOCKLIST_ORIGINAL_ENTRY]);
-
-            // Set the sublist text.
-            switch (requestDetails[BlocklistHelper.REQUEST_SUBLIST]) {
-                case BlocklistHelper.MAIN_WHITELIST:
-                    requestSubList.setText(R.string.main_whitelist);
-                    break;
-
-                case BlocklistHelper.FINAL_WHITELIST:
-                    requestSubList.setText(R.string.final_whitelist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_WHITELIST:
-                    requestSubList.setText(R.string.domain_whitelist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_INITIAL_WHITELIST:
-                    requestSubList.setText(R.string.domain_initial_whitelist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_FINAL_WHITELIST:
-                    requestSubList.setText(R.string.domain_final_whitelist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_WHITELIST:
-                    requestSubList.setText(R.string.third_party_whitelist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_DOMAIN_WHITELIST:
-                    requestSubList.setText(R.string.third_party_domain_whitelist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_DOMAIN_INITIAL_WHITELIST:
-                    requestSubList.setText(R.string.third_party_domain_initial_whitelist);
-                    break;
-
-                case BlocklistHelper.MAIN_BLACKLIST:
-                    requestSubList.setText(R.string.main_blacklist);
-                    break;
-
-                case BlocklistHelper.INITIAL_BLACKLIST:
-                    requestSubList.setText(R.string.initial_blacklist);
-                    break;
-
-                case BlocklistHelper.FINAL_BLACKLIST:
-                    requestSubList.setText(R.string.final_blacklist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_BLACKLIST:
-                    requestSubList.setText(R.string.domain_blacklist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_INITIAL_BLACKLIST:
-                    requestSubList.setText(R.string.domain_initial_blacklist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_FINAL_BLACKLIST:
-                    requestSubList.setText(R.string.domain_final_blacklist);
-                    break;
-
-                case BlocklistHelper.DOMAIN_REGULAR_EXPRESSION_BLACKLIST:
-                    requestSubList.setText(R.string.domain_regular_expression_blacklist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_BLACKLIST:
-                    requestSubList.setText(R.string.third_party_blacklist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_INITIAL_BLACKLIST:
-                    requestSubList.setText(R.string.third_party_initial_blacklist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_DOMAIN_BLACKLIST:
-                    requestSubList.setText(R.string.third_party_domain_blacklist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_DOMAIN_INITIAL_BLACKLIST:
-                    requestSubList.setText(R.string.third_party_domain_initial_blacklist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_REGULAR_EXPRESSION_BLACKLIST:
-                    requestSubList.setText(R.string.third_party_regular_expression_blacklist);
-                    break;
-
-                case BlocklistHelper.THIRD_PARTY_DOMAIN_REGULAR_EXPRESSION_BLACKLIST:
-                    requestSubList.setText(R.string.third_party_domain_regular_expression_blacklist);
-                    break;
-
-                case BlocklistHelper.REGULAR_EXPRESSION_BLACKLIST:
-                    requestSubList.setText(R.string.regular_expression_blacklist);
-                    break;
-            }
-        }
-
-        // `onCreateDialog` requires the return of an alert dialog.
-        return alertDialog;
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewRequestDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewRequestDialog.kt
new file mode 100644 (file)
index 0000000..2b7e345
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+ * Copyright © 2018-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.helpers.BlocklistHelper
+
+// Define the class constants.
+private const val ID = "id"
+private const val IS_LAST_REQUEST = "is_last_request"
+private const val REQUEST_DETAILS = "request_details"
+
+class ViewRequestDialog : DialogFragment() {
+    // Define the class variables.
+    private lateinit var viewRequestListener: ViewRequestListener
+
+    // The public interface is used to send information back to the parent activity.
+    interface ViewRequestListener {
+        // Show the previous request.
+        fun onPrevious(currentId: Int)
+
+        // Show the next request.
+        fun onNext(currentId: Int)
+    }
+
+    override fun onAttach(context: Context) {
+        // Run the default commands.
+        super.onAttach(context)
+
+        // Get a handle for the listener from the launching context.
+        viewRequestListener = context as ViewRequestListener
+    }
+
+    companion object {
+        // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
+        @JvmStatic
+        fun request(id: Int, isLastRequest: Boolean, requestDetails: Array<String>): ViewRequestDialog {
+            // Create a bundle.
+            val bundle = Bundle()
+
+            // Store the request details.
+            bundle.putInt(ID, id)
+            bundle.putBoolean(IS_LAST_REQUEST, isLastRequest)
+            bundle.putStringArray(REQUEST_DETAILS, requestDetails)
+
+            // Create a new instance of the view request dialog.
+            val viewRequestDialog = ViewRequestDialog()
+
+            // Add the arguments to the new dialog.
+            viewRequestDialog.arguments = bundle
+
+            // Return the new dialog.
+            return viewRequestDialog
+        }
+    }
+
+    // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Get the arguments from the bundle.
+        val id = requireArguments().getInt(ID)
+        val isLastRequest = requireArguments().getBoolean(IS_LAST_REQUEST)
+        val requestDetails = requireArguments().getStringArray(REQUEST_DETAILS)!!
+
+        // Use an alert dialog builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Get the current theme status.
+        val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+
+        // Set the icon according to the theme.
+        if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+            dialogBuilder.setIcon(R.drawable.block_ads_enabled_day)
+        } else {
+            dialogBuilder.setIcon(R.drawable.block_ads_enabled_night)
+        }
+
+        // Set the title.
+        dialogBuilder.setTitle(resources.getString(R.string.request_details) + " - " + id)
+
+        // Set the view.  The parent view is null because it will be assigned by the alert dialog.
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.view_request_dialog, null))
+
+        // Set the close button.  Using `null` as the listener closes the dialog without doing anything else.
+        dialogBuilder.setNeutralButton(R.string.close, null)
+
+        // Set the previous button.
+        dialogBuilder.setNegativeButton(R.string.previous) { _: DialogInterface?, _: Int ->
+            // Load the previous request.
+            viewRequestListener.onPrevious(id)
+        }
+
+        // Set the next button.
+        dialogBuilder.setPositiveButton(R.string.next) { _: DialogInterface?, _: Int ->
+            // Load the next request.
+            viewRequestListener.onNext(id)
+        }
+
+        // Create an alert dialog from the alert dialog builder.
+        val alertDialog = dialogBuilder.create()
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            // Disable screenshots.
+            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        //The alert dialog must be shown before the contents can be modified.
+        alertDialog.show()
+
+        // Get handles for the dialog views.
+        val requestDisposition = alertDialog.findViewById<TextView>(R.id.request_disposition)!!
+        val requestUrl = alertDialog.findViewById<TextView>(R.id.request_url)!!
+        val requestBlockListLabel = alertDialog.findViewById<TextView>(R.id.request_blocklist_label)!!
+        val requestBlockList = alertDialog.findViewById<TextView>(R.id.request_blocklist)!!
+        val requestSubListLabel = alertDialog.findViewById<TextView>(R.id.request_sublist_label)!!
+        val requestSubList = alertDialog.findViewById<TextView>(R.id.request_sublist)!!
+        val requestBlockListEntriesLabel = alertDialog.findViewById<TextView>(R.id.request_blocklist_entries_label)!!
+        val requestBlockListEntries = alertDialog.findViewById<TextView>(R.id.request_blocklist_entries)!!
+        val requestBlockListOriginalEntryLabel = alertDialog.findViewById<TextView>(R.id.request_blocklist_original_entry_label)!!
+        val requestBlockListOriginalEntry = alertDialog.findViewById<TextView>(R.id.request_blocklist_original_entry)!!
+        val previousButton = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE)
+        val nextButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE)
+
+        // Disable the previous button if the first resource request is displayed.
+        previousButton.isEnabled = (id != 1)
+
+        // Disable the next button if the last resource request is displayed.
+        nextButton.isEnabled = !isLastRequest
+
+        // Set the request action text.
+        when (requestDetails[BlocklistHelper.REQUEST_DISPOSITION]) {
+            BlocklistHelper.REQUEST_DEFAULT -> {
+                // Set the text.
+                requestDisposition.setText(R.string.default_allowed)
+
+                // Set the background color.  The deprecated `getColor()` must be used until the minimum API >= 23.
+                @Suppress("DEPRECATION")
+                requestDisposition.setBackgroundColor(resources.getColor(R.color.transparent))
+            }
+
+            BlocklistHelper.REQUEST_ALLOWED -> {
+                // Set the text.
+                requestDisposition.setText(R.string.allowed)
+
+                // Set the background color according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    @Suppress("DEPRECATION")
+                    requestDisposition.setBackgroundColor(resources.getColor(R.color.blue_100))
+                } else {
+                    @Suppress("DEPRECATION")
+                    requestDisposition.setBackgroundColor(resources.getColor(R.color.blue_700_50))
+                }
+            }
+
+            BlocklistHelper.REQUEST_THIRD_PARTY -> {
+                // Set the text.
+                requestDisposition.setText(R.string.third_party_blocked)
+
+                // Set the background color according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    @Suppress("DEPRECATION")
+                    requestDisposition.setBackgroundColor(resources.getColor(R.color.yellow_100))
+                } else {
+                    @Suppress("DEPRECATION")
+                    requestDisposition.setBackgroundColor(resources.getColor(R.color.yellow_700_50))
+                }
+            }
+            BlocklistHelper.REQUEST_BLOCKED -> {
+                // Set the text.
+                requestDisposition.setText(R.string.blocked)
+
+                // Set the background color according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    @Suppress("DEPRECATION")
+                    requestDisposition.setBackgroundColor(resources.getColor(R.color.red_100))
+                } else {
+                    @Suppress("DEPRECATION")
+                    requestDisposition.setBackgroundColor(resources.getColor(R.color.red_700_40))
+                }
+            }
+        }
+
+        // Display the request URL.
+        requestUrl.text = requestDetails[BlocklistHelper.REQUEST_URL]
+
+        // Modify the dialog based on the request action.
+        if (requestDetails.size == 2) {  // A default request.
+            // Hide the unused views.
+            requestBlockListLabel.visibility = View.GONE
+            requestBlockList.visibility = View.GONE
+            requestSubListLabel.visibility = View.GONE
+            requestSubList.visibility = View.GONE
+            requestBlockListEntriesLabel.visibility = View.GONE
+            requestBlockListEntries.visibility = View.GONE
+            requestBlockListOriginalEntryLabel.visibility = View.GONE
+            requestBlockListOriginalEntry.visibility = View.GONE
+        } else {  // A blocked or allowed request.
+            // Set the text on the text views.
+            requestBlockList.text = requestDetails[BlocklistHelper.REQUEST_BLOCKLIST]
+            requestBlockListEntries.text = requestDetails[BlocklistHelper.REQUEST_BLOCKLIST_ENTRIES]
+            requestBlockListOriginalEntry.text = requestDetails[BlocklistHelper.REQUEST_BLOCKLIST_ORIGINAL_ENTRY]
+            when (requestDetails[BlocklistHelper.REQUEST_SUBLIST]) {
+                BlocklistHelper.MAIN_WHITELIST -> requestSubList.setText(R.string.main_whitelist)
+                BlocklistHelper.FINAL_WHITELIST -> requestSubList.setText(R.string.final_whitelist)
+                BlocklistHelper.DOMAIN_WHITELIST -> requestSubList.setText(R.string.domain_whitelist)
+                BlocklistHelper.DOMAIN_INITIAL_WHITELIST -> requestSubList.setText(R.string.domain_initial_whitelist)
+                BlocklistHelper.DOMAIN_FINAL_WHITELIST -> requestSubList.setText(R.string.domain_final_whitelist)
+                BlocklistHelper.THIRD_PARTY_WHITELIST -> requestSubList.setText(R.string.third_party_whitelist)
+                BlocklistHelper.THIRD_PARTY_DOMAIN_WHITELIST -> requestSubList.setText(R.string.third_party_domain_whitelist)
+                BlocklistHelper.THIRD_PARTY_DOMAIN_INITIAL_WHITELIST -> requestSubList.setText(R.string.third_party_domain_initial_whitelist)
+                BlocklistHelper.MAIN_BLACKLIST -> requestSubList.setText(R.string.main_blacklist)
+                BlocklistHelper.INITIAL_BLACKLIST -> requestSubList.setText(R.string.initial_blacklist)
+                BlocklistHelper.FINAL_BLACKLIST -> requestSubList.setText(R.string.final_blacklist)
+                BlocklistHelper.DOMAIN_BLACKLIST -> requestSubList.setText(R.string.domain_blacklist)
+                BlocklistHelper.DOMAIN_INITIAL_BLACKLIST -> requestSubList.setText(R.string.domain_initial_blacklist)
+                BlocklistHelper.DOMAIN_FINAL_BLACKLIST -> requestSubList.setText(R.string.domain_final_blacklist)
+                BlocklistHelper.DOMAIN_REGULAR_EXPRESSION_BLACKLIST -> requestSubList.setText(R.string.domain_regular_expression_blacklist)
+                BlocklistHelper.THIRD_PARTY_BLACKLIST -> requestSubList.setText(R.string.third_party_blacklist)
+                BlocklistHelper.THIRD_PARTY_INITIAL_BLACKLIST -> requestSubList.setText(R.string.third_party_initial_blacklist)
+                BlocklistHelper.THIRD_PARTY_DOMAIN_BLACKLIST -> requestSubList.setText(R.string.third_party_domain_blacklist)
+                BlocklistHelper.THIRD_PARTY_DOMAIN_INITIAL_BLACKLIST -> requestSubList.setText(R.string.third_party_domain_initial_blacklist)
+                BlocklistHelper.THIRD_PARTY_REGULAR_EXPRESSION_BLACKLIST -> requestSubList.setText(R.string.third_party_regular_expression_blacklist)
+                BlocklistHelper.THIRD_PARTY_DOMAIN_REGULAR_EXPRESSION_BLACKLIST -> requestSubList.setText(R.string.third_party_domain_regular_expression_blacklist)
+                BlocklistHelper.REGULAR_EXPRESSION_BLACKLIST -> requestSubList.setText(R.string.regular_expression_blacklist)
+            }
+        }
+
+        // Return the alert dialog.
+        return alertDialog
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.java
deleted file mode 100644 (file)
index 84848c6..0000000
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
- *
- * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.net.http.SslCertificate;
-import android.os.Bundle;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.ForegroundColorSpan;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.preference.PreferenceManager;
-
-import com.stoutner.privacybrowser.activities.MainWebViewActivity;
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
-import com.stoutner.privacybrowser.views.NestedScrollWebView;
-
-import java.text.DateFormat;
-import java.util.Calendar;
-import java.util.Date;
-
-public class ViewSslCertificateDialog extends DialogFragment {
-    public static ViewSslCertificateDialog displayDialog(long webViewFragmentId) {
-        // Create an arguments bundle.
-        Bundle argumentsBundle = new Bundle();
-
-        // Store the WebView fragment ID in the bundle.
-        argumentsBundle.putLong("webview_fragment_id", webViewFragmentId);
-
-        // Create a new instance of the dialog.
-        ViewSslCertificateDialog viewSslCertificateDialog = new ViewSslCertificateDialog();
-
-        // Add the bundle to the dialog.
-        viewSslCertificateDialog.setArguments(argumentsBundle);
-
-        // Return the new dialog.
-        return viewSslCertificateDialog;
-    }
-
-    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
-    @SuppressLint("InflateParams")
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Get a handle for the activity and the context.
-        Activity activity = requireActivity();
-        Context context = requireContext();
-
-        // Get the activity's layout inflater.
-        LayoutInflater layoutInflater = activity.getLayoutInflater();
-
-        // Get the arguments.
-        Bundle arguments = getArguments();
-
-        // Remove the incorrect lint warning below that the arguments might be null.
-        assert arguments != null;
-
-        // Get the current position of this WebView fragment.
-        int webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(arguments.getLong("webview_fragment_id"));
-
-        // Get the WebView tab fragment.
-        WebViewTabFragment webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition);
-
-        // Get the fragment view.
-        View fragmentView = webViewTabFragment.getView();
-
-        // Remove the incorrect lint warning below that the fragment view might be null.
-        assert fragmentView != null;
-
-        // Get a handle for the current WebView.
-        NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
-
-
-        // Use a builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog);
-
-        // Create a drawable version of the favorite icon.
-        Drawable favoriteIconDrawable = new BitmapDrawable(getResources(), nestedScrollWebView.getFavoriteOrDefaultIcon());
-
-        // Set the icon.
-        dialogBuilder.setIcon(favoriteIconDrawable);
-
-        // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
-        dialogBuilder.setNegativeButton(R.string.close, null);
-
-        // Get the SSL certificate.
-        SslCertificate sslCertificate = nestedScrollWebView.getCertificate();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
-
-        // Check to see if the website is encrypted.
-        if (sslCertificate == null) {  // The website is not encrypted.
-            // Set the title.
-            dialogBuilder.setTitle(R.string.unencrypted_website);
-
-            // Set the Layout.  The parent view is `null` because it will be assigned by the alert dialog.
-            dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null));
-
-            // Create an alert dialog from the alert dialog builder.
-            final AlertDialog alertDialog = dialogBuilder.create();
-
-            // Disable screenshots if not allowed.
-            if (!allowScreenshots) {
-                // Remove the warning below that `getWindow()` might be null.
-                assert alertDialog.getWindow() != null;
-
-                // Disable screenshots.
-                alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-            }
-
-            // `onCreateDialog` requires the return of an `AlertDialog`.
-            return alertDialog;
-        } else {  // Display the SSL certificate information
-            // Set the title.
-            dialogBuilder.setTitle(R.string.ssl_certificate);
-
-            // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
-            dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null));
-
-            // Create an alert dialog from the builder.
-            final AlertDialog alertDialog = dialogBuilder.create();
-
-            // Disable screenshots if not allowed.
-            if (!allowScreenshots) {
-                // Remove the warning below that `getWindow()` might be null.
-                assert alertDialog.getWindow() != null;
-
-                // Disable screenshots.
-                alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-            }
-
-            // The alert dialog must be shown before items in the layout can be modified.
-            alertDialog.show();
-
-            // Get handles for the text views.
-            TextView domainTextView = alertDialog.findViewById(R.id.domain);
-            TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
-            TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
-            TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
-            TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
-            TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
-            TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
-            TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
-            TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
-            TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
-
-            // Remove the incorrect warning that the views might be null.
-            assert domainTextView != null;
-            assert ipAddressesTextView != null;
-            assert issuedToCNameTextView != null;
-            assert issuedToONameTextView != null;
-            assert issuedToUNameTextView != null;
-            assert issuedByCNameTextView != null;
-            assert issuedByONameTextView != null;
-            assert issuedByUNameTextView != null;
-            assert startDateTextView != null;
-            assert endDateTextView != null;
-
-            // Setup the labels.
-            String domainLabel = getString(R.string.domain_label) + "  ";
-            String ipAddressesLabel = getString(R.string.ip_addresses) + "  ";
-            String cNameLabel = getString(R.string.common_name) + "  ";
-            String oNameLabel = getString(R.string.organization) + "  ";
-            String uNameLabel = getString(R.string.organizational_unit) + "  ";
-            String startDateLabel = getString(R.string.start_date) + "  ";
-            String endDateLabel = getString(R.string.end_date) + "  ";
-
-            // Convert the formatted URL string to a URI.
-            Uri uri = Uri.parse(nestedScrollWebView.getUrl());
-
-            // Extract the domain name from the URI.
-            String domainString = uri.getHost();
-
-            // Get the strings from the SSL certificate.
-            String issuedToCName = sslCertificate.getIssuedTo().getCName();
-            String issuedToOName = sslCertificate.getIssuedTo().getOName();
-            String issuedToUName = sslCertificate.getIssuedTo().getUName();
-            String issuedByCName = sslCertificate.getIssuedBy().getCName();
-            String issuedByOName = sslCertificate.getIssuedBy().getOName();
-            String issuedByUName = sslCertificate.getIssuedBy().getUName();
-            Date startDate = sslCertificate.getValidNotBeforeDate();
-            Date endDate = sslCertificate.getValidNotAfterDate();
-
-            // Create spannable string builders for each text view that needs multiple colors of text.
-            SpannableStringBuilder domainStringBuilder = new SpannableStringBuilder(domainLabel + domainString);
-            SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + nestedScrollWebView.getCurrentIpAddresses());
-            SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
-            SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
-            SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
-            SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
-            SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
-            SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
-            SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate));
-            SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate));
-
-            // Define the color spans.
-            ForegroundColorSpan blueColorSpan;
-            ForegroundColorSpan redColorSpan;
-
-            // Get the current theme status.
-            int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-            // Set the color spans according to the theme.  The deprecated `getResources()` must be used until the minimum API >= 23.
-            if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
-                blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
-                redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
-            } else {
-                blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.violet_700));
-                redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
-            }
-
-            // Remove the incorrect lint error that `.equals` might produce a NullPointerException.
-            assert domainString != null;
-
-            // Formet the domain string and issued to CName colors.
-            if (domainString.equals(issuedToCName)) {  // `domainString` and `issuedToCName` match.
-                // Set the strings to be blue.
-                domainStringBuilder.setSpan(blueColorSpan, domainLabel.length(), domainStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            } else if(issuedToCName.startsWith("*.")){  // `issuedToCName` begins with a wildcard.
-                // Remove the initial `*.`.
-                String baseCertificateDomain = issuedToCName.substring(2);
-
-                // Setup a copy of `domainString` to test subdomains.
-                String domainStringSubdomain = domainString;
-
-                // Initialize `domainNamesMatch`.
-                boolean domainNamesMatch = false;
-
-                // Check all the subdomains in `domainStringSubdomain` against `baseCertificateDomain`.
-                while (!domainNamesMatch && domainStringSubdomain.contains(".")) {  // Stop checking if we know that `domainNamesMatch` is `true` or if we run out of  `.`.
-                    // Test the `domainStringSubdomain` against `baseCertificateDomain`.
-                    if (domainStringSubdomain.equals(baseCertificateDomain)) {
-                        domainNamesMatch = true;
-                    }
-
-                    // Strip out the lowest subdomain of `certificateCommonNameSubdomain`.
-                    domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1);
-                }
-
-                // Format the domain and issued to Common Name according to `domainNamesMatch`.
-                if (domainNamesMatch) {  // `domainString` is a subdomain of the wildcard `issuedToCNameString`.
-                    // Set the strings to be blue.
-                    domainStringBuilder.setSpan(blueColorSpan, domainLabel.length(), domainStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                    issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                } else {  // `domainString` is not a subdomain of the wildcard `issuedToCNameString`.
-                    // Set the string to be red.
-                    domainStringBuilder.setSpan(redColorSpan, domainLabel.length(), domainStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                    issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                }
-            } else {  // The strings do not match and `issuedToCNameString` does not begin with a wildcard.
-                // Set the strings to be red.
-                domainStringBuilder.setSpan(redColorSpan, domainLabel.length(), domainStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-                issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            }
-
-            // Set the IP addresses, issued to, and issued by spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
-            ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-
-            // Get the current date.
-            Date currentDate = Calendar.getInstance().getTime();
-
-            //  Format the start date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
-            if (startDate.after(currentDate)) {  // The certificate start date is in the future.
-                startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            } else {  // The certificate start date is in the past.
-                startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            }
-
-            // Format the end date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
-            if (endDate.before(currentDate)) {  // The certificate end date is in the past.
-                endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            } else {  // The certificate end date is in the future.
-                endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
-            }
-
-            // Display the strings.
-            domainTextView.setText(domainStringBuilder);
-            ipAddressesTextView.setText(ipAddressesStringBuilder);
-            issuedToCNameTextView.setText(issuedToCNameStringBuilder);
-            issuedToONameTextView.setText(issuedToONameStringBuilder);
-            issuedToUNameTextView.setText(issuedToUNameStringBuilder);
-            issuedByCNameTextView.setText(issuedByCNameStringBuilder);
-            issuedByONameTextView.setText(issuedByONameStringBuilder);
-            issuedByUNameTextView.setText(issuedByUNameStringBuilder);
-            startDateTextView.setText(startDateStringBuilder);
-            endDateTextView.setText(endDateStringBuilder);
-
-            // `onCreateDialog` requires the return of an alert dialog.
-            return alertDialog;
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt
new file mode 100644 (file)
index 0000000..2bebf90
--- /dev/null
@@ -0,0 +1,297 @@
+/*
+ * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.res.Configuration
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import android.view.WindowManager
+import android.widget.TextView
+
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.activities.MainWebViewActivity
+import com.stoutner.privacybrowser.views.NestedScrollWebView
+
+import java.text.DateFormat
+import java.util.Calendar
+
+// Define the class constants.
+private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
+
+class ViewSslCertificateDialog : DialogFragment() {
+    companion object {
+        // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
+        @JvmStatic
+        fun displayDialog(webViewFragmentId: Long): ViewSslCertificateDialog {
+            // Create an arguments bundle.
+            val argumentsBundle = Bundle()
+
+            // Store the WebView fragment ID in the bundle.
+            argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
+
+            // Create a new instance of the view SSL certificate dialog.
+            val viewSslCertificateDialog = ViewSslCertificateDialog()
+
+            // Add the bundle to the new dialog.
+            viewSslCertificateDialog.arguments = argumentsBundle
+
+            // Return the new dialog.
+            return viewSslCertificateDialog
+        }
+    }
+
+    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Get the current position of this WebView fragment.
+        val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
+
+        // Get the WebView tab fragment.
+        val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
+
+        // Get the fragment view.
+        val fragmentView = webViewTabFragment.requireView()
+
+        // Get a handle for the current nested scroll WebView.
+        val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
+
+        // Use a builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Create a drawable version of the favorite icon.
+        val favoriteIconDrawable: Drawable = BitmapDrawable(resources, nestedScrollWebView.favoriteOrDefaultIcon)
+
+        // Set the icon.
+        dialogBuilder.setIcon(favoriteIconDrawable)
+
+        // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
+        dialogBuilder.setNegativeButton(R.string.close, null)
+
+        // Get the SSL certificate.
+        val sslCertificate = nestedScrollWebView.certificate
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Check to see if the website is encrypted.
+        if (sslCertificate == null) {  // The website is not encrypted.
+            // Set the title.
+            dialogBuilder.setTitle(R.string.unencrypted_website)
+
+            // Set the Layout.  The parent view is `null` because it will be assigned by the alert dialog.
+            dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null))
+
+            // Create an alert dialog from the builder.
+            val alertDialog = dialogBuilder.create()
+
+            // Disable screenshots if not allowed.
+            if (!allowScreenshots) {
+                // Disable screenshots.
+                alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+            }
+
+            // Return the alert dialog.
+            return alertDialog
+        } else {  // The website is encrypted.
+            // Set the title.
+            dialogBuilder.setTitle(R.string.ssl_certificate)
+
+            // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
+            dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null))
+
+            // Create an alert dialog from the builder.
+            val alertDialog = dialogBuilder.create()
+
+            // Disable screenshots if not allowed.
+            if (!allowScreenshots) {
+                // Disable screenshots.
+                alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+            }
+
+            // The alert dialog must be shown before items in the layout can be modified.
+            alertDialog.show()
+
+            // Get handles for the text views.
+            val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
+            val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
+            val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
+            val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
+            val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
+            val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
+            val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
+            val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
+            val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
+            val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
+
+            // Setup the labels.
+            val domainLabel = getString(R.string.domain_label) + "  "
+            val ipAddressesLabel = getString(R.string.ip_addresses) + "  "
+            val cNameLabel = getString(R.string.common_name) + "  "
+            val oNameLabel = getString(R.string.organization) + "  "
+            val uNameLabel = getString(R.string.organizational_unit) + "  "
+            val startDateLabel = getString(R.string.start_date) + "  "
+            val endDateLabel = getString(R.string.end_date) + "  "
+
+            // Convert the URL to a URI.
+            val uri = Uri.parse(nestedScrollWebView.url)
+
+            // Extract the domain name from the URI.
+            val domainString = uri.host
+
+            // Get the strings from the SSL certificate.
+            val issuedToCName = sslCertificate.issuedTo.cName
+            val issuedToOName = sslCertificate.issuedTo.oName
+            val issuedToUName = sslCertificate.issuedTo.uName
+            val issuedByCName = sslCertificate.issuedBy.cName
+            val issuedByOName = sslCertificate.issuedBy.oName
+            val issuedByUName = sslCertificate.issuedBy.uName
+            val startDate = sslCertificate.validNotBeforeDate
+            val endDate = sslCertificate.validNotAfterDate
+
+            // Create spannable string builders for each text view that needs multiple colors of text.
+            val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
+            val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + nestedScrollWebView.currentIpAddresses)
+            val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
+            val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
+            val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
+            val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
+            val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
+            val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
+            val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
+            val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
+
+            // Define the color spans.
+            val blueColorSpan: ForegroundColorSpan
+            val redColorSpan: ForegroundColorSpan
+
+            // Get the current theme status.
+            val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+
+            // Set the color spans according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
+            if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                @Suppress("DEPRECATION")
+                blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
+                @Suppress("DEPRECATION")
+                redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
+            } else {
+                @Suppress("DEPRECATION")
+                blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
+                @Suppress("DEPRECATION")
+                redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
+            }
+
+            // Format the domain string and issued to CName colors.
+            if (domainString == issuedToCName) {  // The domain and issued to CName match.
+                // Set the strings to be blue.
+                domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            } else if (issuedToCName.startsWith("*.")) {  // The issued to CName begins with a wildcard.
+                // Remove the initial `*.`.
+                val baseCertificateDomain = issuedToCName.substring(2)
+
+                // Setup a copy of the domain string to test subdomains.
+                var domainStringSubdomain = domainString!!
+
+                // Define a domain names match variable.
+                var domainNamesMatch = false
+
+                // Check all the subdomains against the base certificate domain.
+                while (!domainNamesMatch && domainStringSubdomain.contains(".")) {  // Stop checking if we know that the domain names match or if we run out of subdomains.
+                    // Test the subdomain against the base certificate domain.
+                    if (domainStringSubdomain == baseCertificateDomain) {
+                        domainNamesMatch = true
+                    }
+
+                    // Strip out the lowest subdomain.
+                    domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
+                }
+
+                // Format the domain and issued to CName.
+                if (domainNamesMatch) {  // The domain is a subdomain of the wildcard certificate.
+                    // Set the strings to be blue.
+                    domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                    issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                } else {  // The domain is not a subdomain of the wildcard certificate.
+                    // Set the string to be red.
+                    domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                    issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                }
+            } else {  // The strings do not match and issued to CName does not begin with a wildcard.
+                // Set the strings to be red.
+                domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+                issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            }
+
+            // Set the IP addresses, issued to, and issued by spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
+            ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+            // Get the current date.
+            val currentDate = Calendar.getInstance().time
+
+            //  Format the start date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
+            if (startDate.after(currentDate)) {  // The certificate start date is in the future.
+                startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            } else {  // The certificate start date is in the past.
+                startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            }
+
+            // Format the end date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
+            if (endDate.before(currentDate)) {  // The certificate end date is in the past.
+                endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            } else {  // The certificate end date is in the future.
+                endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+            }
+
+            // Display the strings.
+            domainTextView.text = domainStringBuilder
+            ipAddressesTextView.text = ipAddressesStringBuilder
+            issuedToCNameTextView.text = issuedToCNameStringBuilder
+            issuedToONameTextView.text = issuedToONameStringBuilder
+            issuedToUNameTextView.text = issuedToUNameStringBuilder
+            issuedByCNameTextView.text = issuedByCNameStringBuilder
+            issuedByONameTextView.text = issuedByONameStringBuilder
+            issuedByUNameTextView.text = issuedByUNameStringBuilder
+            startDateTextView.text = startDateStringBuilder
+            endDateTextView.text = endDateStringBuilder
+
+            // Return the alert dialog.
+            return alertDialog
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/WaitingForProxyDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/WaitingForProxyDialog.java
deleted file mode 100644 (file)
index d87385d..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
- *
- * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.preference.PreferenceManager;
-
-import com.stoutner.privacybrowser.R;
-
-public class WaitingForProxyDialog extends DialogFragment {
-    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
-    @SuppressLint("InflateParams")
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Get a handle for the context.
-        Context context = requireContext();
-
-        // Get the activity's layout inflater.
-        LayoutInflater layoutInflater = requireActivity().getLayoutInflater();
-
-        // Use a builder to create the alert dialog.
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialog);
-
-        // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
-        dialogBuilder.setView(layoutInflater.inflate(R.layout.waiting_for_proxy_dialog, null));
-
-        // Create an alert dialog from the alert dialog builder.
-        AlertDialog alertDialog = dialogBuilder.create();
-
-        // Get a handle for the shared preferences.
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Get the screenshot preference.
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            // Remove the warning below that `getWindow()` might be null.
-            assert alertDialog.getWindow() != null;
-
-            // Disable screenshots.
-            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
-        }
-
-        // The alert dialog must be shown before items in the layout can be modified.
-        alertDialog.show();
-
-        // Return the alert dialog.
-        return alertDialog;
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/WaitingForProxyDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/WaitingForProxyDialog.kt
new file mode 100644 (file)
index 0000000..145580b
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.os.Bundle
+import android.view.WindowManager
+
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.stoutner.privacybrowser.R
+
+class WaitingForProxyDialog : DialogFragment() {
+    // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Use a builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
+        dialogBuilder.setView(layoutInflater.inflate(R.layout.waiting_for_proxy_dialog, null))
+
+        // Create an alert dialog from the builder.
+        val alertDialog = dialogBuilder.create()
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            // Disable screenshots.
+            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        // Return the alert dialog.
+        return alertDialog
+    }
+}
\ No newline at end of file
index 0a93fb0cdecf8f51dc517c9f2004e5dd46398889..054d200e0236e2320aa43f8ac87d57f868a4bcbf 100644 (file)
@@ -38,11 +38,11 @@ import androidx.webkit.WebViewFeature
 
 import com.stoutner.privacybrowser.R
 
 
 import com.stoutner.privacybrowser.R
 
-// Declare the class constants.
+// Define the class constants.
 private const val TAB_NUMBER = "tab_number"
 
 class AboutWebViewFragment : Fragment() {
 private const val TAB_NUMBER = "tab_number"
 
 class AboutWebViewFragment : Fragment() {
-    // Declare the class variables.
+    // Define the class variables.
     private var tabNumber = 0
 
     // Declare the class views.
     private var tabNumber = 0
 
     // Declare the class views.
index 08946609bda616a88367b7ae246500226d9f8296..861ab8a05c9de5d1619506e896412758c8654018 100644 (file)
                         android:orderInCategory="1201"
                         app:showAsAction="never" />
 
                         android:orderInCategory="1201"
                         app:showAsAction="never" />
 
+                    <!-- TODO. -->
+                    <item
+                        android:id="@+id/save_archive"
+                        android:title="Save Archive"
+                        android:orderInCategory="1202"
+                        app:showAsAction="never" />
+
                     <item
                         android:id="@+id/save_image"
                         android:title="@string/save_image"
                     <item
                         android:id="@+id/save_image"
                         android:title="@string/save_image"
-                        android:orderInCategory="1202"
+                        android:orderInCategory="1203"
                         app:showAsAction="never" />
                 </menu>
             </item>
                         app:showAsAction="never" />
                 </menu>
             </item>
index 509939b6b8291b7dcbce28e6447db0bef2ded4bb..b07a40b20ba074060a0dba0cb44ad9fa3a141ffe 100644 (file)
@@ -26,7 +26,7 @@ buildscript {
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:4.1.3'
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:4.1.3'
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31"
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
index 8f0fb1e119394fc88635cf5364e9aeb5d34bf50b..0d64941150e1d78e149adf5f0279f3d215c2198b 100644 (file)
@@ -15,7 +15,7 @@
 • Fix eines Bugs, durch den gepinnte SSL-Zertifikate manchmal gegen das Zertifikat der vorhergehenden Website geprüft wurden.
 • Mastodon-Eintrag zu "Über Privacy Browser" > Links hinzugefügt.
 • Diverse kleinere Verbesserungen für das Nutzer Erlebnis und der grafischen Oberfläche umgesetzt.
 • Fix eines Bugs, durch den gepinnte SSL-Zertifikate manchmal gegen das Zertifikat der vorhergehenden Website geprüft wurden.
 • Mastodon-Eintrag zu "Über Privacy Browser" > Links hinzugefügt.
 • Diverse kleinere Verbesserungen für das Nutzer Erlebnis und der grafischen Oberfläche umgesetzt.
-• Teilweise Übersetzung in brasilianisches Portugiesisch von Thiago Nazareno Conceição Silva de Jesus.
+• Teilweise Übersetzung in brasilianisch-portugiesische von Thiago Nazareno Conceição Silva de Jesus.
 • Aktualisierte deutsche Übersetzung von Bernhard G. Keller.
 • Aktualisierte französische Übersetzung von Kévin L.
 • Aktualisierte italienische Übersetzung von Francesco Buratti.
 • Aktualisierte deutsche Übersetzung von Bernhard G. Keller.
 • Aktualisierte französische Übersetzung von Kévin L.
 • Aktualisierte italienische Übersetzung von Francesco Buratti.
index efd21a176d42a38c9306666d38e5b8244eace115..81a807a4858f51c81dc6dc94dbb5e68fce298d29 100644 (file)
@@ -10,7 +10,7 @@
 • Fehler behoben, durch den das Hamburger-Menü zu einem Pfeil wurde, wenn das Navigations-Menü beim Neustart der App geöffnet war.
 • Öffnen des Options-Menüs beschleunigt.
 • Aktualisierte deutsche Übersetzung von Bernhard G. Keller.
 • Fehler behoben, durch den das Hamburger-Menü zu einem Pfeil wurde, wenn das Navigations-Menü beim Neustart der App geöffnet war.
 • Öffnen des Options-Menüs beschleunigt.
 • Aktualisierte deutsche Übersetzung von Bernhard G. Keller.
-• Aktualisierte Brasilianisch Portugiesische Übersetzung von Thiago Nazareno Conceição Silva de Jesus.
+• Aktualisierte brasilianisch-portugiesische Übersetzung von Thiago Nazareno Conceição Silva de Jesus.
 • Aktualisierte französische Übersetzung von Kévin L.
 • Aktualisierte italienische Übersetzung von Francesco Buratti.
 • Aktualisierte russische Übersetzung.
 • Aktualisierte französische Übersetzung von Kévin L.
 • Aktualisierte italienische Übersetzung von Francesco Buratti.
 • Aktualisierte russische Übersetzung.