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