Restore saving as MHT web archives. https://redmine.stoutner.com/issues/677
authorSoren Stoutner <soren@stoutner.com>
Wed, 14 Apr 2021 23:27:40 +0000 (16:27 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 14 Apr 2021 23:27:40 +0000 (16:27 -0700)
20 files changed:
app/build.gradle
app/src/free/assets/fr/about_permissions.html
app/src/main/AndroidManifest.xml
app/src/main/assets/fr/about_changelog.html
app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.kt
app/src/main/java/com/stoutner/privacybrowser/fragments/AboutWebViewFragment.kt
app/src/main/res/layout/open_dialog.xml
app/src/main/res/menu/webview_options_menu.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-pt-rBR/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values/strings.xml
app/src/main/res/xml/file_provider_paths.xml
fastlane/metadata/android/fr-FR/changelogs/54.txt

index 4eb4f1ec189e96778de0753d89e4a1b2298a206e..a49c12c35166462ecb98f7be314c7f189e46421b 100644 (file)
@@ -94,5 +94,5 @@ dependencies {
     implementation 'com.google.android.material:material:1.3.0'
 
     // Only compile AdMob ads for the free flavor.
-    freeImplementation 'com.google.android.gms:play-services-ads:19.8.0'
+    freeImplementation 'com.google.android.gms:play-services-ads:20.0.0'
 }
\ No newline at end of file
index b414ebc8913015192142be5d9b8f80b0d5c4e1c9..eee98e059f5ba3def2f9075086bcc38846253b66 100644 (file)
@@ -41,8 +41,9 @@
         <hr/>
         <br/>
 
-        <p>In addition, Privacy Browser Free displays ads from Google’s AdMob network. For the free flavor, AdMob adds the following permissions even though they are not listed in the source code
-            <a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=blob;f=app/src/main/AndroidManifest.xml;hb=HEAD">manifest file</a>.</p>
+        <p>En outre, Privacy Browser Free affiche des publicités provenant du réseau AdMob de Google.
+            Pour la version gratuite, AdMob ajoute les autorisations suivantes, même si elles ne sont pas répertoriées dans le code source
+            <a href="https://gitweb.stoutner.com/?p=PrivacyBrowser.git;a=blob;f=app/src/main/AndroidManifest.xml;hb=HEAD">fichier de manifeste</a>.</p>
 
         <h3>Afficher les connexions réseau</h3>
         <p><a href="https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_NETWORK_STATE">android.permission.ACCESS_NETWORK_STATE</a></p>
@@ -53,9 +54,9 @@
         <p><a href="https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK">android.permission.WAKE_LOCK</a></p>
         <p>Permet aux annonces d'empêcher le processeur de dormir et l'éclairage de l'écran, bien que lors de mes tests, je ne pense pas que les annonces le fassent réellement.</p>
 
-        <h3>Run at startup</h3>
+        <h3>Lancer au démarrage</h3>
         <p><a href="https://developer.android.com/reference/android/Manifest.permission.html#RECEIVE_BOOT_COMPLETED">android.permission.RECEIVE_BOOT_COMPLETED</a></p>
-        <p>Lets AdMob start when the phone boots even if you don't open Privacy Browser Free. This is a concerning permission because it can allow Google to spy on you.
-            I would either like to find a different ad provider or drop the free flavor of Privacy Browser entirely.</p>
+        <p>Laisser AdMob démarrer au démarrage du téléphone même si vous n'ouvrez pas Privacy Browser Free. C'est une permission inquiétante car elle peut permettre à Google de vous espionner.
+            J'aimerais soit trouver un autre fournisseur de publicité, soit abandonner entièrement la version gratuite de Privacy Browser.</p>
     </body>
 </html>
\ No newline at end of file
index 913200fbe848373296cebca20a6334250e265a7f..2fc80d8992d243cb04ce7169c61c33bfa6435a33 100644 (file)
@@ -43,7 +43,8 @@
         <!-- Orbot. -->
         <package android:name="org.torproject.android" />
 
-        <!-- For some reason, OpenKeychain is visible without having to be listed in the queries. -->
+        <!-- OpenKeyChain. -->
+        <package android:name="org.sufficientlysecure.keychain" />
     </queries>
 
     <!-- For API >= 23, app data is automatically backed up to Google cloud servers unless `android:allowBackup="false"` and `android:fullBackupContent="false"` is set. -->
                 <data android:scheme="https" />
             </intent-filter>
 
-            <!-- Process content intents for text files. -->
+            <!-- Process all content intents, including text, images, and MHT archives. -->
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
                 <category android:name="android.intent.category.DEFAULT" />
 
                 <data android:scheme="content" />
-                <data android:mimeType="text/*" />
+                <data android:mimeType="*/*" />
             </intent-filter>
 
             <!-- Process intents for text strings.  Sometimes URLs are presented this way. -->
index 9a9638795cef3d3db1e4e0a78ea0b7c14cb7be7d..fdba6ae6a666aae24928d7d3b6919195917ed30a 100644 (file)
@@ -1,7 +1,7 @@
 <!--
   Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
 
-  Translation 2019-2020 Kévin L. <kevinliste@framalistes.org>.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+  Translation 2019-2021 Kévin L. <kevinliste@framalistes.org>.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
         <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.
-                Unfortunately, due to a bug in Android’s WebView, this also temporarily removes the ability to <a href="https://redmine.stoutner.com/issues/677">save a web archive</a>.</li>
-            <li>Update <a href="https://redmine.stoutner.com/issues/678">About > Permissions</a>.</li>
-            <li>Improve the descriptiveness of the <a href="https://redmine.stoutner.com/issues/676">save URL snackbar</a>.</li>
-            <li>Add <a href="https://redmine.stoutner.com/issues/568">Metager</a> to the list of search engines.</li>
-            <li>Fix <a href="https://redmine.stoutner.com/issues/674">I2P detection</a>.</li>
-            <li>Fix the alignment of <a href="https://redmine.stoutner.com/issues/228">icons and radio buttons</a> in the dialogs.</li>
-            <li>Update the URL bar when switching tabs <a href="https://redmine.stoutner.com/issues/654">even if it is being edited</a>.</li>
-            <li>Allow <a href="https://redmine.stoutner.com/issues/620">displaying of the password</a> in the HTTP authentication dialog.</li>
-            <li>Fix a <a href="https://redmine.stoutner.com/issues/645">number</a> <a href="https://redmine.stoutner.com/issues/646">of</a> <a href="https://redmine.stoutner.com/issues/651">rare</a>
-                <a href="https://redmine.stoutner.com/issues/663">crashes</a>.</li>
-            <li>Fix the hamburger icon <a href="https://redmine.stoutner.com/issues/616">turning into an arrow</a> if the drawer is open when the app is restarted.</li>
-            <li>Speed up the opening of the <a href="https://redmine.stoutner.com/issues/650">options menu</a>.</li>
+            <li>Reconception de l'accès aux fichiers pour travailler avec <a href="https://redmine.stoutner.com/issues/546">le stockage cible et le Storage Access Framework</a>.
+                Cela permet de faire passer l'API cible à 30 et de supprimer la nécessité des permissions dangereuses READ_EXTERNAL_STORAGE et WRITE_EXTERNAL_STORAGE.
+                Malheureusement, en raison d'un bogue dans la WebView d'Android, cela supprime aussi temporairement la possibilité
+                <a href="https://redmine.stoutner.com/issues/677">d'enregistrer une archive web</a>.</li>
+            <li>Mise à jour des <a href="https://redmine.stoutner.com/issues/678">A propos de > permissions</a>.</li>
+            <li>Amélioration de la description de la <a href="https://redmine.stoutner.com/issues/676">bare d'enregistrement d'URL</a>.</li>
+            <li>Ajout de <a href="https://redmine.stoutner.com/issues/568">Metager</a> à la liste des moteurs de recherche.</li>
+            <li>Correction de <a href="https://redmine.stoutner.com/issues/674">détection I2P</a>.</li>
+            <li>Correction de l'alignement des <a href="https://redmine.stoutner.com/issues/228">icônes et boutons radio</a> dans les boîtes de dialogue.</li>
+            <li>Mise à jour de la barre d'URL lors du changement d'onglet <a href="https://redmine.stoutner.com/issues/654">même s'il est en cours d'édition</a>.</li>
+            <li>Autorisation de <a href="https://redmine.stoutner.com/issues/620">l'affichage du mot de passe</a> dans le dialogue d'authentification HTTP.</li>
+            <li>Correction d'un <a href="https://redmine.stoutner.com/issues/645">nombre</a> <a href="https://redmine.stoutner.com/issues/646">de</a> <a href="https://redmine.stoutner.com/issues/651">rare</a>
+                <a href="https://redmine.stoutner.com/issues/663">crash</a>.</li>
+            <li>Correction de l'icône du hamburger <a href="https://redmine.stoutner.com/issues/616">se transformant en flèche</a> si le tiroir est ouvert lors du redémarrage de l'app.</li>
+            <li>Accélération de l'ouverture du <a href="https://redmine.stoutner.com/issues/650">menu des options</a>.</li>
             <li>Traduction française mise à jour fournie par <a href="mailto:kevinliste@framalistes.org">Kévin L</a>.</li>
-            <li>Updated Brazilian Portuguese translation provided by <a href="mailto:mochileiro2006-trilhas@yahoo.com.br">Thiago Nazareno Conceição Silva de Jesus</a>.</li>
+            <li>Traduction portugaise brésilienne mise à jour fournie par <a href="mailto:mochileiro2006-trilhas@yahoo.com.br">Thiago Nazareno Conceição Silva de Jesus</a>.</li>
             <li>Traduction allemande mise à jour fournie par Bernhard G. Keller.</li>
             <li>Traduction italienne mise à jour fournie par Francesco Buratti.</li>
             <li>Traduction russe mise à jour.</li>
index 1d000e381e36697ff3778a8d77371e1e93f55d98..b9acb8c5b420bff373ee7f00dbf50bb46a34f3ad 100644 (file)
@@ -698,8 +698,7 @@ public class ImportExportActivity extends AppCompatActivity {
 
                         // Close the streams.
                         inputStream.close();
-                        temporaryPgpEncryptedImportFileOutputStream.flush();
-
+                        temporaryPgpEncryptedImportFileOutputStream.close();
 
                         // Create an decryption intent for OpenKeychain.
                         Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA");
index 6b2d7eb0d8965ff055bc69c128a2a1aad12c8c65..b5e3c53479a4b0924109fbc4521c4529cea6dcb4 100644 (file)
@@ -82,6 +82,7 @@ import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.webkit.WebViewDatabase;
 import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
 import android.widget.CursorAdapter;
 import android.widget.EditText;
 import android.widget.FrameLayout;
@@ -150,7 +151,11 @@ import com.stoutner.privacybrowser.views.NestedScrollWebView;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -1755,6 +1760,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
 
             // Consume the event.
+            return true;
+        } else if (menuItemId == R.id.save_archive) {
+            // Instantiate the save dialog.  TODO.  Replace the hard coded file name.
+            DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(SaveWebpageDialog.SAVE_ARCHIVE, null, null, "Webpage.mht", null,
+                    false);
+
+            // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
+            saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
+
             return true;
         } else if (menuItemId == R.id.save_image) {  // Save image.
             // Instantiate the save dialog.
@@ -1765,24 +1779,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             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.
@@ -3013,8 +3009,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Remove the incorrect lint warning below that the dialog might be null.
         assert dialog != null;
 
-        // Get a handle for the file name edit text.
+        // Get handles for the views.
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
+        CheckBox mhtCheckBox = dialog.findViewById(R.id.mht_checkbox);
 
         // Get the file path string.
         String openFilePath = fileNameEditText.getText().toString();
@@ -3022,8 +3019,46 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
         applyDomainSettings(currentWebView, openFilePath, true, false, false);
 
-        // Open the file.
-        currentWebView.loadUrl(openFilePath);
+        // Open the file according to the type.
+        if (mhtCheckBox.isChecked()) {  // Force opening of an MHT file.
+            try {
+                // Get the MHT file input stream.
+                InputStream mhtFileInputStream = getContentResolver().openInputStream(Uri.parse(openFilePath));
+
+                // Create a temporary MHT file.
+                File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
+
+                // Get a file output stream for the temporary MHT file.
+                FileOutputStream temporaryMhtFileOutputStream = new FileOutputStream(temporaryMhtFile);
+
+                // Create a transfer byte array.
+                byte[] transferByteArray = new byte[1024];
+
+                // Create an integer to track the number of bytes read.
+                int bytesRead;
+
+                // Copy the temporary MHT file input stream to the MHT output stream.
+                while ((bytesRead = mhtFileInputStream.read(transferByteArray)) > 0) {
+                    temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead);
+                }
+
+                // Flush the temporary MHT file output stream.
+                temporaryMhtFileOutputStream.flush();
+
+                // Close the streams.
+                temporaryMhtFileOutputStream.close();
+                mhtFileInputStream.close();
+
+                // Load the temporary MHT file.
+                currentWebView.loadUrl(temporaryMhtFile.toString());
+            } catch (Exception exception) {
+                // Display a snackbar.
+                Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
+            }
+        } else {  // Let the WebView handle opening of the file.
+            // Open the file.
+            currentWebView.loadUrl(openFilePath);
+        }
     }
 
     @Override
