From c2b5bdf009503f6761dc830fb65502ad2910c284 Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Tue, 13 Dec 2022 13:57:50 -0700 Subject: [PATCH] Convert five AsyncTasks to Kotlin. https://redmine.stoutner.com/issues/931 --- app/src/main/assets/de/about_licenses.html | 38 +-- app/src/main/assets/de/guide_interface.html | 3 +- .../activities/MainWebViewActivity.java | 68 ++--- .../adapters/HistoryArrayAdapter.kt | 8 +- .../asynctasks/GetHostIpAddresses.java | 108 ------- .../privacybrowser/asynctasks/GetUrlSize.java | 174 ----------- .../asynctasks/PopulateBlocklists.java | 232 --------------- .../asynctasks/PrepareSaveDialog.java | 277 ------------------ .../asynctasks/SaveAboutVersionImage.java | 171 ----------- .../coroutines/GetHostIpAddressesCoroutine.kt | 74 +++++ .../coroutines/PopulateBlocklistsCoroutine.kt | 149 ++++++++++ .../coroutines/PrepareSaveDialogCoroutine.kt | 226 ++++++++++++++ .../SaveAboutVersionImageCoroutine.kt | 143 +++++++++ .../{History.kt => HistoryDataClass.kt} | 4 +- ...ingDialog.kt => PendingDialogDataClass.kt} | 4 +- .../privacybrowser/dialogs/SaveDialog.kt | 42 +-- .../dialogs/UrlHistoryDialog.kt | 14 +- .../fragments/AboutVersionFragment.kt | 10 +- .../helpers/CheckPinnedMismatchHelper.kt | 15 +- .../helpers/GetUrlSizeHelper.kt | 97 ++++++ app/src/main/res/values-de/strings.xml | 9 +- app/src/main/res/values-es/strings.xml | 9 +- app/src/main/res/values-fr/strings.xml | 5 +- app/src/main/res/values-it/strings.xml | 9 +- app/src/main/res/values-pt-rBR/strings.xml | 9 +- app/src/main/res/values-ru/strings.xml | 9 +- app/src/main/res/values-tr/strings.xml | 7 +- app/src/main/res/values/strings.xml | 9 +- 28 files changed, 827 insertions(+), 1096 deletions(-) delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetHostIpAddresses.java delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetUrlSize.java delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java create mode 100644 app/src/main/java/com/stoutner/privacybrowser/coroutines/GetHostIpAddressesCoroutine.kt create mode 100644 app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt create mode 100644 app/src/main/java/com/stoutner/privacybrowser/coroutines/PrepareSaveDialogCoroutine.kt create mode 100644 app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveAboutVersionImageCoroutine.kt rename app/src/main/java/com/stoutner/privacybrowser/dataclasses/{History.kt => HistoryDataClass.kt} (86%) rename app/src/main/java/com/stoutner/privacybrowser/dataclasses/{PendingDialog.kt => PendingDialogDataClass.kt} (85%) create mode 100644 app/src/main/java/com/stoutner/privacybrowser/helpers/GetUrlSizeHelper.kt diff --git a/app/src/main/assets/de/about_licenses.html b/app/src/main/assets/de/about_licenses.html index 39e5a15f..7c2165f4 100644 --- a/app/src/main/assets/de/about_licenses.html +++ b/app/src/main/assets/de/about_licenses.html @@ -77,25 +77,25 @@ Android Material icon set and are released under the Apache License 2.0. Modifications copyright 2017, 2022 Soren Stoutner. The resulting image is released under the GPLv3+ license.

-

is derived from create_new_folder, - which is part of the Android Material icon set and is released under the Apache License 2.0. - Modifications copyright 2017, 2022 Soren Stoutner. - The resulting image is released under the GPLv3+ license.

-

is derived from exit_to_app, - which is part of the Android Material icon set - and is released under the Apache License 2.0. Modifications copyright 2017, 2022 Soren Stoutner. - The resulting image is released under the GPLv3+ license.

-

is derived from compare, - which is part of the Android Material icon set and is released under the Apache License 2.0. - Modifications copyright 2017, 2022 Soren Stoutner. - The resulting image is released under the GPLv3+ license.

-

is derived from sort, which is part of the Android Material icon set - and is released under the Apache License 2.0. Modifications copyright 2019, 2022 Soren Stoutner. - The resulting image is released under the GPLv3+ license.

-

is derived from push_pin_selected, - which is part of the Android Material icon set and is released under the Apache License 2.0. - Modifications copyright 2019-2020, 2022 Soren Stoutner. - The resulting image is released under the GPLv3+ license.

+

ist abgeleitet von create_new_folder, + das Teil des Android Material icon set unter der Apache Lizenz 2.0 veröffentlicht wird. + Änderungen Copyright 2017, 2022 Soren Stoutner. + Die resultierende Grafik wird unter der GPLv3+ Lizenz veröffentlicht.

+

ist abgeleitet von exit_to_app, + das Teil des Android Material icon set unter der Apache Lizenz 2.0 veröffentlicht wird. + Änderungen Copyright 2017, 2022 Soren Stoutner. + Die resultierende Grafik wird unter der GPLv3+ Lizenz veröffentlicht.

+

ist abgeleitet von compare, + das Teil des Android Material icon set unter der Apache Lizenz 2.0 veröffentlicht wird. + Änderungen Copyright 2017, 2022 Soren Stoutner. + Die resultierende Grafik wird unter der GPLv3+ Lizenz veröffentlicht.

+

ist abgeleitet von sort, das Teil des Android Material icon set + unter der Apache Lizenz 2.0 veröffentlicht wird. Änderungen Copyright 2019, 2022 Soren Stoutner. + Die resultierende Grafik wird unter der GPLv3+ Lizenz veröffentlicht.

+

ist abgeleitet von push_pin_selected, + das Teil des Android Material icon set unter der Apache Lizenz 2.0 veröffentlicht wird. + Änderungen Copyright 2019-2020, 2022 Soren Stoutner. + Die resultierende Grafik wird unter der GPLv3+ Lizenz veröffentlicht.

cookie was created by Google. It is released under the Apache License 2.0 and can be downloaded from Material Design Icons. It is unchanged except for layout information like color and size.

diff --git a/app/src/main/assets/de/guide_interface.html b/app/src/main/assets/de/guide_interface.html index 95d00dda..73646278 100644 --- a/app/src/main/assets/de/guide_interface.html +++ b/app/src/main/assets/de/guide_interface.html @@ -36,7 +36,8 @@ bis das Menü angezeigt wird und wischen dann, um es zu öffnen. Auf manchen Geräten mit abgerundeten Kanten kann das Ausklappen des Menüs schwierig sein. Daher kann das Lesezeichen-Menü auch über das Options-Menü geöffnet werden.

-

Tapping on a bookmark opens it in the current tab. Long-pressing on a bookmark opens it in a new tab and long-pressing on a folder opens all the bookmarks it directly contains in new tabs. +

Kurzes Antippen eines Lesezeichens öffnet dieses im aktuellen Tab, + langes Drücken öffnet es in einem neuen Tab und langes Drücken auf einen Ordner öffnet alle direkt darin enthaltenen Lesezeichen in neuen Tabs. Durch Antippen des obersten Symbols im Lesezeichen-Menü können abgespeicherte Lesezeichen bearbeitet und umgeordnet werden.

diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 637e9a52..649cf644 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -46,7 +46,6 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.http.SslCertificate; import android.net.http.SslError; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -129,12 +128,12 @@ import com.google.android.material.tabs.TabLayout; import com.stoutner.privacybrowser.R; import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter; -import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses; -import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists; -import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog; import com.stoutner.privacybrowser.asynctasks.SaveUrl; import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage; -import com.stoutner.privacybrowser.dataclasses.PendingDialog; +import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine; +import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine; +import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine; +import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass; import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog; import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog; @@ -186,12 +185,12 @@ import kotlin.Pair; public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, PinnedMismatchDialog.PinnedMismatchListener, - PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener { + PopulateBlocklistsCoroutine.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener { // Define the public static variables. public static final ExecutorService executorService = Executors.newFixedThreadPool(4); public static String orbotStatus = "unknown"; - public static final ArrayList pendingDialogsArrayList = new ArrayList<>(); + public static final ArrayList pendingDialogsArrayList = new ArrayList<>(); public static String proxyMode = ProxyHelper.NONE; // Declare the public static variables. @@ -223,10 +222,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private int savedTabPosition; private String savedProxyMode; - // Define the class variables. - @SuppressWarnings("rawtypes") - AsyncTask populateBlocklists; - // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`. private NestedScrollWebView currentWebView; @@ -664,8 +659,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Register the on back pressed callback. getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + // Instantiate the populate blocklists coroutine. + PopulateBlocklistsCoroutine populateBlocklistsCoroutine = new PopulateBlocklistsCoroutine(this); + // Populate the blocklists. - populateBlocklists = new PopulateBlocklists(this, this).execute(); + populateBlocklistsCoroutine.populateBlocklists(this); } @Override @@ -858,10 +856,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Show any pending dialogs. for (int i = 0; i < pendingDialogsArrayList.size(); i++) { // Get the pending dialog from the array list. - PendingDialog pendingDialog = pendingDialogsArrayList.get(i); + PendingDialogDataClass pendingDialogDataClass = pendingDialogsArrayList.get(i); // Show the pending dialog. - pendingDialog.dialogFragment.show(getSupportFragmentManager(), pendingDialog.tag); + pendingDialogDataClass.dialogFragment.show(getSupportFragmentManager(), pendingDialogDataClass.tag); } // Clear the pending dialogs array list. @@ -959,11 +957,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook bookmarksDatabaseHelper.close(); } - // Stop populating the blocklists if the AsyncTask is running in the background. - if (populateBlocklists != null) { - populateBlocklists.cancel(true); - } - // Run the default commands. super.onDestroy(); } @@ -1856,8 +1849,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(currentWebView.getCurrentUrl()); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl()); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), currentWebView.getCurrentUrl(), currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()); } // Consume the event. @@ -2353,8 +2346,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(linkUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(linkUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), linkUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2425,8 +2417,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(imageUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(imageUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), imageUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2530,8 +2521,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(imageUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(imageUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), imageUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2557,8 +2547,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(linkUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(linkUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), linkUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -4098,7 +4087,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog)); } catch (Exception waitingForTorException) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog))); } } } @@ -4114,7 +4103,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog)); } catch (Exception orbotNotInstalledException) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); } } } @@ -4150,7 +4139,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog)); } catch (Exception i2pNotInstalledException) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); } } } @@ -5151,7 +5140,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } // Get the file name from the content disposition. - String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl); + String fileNameString = PrepareSaveDialogCoroutine.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl); // Instantiate the save dialog. DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, formattedFileSizeString, fileNameString, userAgent, @@ -5163,7 +5152,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); } catch (Exception exception) { // The dialog could not be shown. // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(saveDialogFragment, getString(R.string.save_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog))); } } }); @@ -5872,8 +5861,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get a URI for the current URL. Uri currentUri = Uri.parse(url); - // Get the IP addresses for the host. - new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost()); + // Get the current domain name. + String currentDomainName = currentUri.getHost(); + + if ((currentDomainName != null) && !currentDomainName.isEmpty()) { + // Get the IP addresses for the current URI. + GetHostIpAddressesCoroutine.getAddresses(currentDomainName, nestedScrollWebView, getSupportFragmentManager(), getString(R.string.pinned_mismatch)); + } // Replace Refresh with Stop if the options menu has been created. (The first WebView typically begins loading before the menu items are instantiated.) if (optionsMenu != null) { @@ -6060,7 +6054,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); } catch (Exception exception) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error))); + pendingDialogsArrayList.add(new PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error))); } } } diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/HistoryArrayAdapter.kt b/app/src/main/java/com/stoutner/privacybrowser/adapters/HistoryArrayAdapter.kt index acfcc422..3c746957 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/adapters/HistoryArrayAdapter.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/adapters/HistoryArrayAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2019,2021-2022 Soren Stoutner . + * Copyright 2016-2019,2021-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -29,11 +29,11 @@ import android.widget.ImageView import android.widget.TextView import com.stoutner.privacybrowser.R -import com.stoutner.privacybrowser.dataclasses.History +import com.stoutner.privacybrowser.dataclasses.HistoryDataClass import java.util.ArrayList -class HistoryArrayAdapter(context: Context, historyArrayList: ArrayList, private val currentPageId: Int) : ArrayAdapter(context, 0, historyArrayList) { +class HistoryArrayAdapter(context: Context, historyDataClassArrayList: ArrayList, private val currentPageId: Int) : ArrayAdapter(context, 0, historyDataClassArrayList) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { // Initialize a populated view from the convert view. var populatedView = convertView @@ -64,4 +64,4 @@ class HistoryArrayAdapter(context: Context, historyArrayList: ArrayList // Return the populated view. return populatedView } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetHostIpAddresses.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetHostIpAddresses.java deleted file mode 100644 index c3e30c61..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetHostIpAddresses.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright © 2019,2021-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -package com.stoutner.privacybrowser.asynctasks; - -import android.app.Activity; -import android.os.AsyncTask; - -import androidx.fragment.app.FragmentManager; - -import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper; -import com.stoutner.privacybrowser.views.NestedScrollWebView; - -import java.lang.ref.WeakReference; -import java.net.InetAddress; -import java.net.UnknownHostException; - -// This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `String` contains the results. -public class GetHostIpAddresses extends AsyncTask { - // The weak references are used to determine if the activity have disappeared while the AsyncTask is running. - private final WeakReference activityWeakReference; - private final WeakReference fragmentManagerWeakReference; - private final WeakReference nestedScrollWebViewWeakReference; - - public GetHostIpAddresses(Activity activity, FragmentManager fragmentManager, NestedScrollWebView nestedScrollWebView) { - // Populate the weak references. - activityWeakReference = new WeakReference<>(activity); - fragmentManagerWeakReference = new WeakReference<>(fragmentManager); - nestedScrollWebViewWeakReference = new WeakReference<>(nestedScrollWebView); - } - - @Override - protected String doInBackground(String... domainName) { - // Get a handles for the weak references. - Activity activity = activityWeakReference.get(); - FragmentManager fragmentManager = fragmentManagerWeakReference.get(); - NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get(); - - // Abort if the activity or its components are gone. - if ((activity == null) || activity.isFinishing() || fragmentManager == null || nestedScrollWebView == null) { - // Return an empty spannable string builder. - return ""; - } - - // Initialize an IP address string builder. - StringBuilder ipAddresses = new StringBuilder(); - - // Get an array with the IP addresses for the host. - try { - // Get an array with all the IP addresses for the domain. - InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]); - - // Add each IP address to the string builder. - for (InetAddress inetAddress : inetAddressesArray) { - // Add a line break to the string builder if this is not the first IP address. - if (ipAddresses.length() > 0) { - ipAddresses.append("\n"); - } - - // Add the IP address to the string builder. - ipAddresses.append(inetAddress.getHostAddress()); - } - } catch (UnknownHostException exception) { - // Do nothing. - } - - // Return the string. - return ipAddresses.toString(); - } - - // `onPostExecute()` operates on the UI thread. - @Override - protected void onPostExecute(String ipAddresses) { - // Get a handle for the activity and the nested scroll WebView. - Activity activity = activityWeakReference.get(); - FragmentManager fragmentManager = fragmentManagerWeakReference.get(); - NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get(); - - // Abort if the activity or its components are gone. - if ((activity == null) || activity.isFinishing() || fragmentManager == null || nestedScrollWebView == null) { - return; - } - - // Store the IP addresses. - nestedScrollWebView.setCurrentIpAddresses(ipAddresses); - - // Checked for pinned mismatches if there is pinned information and it is not ignored. - if ((nestedScrollWebView.hasPinnedSslCertificate() || !nestedScrollWebView.getPinnedIpAddresses().equals("")) && !nestedScrollWebView.getIgnorePinnedDomainInformation()) { - CheckPinnedMismatchHelper.checkPinnedMismatch(activity, fragmentManager, nestedScrollWebView); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetUrlSize.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetUrlSize.java deleted file mode 100644 index 2f645baa..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetUrlSize.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright © 2020-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -package com.stoutner.privacybrowser.asynctasks; - -import android.content.Context; -import android.os.AsyncTask; -import android.webkit.CookieManager; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; - -import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.helpers.ProxyHelper; - -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.URL; -import java.text.NumberFormat; - -public class GetUrlSize extends AsyncTask { - // Define weak references for the calling context and alert dialog. - private final WeakReference contextWeakReference; - private final WeakReference alertDialogWeakReference; - - // Define the class variables. - private final String userAgent; - private final boolean cookiesEnabled; - - // The public constructor. - public GetUrlSize(Context context, AlertDialog alertDialog, String userAgent, boolean cookiesEnabled) { - // Populate the week references for the context and alert dialog. - contextWeakReference = new WeakReference<>(context); - alertDialogWeakReference = new WeakReference<>(alertDialog); - - // Store the class variables. - this.userAgent = userAgent; - this.cookiesEnabled = cookiesEnabled; - } - - @Override - protected String doInBackground(String... urlToSave) { - // Get a handle for the context and the fragment. - Context context = contextWeakReference.get(); - AlertDialog alertDialog = alertDialogWeakReference.get(); - - // Abort if the fragment is gone. - if (alertDialog == null) { - return null; - } - - // Initialize the formatted file size string. - String formattedFileSize = context.getString(R.string.unknown_size); - - // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions. - try { - // Get the URL from the calling fragment. - URL url = new URL(urlToSave[0]); - - // 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 { - // Exit if the task has been cancelled. - if (isCancelled()) { - // Disconnect the HTTP URL connection. - httpUrlConnection.disconnect(); - - // Return the formatted file size string. - return formattedFileSize; - } - - // Get the status code. This initiates a network connection. - int responseCode = httpUrlConnection.getResponseCode(); - - // Exit if the task has been cancelled. - if (isCancelled()) { - // Disconnect the HTTP URL connection. - httpUrlConnection.disconnect(); - - // Return the formatted file size string. - return formattedFileSize; - } - - // Check the response code. - if (responseCode >= 400) { // The response code is an error message. - // Set the formatted file size to indicate a bad URL. - formattedFileSize = context.getString(R.string.invalid_url); - } else { // The response code is not an error message. - // Get the content length header. - String contentLengthString = httpUrlConnection.getHeaderField("Content-Length"); - - // Only process the content length string if it isn't null. - if (contentLengthString != null) { - // Convert the content length string to a long. - long fileSize = Long.parseLong(contentLengthString); - - // Format the file size. - formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes); - } - } - } finally { - // Disconnect the HTTP URL connection. - httpUrlConnection.disconnect(); - } - } catch (Exception exception) { - // Set the formatted file size to indicate a bad URL. - formattedFileSize = context.getString(R.string.invalid_url); - } - - // Return the formatted file size string. - return formattedFileSize; - } - - // `onPostExecute()` operates on the UI thread. - @Override - protected void onPostExecute(String fileSize) { - // Get a handle for the alert dialog. - AlertDialog alertDialog = alertDialogWeakReference.get(); - - // Abort if the alert dialog is gone. - if (alertDialog == null) { - return; - } - - // Get a handle for the file size text view. - TextView fileSizeTextView = alertDialog.findViewById(R.id.file_size_textview); - - // Remove the incorrect warning below that the file size text view might be null. - assert fileSizeTextView != null; - - // Update the file size. - fileSizeTextView.setText(fileSize); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java deleted file mode 100644 index af2bea14..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PopulateBlocklists.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright © 2019,2021-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -package com.stoutner.privacybrowser.asynctasks; - -import android.app.Activity; -import android.content.Context; -import android.os.AsyncTask; -import android.view.View; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.drawerlayout.widget.DrawerLayout; - -import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.helpers.BlocklistHelper; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -public class PopulateBlocklists extends AsyncTask>>> { - // The public interface is used to send information back to the parent activity. - public interface PopulateBlocklistsListener { - void finishedPopulatingBlocklists(ArrayList>> combinedBlocklists); - } - - // Define a populate blocklists listener. - private final PopulateBlocklistsListener populateBlocklistsListener; - - // Define weak references for the activity and context. - private final WeakReference contextWeakReference; - private final WeakReference activityWeakReference; - - // The public constructor. - public PopulateBlocklists(Context context, Activity activity) { - // Populate the weak reference to the context. - contextWeakReference = new WeakReference<>(context); - - // Populate the weak reference to the activity. - activityWeakReference = new WeakReference<>(activity); - - // Get a handle for the populate blocklists listener from the launching activity. - populateBlocklistsListener = (PopulateBlocklistsListener) context; - } - - // `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 handles for the views. - RelativeLayout loadingBlocklistsRelativeLayout = activity.findViewById(R.id.loading_blocklists_relativelayout); - - // Show the loading blocklists screen. - loadingBlocklistsRelativeLayout.setVisibility(View.VISIBLE); - } - - @Override - protected ArrayList>> doInBackground(Void... none) { - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - // Get a handle for the context. - Context context = contextWeakReference.get(); - - // Instantiate the blocklist helper. - BlocklistHelper blocklistHelper = new BlocklistHelper(); - - // Create a combined array list. - ArrayList>> combinedBlocklists = new ArrayList<>(); - - // Load the blocklists if the context still exists. - if (context != null) { - // Update the progress. - publishProgress(context.getString(R.string.loading_easylist)); - - // Populate EasyList. - ArrayList> easyList = blocklistHelper.parseBlocklist(context.getAssets(), "blocklists/easylist.txt"); - - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - - // Update the progress. - publishProgress(context.getString(R.string.loading_easyprivacy)); - - // Populate EasyPrivacy. - ArrayList> easyPrivacy = blocklistHelper.parseBlocklist(context.getAssets(), "blocklists/easyprivacy.txt"); - - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - - - // Update the progress. - publishProgress(context.getString(R.string.loading_fanboys_annoyance_list)); - - // Populate Fanboy's Annoyance List. - ArrayList> fanboysAnnoyanceList = blocklistHelper.parseBlocklist(context.getAssets(), "blocklists/fanboy-annoyance.txt"); - - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - - - // Update the progress. - publishProgress(context.getString(R.string.loading_fanboys_social_blocking_list)); - - // Populate Fanboy's Social Blocking List. - ArrayList> fanboysSocialList = blocklistHelper.parseBlocklist(context.getAssets(), "blocklists/fanboy-social.txt"); - - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - - - // Update the progress. - publishProgress(context.getString(R.string.loading_ultralist)); - - // Populate UltraList. - ArrayList> ultraList = blocklistHelper.parseBlocklist(context.getAssets(), "blocklists/ultralist.txt"); - - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - - - // Update the progress. - publishProgress(context.getString(R.string.loading_ultraprivacy)); - - // Populate UltraPrivacy. - ArrayList> ultraPrivacy = blocklistHelper.parseBlocklist(context.getAssets(), "blocklists/ultraprivacy.txt"); - - // Exit the AsyncTask if the app has been restarted. - if (isCancelled()) { - return null; - } - - - - // Populate the combined array list. - combinedBlocklists.add(easyList); - combinedBlocklists.add(easyPrivacy); - combinedBlocklists.add(fanboysAnnoyanceList); - combinedBlocklists.add(fanboysSocialList); - combinedBlocklists.add(ultraList); - combinedBlocklists.add(ultraPrivacy); - } - - // Return the combined array list. - return combinedBlocklists; - } - - @Override - protected void onProgressUpdate(String... loadingBlocklist) { - // 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 loading blocklist text view. - TextView loadingBlocklistTextView = activity.findViewById(R.id.loading_blocklist_textview); - - // Update the status. - loadingBlocklistTextView.setText(loadingBlocklist[0]); - } - - @Override - protected void onPostExecute(ArrayList>> combinedBlocklists) { - // Get a handle for the activity. - Activity activity = activityWeakReference.get(); - - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return; - } - - // Get handles for the views. - DrawerLayout drawerLayout = activity.findViewById(R.id.drawerlayout); - RelativeLayout loadingBlocklistsRelativeLayout = activity.findViewById(R.id.loading_blocklists_relativelayout); - - // Show the drawer layout. - drawerLayout.setVisibility(View.VISIBLE); - - // Hide the loading blocklists screen. - loadingBlocklistsRelativeLayout.setVisibility(View.GONE); - - // Enable the sliding drawers. - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); - - // Add the first tab. - populateBlocklistsListener.finishedPopulatingBlocklists(combinedBlocklists); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java deleted file mode 100644 index b869099d..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/PrepareSaveDialog.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright © 2020-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -package com.stoutner.privacybrowser.asynctasks; - -import android.app.Activity; -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; -import android.webkit.CookieManager; -import android.webkit.MimeTypeMap; - -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentManager; - -import com.stoutner.privacybrowser.R; -import com.stoutner.privacybrowser.activities.MainWebViewActivity; -import com.stoutner.privacybrowser.dataclasses.PendingDialog; -import com.stoutner.privacybrowser.dialogs.SaveDialog; -import com.stoutner.privacybrowser.helpers.ProxyHelper; - -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.URL; -import java.text.NumberFormat; - -public class PrepareSaveDialog extends AsyncTask { - // Define weak references. - private final WeakReference activityWeakReference; - private final WeakReference contextWeakReference; - private final WeakReference fragmentManagerWeakReference; - - // Define the class variables. - private final String userAgent; - private final boolean cookiesEnabled; - private String urlString; - - // The public constructor. - public PrepareSaveDialog(Activity activity, Context context, FragmentManager fragmentManager, String userAgent, boolean cookiesEnabled) { - // Populate the weak references. - activityWeakReference = new WeakReference<>(activity); - contextWeakReference = new WeakReference<>(context); - fragmentManagerWeakReference = new WeakReference<>(fragmentManager); - - // Store the class variables. - this.userAgent = userAgent; - this.cookiesEnabled = cookiesEnabled; - } - - @Override - protected String[] doInBackground(String... urlToSave) { - // Get a handle for the activity and context. - Activity activity = activityWeakReference.get(); - Context context = contextWeakReference.get(); - - // Abort if the activity is gone. - if (activity == null || activity.isFinishing()) { - // Return a null string array. - return null; - } - - // Get the URL string. - urlString = urlToSave[0]; - - // Define the strings. - String formattedFileSize; - String fileNameString; - - // Populate the file size and name strings. - if (urlString.startsWith("data:")) { // The URL contains the entire data of an image. - // Remove `data:` from the beginning of the URL. - String urlWithoutData = urlString.substring(5); - - // Get the URL MIME type, which ends with a `;`. - String urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";")); - - // Get the Base64 data, which begins after a `,`. - String base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1); - - // Calculate the file size of the data URL. Each Base64 character represents 6 bits. - formattedFileSize = NumberFormat.getInstance().format(base64DataString.length() * 3L / 4) + " " + context.getString(R.string.bytes); - - // Set the file name according to the MIME type. - fileNameString = context.getString(R.string.file) + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(urlMimeType); - } else { // The URL refers to the location of the data. - // Initialize the formatted file size string. - formattedFileSize = context.getString(R.string.unknown_size); - - // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions. - try { - // Convert the URL string to a URL. - 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 status code. This initiates a network connection. - int responseCode = httpUrlConnection.getResponseCode(); - - // Check the response code. - if (responseCode >= 400) { // The response code is an error message. - // Set the formatted file size to indicate a bad URL. - formattedFileSize = context.getString(R.string.invalid_url); - - // Set the file name according to the URL. - fileNameString = getFileNameFromUrl(context, urlString, null); - } else { // The response code is not an error message. - // Get the headers. - String contentLengthString = httpUrlConnection.getHeaderField("Content-Length"); - String contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition"); - String contentTypeString = httpUrlConnection.getContentType(); - - // Remove anything after the MIME type in the content type string. - if (contentTypeString.contains(";")) { - // Remove everything beginning with the `;`. - contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";")); - } - - // Only process the content length string if it isn't null. - if (contentLengthString != null) { - // Convert the content length string to a long. - long fileSize = Long.parseLong(contentLengthString); - - // Format the file size. - formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes); - } - - // Get the file name string from the content disposition. - fileNameString = getFileNameFromHeaders(context, contentDispositionString, contentTypeString, urlString); - } - } finally { - // Disconnect the HTTP URL connection. - httpUrlConnection.disconnect(); - } - } catch (Exception exception) { - // Set the formatted file size to indicate a bad URL. - formattedFileSize = context.getString(R.string.invalid_url); - - // Set the file name according to the URL. - fileNameString = getFileNameFromUrl(context, urlString, null); - } - } - - // Return the formatted file size and name as a string array. - return new String[] {formattedFileSize, fileNameString}; - } - - // `onPostExecute()` operates on the UI thread. - @Override - protected void onPostExecute(String[] fileStringArray) { - // Get a handle for the activity and the fragment manager. - Activity activity = activityWeakReference.get(); - FragmentManager fragmentManager = fragmentManagerWeakReference.get(); - - // Abort if the activity is gone. - if (activity == null || activity.isFinishing()) { - // Exit. - return; - } - - // Instantiate the save dialog. - DialogFragment saveDialogFragment = SaveDialog.saveUrl(urlString, fileStringArray[0], fileStringArray[1], userAgent, cookiesEnabled); - - // Try to show the dialog. Sometimes the window is not active. - try { - // Show the save dialog. It must be named `save_dialog` so that the file picker can update the file name. - saveDialogFragment.show(fragmentManager, activity.getString(R.string.save_dialog)); - } catch (Exception exception) { - // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - MainWebViewActivity.pendingDialogsArrayList.add(new PendingDialog(saveDialogFragment, activity.getString(R.string.save_dialog))); - } - } - - // Content dispositions can contain other text besides the file name, and they can be in any order. - // Elements are separated by semicolons. Sometimes the file names are contained in quotes. - public static String getFileNameFromHeaders(Context context, String contentDispositionString, String contentTypeString, String urlString) { - // Define a file name string. - String fileNameString; - - // Only process the content disposition string if it isn't null. - if (contentDispositionString != null) { // The content disposition is not null. - // Check to see if the content disposition contains a file name. - if (contentDispositionString.contains("filename=")) { // The content disposition contains a filename. - // Get the part of the content disposition after `filename=`. - fileNameString = contentDispositionString.substring(contentDispositionString.indexOf("filename=") + 9); - - // Remove any `;` and anything after it. This removes any entries after the filename. - if (fileNameString.contains(";")) { - // Remove the first `;` and everything after it. - fileNameString = fileNameString.substring(0, fileNameString.indexOf(";") - 1); - } - - // Remove any `"` at the beginning of the string. - if (fileNameString.startsWith("\"")) { - // Remove the first character. - fileNameString = fileNameString.substring(1); - } - - // Remove any `"` at the end of the string. - if (fileNameString.endsWith("\"")) { - // Remove the last character. - fileNameString = fileNameString.substring(0, fileNameString.length() - 1); - } - } else { // The headers contain no useful information. - // Get the file name string from the URL. - fileNameString = getFileNameFromUrl(context, urlString, contentTypeString); - } - } else { // The content disposition is null. - // Get the file name string from the URL. - fileNameString = getFileNameFromUrl(context, urlString, contentTypeString); - } - - // Return the file name string. - return fileNameString; - } - - private static String getFileNameFromUrl(Context context, String urlString, String contentTypeString) { - // Convert the URL string to a URI. - Uri uri = Uri.parse(urlString); - - // Get the last path segment. - String lastPathSegment = uri.getLastPathSegment(); - - // Use a default file name if the last path segment is null. - if (lastPathSegment == null) { - lastPathSegment = context.getString(R.string.file); - - if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString)) { // The content type contains a MIME type. - // Add the file extension that matches the MIME type. - lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString); - } - } - - // Return the last path segment as the file name. - return lastPathSegment; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java deleted file mode 100644 index 49ae1d9f..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveAboutVersionImage.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2020-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -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 android.widget.LinearLayout; - -import com.google.android.material.snackbar.Snackbar; - -import com.stoutner.privacybrowser.R; - -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; - -public class SaveAboutVersionImage extends AsyncTask { - // Declare the class constants. - private final String SUCCESS = "Success"; - - // Declare the weak references. - private final WeakReference activityWeakReference; - private final WeakReference aboutVersionLinearLayoutWeakReference; - - // Declare the class variables. - private Snackbar savingImageSnackbar; - private Bitmap aboutVersionBitmap; - private final Uri fileUri; - private final String fileNameString; - - // The public constructor. - public SaveAboutVersionImage(Activity activity, Uri fileUri, LinearLayout aboutVersionLinearLayout) { - // Populate the weak references. - activityWeakReference = new WeakReference<>(activity); - aboutVersionLinearLayoutWeakReference = new WeakReference<>(aboutVersionLinearLayout); - - // Store 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 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 linear layout. - Activity activity = activityWeakReference.get(); - LinearLayout aboutVersionLinearLayout = aboutVersionLinearLayoutWeakReference.get(); - - // Abort if the activity or the linear layout is gone. - if ((activity == null) || activity.isFinishing() || aboutVersionLinearLayout == null) { - return; - } - - // Create a saving image snackbar. - savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image) + " " + fileNameString, Snackbar.LENGTH_INDEFINITE); - - // Display the saving image snackbar. - savingImageSnackbar.show(); - - // Create the about version bitmap. This can be replaced by PixelCopy once the minimum API >= 26. - // Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The linear layout commands must be run on the UI thread. - aboutVersionBitmap = Bitmap.createBitmap(aboutVersionLinearLayout.getWidth(), aboutVersionLinearLayout.getHeight(), Bitmap.Config.ARGB_8888); - - // Create a canvas. - Canvas aboutVersionCanvas = new Canvas(aboutVersionBitmap); - - // Draw the current about version onto the bitmap. The linear layout commands must be run on the UI thread. - aboutVersionLinearLayout.draw(aboutVersionCanvas); - } - - @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 an about version PNG byte array output stream. - ByteArrayOutputStream aboutVersionByteArrayOutputStream = 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. - aboutVersionBitmap.compress(Bitmap.CompressFormat.PNG, 0, aboutVersionByteArrayOutputStream); - - // Create a file creation disposition string. - String fileCreationDisposition = SUCCESS; - - try { - // Open an output stream. - OutputStream outputStream = activity.getContentResolver().openOutputStream(fileUri); - - // Write the webpage image to the image file. - aboutVersionByteArrayOutputStream.writeTo(outputStream); - - // Close the output stream. - outputStream.close(); - } 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(); - LinearLayout aboutVersionLinearLayout = aboutVersionLinearLayoutWeakReference.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)) { - // Create a file saved snackbar. - Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.error_saving_file, fileNameString, fileCreationDisposition), Snackbar.LENGTH_INDEFINITE).show(); - } - } -} diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/GetHostIpAddressesCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/GetHostIpAddressesCoroutine.kt new file mode 100644 index 00000000..cc10176f --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/GetHostIpAddressesCoroutine.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019,2021-2022 Soren Stoutner . + * + * This file is part of 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 . + */ + +package com.stoutner.privacybrowser.coroutines + +import androidx.fragment.app.FragmentManager +import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper +import com.stoutner.privacybrowser.views.NestedScrollWebView + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +import java.lang.StringBuilder +import java.net.InetAddress +import java.net.UnknownHostException + +object GetHostIpAddressesCoroutine { + @JvmStatic + fun getAddresses(domainName: String, nestedScrollWebView: NestedScrollWebView, supportFragmentManager: FragmentManager, pinnedMismatchString: String) { + // Get the IP addresses using a coroutine. + CoroutineScope(Dispatchers.Main).launch { + // Get the IP addresses on the IO thread. + withContext(Dispatchers.IO) { + // Get an array with the IP addresses for the host. + try { + // Initialize an IP address string builder. + val ipAddresses = StringBuilder() + + // Get an array with all the IP addresses for the domain. + val inetAddressesArray = InetAddress.getAllByName(domainName) + + // Add each IP address to the string builder. + for (inetAddress in inetAddressesArray) { + // Add a line break to the string builder if this is not the first IP address. + if (ipAddresses.isNotEmpty()) { + ipAddresses.append("\n") + } + + // Add the IP address to the string builder. + ipAddresses.append(inetAddress.hostAddress) + } + + // Store the IP addresses. + nestedScrollWebView.currentIpAddresses = ipAddresses.toString() + } catch (exception: UnknownHostException) { + // Do nothing. + } + } + } + + // Checked for pinned mismatches if there is pinned information and it is not ignored. + if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.pinnedIpAddresses != "") && !nestedScrollWebView.ignorePinnedDomainInformation) { + CheckPinnedMismatchHelper.checkPinnedMismatch(nestedScrollWebView, supportFragmentManager, pinnedMismatchString) + } + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt new file mode 100644 index 00000000..4311ef5e --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2019,2021-2022 Soren Stoutner . + * + * This file is part of 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 . + */ + +package com.stoutner.privacybrowser.coroutines + +import android.app.Activity +import android.content.Context +import android.view.View +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.drawerlayout.widget.DrawerLayout + +import com.stoutner.privacybrowser.R +import com.stoutner.privacybrowser.helpers.BlocklistHelper + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +import java.util.ArrayList + +class PopulateBlocklistsCoroutine(context: Context) { + // The public interface is used to send information back to the parent activity. + interface PopulateBlocklistsListener { + fun finishedPopulatingBlocklists(combinedBlocklists: ArrayList>>>) + } + + // Define a populate blocklists listener. + private val populateBlocklistsListener: PopulateBlocklistsListener + + // Define the class variables. + private val context: Context + + // The public constructor. + init { + // Get a handle for the populate blocklists listener from the launching activity. + populateBlocklistsListener = context as PopulateBlocklistsListener + + // Store the context. + this.context = context + } + + fun populateBlocklists(activity: Activity) { + // Use a coroutine to populate the blocklists. + CoroutineScope(Dispatchers.Main).launch { + // Get handles for the views. + val drawerLayout = activity.findViewById(R.id.drawerlayout) + val loadingBlocklistsRelativeLayout = activity.findViewById(R.id.loading_blocklists_relativelayout) + val loadingBlocklistTextView = activity.findViewById(R.id.loading_blocklist_textview) + + // Show the loading blocklists screen. + loadingBlocklistsRelativeLayout.visibility = View.VISIBLE + + // Instantiate the blocklist helper. + val blocklistHelper = BlocklistHelper() + + // Create a combined array list. + val combinedBlocklists = ArrayList>>>() + + // Advertise the loading of EasyList. + loadingBlocklistTextView.text = context.getString(R.string.loading_easylist) + + withContext(Dispatchers.IO) { + // Populate EasyList. + val easyList = blocklistHelper.parseBlocklist(context.assets, "blocklists/easylist.txt") + + // Advertise the loading of EasyPrivacy. + withContext(Dispatchers.Main) { + loadingBlocklistTextView.text = context.getString(R.string.loading_easyprivacy) + } + + // Populate EasyPrivacy. + val easyPrivacy = blocklistHelper.parseBlocklist(context.assets, "blocklists/easyprivacy.txt") + + // Advertise the loading of Fanboy's Annoyance List. + withContext(Dispatchers.Main) { + loadingBlocklistTextView.text = context.getString(R.string.loading_fanboys_annoyance_list) + } + + // Populate Fanboy's Annoyance List. + val fanboysAnnoyanceList = blocklistHelper.parseBlocklist(context.assets, "blocklists/fanboy-annoyance.txt") + + // Advertise the loading of Fanboy's social blocking list. + withContext(Dispatchers.Main) { + loadingBlocklistTextView.text = context.getString(R.string.loading_fanboys_social_blocking_list) + } + + // Populate Fanboy's Social Blocking List. + val fanboysSocialList = blocklistHelper.parseBlocklist(context.assets, "blocklists/fanboy-social.txt") + + // Advertise the loading of UltraList + withContext(Dispatchers.Main) { + loadingBlocklistTextView.text = context.getString(R.string.loading_ultralist) + } + + // Populate UltraList. + val ultraList = blocklistHelper.parseBlocklist(context.assets, "blocklists/ultralist.txt") + + // Advertise the loading of UltraPrivacy. + withContext(Dispatchers.Main) { + loadingBlocklistTextView.text = context.getString(R.string.loading_ultraprivacy) + } + + // Populate UltraPrivacy. + val ultraPrivacy = blocklistHelper.parseBlocklist(context.assets, "blocklists/ultraprivacy.txt") + + // Populate the combined array list. + combinedBlocklists.add(easyList) + combinedBlocklists.add(easyPrivacy) + combinedBlocklists.add(fanboysAnnoyanceList) + combinedBlocklists.add(fanboysSocialList) + combinedBlocklists.add(ultraList) + combinedBlocklists.add(ultraPrivacy) + + // Update the UI. + withContext(Dispatchers.Main) { + // Show the drawer layout. + drawerLayout.visibility = View.VISIBLE + + // Hide the loading blocklists screen. + loadingBlocklistsRelativeLayout.visibility = View.GONE + + // Enable the sliding drawers. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + + // Add the first tab. + populateBlocklistsListener.finishedPopulatingBlocklists(combinedBlocklists) + } + } + } + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/PrepareSaveDialogCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/PrepareSaveDialogCoroutine.kt new file mode 100644 index 00000000..1af03207 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/PrepareSaveDialogCoroutine.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2020-2022 Soren Stoutner . + * + * This file is part of 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 . + */ + +package com.stoutner.privacybrowser.coroutines + +import android.content.Context +import android.net.Uri +import android.webkit.CookieManager +import android.webkit.MimeTypeMap + +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager + +import com.stoutner.privacybrowser.R +import com.stoutner.privacybrowser.activities.MainWebViewActivity +import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass +import com.stoutner.privacybrowser.dialogs.SaveDialog +import com.stoutner.privacybrowser.helpers.ProxyHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +import java.lang.Exception +import java.net.HttpURLConnection +import java.net.URL +import java.text.NumberFormat + +object PrepareSaveDialogCoroutine { + @JvmStatic + fun prepareSaveDialog(context: Context, supportFragmentManager: FragmentManager, urlString: String, userAgent: String, cookiesEnabled: Boolean) { + // Use a coroutine to prepare the save dialog. + CoroutineScope(Dispatchers.Main).launch { + // Make the network requests on the IO thread. + withContext(Dispatchers.IO) { + // Define the strings. + var formattedFileSize: String + var fileNameString: String + + // Populate the file size and name strings. + if (urlString.startsWith("data:")) { // The URL contains the entire data of an image. + // Remove `data:` from the beginning of the URL. + val urlWithoutData = urlString.substring(5) + + // Get the URL MIME type, which ends with a `;`. + val urlMimeType = urlWithoutData.substring(0, urlWithoutData.indexOf(";")) + + // Get the Base64 data, which begins after a `,`. + val base64DataString = urlWithoutData.substring(urlWithoutData.indexOf(",") + 1) + + // Calculate the file size of the data URL. Each Base64 character represents 6 bits. + formattedFileSize = NumberFormat.getInstance().format(base64DataString.length * 3L / 4) + " " + context.getString(R.string.bytes) + + // Set the file name according to the MIME type. + fileNameString = context.getString(R.string.file) + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(urlMimeType) + } else { // The URL refers to the location of the data. + // Initialize the formatted file size string. + formattedFileSize = context.getString(R.string.unknown_size) + + // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions. + try { + // Convert the URL string to a URL. + 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()) + + // Add the cookies if they are not null. + if (cookiesString != null) + 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 status code. This initiates a network connection. + val responseCode = httpUrlConnection.responseCode + + // Check the response code. + if (responseCode >= 400) { // The response code is an error message. + // Set the formatted file size to indicate a bad URL. + formattedFileSize = context.getString(R.string.invalid_url) + + // Set the file name according to the URL. + fileNameString = getFileNameFromUrl(context, urlString, null) + } else { // The response code is not an error message. + // Get the headers. + val contentLengthString = httpUrlConnection.getHeaderField("Content-Length") + val contentDispositionString = httpUrlConnection.getHeaderField("Content-Disposition") + var contentTypeString = httpUrlConnection.contentType + + // Remove anything after the MIME type in the content type string. + if (contentTypeString.contains(";")) + contentTypeString = contentTypeString.substring(0, contentTypeString.indexOf(";")) + + // Only process the content length string if it isn't null. + if (contentLengthString != null) { + // Convert the content length string to a long. + val fileSize = contentLengthString.toLong() + + // Format the file size. + formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes) + } + + // Get the file name string from the content disposition. + fileNameString = getFileNameFromHeaders(context, contentDispositionString, contentTypeString, urlString) + } + } finally { + // Disconnect the HTTP URL connection. + httpUrlConnection.disconnect() + } + } catch (exception: Exception) { + // Set the formatted file size to indicate a bad URL. + formattedFileSize = context.getString(R.string.invalid_url) + + // Set the file name according to the URL. + fileNameString = getFileNameFromUrl(context, urlString, null) + } + } + + // Display the dialog on the main thread. + withContext(Dispatchers.Main) { + // Instantiate the save dialog. + val saveDialogFragment: DialogFragment = SaveDialog.saveUrl(urlString, formattedFileSize, fileNameString, userAgent, cookiesEnabled) + + // Try to show the dialog. Sometimes the window is not active. + try { + // Show the save dialog. + saveDialogFragment.show(supportFragmentManager, context.getString(R.string.save_dialog)) + } catch (exception: Exception) { + // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. + MainWebViewActivity.pendingDialogsArrayList.add(PendingDialogDataClass(saveDialogFragment, context.getString(R.string.save_dialog))) + } + } + } + } + } + + // Content dispositions can contain other text besides the file name, and they can be in any order. + // Elements are separated by semicolons. Sometimes the file names are contained in quotes. + @JvmStatic + fun getFileNameFromHeaders(context: Context, contentDispositionString: String?, contentTypeString: String?, urlString: String): String { + // Define a file name string. + var fileNameString: String + + // Only process the content disposition string if it isn't null. + if (contentDispositionString != null) { // The content disposition is not null. + // Check to see if the content disposition contains a file name. + if (contentDispositionString.contains("filename=")) { // The content disposition contains a filename. + // Get the part of the content disposition after `filename=`. + fileNameString = contentDispositionString.substring(contentDispositionString.indexOf("filename=") + 9) + + // Remove any `;` and anything after it. This removes any entries after the filename. + if (fileNameString.contains(";")) + fileNameString = fileNameString.substring(0, fileNameString.indexOf(";") - 1) + + // Remove any `"` at the beginning of the string. + if (fileNameString.startsWith("\"")) + fileNameString = fileNameString.substring(1) + + // Remove any `"` at the end of the string. + if (fileNameString.endsWith("\"")) + fileNameString = fileNameString.substring(0, fileNameString.length - 1) + } else { // The headers contain no useful information. + // Get the file name string from the URL. + fileNameString = getFileNameFromUrl(context, urlString, contentTypeString) + } + } else { // The content disposition is null. + // Get the file name string from the URL. + fileNameString = getFileNameFromUrl(context, urlString, contentTypeString) + } + + // Return the file name string. + return fileNameString + } + + private fun getFileNameFromUrl(context: Context, urlString: String, contentTypeString: String?): String { + // Convert the URL string to a URI. + val uri = Uri.parse(urlString) + + // Get the last path segment. + var lastPathSegment = uri.lastPathSegment + + // Use a default file name if the last path segment is null. + if (lastPathSegment == null) { + // Set the last path segment to be the generic file name. + lastPathSegment = context.getString(R.string.file) + + // Add a file extension if it can be detected. + if (MimeTypeMap.getSingleton().hasMimeType(contentTypeString)) + lastPathSegment = lastPathSegment + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentTypeString) + } + + // Return the last path segment as the file name. + return lastPathSegment + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveAboutVersionImageCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveAboutVersionImageCoroutine.kt new file mode 100644 index 00000000..8fdd9d07 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveAboutVersionImageCoroutine.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2020-2022 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 android.widget.LinearLayout + +import com.google.android.material.snackbar.Snackbar + +import com.stoutner.privacybrowser.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +import java.io.ByteArrayOutputStream +import java.lang.Exception + +// Declare the class constants. +private const val SUCCESS = "Success" + +object SaveAboutVersionImageCoroutine { + fun saveImage(activity: Activity, fileUri: Uri, aboutVersionLinearLayout: LinearLayout) { + // Save the image using a coroutine. + CoroutineScope(Dispatchers.Main).launch { + // Create a saving image snackbar. + val savingImageSnackbar: Snackbar + + // Process the image on the IO thread. + withContext(Dispatchers.IO) { + // Instantiate a file name string. + val fileNameString: String + + // Query the exact file name if the API >= 26. + if (Build.VERSION.SDK_INT >= 26) { // The API >= 26. + // Get a cursor from the content resolver. + val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null) + + // Get the file display name if the content resolve cursor is not null. + if (contentResolverCursor != null) { // The content resolve cursor is not 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 { // The content resolve cursor is null. + // Use the URI last path segment as the file name string. + fileNameString = fileUri.lastPathSegment.toString() + } + } else { // The API is < 26. + // Use the URI last path segment as the file name string. + fileNameString = fileUri.lastPathSegment.toString() + } + + // Use the main thread to display a snackbar. + withContext(Dispatchers.Main) { + // Create a saving image snackbar. + savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image, fileNameString), Snackbar.LENGTH_INDEFINITE) + + // Display the saving image snackbar. + savingImageSnackbar.show() + } + + // Create the about version bitmap. This can be replaced by PixelCopy once the minimum API >= 26. + // Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The linear layout commands must be run on the UI thread. + val aboutVersionBitmap = Bitmap.createBitmap(aboutVersionLinearLayout.width, aboutVersionLinearLayout.height, Bitmap.Config.ARGB_8888) + + // Create a canvas. + val aboutVersionCanvas = Canvas(aboutVersionBitmap) + + // Use the main thread to interact with the linear layout. + withContext(Dispatchers.Main) { + // Draw the current about version onto the bitmap. + aboutVersionLinearLayout.draw(aboutVersionCanvas) + } + + // Create an about version PNG byte array output stream. + val aboutVersionByteArrayOutputStream = 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. + aboutVersionBitmap.compress(Bitmap.CompressFormat.PNG, 0, aboutVersionByteArrayOutputStream) + + // Create a file creation disposition string. + var fileCreationDisposition = SUCCESS + + // Write the image inside a try block to capture any write exceptions. + try { + // Open an output stream. + val outputStream = activity.contentResolver.openOutputStream(fileUri)!! + + // Write the webpage image to the image file. + aboutVersionByteArrayOutputStream.writeTo(outputStream) + + // Close the output stream. + outputStream.close() + } catch (exception: Exception) { + // Store the error in the file creation disposition string. + fileCreationDisposition = exception.toString() + } + + // Use the main thread to update the snackbars. + withContext(Dispatchers.Main) { + // Dismiss the saving image snackbar. + savingImageSnackbar.dismiss() + + // Display a file creation disposition snackbar. + if (fileCreationDisposition == SUCCESS) { + // Create a file saved snackbar. + Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show() + } else { + Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.error_saving_file, fileNameString, fileCreationDisposition), Snackbar.LENGTH_INDEFINITE).show() + } + } + } + } + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/dataclasses/History.kt b/app/src/main/java/com/stoutner/privacybrowser/dataclasses/HistoryDataClass.kt similarity index 86% rename from app/src/main/java/com/stoutner/privacybrowser/dataclasses/History.kt rename to app/src/main/java/com/stoutner/privacybrowser/dataclasses/HistoryDataClass.kt index a293c935..c6782021 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dataclasses/History.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dataclasses/HistoryDataClass.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017,2021-2022 Soren Stoutner . + * Copyright 2016-2017,2021-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -22,4 +22,4 @@ package com.stoutner.privacybrowser.dataclasses import android.graphics.Bitmap // Define the history data class. The `@JvmField` notation can be remove once all the code has migrated to Kotlin. -data class History(@JvmField val favoriteIcon: Bitmap, @JvmField val url: String) \ No newline at end of file +data class HistoryDataClass(@JvmField val favoriteIcon: Bitmap, @JvmField val url: String) diff --git a/app/src/main/java/com/stoutner/privacybrowser/dataclasses/PendingDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dataclasses/PendingDialogDataClass.kt similarity index 85% rename from app/src/main/java/com/stoutner/privacybrowser/dataclasses/PendingDialog.kt rename to app/src/main/java/com/stoutner/privacybrowser/dataclasses/PendingDialogDataClass.kt index 916f4c48..87d59726 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dataclasses/PendingDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dataclasses/PendingDialogDataClass.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2021-2022 Soren Stoutner . + * Copyright 2021-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -22,4 +22,4 @@ package com.stoutner.privacybrowser.dataclasses import androidx.fragment.app.DialogFragment // Define the pending dialogs data class. The `@JvmField` notation can be remove once all the code has migrated to Kotlin. -data class PendingDialog (@JvmField val dialogFragment: DialogFragment, @JvmField val tag: String) \ No newline at end of file +data class PendingDialogDataClass (@JvmField val dialogFragment: DialogFragment, @JvmField val tag: String) diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt index 77cd1c1a..63a6eaa3 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2022 Soren Stoutner . + * Copyright 2019-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -22,7 +22,6 @@ package com.stoutner.privacybrowser.dialogs import android.app.Dialog import android.content.Context import android.content.DialogInterface -import android.os.AsyncTask import android.os.Bundle import android.text.Editable import android.text.InputType @@ -36,7 +35,14 @@ import androidx.fragment.app.DialogFragment import androidx.preference.PreferenceManager import com.stoutner.privacybrowser.R -import com.stoutner.privacybrowser.asynctasks.GetUrlSize +import com.stoutner.privacybrowser.helpers.GetUrlSizeHelper + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +import java.net.URL // Define the class constants. private const val URL_STRING = "url_string" @@ -49,9 +55,6 @@ class SaveDialog : DialogFragment() { // Declare the class variables. private lateinit var saveListener: SaveListener - // Define the class variables. - private var getUrlSize: AsyncTask<*, *, *>? = null - // The public interface is used to send information back to the parent activity. interface SaveListener { fun onSaveUrl(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) @@ -170,26 +173,29 @@ class SaveDialog : DialogFragment() { } override fun afterTextChanged(editable: Editable) { - // Cancel the get URL size AsyncTask if it is running. - if (getUrlSize != null) { - getUrlSize!!.cancel(true) - } - // Get the current URL to save. val urlToSave = urlEditText.text.toString() - // Wipe the file size text view. - fileSizeTextView.text = "" - - // Get the file size for the current URL. - getUrlSize = GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave) - // Enable the save button if the URL is populated. saveButton.isEnabled = urlToSave.isNotEmpty() + + CoroutineScope(Dispatchers.Main).launch { + // Create a URL size string. + var urlSize: String + + // Get the URL size on the IO thread. + withContext(Dispatchers.IO) { + // Get the URL size. + urlSize = GetUrlSizeHelper.getUrl(requireContext(), URL(urlToSave), userAgentString, cookiesEnabled) + } + + // Display the updated URL. + fileSizeTextView.text = urlSize + } } }) // Return the alert dialog. return alertDialog } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt index e9de8b31..23cdd5f2 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/UrlHistoryDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2022 Soren Stoutner . + * Copyright 2016-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -39,7 +39,7 @@ import androidx.preference.PreferenceManager import com.stoutner.privacybrowser.R import com.stoutner.privacybrowser.activities.MainWebViewActivity import com.stoutner.privacybrowser.adapters.HistoryArrayAdapter -import com.stoutner.privacybrowser.dataclasses.History +import com.stoutner.privacybrowser.dataclasses.HistoryDataClass import com.stoutner.privacybrowser.views.NestedScrollWebView // Define the class constants. @@ -115,7 +115,7 @@ class UrlHistoryDialog : DialogFragment() { val defaultFavoriteIcon = defaultFavoriteIconBitmapDrawable.bitmap // Create a history array list. - val historyArrayList = ArrayList() + val historyDataClassArrayList = ArrayList() // Populate the history array list, descending from the end of the list so that the newest entries are at the top. `-1` is needed because the history array list is zero-based. for (i in webBackForwardList.size - 1 downTo 0) { @@ -128,10 +128,10 @@ class UrlHistoryDialog : DialogFragment() { } // Store the favorite icon and the URL in history entry. - val historyEntry = History(favoriteIconBitmap!!, webBackForwardList.getItemAtIndex(i).url) + val historyDataClassEntry = HistoryDataClass(favoriteIconBitmap!!, webBackForwardList.getItemAtIndex(i).url) // Add this history entry to the history array list. - historyArrayList.add(historyEntry) + historyDataClassArrayList.add(historyDataClassEntry) } // Subtract the original current page ID from the array size because the order of the array is reversed so that the newest entries are at the top. `-1` is needed because the array is zero-based. @@ -174,7 +174,7 @@ class UrlHistoryDialog : DialogFragment() { alertDialog.show() // Instantiate a history array adapter. - val historyArrayAdapter = HistoryArrayAdapter(requireContext(), historyArrayList, currentPageId) + val historyArrayAdapter = HistoryArrayAdapter(requireContext(), historyDataClassArrayList, currentPageId) // Get a handle for the list view. val listView = alertDialog.findViewById(R.id.history_listview)!! @@ -206,4 +206,4 @@ class UrlHistoryDialog : DialogFragment() { // Return the alert dialog. return alertDialog } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt index 6e5c0668..1d129078 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt @@ -53,7 +53,7 @@ import com.google.android.material.snackbar.Snackbar import com.stoutner.privacybrowser.BuildConfig import com.stoutner.privacybrowser.R -import com.stoutner.privacybrowser.asynctasks.SaveAboutVersionImage +import com.stoutner.privacybrowser.coroutines.SaveAboutVersionImageCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -210,11 +210,9 @@ class AboutVersionFragment : Fragment() { // Define the save about version image activity result launcher. It must be defined before `onCreate()` is run or the app will crash. private val saveAboutVersionImageActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("image/png")) { fileUri: Uri? -> - // Only save the file if the URI is not null, which happens if the user exited the file picker by pressing back. - if (fileUri != null) { - // Save the about version image. - SaveAboutVersionImage(requireActivity(), fileUri, aboutVersionLayout.findViewById(R.id.about_version_linearlayout)).execute() - } + // Save the file if the URI is not null, which happens if the user exits the file picker by pressing back. + if (fileUri != null) + SaveAboutVersionImageCoroutine.saveImage(requireActivity(), fileUri, aboutVersionLayout.findViewById(R.id.about_version_linearlayout)) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/CheckPinnedMismatchHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/CheckPinnedMismatchHelper.kt index b67a7f3f..8926f289 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/helpers/CheckPinnedMismatchHelper.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/CheckPinnedMismatchHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2019,2021-2022 Soren Stoutner . + * Copyright 2018-2019,2021-2022 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -19,14 +19,11 @@ package com.stoutner.privacybrowser.helpers -import android.app.Activity - import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager -import com.stoutner.privacybrowser.R import com.stoutner.privacybrowser.activities.MainWebViewActivity -import com.stoutner.privacybrowser.dataclasses.PendingDialog +import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog.Companion.displayDialog import com.stoutner.privacybrowser.views.NestedScrollWebView @@ -36,7 +33,7 @@ import java.util.Date object CheckPinnedMismatchHelper { @JvmStatic - fun checkPinnedMismatch(activity: Activity, fragmentManager: FragmentManager, nestedScrollWebView: NestedScrollWebView) { + fun checkPinnedMismatch(nestedScrollWebView: NestedScrollWebView, supportFragmentManager: FragmentManager, pinnedMismatchString: String) { // Initialize the current SSL certificate variables. var currentWebsiteIssuedToCName = "" var currentWebsiteIssuedToOName = "" @@ -127,11 +124,11 @@ object CheckPinnedMismatchHelper { // Try to show the dialog. Sometimes the window is not active. try { // Show the pinned mismatch alert dialog. - pinnedMismatchDialogFragment.show(fragmentManager, activity.getString(R.string.pinned_mismatch)) + pinnedMismatchDialogFragment.show(supportFragmentManager, pinnedMismatchString) } catch (exception: Exception) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - MainWebViewActivity.pendingDialogsArrayList.add(PendingDialog(pinnedMismatchDialogFragment, activity.getString(R.string.pinned_mismatch))) + MainWebViewActivity.pendingDialogsArrayList.add(PendingDialogDataClass(pinnedMismatchDialogFragment, pinnedMismatchString)) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/GetUrlSizeHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/GetUrlSizeHelper.kt new file mode 100644 index 00000000..6c3b3f87 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/GetUrlSizeHelper.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2020-2022 Soren Stoutner . + * + * This file is part of 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 . + */ + +package com.stoutner.privacybrowser.helpers + +import android.content.Context +import android.webkit.CookieManager + +import com.stoutner.privacybrowser.R + +import java.lang.Exception +import java.net.HttpURLConnection +import java.net.URL +import java.text.NumberFormat + +object GetUrlSizeHelper { + fun getUrl(context: Context, url: URL, userAgent: String, cookiesEnabled: Boolean): String { + // Initialize the formatted file size string. + var formattedFileSize = context.getString(R.string.unknown_size) + + // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch exceptions. + try { + // 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) + } + } + + // 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 status code. This initiates a network connection. + val responseCode = httpUrlConnection.responseCode + + // Check the response code. + if (responseCode >= 400) { // The response code is an error message. + // Set the formatted file size to indicate a bad URL. + formattedFileSize = context.getString(R.string.invalid_url) + } else { // The response code is not an error message. + // Get the content length header. + val contentLengthString = httpUrlConnection.getHeaderField("Content-Length") + + // Only process the content length string if it isn't null. + if (contentLengthString != null) { + // Convert the content length string to a long. + val fileSize = contentLengthString.toLong() + + // Format the file size. + formattedFileSize = NumberFormat.getInstance().format(fileSize) + " " + context.getString(R.string.bytes) + } + } + } finally { + // Disconnect the HTTP URL connection. + httpUrlConnection.disconnect() + } + } catch (exception: Exception) { + // Set the formatted file size to indicate a bad URL. + formattedFileSize = context.getString(R.string.invalid_url) + } + + // Return the formatted file size. + return formattedFileSize + } +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 20c45983..0040f4f2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -205,7 +205,7 @@ Unbekannte Größe Ungültige URL Speichere Datei: - Bild wird bearbeitet… : + Bild wird bearbeitet: \u0020 %1$s Fehler beim Speichern der Datei %1$s: \u0020 %2$s Unbekannter Fehler @@ -353,7 +353,8 @@ Gespeicherte IP-Adressen aktuelle IP-Adressen - + Verschlüsselung keine @@ -368,8 +369,8 @@ importieren entschlüsseln Export erfolgreich. - Export fehlgeschlagen: %1$s - Import fehlgeschlagen: %1$s + Export fehlgeschlagen: \u0020 %1$s + Import fehlgeschlagen: \u0020 %1$s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 16cac6a0..6684252e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -202,7 +202,7 @@ Tamaño desconocido URL inválida Guardando archivo: - Procesando imagen… : + Procesando imagen: \u0020 %1$s Error al guardar %1$s: \u0020 %2$s Error desconocido @@ -351,7 +351,8 @@ Direcciones IP guardadas Direcciones IP actuales - + Cifrado Ninguno @@ -366,8 +367,8 @@ Importar Descifrar Exportación exitosa. - Exportación fallida: %1$s - Importación fallida: %1$s + Exportación fallida: \u0020 %1$s + Importación fallida: \u0020 %1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 08c53c98..476012b1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -202,7 +202,7 @@ taille inconnue URL invalide Enregistrement du fichier : - Traitement de l\'image… : + Traitement de l\'image: \u0020 %1$s Erreur lors de l\'enregistrement de %1$s: \u0020 %2$s Erreur inconnue @@ -350,7 +350,8 @@ Adresse(s) IP sauvegardée(s) Adresse(s) IP courante(s) - + Chiffrement Aucun diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f02a29a6..3e9021cf 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -201,7 +201,7 @@ Dimensione sconosciuta URL non valida Salvataggio file: - Creazione immagine… : + Creazione immagine: \u0020 %1$s Error di salvataggio di %1$s: \u0020 %2$s Errore sconosciuto @@ -349,7 +349,8 @@ Indirizzi IP salvati Indirizzi IP attuali - + Cifratura Nessuna @@ -364,8 +365,8 @@ Importa Decripta Esportazione riuscita - Esportazione fallita: %1$s - Importazione fallita: %1$s + Esportazione fallita: \u0020 %1$s + Importazione fallita: \u0020 %1$s diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3ab6a052..687c0305 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -200,7 +200,7 @@ tamanho desconhecido URL inválida Salvando file: - Processando imagem… : + Processando imagem: \u0020 %1$s Erro ao salvar %1$s: \u0020 %2$s Erro desconhecido @@ -348,7 +348,8 @@ Endereços IP salvos Endereços IP atuais - + Encriptação Nenhum @@ -363,8 +364,8 @@ Importar Descriptografar Exportação bem sucedida. - A exportação falhou: %1$s - A importação falhou: %1$s + A exportação falhou: \u0020 %1$s + A importação falhou: \u0020 %1$s diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f38e108e..7557214b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -199,7 +199,7 @@ неизвестный размер неправильный URL Сохранение файла: - Обработка изображения… : + Обработка изображения: \u0020 %1$s Ошибка сохранения %1$s: \u0020 %2$s Неизвестная ошибка @@ -347,7 +347,8 @@ Сохраненные IP-адреса Текущие IP-адреса - + Шифрование Нет @@ -362,8 +363,8 @@ Импорт Расшифровать Экспорт выполнен. - Сбой при экспорте: %1$s - Сбой при импорте: %1$s + Сбой при экспорте: \u0020 %1$s + Сбой при импорте: \u0020 %1$s diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 08b905f4..bef8d5a0 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -312,7 +312,8 @@ Kayıtlı IP adresleri Geçerli IP adresleri - + Şifreleme Hiçbiri @@ -327,8 +328,8 @@ İçeri aktar Şifre çöz (Decrypt) Dışa aktarım başarılı - Dışa aktarım başarısız: %1$s - İçe aktarım başarısız: %1$s + Dışa aktarım başarısız: \u0020 %1$s + İçe aktarım başarısız: \u0020 %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 824d3d62..22f64b94 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,7 +210,7 @@ invalid URL Saving file: %1$s saved. - Processing image… : + Processing image: \u0020 %1$s Error saving %1$s: \u0020 %2$s Unknown error @@ -358,7 +358,8 @@ Saved IP addresses Current IP addresses - + Encryption None @@ -375,8 +376,8 @@ Privacy Browser Settings %1$s.pbs Privacy Browser Settings %1$s.pbs.aes Export successful. - Export failed: %1$s - Import failed: %1$s + Export failed: \u0020 %1$s + Import failed: \u0020 %1$s -- 2.43.0