From 5186b668274b09e37b371c0a134e53255c98ad98 Mon Sep 17 00:00:00 2001 From: Soren Stoutner Date: Wed, 22 Mar 2023 14:24:47 -0700 Subject: [PATCH] Migrate five classes to Kotlin. https://redmine.stoutner.com/issues/950 --- .../activities/MainWebViewActivity.java | 16 +- .../adapters/RequestsArrayAdapter.java | 122 ------- .../adapters/RequestsArrayAdapter.kt | 96 +++++ .../adapters/WebViewPagerAdapter.java | 188 ---------- .../adapters/WebViewPagerAdapter.kt | 173 +++++++++ .../privacybrowser/asynctasks/SaveUrl.java | 288 --------------- .../asynctasks/SaveWebpageImage.java | 168 --------- ...ndTask.java => GetSourceBackgroundTask.kt} | 336 +++++++++--------- .../coroutines/PopulateBlocklistsCoroutine.kt | 3 +- .../coroutines/SaveUrlCoroutine.kt | 222 ++++++++++++ .../coroutines/SaveWebpageImageCoroutine.kt | 122 +++++++ .../fragments/AboutVersionFragment.kt | 6 +- .../fragments/WebViewTabFragment.kt | 9 +- .../privacybrowser/helpers/UrlHelper.kt | 4 +- app/src/main/res/values-de/strings.xml | 10 +- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values-fr/strings.xml | 9 +- app/src/main/res/values-it/strings.xml | 8 +- app/src/main/res/values-pt-rBR/strings.xml | 14 +- app/src/main/res/values-ru/strings.xml | 10 +- app/src/main/res/values-tr/strings.xml | 4 +- app/src/main/res/values-zh-rCN/strings.xml | 8 +- app/src/main/res/values/strings.xml | 12 +- 23 files changed, 863 insertions(+), 977 deletions(-) delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java create mode 100644 app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java create mode 100644 app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java delete mode 100644 app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java rename app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/{GetSourceBackgroundTask.java => GetSourceBackgroundTask.kt} (50%) create mode 100644 app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt create mode 100644 app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 39b95dca..f2bf7536 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -127,11 +127,11 @@ import com.google.android.material.tabs.TabLayout; import com.stoutner.privacybrowser.R; import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter; -import com.stoutner.privacybrowser.asynctasks.SaveUrl; -import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage; import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine; import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine; import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine; +import com.stoutner.privacybrowser.coroutines.SaveUrlCoroutine; +import com.stoutner.privacybrowser.coroutines.SaveWebpageImageCoroutine; import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass; import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog; import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog; @@ -363,7 +363,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook public void onActivityResult(Uri fileUri) { // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back. if (fileUri != null) { - new SaveUrl(getApplicationContext(), resultLauncherActivityHandle, fileUri, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveUrlString); + // Instantiate the save URL coroutine. + SaveUrlCoroutine saveUrlCoroutine = new SaveUrlCoroutine(); + + // Save the URL. + saveUrlCoroutine.save(getApplicationContext(), resultLauncherActivityHandle, saveUrlString, fileUri, currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()); } // Reset the save URL string. @@ -456,8 +461,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook public void onActivityResult(Uri fileUri) { // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back. if (fileUri != null) { + // Instantiate the save webpage image coroutine. + SaveWebpageImageCoroutine saveWebpageImageCoroutine = new SaveWebpageImageCoroutine(); + // Save the webpage image. - new SaveWebpageImage(resultLauncherActivityHandle, fileUri, currentWebView).execute(); + saveWebpageImageCoroutine.save(resultLauncherActivityHandle, fileUri, currentWebView); } } }); diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java b/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java deleted file mode 100644 index d253307d..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2018-2022 Soren Stoutner . - * - * This file is part of Privacy Browser Android . - * - * Privacy Browser Android is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privacy Browser Android is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Privacy Browser Android. If not, see . - */ - -package com.stoutner.privacybrowser.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 { - public RequestsArrayAdapter(Context context, List resourceRequestsList) { - // `super` must be called form the base ArrayAdapter. `0` is the `textViewResourceId`, which is unused. - super(context, 0, resourceRequestsList); - } - - @Override - @NonNull - public View getView(int position, View view, @NonNull ViewGroup parent) { - // Get a handle for the context. - Context context = getContext(); - - // Inflate the view if it is null. - if (view == null) { - view = LayoutInflater.from(context).inflate(R.layout.requests_item_linearlayout, parent, false); - } - - // Get handles for the views. - LinearLayout linearLayout = view.findViewById(R.id.request_item_linearlayout); - TextView dispositionTextView = view.findViewById(R.id.request_item_disposition); - TextView urlTextView = view.findViewById(R.id.request_item_url); - - // Get the string array for this entry. - String[] entryStringArray = getItem(position); - - // Remove the lint warning below that `entryStringArray` might be null. - assert entryStringArray != null; - - // The ID is one greater than the position because it is 0 based. - int id = position + 1; - - // Set the action text and the background color. - switch (entryStringArray[0]) { - case BlocklistHelper.REQUEST_DEFAULT: - // Create the disposition string. - String requestDefault = id + ". " + context.getResources().getString(R.string.allowed); - - // Set the disposition text. - dispositionTextView.setText(requestDefault); - - // Set the background color. - linearLayout.setBackgroundColor(context.getColor(R.color.transparent)); - break; - - case BlocklistHelper.REQUEST_ALLOWED: - // Create the disposition string. - String requestAllowed = id + ". " + context.getResources().getString(R.string.allowed); - - // Set the disposition text. - dispositionTextView.setText(requestAllowed); - - // Set the background color. - linearLayout.setBackgroundColor(context.getColor(R.color.blue_background)); - break; - - case BlocklistHelper.REQUEST_THIRD_PARTY: - // Create the disposition string. - String requestThirdParty = id + ". " + context.getResources().getString(R.string.blocked); - - // Set the disposition text. - dispositionTextView.setText(requestThirdParty); - - // Set the background color. - linearLayout.setBackgroundColor(context.getColor(R.color.yellow_background)); - break; - - - case BlocklistHelper.REQUEST_BLOCKED: - // Create the disposition string. - String requestBlocked = id + ". " + context.getResources().getString(R.string.blocked); - - // Set the disposition text. - dispositionTextView.setText(requestBlocked); - - // Set the background color. - linearLayout.setBackgroundColor(context.getColor(R.color.red_background)); - break; - } - - // Set the URL text. - urlTextView.setText(entryStringArray[1]); - - // Return the modified view. - return view; - } -} diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt b/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt new file mode 100644 index 00000000..f79ab07a --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/adapters/RequestsArrayAdapter.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2018-2023 Soren Stoutner . + * + * This file is part of Privacy Browser Android . + * + * Privacy Browser Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser Android. If not, see . + */ + +package com.stoutner.privacybrowser.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>) : ArrayAdapter>(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(R.id.request_item_linearlayout) + val dispositionTextView = newView.findViewById(R.id.request_item_disposition) + val urlTextView = newView.findViewById(R.id.request_item_url) + + // Get the string array for this entry. + val entryStringArray = getItem(position)!! + + // The ID is one greater than the position because it is 0 based. + val id = position + 1 + + // Set the action text and the background color. + when (entryStringArray[0]) { + BlocklistHelper.REQUEST_DEFAULT -> { + // Set the disposition text. + dispositionTextView.text = context.resources.getString(R.string.request_allowed, id) + + // Set the background color. + linearLayout.setBackgroundColor(context.getColor(R.color.transparent)) + } + + BlocklistHelper.REQUEST_ALLOWED -> { + // Set the disposition text. + dispositionTextView.text = context.resources.getString(R.string.request_allowed, id) + + // Set the background color. + linearLayout.setBackgroundColor(context.getColor(R.color.blue_background)) + } + + BlocklistHelper.REQUEST_THIRD_PARTY -> { + // Set the disposition text. + dispositionTextView.text = context.resources.getString(R.string.request_blocked, id) + + // Set the background color. + linearLayout.setBackgroundColor(context.getColor(R.color.yellow_background)) + } + + BlocklistHelper.REQUEST_BLOCKED -> { + // Set the disposition text. + dispositionTextView.text = context.resources.getString(R.string.request_blocked, id) + + // Set the background color. + linearLayout.setBackgroundColor(context.getColor(R.color.red_background)) + } + } + + // Set the URL text. + urlTextView.text = entryStringArray[1] + + // Return the modified view. + return newView + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java deleted file mode 100644 index 52e72345..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright © 2019-2022 Soren Stoutner . - * - * This file is part of Privacy Browser Android . - * - * Privacy Browser Android is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privacy Browser Android is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Privacy Browser Android. If not, see . - */ - -package com.stoutner.privacybrowser.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 webViewFragmentsList = new LinkedList<>(); - - // Define the constructor. - public WebViewPagerAdapter(FragmentManager fragmentManager) { - // Run the default commands. - super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - } - - @Override - public int getCount() { - // Return the number of pages. - return webViewFragmentsList.size(); - } - - @Override - public int getItemPosition(@NonNull Object object) { - //noinspection SuspiciousMethodCalls - if (webViewFragmentsList.contains(object)) { - // Return the current page position. - //noinspection SuspiciousMethodCalls - return webViewFragmentsList.indexOf(object); - } else { - // The tab has been deleted. - return POSITION_NONE; - } - } - - @Override - @NonNull - public Fragment getItem(int pageNumber) { - // Get the fragment for a particular page. Page numbers are 0 indexed. - return webViewFragmentsList.get(pageNumber); - } - - @Override - public long getItemId(int position) { - // Return the unique ID for this page. - return webViewFragmentsList.get(position).fragmentId; - } - - public int getPositionForId(long fragmentId) { - // Initialize the position variable. - int position = -1; - - // Initialize the while counter. - int i = 0; - - // Find the current position of the WebView fragment with the given ID. - while (position < 0 && i < webViewFragmentsList.size()) { - // Check to see if the tab ID of this WebView matches the page ID. - if (webViewFragmentsList.get(i).fragmentId == fragmentId) { - // Store the position if they are a match. - position = i; - } - - // Increment the counter. - i++; - } - - // Set the position to be the last tab if it is not found. - // Sometimes there is a race condition in populating the webView fragments list when resuming Privacy Browser and displaying an SSL certificate error while loading a new intent. - // In that case, the last tab should be the one it is looking for. - if (position == -1) { - position = webViewFragmentsList.size() - 1; - } - - // Return the position. - return position; - } - - public void addPage(int pageNumber, ViewPager webViewPager, String url, boolean moveToNewPage) { - // Add a new page. - webViewFragmentsList.add(WebViewTabFragment.createPage(pageNumber, url)); - - // Update the view pager. - notifyDataSetChanged(); - - // Move to the new page if indicated. - if (moveToNewPage) { - moveToNewPage(pageNumber, webViewPager); - } - } - - public void restorePage(Bundle savedState, Bundle savedNestedScrollWebViewState) { - // Restore the page. - webViewFragmentsList.add(WebViewTabFragment.restorePage(savedState, savedNestedScrollWebViewState)); - - // Update the view pager. - notifyDataSetChanged(); - } - - public boolean deletePage(int pageNumber, ViewPager webViewPager) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewFragmentsList.get(pageNumber); - - // Get the WebView frame layout. - FrameLayout webViewFrameLayout = (FrameLayout) webViewTabFragment.getView(); - - // Remove the warning below that the WebView frame layout might be null. - assert webViewFrameLayout != null; - - // Get a handle for the nested scroll WebView. - NestedScrollWebView nestedScrollWebView = webViewFrameLayout.findViewById(R.id.nestedscroll_webview); - - // Pause the current WebView. - nestedScrollWebView.onPause(); - - // Remove all the views from the frame layout. - webViewFrameLayout.removeAllViews(); - - // Destroy the current WebView. - nestedScrollWebView.destroy(); - - // Delete the page. - webViewFragmentsList.remove(pageNumber); - - // Update the view pager. - notifyDataSetChanged(); - - // Return true if the selected page number did not change after the delete (because the newly selected tab has has same number as the previously deleted tab). - // This will cause the calling method to reset the current WebView to the new contents of this page number. - return (webViewPager.getCurrentItem() == pageNumber); - } - - public WebViewTabFragment getPageFragment(int pageNumber) { - // Return the page fragment. - return webViewFragmentsList.get(pageNumber); - } - - private void moveToNewPage(int pageNumber, ViewPager webViewPager) { - // Check to see if the new page has been populated. - if (webViewPager.getChildCount() >= pageNumber) { // The new page is ready. - // Move to the new page. - webViewPager.setCurrentItem(pageNumber); - } else { // The new page is not yet ready. - // Create a handler. - Handler moveToNewPageHandler = new Handler(); - - // Create a runnable. - Runnable moveToNewPageRunnable = () -> { - // Move to the new page. - webViewPager.setCurrentItem(pageNumber); - }; - - // Try again to move to the new page after 50 milliseconds. - moveToNewPageHandler.postDelayed(moveToNewPageRunnable, 50); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt new file mode 100644 index 00000000..6aef0cdb --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/adapters/WebViewPagerAdapter.kt @@ -0,0 +1,173 @@ +/* + * Copyright © 2019-2023 Soren Stoutner . + * + * This file is part of Privacy Browser Android . + * + * Privacy Browser Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser Android. If not, see . + */ + +package com.stoutner.privacybrowser.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() + + 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(R.id.nestedscroll_webview) + + // Pause the current WebView. + nestedScrollWebView.onPause() + + // Remove all the views from the frame layout. + webViewFrameLayout.removeAllViews() + + // Destroy the current WebView. + nestedScrollWebView.destroy() + + // Delete the page. + webViewFragmentsList.removeAt(pageNumber) + + // Update the view pager. + notifyDataSetChanged() + + // Return true if the selected page number did not change after the delete (because the newly selected tab has has same number as the previously deleted tab). + // This will cause the calling method to reset the current WebView to the new contents of this page number. + return webViewPager.currentItem == pageNumber + } + + fun getPageFragment(pageNumber: Int): WebViewTabFragment { + // Return the page fragment. + return webViewFragmentsList[pageNumber] + } + + fun getPositionForId(fragmentId: Long): Int { + // Initialize the position variable. + var position = -1 + + // Initialize the while counter. + var i = 0 + + // Find the current position of the WebView fragment with the given ID. + while (position < 0 && i < webViewFragmentsList.size) { + // Check to see if the tab ID of this WebView matches the page ID. + if (webViewFragmentsList[i].fragmentId == fragmentId) { + // Store the position if they are a match. + position = i + } + + // Increment the counter. + i++ + } + + // Set the position to be the last tab if it is not found. + // Sometimes there is a race condition in populating the webView fragments list when resuming Privacy Browser and displaying an SSL certificate error while loading a new intent. + // In that case, the last tab should be the one it is looking for. + if (position == -1) { + position = webViewFragmentsList.size - 1 + } + + // Return the position. + return position + } + + fun restorePage(savedState: Bundle, savedNestedScrollWebViewState: Bundle) { + // Restore the page. + webViewFragmentsList.add(WebViewTabFragment.restorePage(savedState, savedNestedScrollWebViewState)) + + // Update the view pager. + notifyDataSetChanged() + } + + private fun moveToNewPage(pageNumber: Int, webViewPager: ViewPager) { + // Check to see if the new page has been populated. + if (webViewPager.childCount >= pageNumber) { // The new page is ready. + // Move to the new page. + webViewPager.currentItem = pageNumber + } else { // The new page is not yet ready. + // Create a handler. + val moveToNewPageHandler = Handler(Looper.getMainLooper()) + + // Create a runnable. + val moveToNewPageRunnable = Runnable { + // Move to the new page. + webViewPager.currentItem = pageNumber + } + + // Try again to move to the new page after 50 milliseconds. + moveToNewPageHandler.postDelayed(moveToNewPageRunnable, 50) + } + } +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java deleted file mode 100644 index 029929dd..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright 2020-2022 Soren Stoutner . - * - * This file is part of Privacy Browser Android . - * - * Privacy Browser Android is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privacy Browser Android is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Privacy Browser Android. If not, see . - */ - -package com.stoutner.privacybrowser.asynctasks; - -import android.app.Activity; -import android.content.Context; -import android.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 { - // Declare the weak references. - private final WeakReference contextWeakReference; - private final WeakReference 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. - byte[] conversionBufferByteArray = new byte[1048576]; - - // Initialize the downloaded kilobytes counter. - long downloadedKilobytesCounter = 0; - - // Define the buffer length variable. - int bufferLength; - - // Attempt to read data from the input stream and store it in the output stream. Also store the amount of data read in the buffer length variable. - while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer in > 0. - // Write the contents of the conversion buffer to the file output stream. - outputStream.write(conversionBufferByteArray, 0, bufferLength); - - // Update the downloaded kilobytes counter. - downloadedKilobytesCounter = downloadedKilobytesCounter + bufferLength; - - // Update the file download progress snackbar. - publishProgress(downloadedKilobytesCounter); - } - - // Close the input stream. - inputStream.close(); - } finally { - // Disconnect the HTTP URL connection. - httpUrlConnection.disconnect(); - } - } - - // Close the output stream. - outputStream.close(); - } catch (Exception exception) { - // Store the error in the save disposition string. - saveDisposition = exception.toString(); - } - - // Return the save disposition string. - return saveDisposition; - } - - // `onProgressUpdate()` operates on the UI thread. - @Override - protected void onProgressUpdate(Long... numberOfBytesDownloaded) { - // Get a handle for the activity. - Activity activity = activityWeakReference.get(); - - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return; - } - - // Format the number of bytes downloaded. - String formattedNumberOfBytesDownloaded = NumberFormat.getInstance().format(numberOfBytesDownloaded[0]); - - // Check to see if the file size is known. - if (fileSize == -1) { // The size of the download file is not known. - // Update the snackbar. - savingFileSnackbar.setText(activity.getString(R.string.saving_file) + " " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " - " + fileNameString); - } else { // The size of the download file is known. - // Calculate the download percentage. - long downloadPercentage = (numberOfBytesDownloaded[0] * 100) / fileSize; - - // Update the snackbar. - savingFileSnackbar.setText(activity.getString(R.string.saving_file) + " " + downloadPercentage + "% - " + formattedNumberOfBytesDownloaded + " " + activity.getString(R.string.bytes) + " / " + - formattedFileSize + " " + activity.getString(R.string.bytes) + " - " + fileNameString); - } - } - - // `onPostExecute()` operates on the UI thread. - @Override - protected void onPostExecute(String saveDisposition) { - // Get handles for the context and activity. - Activity activity = activityWeakReference.get(); - - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return; - } - - // Get a handle for the no swipe view pager. - NoSwipeViewPager noSwipeViewPager = activity.findViewById(R.id.webviewpager); - - // Dismiss the saving file snackbar. - savingFileSnackbar.dismiss(); - - // Display a save disposition snackbar. - if (saveDisposition.equals(SUCCESS)) { - // Display the file saved snackbar. - Snackbar.make(noSwipeViewPager, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show(); - } else { - // Display the file saving error. - Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file, fileNameString, saveDisposition), Snackbar.LENGTH_INDEFINITE).show(); - } - } -} diff --git a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java b/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java deleted file mode 100644 index 5a92123a..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveWebpageImage.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright0 2019-2022 Soren Stoutner . - * - * This file is part of Privacy Browser Android . - * - * Privacy Browser Android is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privacy Browser Android is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Privacy Browser Android. If not, see . - */ - -package com.stoutner.privacybrowser.asynctasks; - -import android.app.Activity; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.provider.OpenableColumns; - -import 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 { - // Declare the weak references. - private final WeakReference activityWeakReference; - private final WeakReference nestedScrollWebViewWeakReference; - - // Declare the class constants. - private final String SUCCESS = "Success"; - - // Declare the class variables. - private Snackbar savingImageSnackbar; - private Bitmap webpageBitmap; - private final Uri fileUri; - private final String fileNameString; - - // The public constructor. - public SaveWebpageImage(Activity activity, Uri fileUri, NestedScrollWebView nestedScrollWebView) { - // Populate the weak references. - activityWeakReference = new WeakReference<>(activity); - nestedScrollWebViewWeakReference = new WeakReference<>(nestedScrollWebView); - - // Populate the class variables. - this.fileUri = fileUri; - - // Query the exact file name if the API >= 26. - if (Build.VERSION.SDK_INT >= 26) { - // Get a cursor from the content resolver. - Cursor contentResolverCursor = activity.getContentResolver().query(fileUri, null, null, null); - - // Move to the first row. - contentResolverCursor.moveToFirst(); - - // Get the file name from the cursor. - fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); - - // Close the cursor. - contentResolverCursor.close(); - } else { - // Use the file URI last path segment as the file name string. - fileNameString = fileUri.getLastPathSegment(); - } - } - - // `onPreExecute()` operates on the UI thread. - @Override - protected void onPreExecute() { - // Get handles for the activity and the nested scroll WebView. - Activity activity = activityWeakReference.get(); - NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get(); - - // Abort if the activity or the nested scroll WebView is gone. - if ((activity == null) || activity.isFinishing() || nestedScrollWebView == null) { - return; - } - - // Create a saving image snackbar. - savingImageSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.processing_image) + " " + fileNameString, Snackbar.LENGTH_INDEFINITE); - - // Display the saving image snackbar. - savingImageSnackbar.show(); - - // Create a webpage bitmap. Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The nested scroll WebView commands must be run on the UI thread. - webpageBitmap = Bitmap.createBitmap(nestedScrollWebView.getHorizontalScrollRange(), nestedScrollWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888); - - // Create a canvas. - Canvas webpageCanvas = new Canvas(webpageBitmap); - - // Draw the current webpage onto the bitmap. The nested scroll WebView commands must be run on the UI thread. - nestedScrollWebView.draw(webpageCanvas); - } - - @Override - protected String doInBackground(Void... Void) { - // Get a handle for the activity. - Activity activity = activityWeakReference.get(); - - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return ""; - } - - // Create a webpage PNG byte array output stream. - ByteArrayOutputStream webpageByteArrayOutputStream = new ByteArrayOutputStream(); - - // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time. Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS. - webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream); - - // Create a file creation disposition string. - String fileCreationDisposition = SUCCESS; - - try { - // Create an image file output stream. - OutputStream imageFileOutputStream = activity.getContentResolver().openOutputStream(fileUri); - - // Write the webpage image to the image file. - webpageByteArrayOutputStream.writeTo(imageFileOutputStream); - } catch (Exception exception) { - // Store the error in the file creation disposition string. - fileCreationDisposition = exception.toString(); - } - - // Return the file creation disposition string. - return fileCreationDisposition; - } - - // `onPostExecute()` operates on the UI thread. - @Override - protected void onPostExecute(String fileCreationDisposition) { - // Get handles for the weak references. - Activity activity = activityWeakReference.get(); - NestedScrollWebView nestedScrollWebView = nestedScrollWebViewWeakReference.get(); - - // Abort if the activity is gone. - if ((activity == null) || activity.isFinishing()) { - return; - } - - // Dismiss the saving image snackbar. - savingImageSnackbar.dismiss(); - - // Display a file creation disposition snackbar. - if (fileCreationDisposition.equals(SUCCESS)) { - // Display the image saved snackbar. - Snackbar.make(nestedScrollWebView, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show(); - } else { - // Display the file saving error. - Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file, fileNameString, fileCreationDisposition), Snackbar.LENGTH_INDEFINITE).show(); - } - } -} diff --git a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.java b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt similarity index 50% rename from app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.java rename to app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt index 26f57a12..f7ad0a5b 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.java +++ b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2022 Soren Stoutner . + * Copyright © 2017-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -17,324 +17,320 @@ * along with Privacy Browser Android. If not, see . */ -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) { +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 { + // Initialize the spannable string builders. - SpannableStringBuilder requestHeadersBuilder = new SpannableStringBuilder(); - SpannableStringBuilder responseMessageBuilder = new SpannableStringBuilder(); - SpannableStringBuilder responseHeadersBuilder = new SpannableStringBuilder(); - SpannableStringBuilder responseBodyBuilder = new SpannableStringBuilder(); + 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. - Uri contentUri = Uri.parse(urlString); + val contentUri = Uri.parse(urlString) // Get a cursor with metadata about the content URL. - Cursor contentCursor = contentResolver.query(contentUri, null, null, null, null); + val contentCursor = contentResolver.query(contentUri, null, null, null, null)!! // Move the content cursor to the first row. - contentCursor.moveToFirst(); + contentCursor.moveToFirst() - for (int i = 0; i < contentCursor.getColumnCount(); i++) { + // 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")); - } + 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)); + 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(); + contentCursor.close() // Create a buffered string reader for the content data. - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(contentResolver.openInputStream(contentUri))); + 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. - for (String contentLineString; ((contentLineString = bufferedReader.readLine()) != null);) { + while (bufferedReader.readLine().also { contentLineString = it } != null) { // Add the line to the response body builder. - responseBodyBuilder.append(contentLineString); + responseBodyBuilder.append(contentLineString) // Append a new line. - responseBodyBuilder.append("\n"); + responseBodyBuilder.append("\n") } - } catch (Exception exception) { + } catch (exception: Exception) { // Return the error message. - webViewSource.returnError(exception.toString()); + 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); + val url = URL(urlString) // Open a connection to the URL. No data is actually sent at this point. - HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy); + val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection // Set the `Host` header property. - httpUrlConnection.setRequestProperty("Host", url.getHost()); + httpUrlConnection.setRequestProperty("Host", url.host) // 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()); + 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"); + 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"); + 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"); + 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"); + 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); + 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); + 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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); + 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); + 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. - String cookiesString = CookieManager.getInstance().getCookie(url.toString()); + 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); + 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); + 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", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - requestHeadersBuilder.append(": gzip"); + 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. - HostnameVerifier hostnameVerifier = (hostname, sslSession) -> { - // Allow all host names. - return true; - }; + 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") 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; - } + @SuppressLint("CustomX509TrustManager") val trustManager = arrayOf( + object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array, authType: String) { + // Do nothing, which trusts all client certificates. + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted(chain: Array, authType: String) { + // Do nothing, which trusts all server certificates. + } + + override fun getAcceptedIssuers(): Array? { + return null } - }; + } + ) // Get an SSL context. `TLS` provides a base instance available from API 1. - SSLContext sslContext = SSLContext.getInstance("TLS"); + val sslContext = SSLContext.getInstance("TLS") // Initialize the SSL context with the blank trust manager. - sslContext.init(null, trustManager, new SecureRandom()); + sslContext.init(null, trustManager, SecureRandom()) // Get the SSL socket factory with the blank trust manager. - SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + val socketFactory = sslContext.socketFactory // Set the HTTPS URL Connection to use the blank host name verifier. - ((HttpsURLConnection) httpUrlConnection).setHostnameVerifier(hostnameVerifier); + (httpUrlConnection as HttpsURLConnection).hostnameVerifier = hostnameVerifier // Set the HTTPS URL connection to use the socket factory with the blank trust manager. - ((HttpsURLConnection) httpUrlConnection).setSSLSocketFactory(socketFactory); + 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. - int responseCode = httpUrlConnection.getResponseCode(); + val responseCode = httpUrlConnection.responseCode // 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()); + responseMessageBuilder.append(responseCode.toString(), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + responseMessageBuilder.append(": ") + responseMessageBuilder.append(httpUrlConnection.responseMessage) // Initialize the iteration variable. - int i = 0; + 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")); - } + 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)); + responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + responseHeadersBuilder.append(": ") + responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i)) // Increment the iteration variable. - i++; + 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()); - } + 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. - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - byte[] conversionBufferByteArray = new byte[1024]; + val byteArrayOutputStream = ByteArrayOutputStream() + val conversionBufferByteArray = ByteArray(1024) // Define the buffer length variable. - int bufferLength; + 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 ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer is > 0. + 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); + byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength) } - } catch (IOException exception) { + } catch (exception: IOException) { // Return the error message. - webViewSource.returnError(exception.toString()); + webViewSource.returnError(exception.toString()) } // Close the input stream. - inputStream.close(); + inputStream.close() // Populate the response body string with the contents of the byte array output stream. - responseBodyBuilder.append(byteArrayOutputStream.toString()); + responseBodyBuilder.append(byteArrayOutputStream.toString()) } finally { // Disconnect HTTP URL connection. - httpUrlConnection.disconnect(); + httpUrlConnection.disconnect() } - } catch (Exception exception) { + } catch (exception: Exception) { // Return the error message. - webViewSource.returnError(exception.toString()); + webViewSource.returnError(exception.toString()) } } // Return the spannable string builders. - return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder}; + return arrayOf(requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder) } } \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt index 4311ef5e..fb0c1e8f 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateBlocklistsCoroutine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019,2021-2022 Soren Stoutner . + * Copyright 2019,2021-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -77,6 +77,7 @@ class PopulateBlocklistsCoroutine(context: Context) { // Advertise the loading of EasyList. loadingBlocklistTextView.text = context.getString(R.string.loading_easylist) + // Populate the blocklists on the IO thread. withContext(Dispatchers.IO) { // Populate EasyList. val easyList = blocklistHelper.parseBlocklist(context.assets, "blocklists/easylist.txt") diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt new file mode 100644 index 00000000..c6fd36a2 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2020-2023 Soren Stoutner . + * + * This file is part of Privacy Browser Android . + * + * Privacy Browser Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser Android. If not, see . + */ + +package com.stoutner.privacybrowser.coroutines + +import android.app.Activity +import android.content.Context +import android.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(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. + val conversionBufferByteArray = ByteArray(1048576) + + // Initialize the downloaded kilobytes counter. + var downloadedKilobytesCounter: Long = 0 + + // Define the buffer length variable. + var bufferLength: Int + + // Attempt to read data from the input stream and store it in the output stream. Also store the amount of data read in the buffer length variable. + while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) { // Proceed while the amount of data stored in the buffer in > 0. + // Write the contents of the conversion buffer to the file output stream. + outputStream.write(conversionBufferByteArray, 0, bufferLength) + + // Update the downloaded kilobytes counter. + downloadedKilobytesCounter += bufferLength + + // Format the number of bytes downloaded. + val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedKilobytesCounter) + + // Update the UI. + withContext(Dispatchers.Main) { + // Check to see if the file size is known. + if (fileSize == -1L) { // The size of the download file is not known. + // Update the snackbar. + savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString)) + } else { // The size of the download file is known. + // Calculate the download percentage. + val downloadPercentage = downloadedKilobytesCounter * 100 / fileSize + + // Update the snackbar. + savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize, + fileNameString) + ) + } + } + } + + // Close the input stream. + inputStream.close() + } finally { + // Disconnect the HTTP URL connection. + httpUrlConnection.disconnect() + } + } + + // Close the output stream. + outputStream.close() + + // Update the UI. + withContext(Dispatchers.Main) { + // Dismiss the saving file snackbar. + savingFileSnackbar.dismiss() + + // Display the file saved snackbar. + Snackbar.make(noSwipeViewPager, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_LONG).show() + } + } catch (exception: Exception) { + // Update the UI. + withContext(Dispatchers.Main) { + // Dismiss the saving file snackbar. + savingFileSnackbar.dismiss() + + // Display the file saving error. + Snackbar.make(noSwipeViewPager, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt new file mode 100644 index 00000000..1c3a2c03 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveWebpageImageCoroutine.kt @@ -0,0 +1,122 @@ +/* + * Copyright0 2019-2023 Soren Stoutner . + * + * This file is part of Privacy Browser Android . + * + * Privacy Browser Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser Android. If not, see . + */ + +package com.stoutner.privacybrowser.coroutines + +import android.app.Activity +import android.graphics.Bitmap +import android.graphics.Canvas +import android.net.Uri +import android.os.Build +import android.provider.OpenableColumns + +import 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 diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt index 1d129078..079b2ac8 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/AboutVersionFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 Soren Stoutner . + * Copyright 2016-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -716,7 +716,7 @@ class AboutVersionFragment : Fragment() { updateMemoryUsageBoolean = true } - fun updateMemoryUsage(activity: Activity) { + private fun updateMemoryUsage(activity: Activity) { try { // Update the memory usage if enabled. if (updateMemoryUsageBoolean) { @@ -797,7 +797,7 @@ class AboutVersionFragment : Fragment() { } } - fun getAboutVersionString(): String { + private fun getAboutVersionString(): String { // Initialize an about version string builder. val aboutVersionStringBuilder = StringBuilder() diff --git a/app/src/main/java/com/stoutner/privacybrowser/fragments/WebViewTabFragment.kt b/app/src/main/java/com/stoutner/privacybrowser/fragments/WebViewTabFragment.kt index 5d37613c..154ea2dd 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/fragments/WebViewTabFragment.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/fragments/WebViewTabFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2020,2022 Soren Stoutner . + * Copyright © 2019-2020,2022-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -43,7 +43,6 @@ private const val SAVED_NESTED_SCROLL_WEBVIEW_STATE = "saved_nested_scroll_webvi class WebViewTabFragment : Fragment() { // Define the public variables. - @JvmField // TODO. `@JvmField` can be removed once the entire project has been converted to Kotlin. var fragmentId = Calendar.getInstance().timeInMillis // The public interface is used to send information back to the parent activity. @@ -59,7 +58,6 @@ class WebViewTabFragment : Fragment() { private lateinit var nestedScrollWebView: NestedScrollWebView companion object { - @JvmStatic // TODO. `@JvmStatic` can be removed once the entire project has been converted to Kotlin. fun createPage(pageNumber: Int, url: String?): WebViewTabFragment { // Create an arguments bundle. val argumentsBundle = Bundle() @@ -79,8 +77,7 @@ class WebViewTabFragment : Fragment() { return webViewTabFragment } - @JvmStatic // TODO. `@JvmStatic` can be removed once the entire project has been converted to Kotlin. - fun restorePage(savedState: Bundle?, savedNestedScrollWebViewState: Bundle?): WebViewTabFragment { + fun restorePage(savedState: Bundle, savedNestedScrollWebViewState: Bundle): WebViewTabFragment { // Create an arguments bundle val argumentsBundle = Bundle() @@ -165,4 +162,4 @@ class WebViewTabFragment : Fragment() { null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt b/app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt index a2e586e6..68a760d2 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt +++ b/app/src/main/java/com/stoutner/privacybrowser/helpers/UrlHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Soren Stoutner . + * Copyright 2020-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * @@ -196,6 +196,7 @@ object UrlHelper { return Pair(fileNameString, formattedFileSize) } + /* This entire method might not be needed. fun getSize(context: Context, url: URL, userAgent: String, cookiesEnabled: Boolean): String { // Initialize the formatted file size string. var formattedFileSize = context.getString(R.string.unknown_size) @@ -260,6 +261,7 @@ object UrlHelper { // Return the formatted file size. return formattedFileSize } + */ @JvmStatic fun highlightSyntax(urlEditText: EditText, initialGrayColorSpan: ForegroundColorSpan, finalGrayColorSpan: ForegroundColorSpan, redColorSpan: ForegroundColorSpan) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index be02c677..2b68bfc8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -194,8 +194,8 @@ Die Datei ist ein MHT-Web-Archiv. Manchmal müssen MIME-gekapselte HTML-Web-Archive (MHT) manuell festgelegt werden, um korrekt geöffnet zu werden. - + URL speichern Archiv speichern Text speichern @@ -205,7 +205,9 @@ Bytes Unbekannte Größe Ungültige URL - Speichere Datei: + Speichere Datei: \u0020 %1$d%% - %2$s + Speichere Datei: \u0020 %1$s Bytes - %2$s + Speichere Datei: \u0020 %1$d%% - %2$s Bytes / %3$s Bytes - %4$s Bild wird bearbeitet: \u0020 %1$s Fehler beim Speichern der Datei %1$s: \u0020 %2$s Unbekannter Fehler @@ -281,10 +283,12 @@ Standard Standard - erlaubt erlaubt + %1$d. erlaubt erlaubt Drittanbieter Drittanbieter - blockiert blockiert + %1$d. blockiert blockiert Filterliste Unterliste diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f189e320..bad35e6a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,7 +1,7 @@ + Guardar URL Guardar archivo Guardar texto @@ -201,7 +201,9 @@ bytes Tamaño desconocido URL inválida - Guardando archivo: + Guardando archivo: \u0020 %1$d%% - %2$s + Guardando archivo: \u0020 %1$s bytes - %2$s + Guardando archivo: \u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s Procesando imagen: \u0020 %1$s Error al guardar %1$s: \u0020 %2$s Error desconocido @@ -278,10 +280,12 @@ Por defecto Por defecto - Permitida Permitida + %1$d. Permitida Permitidas Terceras partes Tercera parte - Bloqueada Bloqueada + %1$d. Bloqueada Bloqueadas Lista de bloqueo Sublista diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fec22646..5d68d58f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -191,7 +191,8 @@ Le fichier est une archive web MHT. Parfois, les archives web MHT (MIME Encapsulated HTML) doivent être spécifiées manuellement pour être ouvertes correctement. - + Enregistrer l\'URL Enregistrer l\'archive Sauvegarder le texte @@ -201,7 +202,9 @@ octets taille inconnue URL invalide - Enregistrement du fichier : + Enregistrement du fichier : %1$d%% - %2$s + Enregistrement du fichier : \u0020 %1$s octets - %2$s + Enregistrement du fichier : \u0020 %1$d%% - %2$s octets / %3$s octets - %4$s Traitement de l\'image : %1$s Erreur lors de l\'enregistrement de %1$s : %2$s Erreur inconnue @@ -277,10 +280,12 @@ Par défaut Par défaut - Autorisées Autorisée + %1$d. Autorisée Autorisées Tiers Tiers - Bloquées Bloquée + %1$d. Bloquée Bloquées Liste noires Sous-listes diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 23c02129..3fde1f5c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -190,7 +190,7 @@ Questo file è un archivio web MHT. Talvolta gli archivi web del tipo MIME Encapsulated HTML (MHT) devono essere specificati manualmente per essere aperti correttamente. - Salva URL Salva Archivio @@ -201,7 +201,9 @@ byte Dimensione sconosciuta URL non valida - Salvataggio file: + Salvataggio file: \u0020 %1$d%% - %2$s + Salvataggio file: \u0020 %1$s byte - %2$s + Salvataggio file: \u0020 %1$d%% - %2$s byte / %3$s byte - %4$s Creazione immagine: \u0020 %1$s Error di salvataggio di %1$s: \u0020 %2$s Errore sconosciuto @@ -277,10 +279,12 @@ Default Default - Permessa Permessa + %1$d. Permessa Permesse Terze parti Terze parti - Bloccate Bloccata + %1$d. Bloccata Bloccate Blocklist Sublist diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 687c0305..fc55bb85 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,7 +1,7 @@ Salvar URL Salvar Arquivo @@ -199,7 +199,9 @@ bytes tamanho desconhecido URL inválida - Salvando file: + Salvando file: \u0020 %1$d%% - %2$s + Salvando file: \u0020 %1$s bytes - %2$s + Salvando file: \u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s Processando imagem: \u0020 %1$s Erro ao salvar %1$s: \u0020 %2$s Erro desconhecido @@ -275,10 +277,12 @@ Padrão Padrão - Permitido Permitido - Permitido + %1$d. Permitido + Permitidos Terceiros - Terceiros - Bloqueados + Terceiros - Bloqueado Bloqueado + %1$d. Bloqueado Bloqueados Lista de bloqueios Sublista diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 782da2cd..e6263a6b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,7 +1,7 @@ Сохранить URL Сохранить архив @@ -198,7 +198,9 @@ байтов неизвестный размер неправильный URL - Сохранение файла: + Сохранение файла: \u0020 %1$d%% - %2$s + Сохранение файла: \u0020 %1$s байтов - %2$s + Сохранение файла: \u0020 %1$d%% - %2$s байтов / %3$s байтов - %4$s Обработка изображения: \u0020 %1$s Ошибка сохранения %1$s: \u0020 %2$s Неизвестная ошибка @@ -274,10 +276,12 @@ По умолчанию По умолчанию - Разрешен Разрешен + %1$d. Разрешен Разрешено Сторонние Сторонние - Блокировано Блокирован + %1$d. Блокирован Блокировано Список блокировки Подсписок diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index bef8d5a0..2fd4676c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,7 +1,7 @@ 保存链接 保存存档 @@ -204,7 +204,9 @@ 字节 未知大小 错误的网址 - 保存文件: + 保存文件: \u0020 %1$d%% - %2$s + 保存文件: \u0020 %1$s 字节 - %2$s + 保存文件: \u0020 %1$d%% - %2$s 字节 / %3$s 字节 - %4$s %1$s 保存. 处理图片: \u0020 %1$s 保存失败 %1$s: \u0020 %2$s @@ -279,10 +281,12 @@ 默认 默认允许 允许 + %1$d. 允许 允许 第三方 不允许第三方 不允许 + %1$d. 不允许 不允许 不允许名单 子表 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb892ed2..22a67a33 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ + Save Dialog Save URL Save Archive @@ -208,7 +208,9 @@ bytes unknown size invalid URL - Saving file: + Saving file: \u0020 %1$d%% - %2$s + Saving file: \u0020 %1$s bytes - %2$s + Saving file: \u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s %1$s saved. Processing image: \u0020 %1$s Error saving %1$s: \u0020 %2$s @@ -285,10 +287,12 @@ Default Default - Allowed Allowed + %1$d. Allowed Allowed Third-party Third-party - Blocked Blocked + %1$d. Blocked Blocked Blocklist Sublist -- 2.43.0