@@ -3062,6 +3097,57 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
                 break;
 
+            case SaveWebpageDialog.SAVE_ARCHIVE:
+                try {
+                    // Create a temporary MHT file.
+                    File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
+
+                    // Save the temporary MHT file.
+                    currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> {
+                        if (callbackValue != null) {  // The temporary MHT file was saved successfully.
+                            try {
+                                // Create a temporary MHT file input stream.
+                                FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile);
+
+                                // Get an output stream for the save webpage file path.
+                                OutputStream mhtOutputStream = getContentResolver().openOutputStream(Uri.parse(saveWebpageFilePath));
+
+                                // Create a transfer byte array.
+                                byte[] transferByteArray = new byte[1024];
+
+                                // Create an integer to track the number of bytes read.
+                                int bytesRead;
+
+                                // Copy the temporary MHT file input stream to the MHT output stream.
+                                while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) {
+                                    mhtOutputStream.write(transferByteArray, 0, bytesRead);
+                                }
+
+                                // Close the streams.
+                                mhtOutputStream.close();
+                                temporaryMhtFileInputStream.close();
+
+                                // Display a snackbar.
+                                Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + saveWebpageFilePath, Snackbar.LENGTH_SHORT).show();
+                            } catch (Exception exception) {
+                                // Display a snackbar with the exception.
+                                Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
+                            } finally {
+                                // Delete the temporary MHT file.
+                                //noinspection ResultOfMethodCallIgnored
+                                temporaryMhtFile.delete();
+                            }
+                        } else {  // There was an unspecified error while saving the temporary MHT file.
+                            // Display an error snackbar.
+                            Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show();
+                        }
+                    });
+                } catch (IOException ioException) {
+                    // Display a snackbar with the IO exception.
+                    Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + ioException.toString(), Snackbar.LENGTH_INDEFINITE).show();
+                }
+                break;
+
             case SaveWebpageDialog.SAVE_IMAGE:
                 // Save the webpage image.
                 new SaveWebpageImage(this, saveWebpageFilePath, currentWebView).execute();
