]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/commitdiff
Migrate five classes to Kotlin. https://redmine.stoutner.com/issues/950
authorSoren Stoutner <soren@stoutner.com>
Wed, 22 Mar 2023 21:24:47 +0000 (14:24 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 22 Mar 2023 21:24:47 +0000 (14:24 -0700)
24 files changed:
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.java [deleted file]
app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt
app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt
app/src/main/java/com/stoutner/privacybrowser/fragments/WebViewTabFragment.kt
app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt
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-tr/strings.xml
app/src/main/res/values-zh-rCN/strings.xml
app/src/main/res/values/strings.xml

index 39b95dca65d30d4ab5f35a3b31b1d47fd6969873..f2bf75366c0288d993e970a2615f7b623e54d990 100644 (file)
@@ -127,11 +127,11 @@ import com.google.android.material.tabs.TabLayout;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
-import com.stoutner.privacybrowser.asynctasks.SaveUrl;
-import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
 import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine;
 import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine;
 import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine;
+import com.stoutner.privacybrowser.coroutines.SaveUrlCoroutine;
+import com.stoutner.privacybrowser.coroutines.SaveWebpageImageCoroutine;
 import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
@@ -363,7 +363,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 public void onActivityResult(Uri fileUri) {
                     // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
                     if (fileUri != null) {
-                        new SaveUrl(getApplicationContext(), resultLauncherActivityHandle, fileUri, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveUrlString);
+                        // Instantiate the save URL coroutine.
+                        SaveUrlCoroutine saveUrlCoroutine = new SaveUrlCoroutine();
+
+                        // Save the URL.
+                        saveUrlCoroutine.save(getApplicationContext(), resultLauncherActivityHandle, saveUrlString, fileUri, currentWebView.getSettings().getUserAgentString(),
+                                currentWebView.getAcceptCookies());
                     }
 
                     // Reset the save URL string.
@@ -456,8 +461,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 public void onActivityResult(Uri fileUri) {
                     // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
                     if (fileUri != null) {
+                        // Instantiate the save webpage image coroutine.
+                        SaveWebpageImageCoroutine saveWebpageImageCoroutine = new SaveWebpageImageCoroutine();
+
                         // Save the webpage image.
-                        new SaveWebpageImage(resultLauncherActivityHandle, fileUri, currentWebView).execute();
+                        saveWebpageImageCoroutine.save(resultLauncherActivityHandle, fileUri, currentWebView);
                     }
                 }
             });
diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java b/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java
deleted file mode 100644 (file)
index d253307..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2018-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.adapters;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.helpers.BlocklistHelper;
-
-import java.util.List;
-
-public class RequestsArrayAdapter extends ArrayAdapter<String[]> {
-    public RequestsArrayAdapter(Context context, List<String[]> resourceRequestsList) {
-        // `super` must be called form the base ArrayAdapter.  `0` is the `textViewResourceId`, which is unused.
-        super(context, 0, resourceRequestsList);
-    }
-
-    @Override
-    @NonNull
-    public View getView(int position, View view, @NonNull ViewGroup parent) {
-        // Get a handle for the context.
-        Context context = getContext();
-
-        // Inflate the view if it is null.
-        if (view == null) {
-            view = LayoutInflater.from(context).inflate(R.layout.requests_item_linearlayout, parent, false);
-        }
-
-        // Get handles for the views.
-        LinearLayout linearLayout = view.findViewById(R.id.request_item_linearlayout);
-        TextView dispositionTextView = view.findViewById(R.id.request_item_disposition);
-        TextView urlTextView = view.findViewById(R.id.request_item_url);
-
-        // Get the string array for this entry.
-        String[] entryStringArray = getItem(position);
-
-        // Remove the lint warning below that `entryStringArray` might be null.
-        assert entryStringArray != null;
-
-        // The ID is one greater than the position because it is 0 based.
-        int id = position + 1;
-
-        // Set the action text and the background color.
-        switch (entryStringArray[0]) {
-            case BlocklistHelper.REQUEST_DEFAULT:
-                // Create the disposition string.
-                String requestDefault = id + ". " + context.getResources().getString(R.string.allowed);
-
-                // Set the disposition text.
-                dispositionTextView.setText(requestDefault);
-
-                // Set the background color.
-                linearLayout.setBackgroundColor(context.getColor(R.color.transparent));
-                break;
-
-            case BlocklistHelper.REQUEST_ALLOWED:
-                // Create the disposition string.
-                String requestAllowed = id + ". " + context.getResources().getString(R.string.allowed);
-
-                // Set the disposition text.
-                dispositionTextView.setText(requestAllowed);
-
-                // Set the background color.
-                linearLayout.setBackgroundColor(context.getColor(R.color.blue_background));
-                break;
-
-            case BlocklistHelper.REQUEST_THIRD_PARTY:
-                // Create the disposition string.
-                String requestThirdParty = id + ". " + context.getResources().getString(R.string.blocked);
-
-                // Set the disposition text.
-                dispositionTextView.setText(requestThirdParty);
-
-                // Set the background color.
-                linearLayout.setBackgroundColor(context.getColor(R.color.yellow_background));
-                break;
-
-
-            case BlocklistHelper.REQUEST_BLOCKED:
-                // Create the disposition string.
-                String requestBlocked = id + ". " + context.getResources().getString(R.string.blocked);
-
-                // Set the disposition text.
-                dispositionTextView.setText(requestBlocked);
-
-                // Set the background color.
-                linearLayout.setBackgroundColor(context.getColor(R.color.red_background));
-                break;
-        }
-
-        // Set the URL text.
-        urlTextView.setText(entryStringArray[1]);
-
-        // Return the modified view.
-        return view;
-    }
-}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt b/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt
new file mode 100644 (file)
index 0000000..f79ab07
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.LinearLayout
+import android.widget.TextView
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.helpers.BlocklistHelper
+
+// `0` is the `textViewResourceId`, which is unused in this implementation.
+class RequestsArrayAdapter(context: Context, resourceRequestsList: List<Array<String>>) : ArrayAdapter<Array<String>>(context, 0, resourceRequestsList) {
+    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
+        // Copy the input view to a new view..
+        var newView = view
+
+        // Inflate the new view if it is null.
+        if (newView == null) {
+            newView = LayoutInflater.from(context).inflate(R.layout.requests_item_linearlayout, parent, false)
+        }
+
+        // Get handles for the views.
+        val linearLayout = newView!!.findViewById<LinearLayout>(R.id.request_item_linearlayout)
+        val dispositionTextView = newView.findViewById<TextView>(R.id.request_item_disposition)
+        val urlTextView = newView.findViewById<TextView>(R.id.request_item_url)
+
+        // Get the string array for this entry.
+        val entryStringArray = getItem(position)!!
+
+        // The ID is one greater than the position because it is 0 based.
+        val id = position + 1
+
+        // Set the action text and the background color.
+        when (entryStringArray[0]) {
+            BlocklistHelper.REQUEST_DEFAULT -> {
+                // Set the disposition text.
+                dispositionTextView.text = context.resources.getString(R.string.request_allowed, id)
+
+                // Set the background color.
+                linearLayout.setBackgroundColor(context.getColor(R.color.transparent))
+            }
+
+            BlocklistHelper.REQUEST_ALLOWED -> {
+                // Set the disposition text.
+                dispositionTextView.text = context.resources.getString(R.string.request_allowed, id)
+
+                // Set the background color.
+                linearLayout.setBackgroundColor(context.getColor(R.color.blue_background))
+            }
+
+            BlocklistHelper.REQUEST_THIRD_PARTY -> {
+                // Set the disposition text.
+                dispositionTextView.text = context.resources.getString(R.string.request_blocked, id)
+
+                // Set the background color.
+                linearLayout.setBackgroundColor(context.getColor(R.color.yellow_background))
+            }
+
+            BlocklistHelper.REQUEST_BLOCKED -> {
+                // Set the disposition text.
+                dispositionTextView.text = context.resources.getString(R.string.request_blocked, id)
+
+                // Set the background color.
+                linearLayout.setBackgroundColor(context.getColor(R.color.red_background))
+            }
+        }
+
+        // Set the URL text.
+        urlTextView.text = entryStringArray[1]
+
+        // Return the modified view.
+        return newView
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java
deleted file mode 100644 (file)
index 52e7234..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright © 2019-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.adapters;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentPagerAdapter;
-import androidx.viewpager.widget.ViewPager;
-
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
-import com.stoutner.privacybrowser.views.NestedScrollWebView;
-
-import java.util.LinkedList;
-
-public class WebViewPagerAdapter extends FragmentPagerAdapter {
-    // The WebView fragments list contains all the WebViews.
-    private final LinkedList<WebViewTabFragment> webViewFragmentsList = new LinkedList<>();
-
-    // Define the constructor.
-    public WebViewPagerAdapter(FragmentManager fragmentManager) {
-        // Run the default commands.
-        super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
-    }
-
-    @Override
-    public int getCount() {
-        // Return the number of pages.
-        return webViewFragmentsList.size();
-    }
-
-    @Override
-    public int getItemPosition(@NonNull Object object) {
-        //noinspection SuspiciousMethodCalls
-        if (webViewFragmentsList.contains(object)) {
-            // Return the current page position.
-            //noinspection SuspiciousMethodCalls
-            return webViewFragmentsList.indexOf(object);
-        } else {
-            // The tab has been deleted.
-            return POSITION_NONE;
-        }
-    }
-
-    @Override
-    @NonNull
-    public Fragment getItem(int pageNumber) {
-        // Get the fragment for a particular page.  Page numbers are 0 indexed.
-        return webViewFragmentsList.get(pageNumber);
-    }
-
-    @Override
-    public long getItemId(int position) {
-        // Return the unique ID for this page.
-        return webViewFragmentsList.get(position).fragmentId;
-    }
-
-    public int getPositionForId(long fragmentId) {
-        // Initialize the position variable.
-        int position = -1;
-
-        // Initialize the while counter.
-        int i = 0;
-
-        // Find the current position of the WebView fragment with the given ID.
-        while (position < 0 && i < webViewFragmentsList.size()) {
-            // Check to see if the tab ID of this WebView matches the page ID.
-            if (webViewFragmentsList.get(i).fragmentId == fragmentId) {
-                // Store the position if they are a match.
-                position = i;
-            }
-
-            // Increment the counter.
-            i++;
-        }
-
-        // Set the position to be the last tab if it is not found.
-        // Sometimes there is a race condition in populating the webView fragments list when resuming Privacy Browser and displaying an SSL certificate error while loading a new intent.
-        // In that case, the last tab should be the one it is looking for.
-        if (position == -1) {
-            position = webViewFragmentsList.size() - 1;
-        }
-
-        // Return the position.
-        return position;
-    }
-
-    public void addPage(int pageNumber, ViewPager webViewPager, String url, boolean moveToNewPage) {
-        // Add a new page.
-        webViewFragmentsList.add(WebViewTabFragment.createPage(pageNumber, url));
-
-        // Update the view pager.
-        notifyDataSetChanged();
-
-        // Move to the new page if indicated.
-        if (moveToNewPage) {
-            moveToNewPage(pageNumber, webViewPager);
-        }
-    }
-
-    public void restorePage(Bundle savedState, Bundle savedNestedScrollWebViewState) {
-        // Restore the page.
-        webViewFragmentsList.add(WebViewTabFragment.restorePage(savedState, savedNestedScrollWebViewState));
-
-        // Update the view pager.
-        notifyDataSetChanged();
-    }
-
-    public boolean deletePage(int pageNumber, ViewPager webViewPager) {
-        // Get the WebView tab fragment.
-        WebViewTabFragment webViewTabFragment = webViewFragmentsList.get(pageNumber);
-
-        // Get the WebView frame layout.
-        FrameLayout webViewFrameLayout = (FrameLayout) webViewTabFragment.getView();
-
-        // Remove the warning below that the WebView frame layout might be null.
-        assert webViewFrameLayout != null;
-
-        // Get a handle for the nested scroll WebView.
-        NestedScrollWebView nestedScrollWebView = webViewFrameLayout.findViewById(R.id.nestedscroll_webview);
-
-        // Pause the current WebView.
-        nestedScrollWebView.onPause();
-
-        // Remove all the views from the frame layout.
-        webViewFrameLayout.removeAllViews();
-
-        // Destroy the current WebView.
-        nestedScrollWebView.destroy();
-
-        // Delete the page.
-        webViewFragmentsList.remove(pageNumber);
-
-        // Update the view pager.
-        notifyDataSetChanged();
-
-        // Return true if the selected page number did not change after the delete (because the newly selected tab has has same number as the previously deleted tab).
-        // This will cause the calling method to reset the current WebView to the new contents of this page number.
-        return (webViewPager.getCurrentItem() == pageNumber);
-    }
-
-    public WebViewTabFragment getPageFragment(int pageNumber) {
-        // Return the page fragment.
-        return webViewFragmentsList.get(pageNumber);
-    }
-
-    private void moveToNewPage(int pageNumber, ViewPager webViewPager) {
-        // Check to see if the new page has been populated.
-        if (webViewPager.getChildCount() >= pageNumber) {  // The new page is ready.
-            // Move to the new page.
-            webViewPager.setCurrentItem(pageNumber);
-        } else {  // The new page is not yet ready.
-            // Create a handler.
-            Handler moveToNewPageHandler = new Handler();
-
-            // Create a runnable.
-            Runnable moveToNewPageRunnable = () -> {
-                // Move to the new page.
-                webViewPager.setCurrentItem(pageNumber);
-            };
-
-            // Try again to move to the new page after 50 milliseconds.
-            moveToNewPageHandler.postDelayed(moveToNewPageRunnable, 50);
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt
new file mode 100644 (file)
index 0000000..6aef0cd
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Copyright © 2019-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.adapters
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.widget.FrameLayout
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentPagerAdapter
+import androidx.viewpager.widget.ViewPager
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.fragments.WebViewTabFragment
+import com.stoutner.privacybrowser.fragments.WebViewTabFragment.Companion.createPage
+import com.stoutner.privacybrowser.views.NestedScrollWebView
+
+import java.util.LinkedList
+
+class WebViewPagerAdapter(fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
+    // Define the class values.
+    private val webViewFragmentsList = LinkedList<WebViewTabFragment>()
+
+    override fun getCount(): Int {
+        // Return the number of pages.
+        return webViewFragmentsList.size
+    }
+
+    override fun getItem(pageNumber: Int): Fragment {
+        // Get the fragment for a particular page.  Page numbers are 0 indexed.
+        return webViewFragmentsList[pageNumber]
+    }
+
+    override fun getItemId(position: Int): Long {
+        // Return the unique ID for this page.
+        return webViewFragmentsList[position].fragmentId
+    }
+
+    override fun getItemPosition(`object`: Any): Int {
+        return if (webViewFragmentsList.contains(`object`)) {
+            // Return the current page position.
+            webViewFragmentsList.indexOf(`object`)
+        } else {
+            // The tab has been deleted.
+            POSITION_NONE
+        }
+    }
+
+    fun addPage(pageNumber: Int, webViewPager: ViewPager, url: String, moveToNewPage: Boolean) {
+        // Add a new page.
+        webViewFragmentsList.add(createPage(pageNumber, url))
+
+        // Update the view pager.
+        notifyDataSetChanged()
+
+        // Move to the new page if indicated.
+        if (moveToNewPage) {
+            moveToNewPage(pageNumber, webViewPager)
+        }
+    }
+
+    fun deletePage(pageNumber: Int, webViewPager: ViewPager): Boolean {
+        // Get the WebView tab fragment.
+        val webViewTabFragment = webViewFragmentsList[pageNumber]
+
+        // Get the WebView frame layout.
+        val webViewFrameLayout = (webViewTabFragment.view as FrameLayout?)!!
+
+        // Get a handle for the nested scroll WebView.
+        val nestedScrollWebView = webViewFrameLayout.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
+
+        // Pause the current WebView.
+        nestedScrollWebView.onPause()
+
+        // Remove all the views from the frame layout.
+        webViewFrameLayout.removeAllViews()
+
+        // Destroy the current WebView.
+        nestedScrollWebView.destroy()
+
+        // Delete the page.
+        webViewFragmentsList.removeAt(pageNumber)
+
+        // Update the view pager.
+        notifyDataSetChanged()
+
+        // Return true if the selected page number did not change after the delete (because the newly selected tab has has same number as the previously deleted tab).
+        // This will cause the calling method to reset the current WebView to the new contents of this page number.
+        return webViewPager.currentItem == pageNumber
+    }
+
+    fun getPageFragment(pageNumber: Int): WebViewTabFragment {
+        // Return the page fragment.
+        return webViewFragmentsList[pageNumber]
+    }
+
+    fun getPositionForId(fragmentId: Long): Int {
+        // Initialize the position variable.
+        var position = -1
+
+        // Initialize the while counter.
+        var i = 0
+
+        // Find the current position of the WebView fragment with the given ID.
+        while (position < 0 && i < webViewFragmentsList.size) {
+            // Check to see if the tab ID of this WebView matches the page ID.
+            if (webViewFragmentsList[i].fragmentId == fragmentId) {
+                // Store the position if they are a match.
+                position = i
+            }
+
+            // Increment the counter.
+            i++
+        }
+
+        // Set the position to be the last tab if it is not found.
+        // Sometimes there is a race condition in populating the webView fragments list when resuming Privacy Browser and displaying an SSL certificate error while loading a new intent.
+        // In that case, the last tab should be the one it is looking for.
+        if (position == -1) {
+            position = webViewFragmentsList.size - 1
+        }
+
+        // Return the position.
+        return position
+    }
+
+    fun restorePage(savedState: Bundle, savedNestedScrollWebViewState: Bundle) {
+        // Restore the page.
+        webViewFragmentsList.add(WebViewTabFragment.restorePage(savedState, savedNestedScrollWebViewState))
+
+        // Update the view pager.
+        notifyDataSetChanged()
+    }
+
+    private fun moveToNewPage(pageNumber: Int, webViewPager: ViewPager) {
+        // Check to see if the new page has been populated.
+        if (webViewPager.childCount >= pageNumber) {  // The new page is ready.
+            // Move to the new page.
+            webViewPager.currentItem = pageNumber
+        } else {  // The new page is not yet ready.
+            // Create a handler.
+            val moveToNewPageHandler = Handler(Looper.getMainLooper())
+
+            // Create a runnable.
+            val moveToNewPageRunnable = Runnable {
+                // Move to the new page.
+                webViewPager.currentItem = pageNumber
+            }
+
+            // Try again to move to the new page after 50 milliseconds.
+            moveToNewPageHandler.postDelayed(moveToNewPageRunnable, 50)
+        }
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java
deleted file mode 100644 (file)
index 029929d..0000000
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright 2020-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.asynctasks;
-
-import android.app.Activity;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.provider.OpenableColumns;
-import android.util.Base64;
-import android.webkit.CookieManager;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.helpers.ProxyHelper;
-import com.stoutner.privacybrowser.views.NoSwipeViewPager;
-
-import java.io.BufferedInputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.lang.ref.WeakReference;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.URL;
-import java.text.NumberFormat;
-
-public class SaveUrl extends AsyncTask<String, Long, String> {
-    // Declare the weak references.
-    private final WeakReference<Context> contextWeakReference;
-    private final WeakReference<Activity> activityWeakReference;
-
-    // Define a success string constant.
-    private final String SUCCESS = "Success";
-
-    // Declare the class variables.
-    private final Uri fileUri;
-    private final String userAgent;
-    private final boolean cookiesEnabled;
-    private Snackbar savingFileSnackbar;
-    private long fileSize;
-    private String formattedFileSize;
-    private final String fileNameString;
-
-    // The public constructor.
-    public SaveUrl(Context context, Activity activity, Uri fileUri, String userAgent, boolean cookiesEnabled) {
-        // Populate weak references to the calling context and activity.
-        contextWeakReference = new WeakReference<>(context);
-        activityWeakReference = new WeakReference<>(activity);
-
-        // Store the class variables.
-        this.fileUri = fileUri;
-        this.userAgent = userAgent;
-        this.cookiesEnabled = cookiesEnabled;
-
-        // Query the exact file name if the API >= 26.
-        if (Build.VERSION.SDK_INT >= 26) {
-            // Get a cursor from the content resolver.
-            Cursor contentResolverCursor = activity.getContentResolver().query(fileUri, null, null, null);
-
-            // Move to the first row.
-            contentResolverCursor.moveToFirst();
-
-            // Get the file name from the cursor.
-            fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
-
-            // Close the cursor.
-            contentResolverCursor.close();
-        } else {
-            // Use the file URI last path segment as the file name string.
-            fileNameString = fileUri.getLastPathSegment();
-        }
-    }
-
-    // `onPreExecute()` operates on the UI thread.
-    @Override
-    protected void onPreExecute() {
-        // Get a handle for the activity.
-        Activity activity = activityWeakReference.get();
-
-        // Abort if the activity is gone.
-        if ((activity==null) || activity.isFinishing()) {
-            return;
-        }
-
-        // Get a handle for the no swipe view pager.
-        NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
-
-        // Create a saving file snackbar.
-        savingFileSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.saving_file) + "  0%  -  " + fileNameString, Snackbar.LENGTH_INDEFINITE);
-
-        // Display the saving file snackbar.
-        savingFileSnackbar.show();
-    }
-
-    @Override
-    protected String doInBackground(String... urlToSave) {
-        // Get handles for the context and activity.
-        Context context = contextWeakReference.get();
-        Activity activity = activityWeakReference.get();
-
-        // Abort if the activity is gone.
-        if ((activity == null) || activity.isFinishing()) {
-            return null;
-        }
-
-        // Define a save disposition string.
-        String saveDisposition = SUCCESS;
-
-        // Get the URL string.
-        String urlString = urlToSave[0];
-
-        try {
-            // Open an output stream.
-            OutputStream outputStream = activity.getContentResolver().openOutputStream(fileUri);
-
-            // Save the URL.
-            if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
-                // Get the Base64 data, which begins after a `,`.
-                String base64DataString = urlString.substring(urlString.indexOf(",") + 1);
-
-                // Decode the Base64 string to a byte array.
-                byte[] base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT);
-
-                // Write the Base64 byte array to the output stream.
-                outputStream.write(base64DecodedDataByteArray);
-            } else {  // The URL points to the data location on the internet.
-                // Get the URL from the calling activity.
-                URL url = new URL(urlString);
-
-                // Instantiate the proxy helper.
-                ProxyHelper proxyHelper = new ProxyHelper();
-
-                // Get the current proxy.
-                Proxy proxy = proxyHelper.getCurrentProxy(context);
-
-                // Open a connection to the URL.  No data is actually sent at this point.
-                HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
-
-                // Add the user agent to the header property.
-                httpUrlConnection.setRequestProperty("User-Agent", userAgent);
-
-                // Add the cookies if they are enabled.
-                if (cookiesEnabled) {
-                    // Get the cookies for the current domain.
-                    String cookiesString = CookieManager.getInstance().getCookie(url.toString());
-
-                    // Only add the cookies if they are not null.
-                    if (cookiesString != null) {
-                        // Add the cookies to the header property.
-                        httpUrlConnection.setRequestProperty("Cookie", cookiesString);
-                    }
-                }
-
-                // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
-                try {
-                    // Get the content length header, which causes the connection to the server to be made.
-                    String contentLengthString = httpUrlConnection.getHeaderField("Content-Length");
-
-                    // Make sure the content length isn't null.
-                    if (contentLengthString != null) {  // The content length isn't null.
-                        // Convert the content length to an long.
-                        fileSize = Long.parseLong(contentLengthString);
-
-                        // Format the file size for display.
-                        formattedFileSize = NumberFormat.getInstance().format(fileSize);
-                    } else {  // The content length is null.
-                        // Set the file size to be `-1`.
-                        fileSize = -1;
-                    }
-
-                    // Get the response body stream.
-                    InputStream inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
-
-                    // Initialize the conversion buffer byte array.
-                    // This is set to a megabyte so that frequent updating of the snackbar doesn't freeze the interface on download.  <https://redmine.stoutner.com/issues/709>
-                    byte[] conversionBufferByteArray = new byte[1048576];
-
-                    // Initialize the downloaded kilobytes counter.
-                    long downloadedKilobytesCounter = 0;
-
-                    // Define the buffer length variable.
-                    int bufferLength;
-
-                    // Attempt to read data from the input stream and store it in the output stream.  Also store the amount of data read in the buffer length variable.
-                    while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) {  // Proceed while the amount of data stored in the buffer in > 0.
-                        // Write the contents of the conversion buffer to the file output stream.
-                        outputStream.write(conversionBufferByteArray, 0, bufferLength);
-
-                        // Update the downloaded kilobytes counter.
-                        downloadedKilobytesCounter = downloadedKilobytesCounter + bufferLength;
-
-                        // Update the file download progress snackbar.
-                        publishProgress(downloadedKilobytesCounter);
-                    }
-
-                    // Close the input stream.
-                    inputStream.close();
-                } finally {
-                    // Disconnect the HTTP URL connection.
-                    httpUrlConnection.disconnect();
-                }
-            }
-
-            // Close the output stream.
-            outputStream.close();
-        } catch (Exception exception) {
-            // Store the error in the save disposition string.
-            saveDisposition = exception.toString();
-        }
-
-        // Return the save disposition string.
-        return saveDisposition;
-    }
-
-    // `onProgressUpdate()` operates on the UI thread.
-    @Override
-    protected void onProgressUpdate(Long... numberOfBytesDownloaded) {
-        // Get a handle for the activity.
-        Activity activity = activityWeakReference.get();
-
-        // Abort if the activity is gone.
-        if ((activity == null) || activity.isFinishing()) {
-            return;
-        }
-
-        // Format the number of bytes downloaded.
-        String formattedNumberOfBytesDownloaded = NumberFormat.getInstance().format(numberOfBytesDownloaded[0]);
-
-        // Check to see if the file size is known.
-        if (fileSize == -1) {  // The size of the download file is not known.
-            // Update the snackbar.
-            savingFileSnackbar.setText(activity.getString(R.string.saving_file) + "  " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + "  -  " + fileNameString);
-        } else {  // The size of the download file is known.
-            // Calculate the download percentage.
-            long downloadPercentage = (numberOfBytesDownloaded[0] * 100) / fileSize;
-
-            // Update the snackbar.
-            savingFileSnackbar.setText(activity.getString(R.string.saving_file) + "  " + downloadPercentage + "%  -  " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " / " +
-                    formattedFileSize + " " + activity.getString(R.string.bytes) + "  -  " + fileNameString);
-        }
-    }
-
-    // `onPostExecute()` operates on the UI thread.
-    @Override
-    protected void onPostExecute(String saveDisposition) {
-        // Get handles for the context and activity.
-        Activity activity = activityWeakReference.get();
-
-        // Abort if the activity is gone.
-        if ((activity == null) || activity.isFinishing()) {
-            return;
-        }
-
-        // Get a handle for the no swipe view pager.
-        NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager);
-
-        // Dismiss the saving file snackbar.
-        savingFileSnackbar.dismiss();
-
-        // Display a save disposition snackbar.
-        if (saveDisposition.equals(SUCCESS)) {
-            // Display the file saved snackbar.
-            Snackbar.make(noSwipeViewPager, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show();
-        } else {
-            // Display the file saving error.
-            Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file, fileNameString, saveDisposition), Snackbar.LENGTH_INDEFINITE).show();
-        }
-    }
-}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java
deleted file mode 100644 (file)
index 5a92123..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright0 2019-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.asynctasks;
-
-import android.app.Activity;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.provider.OpenableColumns;
-
-import com.google.android.material.snackbar.Snackbar;
-
-import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.views.NestedScrollWebView;
-
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
-import java.lang.ref.WeakReference;
-
-public class SaveWebpageImage extends AsyncTask<Void, Void, String> {
-    // Declare the weak references.
-    private final WeakReference<Activity> activityWeakReference;
-    private final WeakReference<NestedScrollWebView> nestedScrollWebViewWeakReference;
-
-    // Declare the class constants.
-    private final String SUCCESS = "Success";
-
-    // Declare the class variables.
-    private Snackbar savingImageSnackbar;
-    private Bitmap webpageBitmap;
-    private final Uri fileUri;
-    private final String fileNameString;
-
-    // The public constructor.
-    public SaveWebpageImage(Activity activity, Uri fileUri, NestedScrollWebView nestedScrollWebView) {
-        // Populate the weak references.
-        activityWeakReference = new WeakReference<>(activity);
-        nestedScrollWebViewWeakReference = new WeakReference<>(nestedScrollWebView);
-
-        // Populate the class variables.
-        this.fileUri = fileUri;
-
-        // Query the exact file name if the API >= 26.
-        if (Build.VERSION.SDK_INT >= 26) {
-            // Get a cursor from the content resolver.
-            Cursor contentResolverCursor = activity.getContentResolver().query(fileUri, null, null, null);
-
-            // Move to the first row.
-            contentResolverCursor.moveToFirst();
-
-            // Get the file name from the cursor.
-            fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
-
-            // Close the cursor.
-            contentResolverCursor.close();
-        } else {
-            // Use the file URI last path segment as the file name string.
-            fileNameString = fileUri.getLastPathSegment();
-        }
-    }
-
-    // `onPreExecute()` operates on the UI thread.
-    @Override
-    protected void onPreExecute() {
-        // Get handles for the activity and the nested scroll WebView.
-        Activity activity = activityWeakReference.get();
-        NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get();
-
-        // Abort if the activity or the nested scroll WebView is gone.
-        if ((activity == null) || activity.isFinishing() || nestedScrollWebView == null) {
-            return;
-        }
-
-        // Create a saving image snackbar.
-        savingImageSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.processing_image) + "  " + fileNameString, Snackbar.LENGTH_INDEFINITE);
-
-        // Display the saving image snackbar.
-        savingImageSnackbar.show();
-
-        // Create a webpage bitmap.  Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888.  The nested scroll WebView commands must be run on the UI thread.
-        webpageBitmap = Bitmap.createBitmap(nestedScrollWebView.getHorizontalScrollRange(), nestedScrollWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888);
-
-        // Create a canvas.
-        Canvas webpageCanvas = new Canvas(webpageBitmap);
-
-        // Draw the current webpage onto the bitmap.  The nested scroll WebView commands must be run on the UI thread.
-        nestedScrollWebView.draw(webpageCanvas);
-    }
-
-    @Override
-    protected String doInBackground(Void... Void) {
-        // Get a handle for the activity.
-        Activity activity = activityWeakReference.get();
-
-        // Abort if the activity is gone.
-        if ((activity == null) || activity.isFinishing()) {
-            return "";
-        }
-
-        // Create a webpage PNG byte array output stream.
-        ByteArrayOutputStream webpageByteArrayOutputStream = new ByteArrayOutputStream();
-
-        // Convert the bitmap to a PNG.  `0` is for lossless compression (the only option for a PNG).  This compression takes a long time.  Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
-        webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream);
-
-        // Create a file creation disposition string.
-        String fileCreationDisposition = SUCCESS;
-
-        try {
-            // Create an image file output stream.
-            OutputStream imageFileOutputStream = activity.getContentResolver().openOutputStream(fileUri);
-
-            // Write the webpage image to the image file.
-            webpageByteArrayOutputStream.writeTo(imageFileOutputStream);
-        } catch (Exception exception) {
-            // Store the error in the file creation disposition string.
-            fileCreationDisposition = exception.toString();
-        }
-
-        // Return the file creation disposition string.
-        return fileCreationDisposition;
-    }
-
-    // `onPostExecute()` operates on the UI thread.
-    @Override
-    protected void onPostExecute(String fileCreationDisposition) {
-        // Get handles for the weak references.
-        Activity activity = activityWeakReference.get();
-        NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get();
-
-        // Abort if the activity is gone.
-        if ((activity == null) || activity.isFinishing()) {
-            return;
-        }
-
-        // Dismiss the saving image snackbar.
-        savingImageSnackbar.dismiss();
-
-        // Display a file creation disposition snackbar.
-        if (fileCreationDisposition.equals(SUCCESS)) {
-            // Display the image saved snackbar.
-            Snackbar.make(nestedScrollWebView, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show();
-        } else {
-            // Display the file saving error.
-            Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file, fileNameString, fileCreationDisposition), Snackbar.LENGTH_INDEFINITE).show();
-        }
-    }
-}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.java b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.java
deleted file mode 100644 (file)
index 26f57a1..0000000
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Copyright © 2017-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.backgroundtasks;
-
-import android.annotation.SuppressLint;
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.graphics.Typeface;
-import android.net.Uri;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.StyleSpan;
-import android.webkit.CookieManager;
-
-import com.stoutner.privacybrowser.viewmodels.WebViewSource;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.URL;
-import java.security.SecureRandom;
-import java.security.cert.X509Certificate;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-public class GetSourceBackgroundTask {
-    public SpannableStringBuilder[] acquire(String urlString, String userAgent, String localeString, Proxy proxy, ContentResolver contentResolver, WebViewSource webViewSource, boolean ignoreSslErrors) {
-        // Initialize the spannable string builders.
-        SpannableStringBuilder requestHeadersBuilder = new SpannableStringBuilder();
-        SpannableStringBuilder responseMessageBuilder = new SpannableStringBuilder();
-        SpannableStringBuilder responseHeadersBuilder = new SpannableStringBuilder();
-        SpannableStringBuilder responseBodyBuilder = new SpannableStringBuilder();
-
-        if (urlString.startsWith("content://")) {  // This is a content URL.
-            // Attempt to read the content data.  Return an error if this fails.
-            try {
-                // Get a URI for the content URL.
-                Uri contentUri = Uri.parse(urlString);
-
-                // Get a cursor with metadata about the content URL.
-                Cursor contentCursor = contentResolver.query(contentUri, null, null, null, null);
-
-                // Move the content cursor to the first row.
-                contentCursor.moveToFirst();
-
-                for (int i = 0; i < contentCursor.getColumnCount(); i++) {
-                    // Add a new line if this is not the first entry.
-                    if (i > 0) {
-                        responseHeadersBuilder.append(System.getProperty("line.separator"));
-                    }
-
-                    // Add each header to the string builder.
-                    responseHeadersBuilder.append(contentCursor.getColumnName(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    responseHeadersBuilder.append(":  ");
-                    responseHeadersBuilder.append(contentCursor.getString(i));
-                }
-
-                // Close the content cursor.
-                contentCursor.close();
-
-                // Create a buffered string reader for the content data.
-                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(contentResolver.openInputStream(contentUri)));
-
-                // Get the data from the buffered reader one line at a time.
-                for (String contentLineString; ((contentLineString = bufferedReader.readLine()) != null);) {
-                    // Add the line to the response body builder.
-                    responseBodyBuilder.append(contentLineString);
-
-                    // Append a new line.
-                    responseBodyBuilder.append("\n");
-                }
-            } catch (Exception exception) {
-                // Return the error message.
-                webViewSource.returnError(exception.toString());
-            }
-        } else {  // This is not a content URL.
-            // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
-            try {
-                // Get the current URL from the main activity.
-                URL url = new URL(urlString);
-
-                // Open a connection to the URL.  No data is actually sent at this point.
-                HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
-
-                // Set the `Host` header property.
-                httpUrlConnection.setRequestProperty("Host", url.getHost());
-
-                // Add the `Host` header to the string builder and format the text.
-                requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  ");
-                requestHeadersBuilder.append(url.getHost());
-
-
-                // Set the `Connection` header property.
-                httpUrlConnection.setRequestProperty("Connection", "keep-alive");
-
-                // Add the `Connection` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  keep-alive");
-
-
-                // Set the `Upgrade-Insecure-Requests` header property.
-                httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
-
-                // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  1");
-
-
-                // Set the `User-Agent` header property.
-                httpUrlConnection.setRequestProperty("User-Agent", userAgent);
-
-                // Add the `User-Agent` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  ");
-                requestHeadersBuilder.append(userAgent);
-
-
-                // Set the `Sec-Fetch-Site` header property.
-                httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none");
-
-                // Add the `Sec-Fetch-Site` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Sec-Fetch-Site", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  none");
-
-
-                // Set the `Sec-Fetch-Mode` header property.
-                httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate");
-
-                // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Sec-Fetch-Mode", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  navigate");
-
-
-                // Set the `Sec-Fetch-User` header property.
-                httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1");
-
-                // Add the `Sec-Fetch-User` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Sec-Fetch-User", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  ?1");
-
-
-                // Set the `Accept` header property.
-                httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
-
-                // Add the `Accept` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  ");
-                requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
-
-
-                // Set the `Accept-Language` header property.
-                httpUrlConnection.setRequestProperty("Accept-Language", localeString);
-
-                // Add the `Accept-Language` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  ");
-                requestHeadersBuilder.append(localeString);
-
-
-                // Get the cookies for the current domain.
-                String cookiesString = CookieManager.getInstance().getCookie(url.toString());
-
-                // Only process the cookies if they are not null.
-                if (cookiesString != null) {
-                    // Add the cookies to the header property.
-                    httpUrlConnection.setRequestProperty("Cookie", cookiesString);
-
-                    // Add the cookie header to the string builder and format the text.
-                    requestHeadersBuilder.append(System.getProperty("line.separator"));
-                    requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    requestHeadersBuilder.append(":  ");
-                    requestHeadersBuilder.append(cookiesString);
-                }
-
-
-                // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
-                // Add the `Accept-Encoding` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"));
-                requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                requestHeadersBuilder.append(":  gzip");
-
-                // Ignore SSL errors if requested.
-                if (ignoreSslErrors){
-                    // Create a new host name verifier.
-                    HostnameVerifier hostnameVerifier = (hostname, sslSession) -> {
-                        // Allow all host names.
-                        return true;
-                    };
-
-                    // Create a new trust manager.  Lint wants to warn us that it is hard to securely implement an X509 trust manager.
-                    // But the point of this trust manager is that it should accept all certificates no matter what, so that isn't an issue in our case.
-                    @SuppressLint("CustomX509TrustManager") TrustManager[] trustManager = new TrustManager[] {
-                            new X509TrustManager() {
-                                @SuppressLint("TrustAllX509TrustManager")
-                                @Override
-                                public void checkClientTrusted(X509Certificate[] chain, String authType) {
-                                    // Do nothing, which trusts all client certificates.
-                                }
-
-                                @SuppressLint("TrustAllX509TrustManager")
-                                @Override
-                                public void checkServerTrusted(X509Certificate[] chain, String authType) {
-                                    // Do nothing, which trusts all server certificates.
-                                }
-
-                                @Override
-                                public X509Certificate[] getAcceptedIssuers() {
-                                    return null;
-                                }
-                            }
-                    };
-
-                    // Get an SSL context.  `TLS` provides a base instance available from API 1.  <https://developer.android.com/reference/javax/net/ssl/SSLContext>
-                    SSLContext sslContext = SSLContext.getInstance("TLS");
-
-                    // Initialize the SSL context with the blank trust manager.
-                    sslContext.init(null, trustManager, new SecureRandom());
-
-                    // Get the SSL socket factory with the blank trust manager.
-                    SSLSocketFactory socketFactory = sslContext.getSocketFactory();
-
-                    // Set the HTTPS URL Connection to use the blank host name verifier.
-                    ((HttpsURLConnection) httpUrlConnection).setHostnameVerifier(hostnameVerifier);
-
-                    // Set the HTTPS URL connection to use the socket factory with the blank trust manager.
-                    ((HttpsURLConnection) httpUrlConnection).setSSLSocketFactory(socketFactory);
-                }
-
-                // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
-                try {
-                    // Get the response code, which causes the connection to the server to be made.
-                    int responseCode = httpUrlConnection.getResponseCode();
-
-                    // Populate the response message string builder.
-                    responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    responseMessageBuilder.append(":  ");
-                    responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
-
-                    // Initialize the iteration variable.
-                    int i = 0;
-
-                    // Iterate through the received header fields.
-                    while (httpUrlConnection.getHeaderField(i) != null) {
-                        // Add a new line if there is already information in the string builder.
-                        if (i > 0) {
-                            responseHeadersBuilder.append(System.getProperty("line.separator"));
-                        }
-
-                        // Add the header to the string builder and format the text.
-                        responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                        responseHeadersBuilder.append(":  ");
-                        responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
-
-                        // Increment the iteration variable.
-                        i++;
-                    }
-
-                    // Instantiate an input stream for the response body.
-                    InputStream inputStream;
-
-                    // Get the correct input stream based on the response code.
-                    if (responseCode == 404) {  // Get the error stream.
-                        inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
-                    } else {  // Get the response body stream.
-                        inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
-                    }
-
-                    // Initialize the byte array output stream and the conversion buffer byte array.
-                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-                    byte[] conversionBufferByteArray = new byte[1024];
-
-                    // Define the buffer length variable.
-                    int bufferLength;
-
-                    try {
-                        // Attempt to read data from the input stream and store it in the conversion buffer byte array.  Also store the amount of data read in the buffer length variable.
-                        while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
-                            // Write the contents of the conversion buffer to the byte array output stream.
-                            byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
-                        }
-                    } catch (IOException exception) {
-                        // Return the error message.
-                        webViewSource.returnError(exception.toString());
-                    }
-
-                    // Close the input stream.
-                    inputStream.close();
-
-                    // Populate the response body string with the contents of the byte array output stream.
-                    responseBodyBuilder.append(byteArrayOutputStream.toString());
-                } finally {
-                    // Disconnect HTTP URL connection.
-                    httpUrlConnection.disconnect();
-                }
-            } catch (Exception exception) {
-                // Return the error message.
-                webViewSource.returnError(exception.toString());
-            }
-        }
-
-        // Return the spannable string builders.
-        return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt
new file mode 100644 (file)
index 0000000..f7ad0a5
--- /dev/null
@@ -0,0 +1,336 @@
+/*
+ * Copyright © 2017-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.backgroundtasks
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.graphics.Typeface
+import android.net.Uri
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.StyleSpan
+import android.webkit.CookieManager
+
+import com.stoutner.privacybrowser.viewmodels.WebViewSource
+
+import java.io.BufferedInputStream
+import java.io.BufferedReader
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+
+import java.net.HttpURLConnection
+import java.net.Proxy
+import java.net.URL
+
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.SSLSession
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+
+class GetSourceBackgroundTask {
+    fun acquire(urlString: String, userAgent: String, localeString: String, proxy: Proxy, contentResolver: ContentResolver, webViewSource: WebViewSource, ignoreSslErrors: Boolean):
+            Array<SpannableStringBuilder> {
+
+        // Initialize the spannable string builders.
+        val requestHeadersBuilder = SpannableStringBuilder()
+        val responseMessageBuilder = SpannableStringBuilder()
+        val responseHeadersBuilder = SpannableStringBuilder()
+        val responseBodyBuilder = SpannableStringBuilder()
+
+        if (urlString.startsWith("content://")) {  // This is a content URL.
+            // Attempt to read the content data.  Return an error if this fails.
+            try {
+                // Get a URI for the content URL.
+                val contentUri = Uri.parse(urlString)
+
+                // Get a cursor with metadata about the content URL.
+                val contentCursor = contentResolver.query(contentUri, null, null, null, null)!!
+
+                // Move the content cursor to the first row.
+                contentCursor.moveToFirst()
+
+                // Populate the response header.
+                for (i in 0 until contentCursor.columnCount) {
+                    // Add a new line if this is not the first entry.
+                    if (i > 0)
+                        responseHeadersBuilder.append(System.getProperty("line.separator"))
+
+                    // Add each header to the string builder.
+                    responseHeadersBuilder.append(contentCursor.getColumnName(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    responseHeadersBuilder.append(":  ")
+                    responseHeadersBuilder.append(contentCursor.getString(i))
+                }
+
+                // Close the content cursor.
+                contentCursor.close()
+
+                // Create a buffered string reader for the content data.
+                val bufferedReader = BufferedReader(InputStreamReader(contentResolver.openInputStream(contentUri)))
+
+                // Create a buffered string reader for the content data.
+                var contentLineString: String?
+
+                // Get the data from the buffered reader one line at a time.
+                while (bufferedReader.readLine().also { contentLineString = it } != null) {
+                    // Add the line to the response body builder.
+                    responseBodyBuilder.append(contentLineString)
+
+                    // Append a new line.
+                    responseBodyBuilder.append("\n")
+                }
+            } catch (exception: Exception) {
+                // Return the error message.
+                webViewSource.returnError(exception.toString())
+            }
+        } else {  // This is not a content URL.
+            // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
+            try {
+                // Get the current URL from the main activity.
+                val url = URL(urlString)
+
+                // Open a connection to the URL.  No data is actually sent at this point.
+                val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
+
+                // Set the `Host` header property.
+                httpUrlConnection.setRequestProperty("Host", url.host)
+
+                // Add the `Host` header to the string builder and format the text.
+                requestHeadersBuilder.append("Host", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append(url.host)
+
+
+                // Set the `Connection` header property.
+                httpUrlConnection.setRequestProperty("Connection", "keep-alive")
+
+                // Add the `Connection` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Connection", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  keep-alive")
+
+
+                // Set the `Upgrade-Insecure-Requests` header property.
+                httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1")
+
+                // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Upgrade-Insecure-Requests", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  1")
+
+
+                // Set the `User-Agent` header property.
+                httpUrlConnection.setRequestProperty("User-Agent", userAgent)
+
+                // Add the `User-Agent` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("User-Agent", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append(userAgent)
+
+
+                // Set the `Sec-Fetch-Site` header property.
+                httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none")
+
+                // Add the `Sec-Fetch-Site` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Sec-Fetch-Site", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  none")
+
+
+                // Set the `Sec-Fetch-Mode` header property.
+                httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate")
+
+                // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Sec-Fetch-Mode", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  navigate")
+
+
+                // Set the `Sec-Fetch-User` header property.
+                httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1")
+
+                // Add the `Sec-Fetch-User` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Sec-Fetch-User", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ?1")
+
+
+                // Set the `Accept` header property.
+                httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
+
+                // Add the `Accept` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Accept", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
+
+
+                // Set the `Accept-Language` header property.
+                httpUrlConnection.setRequestProperty("Accept-Language", localeString)
+
+                // Add the `Accept-Language` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Accept-Language", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append(localeString)
+
+
+                // Get the cookies for the current domain.
+                val cookiesString = CookieManager.getInstance().getCookie(url.toString())
+
+                // Only process the cookies if they are not null.
+                if (cookiesString != null) {
+                    // Add the cookies to the header property.
+                    httpUrlConnection.setRequestProperty("Cookie", cookiesString)
+
+                    // Add the cookie header to the string builder and format the text.
+                    requestHeadersBuilder.append(System.getProperty("line.separator"))
+                    requestHeadersBuilder.append("Cookie", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    requestHeadersBuilder.append(":  ")
+                    requestHeadersBuilder.append(cookiesString)
+                }
+
+
+                // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
+                // Add the `Accept-Encoding` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Accept-Encoding", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  gzip")
+
+                // Ignore SSL errors if requested.
+                if (ignoreSslErrors) {
+                    // Create a new host name verifier that allows all host names without checking for SSL errors.
+                    val hostnameVerifier = HostnameVerifier { _: String?, _: SSLSession? -> true }
+
+                    // Create a new trust manager.  Lint wants to warn us that it is hard to securely implement an X509 trust manager.
+                    // But the point of this trust manager is that it should accept all certificates no matter what, so that isn't an issue in our case.
+                    @SuppressLint("CustomX509TrustManager") val trustManager = arrayOf<TrustManager>(
+                        object : X509TrustManager {
+                            @SuppressLint("TrustAllX509TrustManager")
+                            override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
+                                // Do nothing, which trusts all client certificates.
+                            }
+
+                            @SuppressLint("TrustAllX509TrustManager")
+                            override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
+                                // Do nothing, which trusts all server certificates.
+                            }
+
+                            override fun getAcceptedIssuers(): Array<X509Certificate>? {
+                                return null
+                            }
+                        }
+                    )
+
+                    // Get an SSL context.  `TLS` provides a base instance available from API 1.  <https://developer.android.com/reference/javax/net/ssl/SSLContext>
+                    val sslContext = SSLContext.getInstance("TLS")
+
+                    // Initialize the SSL context with the blank trust manager.
+                    sslContext.init(null, trustManager, SecureRandom())
+
+                    // Get the SSL socket factory with the blank trust manager.
+                    val socketFactory = sslContext.socketFactory
+
+                    // Set the HTTPS URL Connection to use the blank host name verifier.
+                    (httpUrlConnection as HttpsURLConnection).hostnameVerifier = hostnameVerifier
+
+                    // Set the HTTPS URL connection to use the socket factory with the blank trust manager.
+                    httpUrlConnection.sslSocketFactory = socketFactory
+                }
+
+                // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
+                try {
+                    // Get the response code, which causes the connection to the server to be made.
+                    val responseCode = httpUrlConnection.responseCode
+
+                    // Populate the response message string builder.
+                    responseMessageBuilder.append(responseCode.toString(), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    responseMessageBuilder.append(":  ")
+                    responseMessageBuilder.append(httpUrlConnection.responseMessage)
+
+                    // Initialize the iteration variable.
+                    var i = 0
+
+                    // Iterate through the received header fields.
+                    while (httpUrlConnection.getHeaderField(i) != null) {
+                        // Add a new line if there is already information in the string builder.
+                        if (i > 0)
+                            responseHeadersBuilder.append(System.getProperty("line.separator"))
+
+                        // Add the header to the string builder and format the text.
+                        responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                        responseHeadersBuilder.append(":  ")
+                        responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i))
+
+                        // Increment the iteration variable.
+                        i++
+                    }
+
+                    // Get the correct input stream based on the response code.
+                    val inputStream: InputStream = if (responseCode == 404)  // Get the error stream.
+                        BufferedInputStream(httpUrlConnection.errorStream)
+                    else  // Get the response body stream.
+                        BufferedInputStream(httpUrlConnection.inputStream)
+
+                    // Initialize the byte array output stream and the conversion buffer byte array.
+                    val byteArrayOutputStream = ByteArrayOutputStream()
+                    val conversionBufferByteArray = ByteArray(1024)
+
+                    // Define the buffer length variable.
+                    var bufferLength: Int
+
+                    try {
+                        // Attempt to read data from the input stream and store it in the conversion buffer byte array.  Also store the amount of data read in the buffer length variable.
+                        while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
+                            // Write the contents of the conversion buffer to the byte array output stream.
+                            byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength)
+                        }
+                    } catch (exception: IOException) {
+                        // Return the error message.
+                        webViewSource.returnError(exception.toString())
+                    }
+
+                    // Close the input stream.
+                    inputStream.close()
+
+                    // Populate the response body string with the contents of the byte array output stream.
+                    responseBodyBuilder.append(byteArrayOutputStream.toString())
+                } finally {
+                    // Disconnect HTTP URL connection.
+                    httpUrlConnection.disconnect()
+                }
+            } catch (exception: Exception) {
+                // Return the error message.
+                webViewSource.returnError(exception.toString())
+            }
+        }
+
+        // Return the spannable string builders.
+        return arrayOf(requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder)
+    }
+}
\ No newline at end of file
index 4311ef5ea50a2434543497fbd63b23c38b758c5c..fb0c1e8f4011d6869a5432d798ccee4969dee312 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019,2021-2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2019,2021-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
  *
@@ -77,6 +77,7 @@ class PopulateBlocklistsCoroutine(context: Context) {
             // Advertise the loading of EasyList.
             loadingBlocklistTextView.text = context.getString(R.string.loading_easylist)
 
+            // Populate the blocklists on the IO thread.
             withContext(Dispatchers.IO) {
                 // Populate EasyList.
                 val easyList = blocklistHelper.parseBlocklist(context.assets, "blocklists/easylist.txt")
diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt
new file mode 100644 (file)
index 0000000..c6fd36a
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.coroutines
+
+import android.app.Activity
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.provider.OpenableColumns
+import android.util.Base64
+import android.webkit.CookieManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.helpers.ProxyHelper
+import com.stoutner.privacybrowser.views.NoSwipeViewPager
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+import java.io.BufferedInputStream
+import java.io.InputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.text.NumberFormat
+
+class SaveUrlCoroutine {
+    fun save(context: Context, activity: Activity, urlString: String, fileUri: Uri, userAgent: String, cookiesEnabled: Boolean) {
+        // Use a coroutine to save the URL.
+        CoroutineScope(Dispatchers.Main).launch {
+            // Create a file name string.
+            val fileNameString: String
+
+            // Query the exact file name if the API >= 26.
+            if (Build.VERSION.SDK_INT >= 26) {
+                // Get a cursor from the content resolver.
+                val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)!!
+
+                // Move to the first row.
+                contentResolverCursor.moveToFirst()
+
+                // Get the file name from the cursor.
+                fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
+
+                // Close the cursor.
+                contentResolverCursor.close()
+            } else {
+                // Use the file URI last path segment as the file name string.
+                fileNameString = fileUri.lastPathSegment!!
+            }
+
+            // Get a handle for the no swipe view pager.
+            val noSwipeViewPager = activity.findViewById<NoSwipeViewPager>(R.id.webviewpager)
+
+            // Create a saving file snackbar.
+            val savingFileSnackbar = Snackbar.make(noSwipeViewPager, activity.getString(R.string.saving_file, 0, fileNameString), Snackbar.LENGTH_INDEFINITE)
+
+            // Display the saving file snackbar.
+            savingFileSnackbar.show()
+
+            // Download the URL on the IO thread.
+            withContext(Dispatchers.IO) {
+                try {
+                    // Open an output stream.
+                    val outputStream = activity.contentResolver.openOutputStream(fileUri)!!
+
+                    // Save the URL.
+                    if (urlString.startsWith("data:")) {  // The URL contains the entire data of an image.
+                        // Get the Base64 data, which begins after a `,`.
+                        val base64DataString = urlString.substring(urlString.indexOf(",") + 1)
+
+                        // Decode the Base64 string to a byte array.
+                        val base64DecodedDataByteArray = Base64.decode(base64DataString, Base64.DEFAULT)
+
+                        // Write the Base64 byte array to the output stream.
+                        outputStream.write(base64DecodedDataByteArray)
+                    } else {  // The URL points to the data location on the internet.
+                        // Get the URL from the calling activity.
+                        val url = URL(urlString)
+
+                        // Instantiate the proxy helper.
+                        val proxyHelper = ProxyHelper()
+
+                        // Get the current proxy.
+                        val proxy = proxyHelper.getCurrentProxy(context)
+
+                        // Open a connection to the URL.  No data is actually sent at this point.
+                        val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
+
+                        // Add the user agent to the header property.
+                        httpUrlConnection.setRequestProperty("User-Agent", userAgent)
+
+                        // Add the cookies if they are enabled.
+                        if (cookiesEnabled) {
+                            // Get the cookies for the current domain.
+                            val cookiesString = CookieManager.getInstance().getCookie(url.toString())
+
+                            // Only add the cookies if they are not null.
+                            if (cookiesString != null) {
+                                // Add the cookies to the header property.
+                                httpUrlConnection.setRequestProperty("Cookie", cookiesString)
+                            }
+                        }
+
+                        // Create the file size value.
+                        val fileSize: Long
+
+                        // Create the formatted file size variable.
+                        var formattedFileSize = ""
+
+                        // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
+                        try {
+                            // Get the content length header, which causes the connection to the server to be made.
+                            val contentLengthString = httpUrlConnection.getHeaderField("Content-Length")
+
+                            // Check to see if the content length is populated.
+                            if (contentLengthString != null) {  // The content length is populated.
+                                // Convert the content length to an long.
+                                fileSize = contentLengthString.toLong()
+
+                                // Format the file size for display.
+                                formattedFileSize = NumberFormat.getInstance().format(fileSize)
+                            } else {  // The content length is null.
+                                // Set the file size to be `-1`.
+                                fileSize = -1
+                            }
+
+                            // Get the response body stream.
+                            val inputStream: InputStream = BufferedInputStream(httpUrlConnection.inputStream)
+
+                            // Initialize the conversion buffer byte array.
+                            // This is set to a megabyte so that frequent updating of the snackbar doesn't freeze the interface on download.  <https://redmine.stoutner.com/issues/709>
+                            val conversionBufferByteArray = ByteArray(1048576)
+
+                            // Initialize the downloaded kilobytes counter.
+                            var downloadedKilobytesCounter: Long = 0
+
+                            // Define the buffer length variable.
+                            var bufferLength: Int
+
+                            // Attempt to read data from the input stream and store it in the output stream.  Also store the amount of data read in the buffer length variable.
+                            while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) {  // Proceed while the amount of data stored in the buffer in > 0.
+                                // Write the contents of the conversion buffer to the file output stream.
+                                outputStream.write(conversionBufferByteArray, 0, bufferLength)
+
+                                // Update the downloaded kilobytes counter.
+                                downloadedKilobytesCounter += bufferLength
+
+                                // Format the number of bytes downloaded.
+                                val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedKilobytesCounter)
+
+                                // Update the UI.
+                                withContext(Dispatchers.Main) {
+                                    // Check to see if the file size is known.
+                                    if (fileSize == -1L) {  // The size of the download file is not known.
+                                        // Update the snackbar.
+                                        savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString))
+                                    } else {  // The size of the download file is known.
+                                        // Calculate the download percentage.
+                                        val downloadPercentage = downloadedKilobytesCounter * 100 / fileSize
+
+                                        // Update the snackbar.
+                                        savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize,
+                                            fileNameString)
+                                        )
+                                    }
+                                }
+                            }
+
+                            // Close the input stream.
+                            inputStream.close()
+                        } finally {
+                            // Disconnect the HTTP URL connection.
+                            httpUrlConnection.disconnect()
+                        }
+                    }
+
+                    // Close the output stream.
+                    outputStream.close()
+
+                    // Update the UI.
+                    withContext(Dispatchers.Main) {
+                        // Dismiss the saving file snackbar.
+                        savingFileSnackbar.dismiss()
+
+                        // Display the file saved snackbar.
+                        Snackbar.make(noSwipeViewPager, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show()
+                    }
+                } catch (exception: Exception) {
+                    // Update the UI.
+                    withContext(Dispatchers.Main) {
+                        // Dismiss the saving file snackbar.
+                        savingFileSnackbar.dismiss()
+
+                        // Display the file saving error.
+                        Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt
new file mode 100644 (file)
index 0000000..1c3a2c0
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Copyright0 2019-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.coroutines
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.net.Uri
+import android.os.Build
+import android.provider.OpenableColumns
+
+import com.google.android.material.snackbar.Snackbar
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.views.NestedScrollWebView
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+import java.io.ByteArrayOutputStream
+
+class SaveWebpageImageCoroutine {
+    fun save(activity: Activity, fileUri: Uri, nestedScrollWebView: NestedScrollWebView) {
+        // Use a coroutine to save the webpage image.
+        CoroutineScope(Dispatchers.Main).launch {
+            // Create a file name string.
+            val fileNameString: String
+
+            // Query the exact file name if the API >= 26.
+            if (Build.VERSION.SDK_INT >= 26) {
+                // Get a cursor from the content resolver.
+                val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)
+
+                // Move to the first row.
+                contentResolverCursor!!.moveToFirst()
+
+                // Get the file name from the cursor.
+                fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
+
+                // Close the cursor.
+                contentResolverCursor.close()
+            } else {
+                // Use the file URI last path segment as the file name string.
+                fileNameString = fileUri.lastPathSegment!!
+            }
+
+            // Create a saving image snackbar.
+            val savingImageSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.processing_image, fileNameString), Snackbar.LENGTH_INDEFINITE)
+
+            // Display the saving image snackbar.
+            savingImageSnackbar.show()
+
+            // Create a webpage bitmap.  Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888.  The nested scroll WebView commands must be run on the UI thread.
+            val webpageBitmap = Bitmap.createBitmap(nestedScrollWebView.getHorizontalScrollRange(), nestedScrollWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888)
+
+            // Create a canvas.
+            val webpageCanvas = Canvas(webpageBitmap)
+
+            // Draw the current webpage onto the bitmap.  The nested scroll WebView commands must be run on the UI thread.
+            nestedScrollWebView.draw(webpageCanvas)
+
+            // Compress the image on the IO thread.
+            withContext(Dispatchers.IO) {
+                // Create a webpage PNG byte array output stream.
+                val webpageByteArrayOutputStream = ByteArrayOutputStream()
+
+                // Convert the bitmap to a PNG.  `0` is for lossless compression (the only option for a PNG).  This compression takes a long time.
+                // Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
+                webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream)
+
+                try {
+                    // Create an image file output stream.
+                    val imageFileOutputStream = activity.contentResolver.openOutputStream(fileUri)!!
+
+                    // Write the webpage image to the image file.
+                    webpageByteArrayOutputStream.writeTo(imageFileOutputStream)
+
+                    // Close the output stream.
+                    imageFileOutputStream.close()
+
+                    // Update the UI.
+                    withContext(Dispatchers.Main) {
+                        // Dismiss the saving image snackbar.
+                        savingImageSnackbar.dismiss()
+
+                        // Display the image saved snackbar.
+                        Snackbar.make(nestedScrollWebView, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
+                    }
+                } catch (exception: Exception) {
+                    // Update the UI.
+                    withContext(Dispatchers.Main) {
+                        // Dismiss the saving image snackbar.
+                        savingImageSnackbar.dismiss()
+
+                        // Display the file saving error.
+                        Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
+                    }
+                }
+
+            }
+        }
+    }
+}
\ No newline at end of file
index 1d129078461dab04428d09f0007a6a24e0c66434..079b2ac87b47c734fef26c33bc63be2e5cbe2ee2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
  *
@@ -716,7 +716,7 @@ class AboutVersionFragment : Fragment() {
         updateMemoryUsageBoolean = true
     }
 
-    fun updateMemoryUsage(activity: Activity) {
+    private fun updateMemoryUsage(activity: Activity) {
         try {
             // Update the memory usage if enabled.
             if (updateMemoryUsageBoolean) {
@@ -797,7 +797,7 @@ class AboutVersionFragment : Fragment() {
         }
     }
 
-    fun getAboutVersionString(): String {
+    private fun getAboutVersionString(): String {
         // Initialize an about version string builder.
         val aboutVersionStringBuilder = StringBuilder()
 
index 5d37613c3427040d48c4a9cf61347ef26a9756ff..154ea2dd20849b76d730407fad3ac5929ae8bc2f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019-2020,2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019-2020,2022-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
  *
@@ -43,7 +43,6 @@ private const val SAVED_NESTED_SCROLL_WEBVIEW_STATE = "saved_nested_scroll_webvi
 
 class WebViewTabFragment : Fragment() {
     // Define the public variables.
-    @JvmField  // TODO.  `@JvmField` can be removed once the entire project has been converted to Kotlin.
     var fragmentId = Calendar.getInstance().timeInMillis
 
     // The public interface is used to send information back to the parent activity.
@@ -59,7 +58,6 @@ class WebViewTabFragment : Fragment() {
     private lateinit var nestedScrollWebView: NestedScrollWebView
 
     companion object {
-        @JvmStatic  // TODO.  `@JvmStatic` can be removed once the entire project has been converted to Kotlin.
         fun createPage(pageNumber: Int, url: String?): WebViewTabFragment {
             // Create an arguments bundle.
             val argumentsBundle = Bundle()
@@ -79,8 +77,7 @@ class WebViewTabFragment : Fragment() {
             return webViewTabFragment
         }
 
-        @JvmStatic  // TODO.  `@JvmStatic` can be removed once the entire project has been converted to Kotlin.
-        fun restorePage(savedState: Bundle?, savedNestedScrollWebViewState: Bundle?): WebViewTabFragment {
+        fun restorePage(savedState: Bundle, savedNestedScrollWebViewState: Bundle): WebViewTabFragment {
             // Create an arguments bundle
             val argumentsBundle = Bundle()
 
@@ -165,4 +162,4 @@ class WebViewTabFragment : Fragment() {
             null
         }
     }
-}
\ No newline at end of file
+}
index a2e586e64e0274d4a026d9854d213c6179ae9b47..68a760d23eb3d6df99129c80ca2487675ef7e61f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
  *
@@ -196,6 +196,7 @@ object UrlHelper {
         return Pair(fileNameString, formattedFileSize)
     }
 
+    /*  This entire method might not be needed.
     fun getSize(context: Context, url: URL, userAgent: String, cookiesEnabled: Boolean): String {
         // Initialize the formatted file size string.
         var formattedFileSize = context.getString(R.string.unknown_size)
@@ -260,6 +261,7 @@ object UrlHelper {
         // Return the formatted file size.
         return formattedFileSize
     }
+    */
 
     @JvmStatic
     fun highlightSyntax(urlEditText: EditText, initialGrayColorSpan: ForegroundColorSpan, finalGrayColorSpan: ForegroundColorSpan, redColorSpan: ForegroundColorSpan) {
index be02c677a3c3212b19ae97dbe7436c32191dad80..2b68bfc88825a5f625708d1005436bb45179b10c 100644 (file)
     <string name="file_is_mht">Die Datei ist ein MHT-Web-Archiv.</string>
     <string name="mht_checkbox_explanation">Manchmal müssen MIME-gekapselte HTML-Web-Archive (MHT) manuell festgelegt werden, um korrekt geöffnet zu werden.</string>
 
-    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
-        The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
+        The `%*$*` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <string name="save_url">URL speichern</string>
     <string name="save_archive">Archiv speichern</string>
     <string name="save_text">Text speichern</string>
     <string name="bytes">Bytes</string>
     <string name="unknown_size">Unbekannte Größe</string>
     <string name="invalid_url">Ungültige URL</string>
-    <string name="saving_file">Speichere Datei:</string>
+    <string name="saving_file">Speichere Datei: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Speichere Datei: \u0020 %1$s Bytes - %2$s</string>
+    <string name="saving_file_percentage_progress">Speichere Datei: \u0020 %1$d%% - %2$s Bytes / %3$s Bytes - %4$s</string>
     <string name="processing_image">Bild wird bearbeitet: \u0020 %1$s</string>
     <string name="error_saving_file">Fehler beim Speichern der Datei %1$s: \u0020 %2$s</string>
     <string name="unknown_error">Unbekannter Fehler</string>
         <string name="default_label">Standard</string>
         <string name="default_allowed">Standard - erlaubt</string>
         <string name="allowed">erlaubt</string>
+        <string name="request_allowed">%1$d. erlaubt</string>
         <string name="allowed_plural">erlaubt</string>
         <string name="third_party_plural">Drittanbieter</string>
         <string name="third_party_blocked">Drittanbieter - blockiert</string>
         <string name="blocked">blockiert</string>
+        <string name="request_blocked">%1$d. blockiert</string>
         <string name="blocked_plural">blockiert</string>
     <string name="blocklist">Filterliste</string>
     <string name="sublist">Unterliste</string>
index f189e320ec69b5062ecf515f9919bfc1976d791f..bad35e6a719a77896beb0949a499c94702a13a46 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright 2016-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
 
   Translation 2017-2022 Jose A. León.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
 
     <string name="file_is_mht">El archivo es un archivo de web MHT.</string>
     <string name="mht_checkbox_explanation">A veces se necesita especificar manualmente los archivos web MIME Encapsulated HTML (MHT) para que se abran correctamente.</string>
 
-    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
-        The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
+        The `%*$*` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <string name="save_url">Guardar URL</string>
     <string name="save_archive">Guardar archivo</string>
     <string name="save_text">Guardar texto</string>
     <string name="bytes">bytes</string>
     <string name="unknown_size">Tamaño desconocido</string>
     <string name="invalid_url">URL inválida</string>
-    <string name="saving_file">Guardando archivo:</string>
+    <string name="saving_file">Guardando archivo: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Guardando archivo: \u0020 %1$s bytes - %2$s</string>
+    <string name="saving_file_percentage_progress">Guardando archivo: \u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s</string>
     <string name="processing_image">Procesando imagen: \u0020 %1$s</string>
     <string name="error_saving_file">Error al guardar %1$s: \u0020 %2$s</string>
     <string name="unknown_error">Error desconocido</string>
         <string name="default_label">Por defecto</string>
         <string name="default_allowed">Por defecto - Permitida</string>
         <string name="allowed">Permitida</string>
+        <string name="request_allowed">%1$d. Permitida</string>
         <string name="allowed_plural">Permitidas</string>
         <string name="third_party_plural">Terceras partes</string>
         <string name="third_party_blocked">Tercera parte - Bloqueada</string>
         <string name="blocked">Bloqueada</string>
+        <string name="request_blocked">%1$d. Bloqueada</string>
         <string name="blocked_plural">Bloqueadas</string>
     <string name="blocklist">Lista de bloqueo</string>
     <string name="sublist">Sublista</string>
index fec22646d4e0ec3320f36ad9faf70d8e3ab1dd1c..5d68d58f326c8ef06ffe2637f33a9e51c25ba388 100644 (file)
     <string name="file_is_mht">Le fichier est une archive web MHT.</string>
     <string name="mht_checkbox_explanation">Parfois, les archives web MHT (MIME Encapsulated HTML) doivent être spécifiées manuellement pour être ouvertes correctement.</string>
 
-    <!-- Save Dialog.  The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
+    <!-- Save Dialog.  `%%` writes a literal `%`.
+      The `%*$*` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <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="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="saving_file">Enregistrement du fichier : %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Enregistrement du fichier : \u0020 %1$s octets - %2$s</string>
+    <string name="saving_file_percentage_progress">Enregistrement du fichier : \u0020 %1$d%% - %2$s octets / %3$s octets - %4$s</string>
     <string name="processing_image">Traitement de l\'image : %1$s</string>
     <string name="error_saving_file">Erreur lors de l\'enregistrement de %1$s : %2$s</string>
     <string name="unknown_error">Erreur inconnue</string>
         <string name="default_label">Par défaut</string>
         <string name="default_allowed">Par défaut - Autorisées</string>
         <string name="allowed">Autorisée</string>
+        <string name="request_allowed">%1$d. Autorisée</string>
         <string name="allowed_plural">Autorisées</string>
         <string name="third_party_plural">Tiers</string>
         <string name="third_party_blocked">Tiers - Bloquées</string>
         <string name="blocked">Bloquée</string>
+        <string name="request_blocked">%1$d. Bloquée</string>
         <string name="blocked_plural">Bloquées</string>
     <string name="blocklist">Liste noires</string>
     <string name="sublist">Sous-listes</string>
index 23c02129b0c07f598665ba58050ad7df558ce567..3fde1f5c89fbd8239489d8aa5868fb82bfadb480 100644 (file)
     <string name="file_is_mht">Questo file è un archivio web MHT.</string>
     <string name="mht_checkbox_explanation">Talvolta gli archivi web del tipo MIME Encapsulated HTML (MHT) devono essere specificati manualmente per essere aperti correttamente.</string>
 
-    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
         The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <string name="save_url">Salva URL</string>
     <string name="save_archive">Salva Archivio</string>
     <string name="bytes">byte</string>
     <string name="unknown_size">Dimensione sconosciuta</string>
     <string name="invalid_url">URL non valida</string>
-    <string name="saving_file">Salvataggio file:</string>
+    <string name="saving_file">Salvataggio file: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Salvataggio file: \u0020 %1$s byte - %2$s</string>
+    <string name="saving_file_percentage_progress">Salvataggio file: \u0020 %1$d%% - %2$s byte / %3$s byte - %4$s</string>
     <string name="processing_image">Creazione immagine: \u0020 %1$s</string>
     <string name="error_saving_file">Error di salvataggio di %1$s: \u0020 %2$s</string>
     <string name="unknown_error">Errore sconosciuto</string>
         <string name="default_label">Default</string>
         <string name="default_allowed">Default - Permessa</string>
         <string name="allowed">Permessa</string>
+        <string name="request_allowed">%1$d. Permessa</string>
         <string name="allowed_plural">Permesse</string>
         <string name="third_party_plural">Terze parti</string>
         <string name="third_party_blocked">Terze parti - Bloccate</string>
         <string name="blocked">Bloccata</string>
+        <string name="request_blocked">%1$d. Bloccata</string>
         <string name="blocked_plural">Bloccate</string>
     <string name="blocklist">Blocklist</string>
     <string name="sublist">Sublist</string>
index 687c0305adebd1078699b24c747a139ee8549788..fc55bb85c5cfaccca253cc7aef73ee82b55c740c 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright 2015-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
 
   Translation 2020-2022 Thiago Nazareno Conceição Silva de Jesus <mochileiro2006-trilhas@yahoo.com.br>.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
 
     <string name="file_is_mht">Este é um arquivo MHT da web.</string>
     <string name="mht_checkbox_explanation">Às vezes, os arquivos da web MIME Encapsulated HTML (MHT) precisam ser especificados manualmente para serem abertos corretamente.</string>
 
-    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
         The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <string name="save_url">Salvar URL</string>
     <string name="save_archive">Salvar Arquivo</string>
     <string name="bytes">bytes</string>
     <string name="unknown_size">tamanho desconhecido</string>
     <string name="invalid_url">URL inválida</string>
-    <string name="saving_file">Salvando file:</string>
+    <string name="saving_file">Salvando file: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Salvando file: \u0020 %1$s bytes - %2$s</string>
+    <string name="saving_file_percentage_progress">Salvando file: \u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s</string>
     <string name="processing_image">Processando imagem: \u0020 %1$s</string>
     <string name="error_saving_file">Erro ao salvar %1$s: \u0020 %2$s</string>
     <string name="unknown_error">Erro desconhecido</string>
         <string name="default_label">Padrão</string>
         <string name="default_allowed">Padrão - Permitido</string>
         <string name="allowed">Permitido</string>
-        <string name="allowed_plural">Permitido</string>
+        <string name="request_allowed">%1$d. Permitido</string>
+        <string name="allowed_plural">Permitidos</string>
         <string name="third_party_plural">Terceiros</string>
-        <string name="third_party_blocked">Terceiros - Bloqueados</string>
+        <string name="third_party_blocked">Terceiros - Bloqueado</string>
         <string name="blocked">Bloqueado</string>
+        <string name="request_blocked">%1$d. Bloqueado</string>
         <string name="blocked_plural">Bloqueados</string>
     <string name="blocklist">Lista de bloqueios</string>
         <string name="sublist">Sublista</string>
index 782da2cd5f1450cc1b98d521afb0f9b1254cd3b7..e6263a6be713c0ba7962e0d4ca7e486bfd601f54 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright 2015-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
     <string name="file_is_mht">Файл представляет собой веб-архив MHT.</string>
     <string name="mht_checkbox_explanation">Иногда для корректного открытия веб-архивов MIME Encapsulated HTML (MHT) необходимо указать вручную.</string>
 
-    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
         The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <string name="save_url">Сохранить URL</string>
     <string name="save_archive">Сохранить архив</string>
     <string name="bytes">байтов</string>
     <string name="unknown_size">неизвестный размер</string>
     <string name="invalid_url">неправильный URL</string>
-    <string name="saving_file">Сохранение файла:</string>
+    <string name="saving_file">Сохранение файла: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Сохранение файла: \u0020 %1$s байтов - %2$s</string>
+    <string name="saving_file_percentage_progress">Сохранение файла: \u0020 %1$d%% - %2$s байтов / %3$s байтов - %4$s</string>
     <string name="processing_image">Обработка изображения: \u0020 %1$s</string>
     <string name="error_saving_file">Ошибка сохранения %1$s: \u0020 %2$s</string>
     <string name="unknown_error">Неизвестная ошибка</string>
         <string name="default_label">По умолчанию</string>
         <string name="default_allowed">По умолчанию - Разрешен</string>
         <string name="allowed">Разрешен</string>
+        <string name="request_allowed">%1$d. Разрешен</string>
         <string name="allowed_plural">Разрешено</string>
         <string name="third_party_plural">Сторонние</string>
         <string name="third_party_blocked">Сторонние - Блокировано</string>
         <string name="blocked">Блокирован</string>
+        <string name="request_blocked">%1$d. Блокирован</string>
         <string name="blocked_plural">Блокировано</string>
     <string name="blocklist">Список блокировки</string>
     <string name="sublist">Подсписок</string>
index bef8d5a00728e36e56fbf31fae8e7098b6dcec69..2fd4676c36170b70b1eb45d7d3af6b59a6d834d9 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright 2015-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
         <string name="default_label">Varsayılan</string>
         <string name="default_allowed">Varsayılan - İzin verildi</string>
         <string name="allowed">İzin verildi</string>
+        <string name="request_allowed">%1$d. İzin verildi</string>
         <string name="allowed_plural">İzin verildi</string>
         <string name="third_party_plural">Üçüncü Taraflar</string>
         <string name="third_party_blocked">Üçüncü Taraf - Engellendi</string>
         <string name="blocked">Engellendi</string>
+        <string name="request_blocked">%1$d. Engellendi</string>
         <string name="blocked_plural">Engellendi</string>
     <string name="blocklist">Engel listesi</string>
     <string name="sublist">Alt liste</string>
index f10a6e82c77aa73979342411413d7095f2d16375..a2053024aa817ff5b17e7fcc309ba8f8b7a65623 100644 (file)
     <string name="file_is_mht">本文件是MHT存档.</string>
     <string name="mht_checkbox_explanation">有时需要手动选择MIME封装才能正确打开.</string>
 
-    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
         The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <string name="save_url">保存链接</string>
     <string name="save_archive">保存存档</string>
     <string name="bytes">字节</string>
     <string name="unknown_size">未知大小</string>
     <string name="invalid_url">错误的网址</string>
-    <string name="saving_file">保存文件:</string>
+    <string name="saving_file">保存文件: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">保存文件: \u0020 %1$s 字节 - %2$s</string>
+    <string name="saving_file_percentage_progress">保存文件: \u0020 %1$d%% - %2$s 字节 / %3$s 字节 - %4$s</string>
     <string name="saved">%1$s 保存.</string>
     <string name="processing_image">处理图片: \u0020 %1$s</string>
     <string name="error_saving_file">保存失败 %1$s: \u0020 %2$s</string>
         <string name="default_label">默认</string>
         <string name="default_allowed">默认允许</string>
         <string name="allowed">允许</string>
+        <string name="request_allowed">%1$d. 允许</string>
         <string name="allowed_plural">允许</string>
         <string name="third_party_plural">第三方</string>
         <string name="third_party_blocked">不允许第三方</string>
         <string name="blocked">不允许</string>
+        <string name="request_blocked">%1$d. 不允许</string>
         <string name="blocked_plural">不允许</string>
     <string name="blocklist">不允许名单</string>
     <string name="sublist">子表</string>
index bb892ed2d08b30b7cfd19f12f3c0fb7f51283404..22a67a337d385b5ff2d16fa735ba3187d410dc0a 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright 2015-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
     <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 Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.
-        The `%*$s` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
+    <!-- Save Dialog.  Android removes double spaces, but extra spaces can be manually specified with the Unicode `\u0020` formatting.  `%%` writes a literal `%`.
+        The `%*$*` code inserts variables into the displayed text and should be preserved in translation.  <https://developer.android.com/reference/kotlin/java/util/Formatter> -->
     <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="bytes">bytes</string>
     <string name="unknown_size">unknown size</string>
     <string name="invalid_url">invalid URL</string>
-    <string name="saving_file">Saving file:</string>
+    <string name="saving_file">Saving file: \u0020 %1$d%% - %2$s</string>
+    <string name="saving_file_progress">Saving file: \u0020 %1$s bytes - %2$s</string>
+    <string name="saving_file_percentage_progress">Saving file: \u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s</string>
     <string name="saved">%1$s saved.</string>
     <string name="processing_image">Processing image: \u0020 %1$s</string>
     <string name="error_saving_file">Error saving %1$s: \u0020 %2$s</string>
         <string name="default_label">Default</string>
         <string name="default_allowed">Default - Allowed</string>
         <string name="allowed">Allowed</string>
+        <string name="request_allowed">%1$d. Allowed</string>
         <string name="allowed_plural">Allowed</string>
         <string name="third_party_plural">Third-party</string>
         <string name="third_party_blocked">Third-party - Blocked</string>
         <string name="blocked">Blocked</string>
+        <string name="request_blocked">%1$d. Blocked</string>
         <string name="blocked_plural">Blocked</string>
     <string name="blocklist">Blocklist</string>
     <string name="sublist">Sublist</string>