@@ -5096,6 +5182,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Explicitly disable geolocation.
         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
 
+        // Allow loading of file:// URLs.  This is necessary for opening MHT web archives, which are copies into a temporary cache location.
+        nestedScrollWebView.getSettings().setAllowFileAccess(true);
+
         // Create a double-tap gesture detector to toggle full-screen mode.
         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
             // Override `onDoubleTap()`.  All other events are handled using the default settings.
index e62b6fa6624b4f0f0c3c5bd4842307c13f3eeccc..15e3be0945b3300910b5a558123a0ccdb183d626 100644 (file)
@@ -28,9 +28,12 @@ import android.content.res.Configuration
 import android.os.Bundle
 import android.text.Editable
 import android.text.TextWatcher
+import android.view.View
 import android.view.WindowManager
 import android.widget.Button
+import android.widget.CheckBox
 import android.widget.EditText
+import android.widget.TextView
 
 import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.DialogFragment
@@ -39,10 +42,16 @@ import androidx.preference.PreferenceManager
 import com.stoutner.privacybrowser.R
 import com.stoutner.privacybrowser.activities.MainWebViewActivity
 
+// Define the class constants.
+private const val MHT_EXPLANATION_VISIBILITY = "mht_explanation_visibility"
+
 class OpenDialog : DialogFragment() {
-    // Define the open listener.
+    // Declare the class variables.
     private lateinit var openListener: OpenListener
 
+    // Declare the class views.
+    private lateinit var mhtExplanationTextView: TextView
+
     // The public interface is used to send information back to the parent activity.
     interface OpenListener {
         fun onOpen(dialogFragment: DialogFragment)
@@ -107,6 +116,8 @@ class OpenDialog : DialogFragment() {
         // Get handles for the layout items.
         val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
+        val mhtCheckBox = alertDialog.findViewById<CheckBox>(R.id.mht_checkbox)!!
+        mhtExplanationTextView = alertDialog.findViewById(R.id.mht_explanation_textview)!!
         val openButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
 
         // Initially disable the open button.
@@ -146,7 +157,35 @@ class OpenDialog : DialogFragment() {
             requireActivity().startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_OPEN_REQUEST_CODE)
         }
 
+        // Handle clicks on the MHT checkbox.
+        mhtCheckBox.setOnClickListener {
+            // Update the visibility of the MHT explanation text view.
+            if (mhtCheckBox.isChecked) {
+                mhtExplanationTextView.visibility = View.VISIBLE
+            } else {
+                mhtExplanationTextView.visibility = View.GONE
+            }
+        }
+
+        // Restore the MHT explanation text view visibility if the saved instance state is not null.
+        if (savedInstanceState != null) {
+            // Restore the MHT explanation text view visibility.
+            if (savedInstanceState.getBoolean(MHT_EXPLANATION_VISIBILITY)) {
+                mhtExplanationTextView.visibility = View.VISIBLE
+            } else {
+                mhtExplanationTextView.visibility = View.GONE
+            }
+        }
+
         // Return the alert dialog.
         return alertDialog
     }
+
+    override fun onSaveInstanceState(savedInstanceState: Bundle) {
+        // Run the default commands.
+        super.onSaveInstanceState(savedInstanceState)
+
+        // Add the MHT explanation visibility status to the bundle.
+        savedInstanceState.putBoolean(MHT_EXPLANATION_VISIBILITY, mhtExplanationTextView.visibility == View.VISIBLE)
+    }
 }
\ No newline at end of file
index b27e1703b6537b04022560eada2122b9f4556f6d..782526d0f761ed94adc7877978f2717c5aa404a1 100644 (file)
@@ -77,7 +77,8 @@ class SaveWebpageDialog : DialogFragment() {
     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
+        const val SAVE_ARCHIVE = 1
+        const val SAVE_IMAGE = 2
 
         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
         @JvmStatic
@@ -135,6 +136,18 @@ class SaveWebpageDialog : DialogFragment() {
                 }
             }
 
+            SAVE_ARCHIVE -> {
+                // Set the title.
+                dialogBuilder.setTitle(R.string.save_archive)
+
+                // Set the icon according to the theme.
+                if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
+                    dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day)
+                } else {
+                    dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night)
+                }
+            }
+
             SAVE_IMAGE -> {
                 // Set the title.
                 dialogBuilder.setTitle(R.string.save_image)
index 054d200e0236e2320aa43f8ac87d57f868a4bcbf..2d450bc863c3074cc0921ee174c17b86a079be04 100644 (file)
@@ -40,6 +40,8 @@ import com.stoutner.privacybrowser.R
 
 // Define the class constants.
 private const val TAB_NUMBER = "tab_number"
+private const val SCROLL_X = "scroll_x"
+private const val SCROLL_Y = "scroll_y"
 
 class AboutWebViewFragment : Fragment() {
     // Define the class variables.
@@ -128,8 +130,8 @@ class AboutWebViewFragment : Fragment() {
         // Scroll the tab if the saved instance state is not null.
         if (savedInstanceState != null) {
             tabWebView.post {
-                tabWebView.scrollX = savedInstanceState.getInt("scroll_x")
-                tabWebView.scrollY = savedInstanceState.getInt("scroll_y")
+                tabWebView.scrollX = savedInstanceState.getInt(SCROLL_X)
+                tabWebView.scrollY = savedInstanceState.getInt(SCROLL_Y)
             }
         }
 
@@ -146,8 +148,8 @@ class AboutWebViewFragment : Fragment() {
 
         // Save the scroll positions if the layout is not null, which can happen if a tab is not currently selected.
         if (tabWebView != null) {
-            savedInstanceState.putInt("scroll_x", tabWebView.scrollX)
-            savedInstanceState.putInt("scroll_y", tabWebView.scrollY)
+            savedInstanceState.putInt(SCROLL_X, tabWebView.scrollX)
+            savedInstanceState.putInt(SCROLL_Y, tabWebView.scrollY)
         }
     }
 }
\ No newline at end of file
index a7c9d7d6c31d2635708a29772ba04fe4dd726c0d..33528645d1932e75ed527a9c0a35b6ed04731981 100644 (file)
     android:layout_height="wrap_content"
     android:layout_width="match_parent" >
 
-    <!-- Align the edit text and the select file button horizontally. -->
     <LinearLayout
         android:layout_height="wrap_content"
         android:layout_width="match_parent"
-        android:orientation="horizontal"
-        android:layout_marginTop="10dp"
-        android:layout_marginStart="10dp"
-        android:layout_marginEnd="10dp" >
+        android:orientation="vertical" >
 
-        <!-- The text input layout makes the `android:hint` float above the edit text. -->
-        <com.google.android.material.textfield.TextInputLayout
+        <!-- Align the edit text and the select file button horizontally. -->
+        <LinearLayout
             android:layout_height="wrap_content"
-            android:layout_width="0dp"
-            android:layout_weight="1" >
+            android:layout_width="match_parent"
+            android:orientation="horizontal"
+            android:layout_marginTop="10dp"
+            android:layout_marginStart="10dp"
+            android:layout_marginEnd="10dp" >
 
-            <!-- `android:inputType="textUri"` disables spell check and places an `/` on the main keyboard. -->
-            <com.google.android.material.textfield.TextInputEditText
-                android:id="@+id/file_name_edittext"
+            <!-- The text input layout makes the `android:hint` float above the edit text. -->
+            <com.google.android.material.textfield.TextInputLayout
                 android:layout_height="wrap_content"
-                android:layout_width="match_parent"
-                android:hint="@string/file_name"
-                android:inputType="textMultiLine|textUri" />
-        </com.google.android.material.textfield.TextInputLayout>
+                android:layout_width="0dp"
+                android:layout_weight="1" >
 
-        <Button
-            android:id="@+id/browse_button"
+                <!-- `android:inputType="textUri"` disables spell check and places an `/` on the main keyboard. -->
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/file_name_edittext"
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:hint="@string/file_name"
+                    android:inputType="textMultiLine|textUri" />
+            </com.google.android.material.textfield.TextInputLayout>
+
+            <Button
+                android:id="@+id/browse_button"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:text="@string/browse" />
+        </LinearLayout>
+
+        <CheckBox
+            android:id="@+id/mht_checkbox"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:text="@string/file_is_mht"
+            android:layout_marginStart="8dp" />
+
+        <TextView
+            android:id="@+id/mht_explanation_textview"
             android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_vertical"
-            android:text="@string/browse" />
+            android:layout_width="match_parent"
+            android:text="@string/mht_checkbox_explanation"
+            android:textColor="?android:textColorPrimary"
+            android:layout_marginStart="10dp"
+            android:layout_marginEnd="10dp"
+            android:layout_marginTop="8dp"
+            android:gravity="center_horizontal"
+            android:visibility="gone" />
     </LinearLayout>
 </ScrollView>
\ No newline at end of file
index 861ab8a05c9de5d1619506e896412758c8654018..d294e57bf673728e307be16bd8457047542dda28 100644 (file)
                         android:orderInCategory="1201"
                         app:showAsAction="never" />
 
-                    <!-- TODO. -->
                     <item
                         android:id="@+id/save_archive"
-                        android:title="Save Archive"
+                        android:title="@string/save_archive"
                         android:orderInCategory="1202"
                         app:showAsAction="never" />
 
index 28e4570fe5cace53a8ae3c3334063de638bf77f7..1214821586cd461dbe69963057dd38904a08c51e 100644 (file)
 
     <!-- Save Dialogs. -->
     <string name="save_url">URL speichern</string>
+    <string name="save_archive">Archiv speichern</string>
     <string name="save_text">Text speichern</string>
     <string name="save_image">Grafik speichern</string>
     <string name="save_logcat">Logcat speichern</string>
index a0b40afec1eb7c4dedcea1c301cdea7adc42bb6c..9698f3b89746fb80eb7776ba2edd91a47b01fcec 100644 (file)
 
     <!-- Save Dialogs. -->
     <string name="save_url">Guardar URL</string>
+    <string name="save_archive">Guardar archivo</string>
     <string name="save_text">Guardar texto</string>
     <string name="save_image">Guardar imagen</string>
     <string name="save_logcat">Guardar logcat</string>
index 662b2f7d1f9b48a57b9775c68430cf3b6890f2f0..388f6109250c4a857d7e434f62c907556ac75376 100644 (file)
 
     <!-- Save Dialogs. -->
     <string name="save_url">Enregistrer l\'URL</string>
+    <string name="save_archive">Enregistrer l\'archive</string>
     <string name="save_text">Sauvegarder le texte</string>
     <string name="save_image">Sauvegarder en tant qu\'image</string>
     <string name="save_logcat">Sauvegarder le journal système</string>
     <string name="bytes">octets</string>
     <string name="unknown_size">taille inconnue</string>
     <string name="invalid_url">URL invalide</string>
-    <string name="saving_file">Enregistrement du fichier:</string>
-    <string name="file_saved">Fichier enregistré:</string>
+    <string name="saving_file">Enregistrement du fichier :</string>
+    <string name="file_saved">Fichier enregistré :</string>
     <string name="processing_image">Traitement de l\'image… :</string>
-    <string name="error_saving_file">Erreur lors de l\'enregistrement du fichier:</string>
+    <string name="image_saved">Image sauvegardée :</string>
+    <string name="error_saving_file">Erreur lors de l\'enregistrement du fichier :</string>
 
     <!-- View Source. -->
     <string name="request_headers">En-tête de la requête</string>
index de06b0be93f56cbd6e46d9239299018f8d7dba32..d825deb973c46f940c2067d46d95e774ff657e15 100644 (file)
 
     <!-- Save Dialogs. -->
     <string name="save_url">Salva URL</string>
+    <string name="save_archive">Salva Archivio</string>
     <string name="save_text">Salva Testo</string>
     <string name="save_image">Salva Immagine</string>
     <string name="save_logcat">Salva il log</string>
index 378d0857018971948cdca4fb36d84a179301e7c1..7d2d37cef1d5748dda72014ee44e4efb670e065f 100644 (file)
 
     <!-- Save Dialogs. -->
     <string name="save_url">Salvar URL</string>
+    <string name="save_archive">Salvar Arquivo</string>
     <string name="save_text">Salvar Texto</string>
     <string name="save_image">Salvar Imagem</string>
     <string name="save_logcat">Salvar logcat</string>
index 7864eda1a9095cc7092473081c6aaa19c0241968..b4544da92074c5cba361b449c78e49333c9ba4fb 100644 (file)
 
     <!-- Save Dialogs. -->
     <string name="save_url">Сохранить URL</string>
+    <string name="save_archive">Сохранить архив</string>
     <string name="save_text">Сохранить текст</string>
     <string name="save_image">Сохранить изображение</string>
     <string name="save_logcat">Сохранить logcat</string>
index 614121348c0122141a3decaf0ddbfeb5c2d51b27..a8345b5d186628a08e6798e6e230d308e7a766bf 100644 (file)
     <string name="previous">Previous</string>
     <string name="next">Next</string>
 
+    <!-- Open Dialog. -->
+    <string name="file_is_mht">The file is an MHT web archive.</string>
+    <string name="mht_checkbox_explanation">Sometimes MIME Encapsulated HTML (MHT) web archives need to be manually specified to be opened correctly.</string>
+
     <!-- Save Dialogs. -->
     <string name="save_dialog" translatable="false">Save Dialog</string>  <!-- This string is used to tag the save dialog.  It is never displayed to the user. -->
     <string name="save_url">Save URL</string>
+    <string name="save_archive">Save Archive</string>
     <string name="save_text">Save Text</string>
     <string name="save_image">Save Image</string>
     <string name="save_logcat">Save Logcat</string>
             <item>WebView default user agent</item>  <!-- This item must not be translated into other languages because it is referenced in code.  It is never displayed on the screen. -->
             <item>Mozilla/5.0 (Android 11; Mobile; rv:87.0) Gecko/87.0 Firefox/87.0</item>
             <item>Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36</item>
-            <item>Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1</item>
+            <item>Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1</item>
             <item>Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0</item>
             <item>Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36</item>
             <item>Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0</item>
             <item>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36</item>
             <item>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36 Edg/89.0.774.63</item>
             <item>Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko</item>
-            <item>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15</item>
+            <item>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15</item>
             <item>Custom user agent</item>  <!-- This item must not be translated into other languages because it is referenced in code.  It is never displayed on the screen. -->
         </string-array>
         <string name="custom_user_agent">Custom user agent</string>
index 6fb0864978f7ea049459011e4534e65cdc9485f4..cd5f1068844de250602a05c81bd346452d6dfb33 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2018 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>.
 
     <cache-path
         name="private-cache-directory"
         path="." />
-    
-    <external-files-path
-        name="external-app-directories"
-        path="." />
-
-    <external-path
-        name="external-directories"
-        path="." />
 </paths>
\ No newline at end of file
index da4eeda0f8f2c3427dca60f7608b1dea7aebcdf0..4d57ffb284f32fd4f64bdbc42fbba1cfd5d6c1f0 100644 (file)
@@ -1,16 +1,16 @@
-• Redesign file access to work with scoped storage and the Storage Access Framework. 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. Unfortunately, due to a bug in Android’s WebView, this also temporarily removes the ability to save a web archive.
-• Update About > Permissions.
-• Improve the descriptiveness of the save URL snackbar.
-• Add Metager to the list of search engines.
-• Fix I2P detection.
-• Fix the alignment of icons and radio buttons in the dialogs.
-• Update the URL bar when switching tabs even if it is being edited.
-• Allow displaying of the password in the HTTP authentication dialog.
-• Fix a number of rare crashes.
-• Fix the hamburger icon turning into an arrow if the drawer is open when the app is restarted.
-• Speed up the opening of the options menu.
+• Reconception de l'accès aux fichiers pour travailler avec le stockage cible et le Storage Access Framework. Cela permet de faire passer l'API cible à 30 et de supprimer la nécessité des permissions dangereuses READ_EXTERNAL_STORAGE et WRITE_EXTERNAL_STORAGE. Malheureusement, en raison d'un bogue dans la WebView d'Android, cela supprime aussi temporairement la possibilité d'enregistrer une archive web.
+• Mise à jour des A propos de > permissions.
+• Amélioration de la description de la bare d'enregistrement d'URL.
+• Ajout de Metager à la liste des moteurs de recherche.
+• Correction de détection I2P.
+• Correction de l'alignement des icônes et boutons radio dans les boîtes de dialogue.
+• Mise à jour de la barre d'URL lors du changement d'onglet même s'il est en cours d'édition.
+• Autorisation de l'affichage du mot de passe dans le dialogue d'authentification HTTP.
+• Correction d'un nombre de rare crash.
+• Correction de l'icône du hamburger se transformant en flèche si le tiroir est ouvert lors du redémarrage de l'app.
+• Accélération de l'ouverture du menu des options.
 • Traduction française mise à jour fournie par Kévin L.
-• Updated Brazilian Portuguese translation provided by Thiago Nazareno Conceição Silva de Jesus.
+• Traduction portugaise brésilienne mise à jour fournie par Thiago Nazareno Conceição Silva de Jesus.
 • Traduction allemande mise à jour fournie par Bernhard G. Keller.
 • Traduction italienne mise à jour fournie par Francesco Buratti.
 • Traduction russe mise à jour.