X-Git-Url: https://gitweb.stoutner.com/?a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FMainWebViewActivity.java;h=39b95dca65d30d4ab5f35a3b31b1d47fd6969873;hb=514e93baaa8389dc9c5abdb79e68c890c260b8d3;hp=08ab30c351efe3b04ae84c1d1ef1731097c57dda;hpb=b1612e9c92c3aa6cc39e89c2a7859dfeebf71bf9;p=PrivacyBrowserAndroid.git 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 08ab30c3..39b95dca 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 Soren Stoutner . + * Copyright 2015-2023 Soren Stoutner . * * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . * @@ -46,19 +46,16 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.http.SslCertificate; import android.net.http.SslError; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; -import android.preference.PreferenceManager; import android.print.PrintDocumentAdapter; import android.print.PrintManager; import android.provider.DocumentsContract; import android.provider.OpenableColumns; import android.text.Editable; -import android.text.Spanned; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; import android.util.Patterns; @@ -100,6 +97,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -115,6 +113,7 @@ import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager.widget.ViewPager; import androidx.webkit.WebSettingsCompat; @@ -128,16 +127,15 @@ import com.google.android.material.tabs.TabLayout; import com.stoutner.privacybrowser.R; import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter; -import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses; -import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists; -import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog; import com.stoutner.privacybrowser.asynctasks.SaveUrl; import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage; -import com.stoutner.privacybrowser.dataclasses.PendingDialog; +import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine; +import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine; +import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine; +import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass; import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog; import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog; -import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.FontSizeDialog; import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; import com.stoutner.privacybrowser.dialogs.OpenDialog; @@ -154,6 +152,7 @@ import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper; import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper; import com.stoutner.privacybrowser.helpers.ProxyHelper; import com.stoutner.privacybrowser.helpers.SanitizeUrlHelper; +import com.stoutner.privacybrowser.helpers.UrlHelper; import com.stoutner.privacybrowser.views.NestedScrollWebView; import java.io.ByteArrayInputStream; @@ -185,14 +184,13 @@ import java.util.concurrent.Executors; import kotlin.Pair; public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, - EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, - PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener, - WebViewTabFragment.NewTabListener { + FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, PinnedMismatchDialog.PinnedMismatchListener, + PopulateBlocklistsCoroutine.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener { // Define the public static variables. - public static ExecutorService executorService = Executors.newFixedThreadPool(4); + public static final ExecutorService executorService = Executors.newFixedThreadPool(4); public static String orbotStatus = "unknown"; - public static ArrayList pendingDialogsArrayList = new ArrayList<>(); + public static final ArrayList pendingDialogsArrayList = new ArrayList<>(); public static String proxyMode = ProxyHelper.NONE; // Declare the public static variables. @@ -211,15 +209,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2; public final static int DOMAINS_CUSTOM_USER_AGENT = 12; - // Define the start activity for result request codes. The public static entry is accessed from `OpenDialog()`. - private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0; - public final static int BROWSE_OPEN_REQUEST_CODE = 1; - // Define the saved instance state constants. + private final String BOOKMARKS_DRAWER_PINNED = "bookmarks_drawer_pinned"; + private final String PROXY_MODE = "proxy_mode"; private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list"; private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list"; private final String SAVED_TAB_POSITION = "saved_tab_position"; - private final String PROXY_MODE = "proxy_mode"; // Define the saved instance state variables. private ArrayList savedStateArrayList; @@ -227,10 +222,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private int savedTabPosition; private String savedProxyMode; - // Define the class variables. - @SuppressWarnings("rawtypes") - AsyncTask populateBlocklists; - // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`. private NestedScrollWebView currentWebView; @@ -249,20 +240,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`. private ActionBarDrawerToggle actionBarDrawerToggle; - // The color spans are used in `onCreate()` and `highlightUrlText()`. - private ForegroundColorSpan redColorSpan; - private ForegroundColorSpan initialGrayColorSpan; - private ForegroundColorSpan finalGrayColorSpan; - // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. private Cursor bookmarksCursor; // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`. private CursorAdapter bookmarksCursorAdapter; - // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`. - private String oldFolderNameString; - // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`. private ValueCallback fileChooserCallback; @@ -275,21 +258,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private BookmarksDatabaseHelper bookmarksDatabaseHelper; private DomainsDatabaseHelper domainsDatabaseHelper; private ProxyHelper proxyHelper; - private SanitizeUrlHelper sanitizeUrlHelper; // Declare the class variables + private boolean bookmarksDrawerPinned; private boolean bottomAppBar; private boolean displayAdditionalAppBarIcons; private boolean displayingFullScreenVideo; private boolean downloadWithExternalApp; + private ForegroundColorSpan finalGrayColorSpan; private boolean fullScreenBrowsingModeEnabled; private boolean hideAppBar; - private boolean incognitoModeEnabled; private boolean inFullScreenBrowsingMode; + private boolean incognitoModeEnabled; + private ForegroundColorSpan initialGrayColorSpan; private boolean loadingNewIntent; private BroadcastReceiver orbotStatusBroadcastReceiver; private boolean reapplyAppSettingsOnRestart; private boolean reapplyDomainSettingsOnRestart; + private ForegroundColorSpan redColorSpan; private boolean sanitizeAmpRedirects; private boolean sanitizeTrackingQueries; private boolean scrollAppBar; @@ -301,19 +287,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private String saveUrlString = ""; // Declare the class views. - private FrameLayout rootFrameLayout; - private DrawerLayout drawerLayout; - private CoordinatorLayout coordinatorLayout; - private Toolbar toolbar; - private RelativeLayout urlRelativeLayout; - private EditText urlEditText; private ActionBar actionBar; + private CoordinatorLayout coordinatorLayout; + private ImageView bookmarksDrawerPinnedImageView; + private DrawerLayout drawerLayout; private LinearLayout findOnPageLinearLayout; + private FrameLayout fullScreenVideoFrameLayout; + private FrameLayout rootFrameLayout; + private SwipeRefreshLayout swipeRefreshLayout; private LinearLayout tabsLinearLayout; private TabLayout tabLayout; - private SwipeRefreshLayout swipeRefreshLayout; + private Toolbar toolbar; + private EditText urlEditText; + private RelativeLayout urlRelativeLayout; private ViewPager webViewPager; - private FrameLayout fullScreenVideoFrameLayout; // Declare the class menus. private Menu optionsMenu; @@ -370,7 +357,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private Activity resultLauncherActivityHandle; // Define the save URL activity result launcher. It must be defined before `onCreate()` is run or the app will crash. - private final ActivityResultLauncher saveUrlActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/*"), + private final ActivityResultLauncher saveUrlActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("*/*"), new ActivityResultCallback() { @Override public void onActivityResult(Uri fileUri) { @@ -391,11 +378,30 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook public void onActivityResult(Uri fileUri) { // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back. if (fileUri != null) { + // Initialize the file name string from the file URI last path segment. + String temporaryFileNameString = fileUri.getLastPathSegment(); + + // Query the exact file name if the API >= 26. + if (Build.VERSION.SDK_INT >= 26) { + // Get a cursor from the content resolver. + Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null); + + // Move to the fist row. + contentResolverCursor.moveToFirst(); + + // Get the file name from the cursor. + temporaryFileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); + + // Close the cursor. + contentResolverCursor.close(); + } + + // Save the final file name string so it can be used inside the lambdas. This will no longer be needed once this activity has transitioned to Kotlin. + String finalFileNameString = temporaryFileNameString; + try { // Create a temporary MHT file. File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir()); - - // Save the temporary MHT file. currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> { if (callbackValue != null) { // The temporary MHT file was saved successfully. try { @@ -420,29 +426,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook mhtOutputStream.close(); temporaryMhtFileInputStream.close(); - // Initialize the file name string from the file URI last path segment. - String fileNameString = fileUri.getLastPathSegment(); - - // Query the exact file name if the API >= 26. - if (Build.VERSION.SDK_INT >= 26) { - // Get a cursor from the content resolver. - Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null); - - // Move to the fist row. - contentResolverCursor.moveToFirst(); - - // Get the file name from the cursor. - fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); - - // Close the cursor. - contentResolverCursor.close(); - } - // Display a snackbar. - Snackbar.make(currentWebView, getString(R.string.file_saved) + " " + fileNameString, Snackbar.LENGTH_SHORT).show(); + Snackbar.make(currentWebView, getString(R.string.saved, finalFileNameString), Snackbar.LENGTH_SHORT).show(); } catch (Exception exception) { // Display a snackbar with the exception. - Snackbar.make(currentWebView, getString(R.string.error_saving_file) + " " + exception, Snackbar.LENGTH_INDEFINITE).show(); + Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, exception), Snackbar.LENGTH_INDEFINITE).show(); } finally { // Delete the temporary MHT file. //noinspection ResultOfMethodCallIgnored @@ -450,12 +438,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } else { // There was an unspecified error while saving the temporary MHT file. // Display an error snackbar. - Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show(); + Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, getString(R.string.unknown_error)), Snackbar.LENGTH_INDEFINITE).show(); } }); } catch (IOException ioException) { // Display a snackbar with the IO exception. - Snackbar.make(currentWebView, getString(R.string.error_saving_file) + " " + ioException, Snackbar.LENGTH_INDEFINITE).show(); + Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, ioException), Snackbar.LENGTH_INDEFINITE).show(); } } } @@ -474,6 +462,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }); + // Define the save webpage image activity result launcher. It must be defined before `onCreate()` is run or the app will crash. + private final ActivityResultLauncher browseFileUploadActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult activityResult) { + // Pass the file to the WebView. + fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), activityResult.getData())); + } + }); + // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with WebView. @SuppressLint("ClickableViewAccessibility") @Override @@ -484,15 +482,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Populate the result launcher activity. This will no longer be needed once the activity has transitioned to Kotlin. resultLauncherActivityHandle = this; - // Check to see if the activity has been restarted. - if (savedInstanceState != null) { - // Store the saved instance state variables. - savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST); - savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST); - savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION); - savedProxyMode = savedInstanceState.getString(PROXY_MODE); - } - // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default. PreferenceManager.setDefaultValues(this, R.xml.preferences, false); @@ -508,6 +497,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get the theme entry values string array. String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values); + // Get the current theme status. + int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + // Set the app theme according to the preference. A switch statement cannot be used because the theme entry values string array is not a compile time constant. if (appTheme.equals(appThemeEntryValuesStringArray[1])) { // The light theme is selected. // Apply the light theme. @@ -525,135 +517,161 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } - // Disable screenshots if not allowed. - if (!allowScreenshots) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); - } + // Do not continue if the app theme is different than the OS theme. The app always initially starts in the OS theme. + // If the user has specified the opposite theme should be used, the app will restart in that mode after the above `setDefaultNightMode()` code processes. However, the restart is delayed. + // If the blacklist coroutine starts below it will continue to run during the restart, which leads to indeterminate behavior, with the system often not knowing how many tabs exist. + // See https://redmine.stoutner.com/issues/952. + if (appTheme.equals(appThemeEntryValuesStringArray[0]) || // The system default theme is used. + (appTheme.equals(appThemeEntryValuesStringArray[1]) && currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) || // The app is running in day theme as desired. + (appTheme.equals(appThemeEntryValuesStringArray[2]) && currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)) { // The app is running in night theme as desired. + + // Disable screenshots if not allowed. + if (!allowScreenshots) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + } - // Enable the drawing of the entire webpage. This makes it possible to save a website image. This must be done before anything else happens with the WebView. - WebView.enableSlowWholeDocumentDraw(); + // Check to see if the activity has been restarted. + if (savedInstanceState != null) { + // Store the saved instance state variables. + bookmarksDrawerPinned = savedInstanceState.getBoolean(BOOKMARKS_DRAWER_PINNED); + savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST); + savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST); + savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION); + savedProxyMode = savedInstanceState.getString(PROXY_MODE); + } - // Set the content view according to the position of the app bar. - if (bottomAppBar) setContentView(R.layout.main_framelayout_bottom_appbar); - else setContentView(R.layout.main_framelayout_top_appbar); + // Enable the drawing of the entire webpage. This makes it possible to save a website image. This must be done before anything else happens with the WebView. + WebView.enableSlowWholeDocumentDraw(); - // Get handles for the views. - rootFrameLayout = findViewById(R.id.root_framelayout); - drawerLayout = findViewById(R.id.drawerlayout); - coordinatorLayout = findViewById(R.id.coordinatorlayout); - appBarLayout = findViewById(R.id.appbar_layout); - toolbar = findViewById(R.id.toolbar); - findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); - tabsLinearLayout = findViewById(R.id.tabs_linearlayout); - tabLayout = findViewById(R.id.tablayout); - swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - webViewPager = findViewById(R.id.webviewpager); - NavigationView navigationView = findViewById(R.id.navigationview); - fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); + // Set the content view according to the position of the app bar. + if (bottomAppBar) setContentView(R.layout.main_framelayout_bottom_appbar); + else setContentView(R.layout.main_framelayout_top_appbar); - // Get a handle for the navigation menu. - Menu navigationMenu = navigationView.getMenu(); + // Get handles for the views. + rootFrameLayout = findViewById(R.id.root_framelayout); + drawerLayout = findViewById(R.id.drawerlayout); + coordinatorLayout = findViewById(R.id.coordinatorlayout); + appBarLayout = findViewById(R.id.appbar_layout); + toolbar = findViewById(R.id.toolbar); + findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + tabsLinearLayout = findViewById(R.id.tabs_linearlayout); + tabLayout = findViewById(R.id.tablayout); + swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); + webViewPager = findViewById(R.id.webviewpager); + NavigationView navigationView = findViewById(R.id.navigationview); + bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview); + fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout); - // Get handles for the navigation menu items. - navigationBackMenuItem = navigationMenu.findItem(R.id.back); - navigationForwardMenuItem = navigationMenu.findItem(R.id.forward); - navigationHistoryMenuItem = navigationMenu.findItem(R.id.history); - navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests); + // Get a handle for the navigation menu. + Menu navigationMenu = navigationView.getMenu(); - // Listen for touches on the navigation menu. - navigationView.setNavigationItemSelectedListener(this); + // Get handles for the navigation menu items. + navigationBackMenuItem = navigationMenu.findItem(R.id.back); + navigationForwardMenuItem = navigationMenu.findItem(R.id.forward); + navigationHistoryMenuItem = navigationMenu.findItem(R.id.history); + navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests); - // Get a handle for the app compat delegate. - AppCompatDelegate appCompatDelegate = getDelegate(); + // Listen for touches on the navigation menu. + navigationView.setNavigationItemSelectedListener(this); - // Set the support action bar. - appCompatDelegate.setSupportActionBar(toolbar); + // Get a handle for the app compat delegate. + AppCompatDelegate appCompatDelegate = getDelegate(); - // Get a handle for the action bar. - actionBar = appCompatDelegate.getSupportActionBar(); + // Set the support action bar. + appCompatDelegate.setSupportActionBar(toolbar); - // Remove the incorrect lint warning below that the action bar might be null. - assert actionBar != null; + // Get a handle for the action bar. + actionBar = appCompatDelegate.getSupportActionBar(); - // Add the custom layout, which shows the URL text bar. - actionBar.setCustomView(R.layout.url_app_bar); - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + // Remove the incorrect lint warning below that the action bar might be null. + assert actionBar != null; - // Get handles for the views in the URL app bar. - urlRelativeLayout = findViewById(R.id.url_relativelayout); - urlEditText = findViewById(R.id.url_edittext); + // Add the custom layout, which shows the URL text bar. + actionBar.setCustomView(R.layout.url_app_bar); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - // Create the hamburger icon at the start of the AppBar. - actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer); + // Get handles for the views in the URL app bar. + urlRelativeLayout = findViewById(R.id.url_relativelayout); + urlEditText = findViewById(R.id.url_edittext); - // Initially disable the sliding drawers. They will be enabled once the blocklists are loaded. - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + // Create the hamburger icon at the start of the AppBar. + actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer); - // Initialize the web view pager adapter. - webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager()); + // Initially disable the sliding drawers. They will be enabled once the blocklists are loaded. + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - // Set the pager adapter on the web view pager. - webViewPager.setAdapter(webViewPagerAdapter); + // Initially hide the user interface so that only the blocklist loading screen is shown (if reloading). + drawerLayout.setVisibility(View.GONE); - // Store up to 100 tabs in memory. - webViewPager.setOffscreenPageLimit(100); + // Initialize the web view pager adapter. + webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager()); - // Instantiate the helpers. - bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this); - domainsDatabaseHelper = new DomainsDatabaseHelper(this); - proxyHelper = new ProxyHelper(); - sanitizeUrlHelper = new SanitizeUrlHelper(); + // Set the pager adapter on the web view pager. + webViewPager.setAdapter(webViewPagerAdapter); - // Initialize the app. - initializeApp(); + // Store up to 100 tabs in memory. + webViewPager.setOffscreenPageLimit(100); - // Apply the app settings from the shared preferences. - applyAppSettings(); + // Instantiate the helpers. + bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this); + domainsDatabaseHelper = new DomainsDatabaseHelper(this); + proxyHelper = new ProxyHelper(); - // Control what the system back command does. - OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - // Process the different back options. - if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open. - // Close the navigation drawer. - drawerLayout.closeDrawer(GravityCompat.START); - } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open. - // close the bookmarks drawer. - drawerLayout.closeDrawer(GravityCompat.END); - } else if (displayingFullScreenVideo) { // A full screen video is shown. - // Exit the full screen video. - exitFullScreenVideo(); - } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history. - // Get the current web back forward list. - WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); - - // Get the previous entry URL. - String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl(); - - // Apply the domain settings. - applyDomainSettings(currentWebView, previousUrl, false, false, false); - - // Go back. - currentWebView.goBack(); - } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs. - // Close the current tab. - closeCurrentTab(); - } else { // There isn't anything to do in Privacy Browser. - // Close Privacy Browser. `finishAndRemoveTask()` also removes Privacy Browser from the recent app list. - finishAndRemoveTask(); - - // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted. - System.exit(0); + // Update the bookmarks drawer pinned image view. + updateBookmarksDrawerPinnedImageView(); + + // Initialize the app. + initializeApp(); + + // Apply the app settings from the shared preferences. + applyAppSettings(); + + // Control what the system back command does. + OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + // Process the different back options. + if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open. + // Close the navigation drawer. + drawerLayout.closeDrawer(GravityCompat.START); + } else if (drawerLayout.isDrawerVisible(GravityCompat.END)) { // The bookmarks drawer is open. + // close the bookmarks drawer. + drawerLayout.closeDrawer(GravityCompat.END); + } else if (displayingFullScreenVideo) { // A full screen video is shown. + // Exit the full screen video. + exitFullScreenVideo(); + // It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens. + } else if ((currentWebView != null) && (currentWebView.canGoBack())) { // There is at least one item in the current WebView history. + // Get the current web back forward list. + WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); + + // Get the previous entry URL. + String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl(); + + // Apply the domain settings. + applyDomainSettings(currentWebView, previousUrl, false, false, false); + + // Go back. + currentWebView.goBack(); + } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs. + // Close the current tab. + closeCurrentTab(); + } else { // There isn't anything to do in Privacy Browser. + // Run clear and exit. + clearAndExit(); + } } - } - }; + }; + + // Register the on back pressed callback. + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); - // Register the on back pressed callback. - getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + // Instantiate the populate blocklists coroutine. + PopulateBlocklistsCoroutine populateBlocklistsCoroutine = new PopulateBlocklistsCoroutine(this); - // Populate the blocklists. - populateBlocklists = new PopulateBlocklists(this, this).execute(); + // Populate the blocklists. + populateBlocklistsCoroutine.populateBlocklists(this); + } } @Override @@ -733,6 +751,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } } else { // The app has been restarted. + // Set the saved tab position to be the size of the saved state array list. The tab position is 0 based, meaning the at the new tab will be the tab position that is restored. + savedTabPosition = savedStateArrayList.size(); + // Replace the intent that started the app with this one. This will load the tab after the others have been restored. setIntent(intent); } @@ -803,21 +824,23 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Run the default commands. super.onStart(); - // Resume any WebViews. - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Resume any WebViews if the pager adapter exists. If the app is restarting to change the initial app theme it won't have been populated yet. + if (webViewPagerAdapter != null) { + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Only resume the WebViews if they exist (they won't when the app is first created). - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Only resume the WebViews if they exist (they won't when the app is first created). + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Resume the nested scroll WebView. - nestedScrollWebView.onResume(); + // Resume the nested scroll WebView. + nestedScrollWebView.onResume(); + } } } @@ -846,10 +869,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Show any pending dialogs. for (int i = 0; i < pendingDialogsArrayList.size(); i++) { // Get the pending dialog from the array list. - PendingDialog pendingDialog = pendingDialogsArrayList.get(i); + PendingDialogDataClass pendingDialogDataClass = pendingDialogsArrayList.get(i); // Show the pending dialog. - pendingDialog.dialogFragment.show(getSupportFragmentManager(), pendingDialog.tag); + pendingDialogDataClass.dialogFragment.show(getSupportFragmentManager(), pendingDialogDataClass.tag); } // Clear the pending dialogs array list. @@ -862,20 +885,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Run the default commands. super.onStop(); - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Only pause the WebViews if the pager adapter is not null, which is the case if the app is restarting to change the initial app theme. + if (webViewPagerAdapter != null) { + // Pause each web view. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Only pause the WebViews if they exist (they won't when the app is first created). - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Only pause the WebViews if they exist (they won't when the app is first created). + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Pause the nested scroll WebView. - nestedScrollWebView.onPause(); + // Pause the nested scroll WebView. + nestedScrollWebView.onPause(); + } } } @@ -890,43 +917,47 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Run the default commands. super.onSaveInstanceState(savedInstanceState); - // Create the saved state array lists. - ArrayList savedStateArrayList = new ArrayList<>(); - ArrayList savedNestedScrollWebViewStateArrayList = new ArrayList<>(); + // Only save the instance state if the WebView pager adapter is not null, which will be the case if the app is restarting to change the initial app theme. + if (webViewPagerAdapter != null) { + // Create the saved state array lists. + ArrayList savedStateArrayList = new ArrayList<>(); + ArrayList savedNestedScrollWebViewStateArrayList = new ArrayList<>(); - // Get the URLs from each tab. - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Get the URLs from each tab. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - // Create saved state bundle. - Bundle savedStateBundle = new Bundle(); + // Create saved state bundle. + Bundle savedStateBundle = new Bundle(); - // Get the current states. - nestedScrollWebView.saveState(savedStateBundle); - Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState(); + // Get the current states. + nestedScrollWebView.saveState(savedStateBundle); + Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState(); - // Store the saved states in the array lists. - savedStateArrayList.add(savedStateBundle); - savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle); + // Store the saved states in the array lists. + savedStateArrayList.add(savedStateBundle); + savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle); + } } - } - // Get the current tab position. - int currentTabPosition = tabLayout.getSelectedTabPosition(); + // Get the current tab position. + int currentTabPosition = tabLayout.getSelectedTabPosition(); - // Store the saved states in the bundle. - savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList); - savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList); - savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition); - savedInstanceState.putString(PROXY_MODE, proxyMode); + // Store the saved states in the bundle. + savedInstanceState.putBoolean(BOOKMARKS_DRAWER_PINNED, bookmarksDrawerPinned); + savedInstanceState.putString(PROXY_MODE, proxyMode); + savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList); + savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList); + savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition); + } } @Override @@ -946,11 +977,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook bookmarksDatabaseHelper.close(); } - // Stop populating the blocklists if the AsyncTask is running in the background. - if (populateBlocklists != null) { - populateBlocklists.cancel(true); - } - // Run the default commands. super.onDestroy(); } @@ -1018,6 +1044,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly. optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26); + // Only display the dark WebView menu item if the API >= 29. + optionsDarkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 29); + // Set the status of the additional app bar icons. Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones. if (displayAdditionalAppBarIcons) { optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); @@ -1098,16 +1127,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get the current theme status. int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - // Enable dark WebView if the API is < 33 or if night mode is enabled. - optionsDarkWebViewMenuItem.setEnabled((Build.VERSION.SDK_INT < 33) || (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)); + // Enable dark WebView if night mode is enabled. + optionsDarkWebViewMenuItem.setEnabled(currentThemeStatus == Configuration.UI_MODE_NIGHT_YES); - // Set the checkbox status for dark WebView if the WebView supports it. - if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // The device is running API >= 33 and algorithmic darkening is supported. + // Set the checkbox status for dark WebView if the device is running API >= 29 and algorithmic darkening is supported. + if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView.getSettings())); - } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // The device is running API < 33 and the WebView supports force dark. - //noinspection deprecation - optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON); - } } // Set the cookies menu item checked status. @@ -1784,23 +1809,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Consume the event. return true; } else if (menuItemId == R.id.dark_webview) { // Dark WebView. - // Check to see if dark WebView is supported by this WebView. - if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // The device is running API >= 33 and algorithmic darkening is supported. - // Toggle algorithmic darkening. + // Toggle dark WebView if supported. + if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) WebSettingsCompat.setAlgorithmicDarkeningAllowed(currentWebView.getSettings(), !WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView.getSettings())); - } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // The device is running API < 33 and the WebView supports force dark. - // Toggle the dark WebView setting. - //noinspection deprecation - if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) { // Dark WebView is currently enabled. - // Turn off dark WebView. - //noinspection deprecation - WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - } else { // Dark WebView is currently disabled. - // Turn on dark WebView. - //noinspection deprecation - WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } - } // Consume the event. return true; @@ -1858,8 +1869,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(currentWebView.getCurrentUrl()); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl()); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), currentWebView.getCurrentUrl(), currentWebView.getSettings().getUserAgentString(), + currentWebView.getAcceptCookies()); } // Consume the event. @@ -1879,7 +1890,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } else if (menuItemId == R.id.add_to_homescreen) { // Add to homescreen. // Instantiate the create home screen shortcut dialog. DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(), - currentWebView.getFavoriteOrDefaultIcon()); + currentWebView.getFavoriteIcon()); // Show the create home screen shortcut dialog. createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); @@ -2233,6 +2244,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Make it so. startActivity(logcatIntent); + } else if (menuItemId == R.id.webview_devtools) { // WebView Dev. + // Create a WebView DevTools intent. + Intent webViewDevToolsIntent = new Intent("com.android.webview.SHOW_DEV_UI"); + + // Launch as a new task so that the WebView DevTools and Privacy Browser show as a separate windows in the recent tasks list. + webViewDevToolsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Make it so. + startActivity(webViewDevToolsIntent); } else if (menuItemId == R.id.guide) { // Guide. // Create an intent to launch the guide activity. Intent guideIntent = new Intent(this, GuideActivity.class); @@ -2265,7 +2285,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook super.onPostCreate(savedInstanceState); // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon. - actionBarDrawerToggle.syncState(); + // If the app is restarting to change the app theme the action bar drawer toggle will not yet be populated. + if (actionBarDrawerToggle != null) + actionBarDrawerToggle.syncState(); } @Override @@ -2346,8 +2368,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(linkUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(linkUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), linkUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2418,8 +2439,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(imageUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(imageUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), imageUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2523,8 +2543,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(imageUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(imageUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), imageUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2550,8 +2569,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook downloadUrlWithExternalApp(linkUrl); } else { // Handle the download inside of Privacy Browser. // Prepare the save dialog. The dialog will be displayed once the file size and the content disposition have been acquired. - new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(), - currentWebView.getAcceptCookies()).execute(linkUrl); + PrepareSaveDialogCoroutine.prepareSaveDialog(this, getSupportFragmentManager(), linkUrl, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()); } // Consume the event. @@ -2712,151 +2730,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the bookmarks cursor with the current contents of this folder. bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - // Update the `ListView`. + // Update the list view. bookmarksCursorAdapter.changeCursor(bookmarksCursor); // Scroll to the new folder. bookmarksListView.setSelection(0); } - @Override - public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) { - // Remove the incorrect lint warning below that the dialog fragment might be null. - assert dialogFragment != null; - - // Get the dialog. - Dialog dialog = dialogFragment.getDialog(); - - // Remove the incorrect lint warning below that the dialog might be null. - assert dialog != null; - - // Get handles for the views from the dialog. - RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton); - RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton); - ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview); - EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext); - - // Get the new folder name. - String newFolderNameString = editFolderNameEditText.getText().toString(); - - // Check if the favorite icon has changed. - if (currentFolderIconRadioButton.isChecked()) { // Only the name has changed. - // Update the name in the database. - bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString); - } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) { // Only the icon has changed. - // Create the new folder icon Bitmap. - Bitmap folderIconBitmap; - - // Populate the new folder icon bitmap. - if (defaultFolderIconRadioButton.isChecked()) { - // Get the default folder icon drawable. - Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); - - // Convert the folder icon drawable to a bitmap drawable. - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - - // Convert the folder icon bitmap drawable to a bitmap. - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - // Copy the favorite icon bitmap to the folder icon bitmap. - folderIconBitmap = favoriteIconBitmap; - } - - // Create a folder icon byte array output stream. - ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); - - // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream); - - // Convert the folder icon byte array stream to a byte array. - byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); - - // Update the folder icon in the database. - bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray); - } else { // The folder icon and the name have changed. - // Get the new folder icon bitmap. - Bitmap folderIconBitmap; - if (defaultFolderIconRadioButton.isChecked()) { - // Get the default folder icon drawable. - Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable(); - - // Convert the folder icon drawable to a bitmap drawable. - BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable; - - // Convert the folder icon bitmap drawable to a bitmap. - folderIconBitmap = folderIconBitmapDrawable.getBitmap(); - } else { // Use the `WebView` favorite icon. - // Copy the favorite icon bitmap to the folder icon bitmap. - folderIconBitmap = favoriteIconBitmap; - } - - // Create a folder icon byte array output stream. - ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream(); - - // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG). - folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream); - - // Convert the folder icon byte array stream to a byte array. - byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray(); - - // Update the folder name and icon in the database. - bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray); - } - - // Update the bookmarks cursor with the current contents of this folder. - bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); - - // Update the `ListView`. - bookmarksCursorAdapter.changeCursor(bookmarksCursor); - } - - // Process the results of a file browse. - @Override - public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) { - // Run the default commands. - super.onActivityResult(requestCode, resultCode, returnedIntent); - - // Run the commands that correlate to the specified request code. - switch (requestCode) { - case BROWSE_FILE_UPLOAD_REQUEST_CODE: - // Pass the file to the WebView. - fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent)); - break; - - case BROWSE_OPEN_REQUEST_CODE: - // Don't do anything if the user pressed back from the file picker. - if (resultCode == Activity.RESULT_OK) { - // Get a handle for the open dialog fragment. - DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open)); - - // Only update the file name if the dialog still exists. - if (openDialogFragment != null) { - // Get a handle for the open dialog. - Dialog openDialog = openDialogFragment.getDialog(); - - // Remove the incorrect lint warning below that the dialog might be null. - assert openDialog != null; - - // Get a handle for the file name edit text. - EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext); - - // Get the file name URI from the intent. - Uri fileNameUri = returnedIntent.getData(); - - // Get the file name string from the URI. - String fileNameString = fileNameUri.toString(); - - // Set the file name text. - fileNameEditText.setText(fileNameString); - - // Move the cursor to the end of the file name edit text. - fileNameEditText.setSelection(fileNameString.length()); - } - } - break; - } - } - private void loadUrlFromTextBox() { // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. String unformattedUrlString = urlEditText.getText().toString().trim(); @@ -3106,24 +2986,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Remove the lint warning below that the input method manager might be null. assert inputMethodManager != null; - // Initialize the gray foreground color spans for highlighting the URLs. + // Initialize the color spans for highlighting the URLs. initialGrayColorSpan = new ForegroundColorSpan(getColor(R.color.gray_500)); finalGrayColorSpan = new ForegroundColorSpan(getColor(R.color.gray_500)); - - // Get the current theme status. - int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - // Set the red color span according to the theme. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { - redColorSpan = new ForegroundColorSpan(getColor(R.color.red_a700)); - } else { - redColorSpan = new ForegroundColorSpan(getColor(R.color.red_900)); - } + redColorSpan = new ForegroundColorSpan(getColor(R.color.red_text)); // Remove the formatting from the URL edit text when the user is editing the text. urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { if (hasFocus) { // The user is editing the URL text box. - // Remove the highlighting. + // Remove the syntax highlighting. urlEditText.getText().removeSpan(redColorSpan); urlEditText.getText().removeSpan(initialGrayColorSpan); urlEditText.getText().removeSpan(finalGrayColorSpan); @@ -3131,8 +3002,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Move to the beginning of the string. urlEditText.setSelection(0); - // Reapply the highlighting. - highlightUrlText(); + // Reapply the syntax highlighting. + UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan); } }); @@ -3275,7 +3146,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onTabReselected(TabLayout.Tab tab) { // Instantiate the View SSL Certificate dialog. - DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteOrDefaultIcon()); + DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteIcon()); // Display the View SSL Certificate dialog. viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate)); @@ -3291,7 +3162,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the launch bookmarks activity FAB to launch the bookmarks activity. launchBookmarksActivityFab.setOnClickListener(v -> { // Get a copy of the favorite icon bitmap. - Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon(); + Bitmap favoriteIconBitmap = currentWebView.getFavoriteIcon(); // Create a favorite icon byte array output stream. ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream(); @@ -3318,7 +3189,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the create new bookmark folder FAB to display an alert dialog. createBookmarkFolderFab.setOnClickListener(v -> { // Create a create bookmark folder dialog. - DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon()); + DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteIcon()); // Show the create bookmark folder dialog. createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder)); @@ -3327,7 +3198,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the create new bookmark FAB to display an alert dialog. createBookmarkFab.setOnClickListener(view -> { // Instantiate the create bookmark dialog. - DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon()); + DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteIcon()); // Display the create bookmark dialog. createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark)); @@ -3422,14 +3293,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Load the bookmark URL. loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL))); - // Close the bookmarks drawer. - drawerLayout.closeDrawer(GravityCompat.END); + // Close the bookmarks drawer if it is not pinned. + if (!bookmarksDrawerPinned) + drawerLayout.closeDrawer(GravityCompat.END); } - // Close the `Cursor`. + // Close the cursor. bookmarkCursor.close(); }); + // Handle long-presses on bookmarks. bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> { // Convert the database ID from `long` to `int`. int databaseId = (int) id; @@ -3439,14 +3312,23 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Check to see if the bookmark is a folder. if (isFolder) { // The bookmark is a folder. - // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`. - oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)); + // Get a cursor of all the bookmarks in the folder. + Cursor bookmarksCursor = bookmarksDatabaseHelper.getFolderBookmarks(databaseId); + + // Move to the first entry in the cursor. + bookmarksCursor.moveToFirst(); + + // Open each bookmark + for (int i = 0; i < bookmarksCursor.getCount(); i++) { + // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned. + addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), (!bookmarksDrawerPinned && (i == 0))); - // Instantiate the edit folder bookmark dialog. - DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon()); + // Move to the next bookmark. + bookmarksCursor.moveToNext(); + } - // Show the edit folder bookmark dialog. - editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder)); + // Close the cursor. + bookmarksCursor.close(); } else { // The bookmark is not a folder. // Get the bookmark cursor for this ID. Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId); @@ -3454,13 +3336,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Move the bookmark cursor to the first row. bookmarkCursor.moveToFirst(); - // Load the bookmark in a new tab but do not switch to the tab or close the drawer. - addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), false); + // Load the bookmark in a new tab and move to the tab if the drawer is not pinned. + addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned); - // Display a snackbar. - Snackbar.make(drawerLayout, R.string.bookmark_opened_in_background, Snackbar.LENGTH_SHORT).show(); + // Close the cursor. + bookmarkCursor.close(); } + // Close the bookmarks drawer if it is not pinned. + if (!bookmarksDrawerPinned) + drawerLayout.closeDrawer(GravityCompat.END); + // Consume the event. return true; }); @@ -3735,7 +3621,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview); // Set the default favorite icon as the favorite icon for this tab. - tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true)); + tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true)); // Set the loading title text. tabTitleTextView.setText(R.string.loading); @@ -3797,7 +3683,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Store the general preference information. - boolean defaultXRequestedWithHeader = sharedPreferences.getBoolean(getString(R.string.x_requested_with_header_key), true); String defaultFontSizeString = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value)); String defaultUserAgentName = sharedPreferences.getString(getString(R.string.user_agent_key), getString(R.string.user_agent_default_value)); boolean defaultSwipeToRefresh = sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true); @@ -3843,7 +3728,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.setBlockAllThirdPartyRequests(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow( DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1); String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT)); - int xRequestedWithHeaderInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.X_REQUESTED_WITH_HEADER)); int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE)); int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH)); int webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME)); @@ -3883,24 +3767,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.getSettings().setSaveFormData(saveFormData); } - // Set the X-Requested-With header. - switch (xRequestedWithHeaderInt) { - case DomainsDatabaseHelper.SYSTEM_DEFAULT: - if (defaultXRequestedWithHeader) - nestedScrollWebView.setXRequestedWithHeader(); - else - nestedScrollWebView.resetXRequestedWithHeader(); - break; - - case DomainsDatabaseHelper.ENABLED: - nestedScrollWebView.setXRequestedWithHeader(); - break; - - case DomainsDatabaseHelper.DISABLED: - nestedScrollWebView.resetXRequestedWithHeader(); - break; - } - // Apply the font size. try { // Try the specified font size to see if it is valid. if (fontSize == 0) { // Apply the default font size. @@ -3968,8 +3834,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the swipe refresh layout. if (defaultSwipeToRefresh) { // Swipe to refresh is enabled. - // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. - swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0); + // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null). + if (currentWebView != null) { + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0); + } } else { // Swipe to refresh is disabled. // Disable the swipe refresh layout. swipeRefreshLayout.setEnabled(false); @@ -3980,8 +3849,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Store the swipe to refresh status in the nested scroll WebView. nestedScrollWebView.setSwipeToRefresh(true); - // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. - swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0); + + // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null). + if (currentWebView != null) { + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0); + } break; case DomainsDatabaseHelper.DISABLED: @@ -3993,8 +3866,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook break; } - // Check to see if WebView themes are supported. - if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // The device is running API >= 33 and algorithmic darkening is supported. + // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported. + if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // Set the WebView theme. switch (webViewThemeInt) { case DomainsDatabaseHelper.SYSTEM_DEFAULT: @@ -4024,48 +3897,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true); break; } - } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // The device is running API < 33 and the WebView supports force dark. - // Set the WebView theme. - switch (webViewThemeInt) { - case DomainsDatabaseHelper.SYSTEM_DEFAULT: - // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. - if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected. - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected. - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } else { // The system default theme is selected. - // Get the current system theme status. - int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - // Set the WebView theme according to the current system theme status. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode. - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - } else { // The system is in night mode. - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } - } - break; - - case DomainsDatabaseHelper.LIGHT_THEME: - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - break; - - case DomainsDatabaseHelper.DARK_THEME: - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - break; - } } // Set the viewport. @@ -4131,19 +3962,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.getSettings().setSaveFormData(saveFormData); } - // Store the X-Requested-With header status in the nested scroll WebView. - if (defaultXRequestedWithHeader) - nestedScrollWebView.setXRequestedWithHeader(); - else - nestedScrollWebView.resetXRequestedWithHeader(); - // Store the swipe to refresh status in the nested scroll WebView. nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh); // Update the swipe refresh layout. if (defaultSwipeToRefresh) { // Swipe to refresh is enabled. - // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. - swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0); + // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null). + if (currentWebView != null) { + // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes. + swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0); + } } else { // Swipe to refresh is disabled. // Disable the swipe refresh layout. swipeRefreshLayout.setEnabled(false); @@ -4177,8 +4005,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); } - // Apply the WebView theme if supported by the installed WebView. - if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // The device is running API >= 33 and algorithmic darkening is supported. + // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported. + if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // the light theme is selected. // Turn off algorithmic darkening. @@ -4193,31 +4021,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the algorithmic darkening according to the current system theme status. WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), currentThemeStatus == Configuration.UI_MODE_NIGHT_YES); } - } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // The device is running API < 33 and the WebView supports force dark. - // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. - if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected. - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected. - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } else { // The system default theme is selected. - // Get the current system theme status. - int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - // Set the WebView theme according to the current system theme status. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode. - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - } else { // The system is in night mode. - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } - } } // Set the viewport. @@ -4244,7 +4047,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Load the URL if directed. This makes sure that the domain settings are properly loaded before the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent. if (loadUrl) { - nestedScrollWebView.loadUrl(url, nestedScrollWebView.getXRequestedWithHeader()); + nestedScrollWebView.loadUrl(url); } } @@ -4306,7 +4109,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog)); } catch (Exception waitingForTorException) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog))); } } } @@ -4322,7 +4125,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog)); } catch (Exception orbotNotInstalledException) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); } } } @@ -4358,7 +4161,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog)); } catch (Exception i2pNotInstalledException) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog))); } } } @@ -4432,64 +4235,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } - private void highlightUrlText() { - // Only highlight the URL text if the box is not currently selected. - if (!urlEditText.hasFocus()) { - // Get the URL string. - String urlString = urlEditText.getText().toString(); - - // Highlight the URL according to the protocol. - if (urlString.startsWith("file://") || urlString.startsWith("content://")) { // This is a file or content URL. - // De-emphasize everything before the file name. - urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } else { // This is a web URL. - // Get the index of the `/` immediately after the domain name. - int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2)); - - // Create a base URL string. - String baseUrl; - - // Get the base URL. - if (endOfDomainName > 0) { // There is at least one character after the base URL. - // Get the base URL. - baseUrl = urlString.substring(0, endOfDomainName); - } else { // There are no characters after the base URL. - // Set the base URL to be the entire URL string. - baseUrl = urlString; - } - - // Get the index of the last `.` in the domain. - int lastDotIndex = baseUrl.lastIndexOf("."); - - // Get the index of the penultimate `.` in the domain. - int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1); - - // Markup the beginning of the URL. - if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted. - urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - // De-emphasize subdomains. - if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. - urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } - } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted. - if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name. - // De-emphasize the protocol and the additional subdomains. - urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } else { // There is only one subdomain in the domain name. - // De-emphasize only the protocol. - urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } - } - - // De-emphasize the text after the domain name. - if (endOfDomainName > 0) { - urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } - } - } - } - private void loadBookmarksFolder() { // Update the bookmarks cursor with the contents of the bookmarks database for the current folder. bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder); @@ -4590,11 +4335,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook private String sanitizeUrl(String url) { // Sanitize tracking queries. if (sanitizeTrackingQueries) - url = sanitizeUrlHelper.sanitizeTrackingQueries(url); + url = SanitizeUrlHelper.sanitizeTrackingQueries(url); // Sanitize AMP redirects. if (sanitizeAmpRedirects) - url = sanitizeUrlHelper.sanitizeAmpRedirects(url); + url = SanitizeUrlHelper.sanitizeAmpRedirects(url); // Return the sanitized URL. return url; @@ -4614,7 +4359,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Add the first tab. addNewTab("", true); } else { // The activity has been restarted. - // Restore each tab. Once the minimum API >= 24, a `forEach()` command can be used. + // Restore each tab. for (int i = 0; i < savedStateArrayList.size(); i++) { // Add a new tab. tabLayout.addTab(tabLayout.newTab()); @@ -4640,7 +4385,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook if (savedTabPosition == 0) { // The first tab is selected. // Set the first page as the current WebView. setCurrentWebView(0); - } else { // the first tab is not selected. + } else { // The first tab is not selected. // Move to the selected tab. webViewPager.setCurrentItem(savedTabPosition); } @@ -5004,6 +4749,22 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } + public void toggleBookmarksDrawerPinned(View view) { + // Toggle the bookmarks drawer pinned tracker. + bookmarksDrawerPinned = !bookmarksDrawerPinned; + + // Update the bookmarks drawer pinned image view. + updateBookmarksDrawerPinnedImageView(); + } + + private void updateBookmarksDrawerPinnedImageView() { + // Set the current icon. + if (bookmarksDrawerPinned) + bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected); + else + bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin); + } + private void setCurrentWebView(int pageNumber) { // Stop the swipe to refresh indicator if it is running swipeRefreshLayout.setRefreshing(false); @@ -5067,8 +4828,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Display the current URL in the URL text box. urlEditText.setText(url); - // Highlight the URL text. - highlightUrlText(); + // Highlight the URL syntax. + UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan); } } else { // A new intent is being loaded. // Reset the loading new intent tracker. @@ -5083,7 +4844,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Remove any background on the URL relative layout. urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null)); } - } else { // The fragment has not been populated. Try again in 100 milliseconds. + } else if (pageNumber == savedTabPosition){ // The app is being restored but the saved tab position fragment has not been populated yet. Try again in 100 milliseconds. // Create a handler to set the current WebView. Handler setCurrentWebViewHandler = new Handler(); @@ -5110,8 +4871,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get the WebView theme entry values string array. String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values); - // Apply the WebView theme if supported by the installed WebView. - if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // The device is running API >= 33 and algorithmic darkening is supported. + // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported. + if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // Set the WebView them. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected. // Turn off algorithmic darkening. @@ -5140,39 +4901,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true); } } - } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // The device is running API < 33 and the WebView supports force dark. - // Set the WebView theme. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant. - if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) { // The light theme is selected. - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - - // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode. - // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`. - nestedScrollWebView.setVisibility(View.VISIBLE); - } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) { // The dark theme is selected. - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } else { // The system default theme is selected. - // Get the current system theme status. - int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - // Set the WebView theme according to the current system theme status. - if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode. - // Turn off the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); - - // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode. - // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`. - nestedScrollWebView.setVisibility(View.VISIBLE); - } else { // The system is in night mode. - // Turn on the WebView dark mode. - //noinspection deprecation - WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON); - } - } } // Get a handle for the activity @@ -5376,10 +5104,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } // Get the file name from the content disposition. - String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl); + String fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrl); // Instantiate the save dialog. - DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, formattedFileSizeString, fileNameString, userAgent, + DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, fileNameString, formattedFileSizeString, userAgent, nestedScrollWebView.getAcceptCookies()); // Try to show the dialog. The download listener continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash. @@ -5388,7 +5116,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog)); } catch (Exception exception) { // The dialog could not be shown. // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(saveDialogFragment, getString(R.string.save_dialog))); + pendingDialogsArrayList.add(new PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog))); } } }); @@ -5468,10 +5196,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the favorite icon when it changes. @Override public void onReceivedIcon(WebView view, Bitmap icon) { - // Only update the favorite icon if the website has finished loading. - if (progressBar.getVisibility() == View.GONE) { + // Only update the favorite icon if the website has finished loading and the new favorite icon height is greater than the current favorite icon height. + // This prevents low resolution icons from replacing high resolution one. + // The check for the visibility of the progress bar can possibly be removed once https://redmine.stoutner.com/issues/747 is fixed. + if ((progressBar.getVisibility() == View.GONE) && (icon.getHeight() > nestedScrollWebView.getFavoriteIconHeight())) { // Store the new favorite icon. - nestedScrollWebView.setFavoriteOrDefaultIcon(icon); + nestedScrollWebView.setFavoriteIcon(icon); // Get the current page position. int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); @@ -5582,8 +5312,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Check to see if the file chooser intent resolves to an installed package. if (fileChooserIntent.resolveActivity(packageManager) != null) { // The file chooser intent is fine. - // Start the file chooser intent. - startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE); + // Launch the file chooser intent. + browseFileUploadActivityResultLauncher.launch(fileChooserIntent); } else { // The file chooser intent will cause a crash. // Create a generic intent to open a chooser. Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT); @@ -5594,8 +5324,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the file type to everything. genericFileChooserIntent.setType("*/*"); - // Start the generic file chooser intent. - startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE); + // Launch the generic file chooser intent. + browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent); } return true; } @@ -5603,9 +5333,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.setWebViewClient(new WebViewClient() { // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps. - // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24. @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest webResourceRequest) { + // Get the URL from the web resource request. + String url = webResourceRequest.getUrl().toString(); + // Sanitize the url. url = sanitizeUrl(url); @@ -6084,8 +5816,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Display the formatted URL text. urlEditText.setText(url); - // Apply text highlighting to the URL text box. - highlightUrlText(); + // Highlight the URL syntax. + UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan); // Hide the keyboard. inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0); @@ -6097,8 +5829,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get a URI for the current URL. Uri currentUri = Uri.parse(url); - // Get the IP addresses for the host. - new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost()); + // Get the current domain name. + String currentDomainName = currentUri.getHost(); + + if ((currentDomainName != null) && !currentDomainName.isEmpty()) { + // Get the IP addresses for the current URI. + GetHostIpAddressesCoroutine.getAddresses(currentDomainName, nestedScrollWebView, getSupportFragmentManager(), getString(R.string.pinned_mismatch)); + } // Replace Refresh with Stop if the options menu has been created. (The first WebView typically begins loading before the menu items are instantiated.) if (optionsMenu != null) { @@ -6215,8 +5952,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly. urlEditText.setText(sanitizedUrl); - // Apply text highlighting to the URL. - highlightUrlText(); + // Highlight the URL syntax. + UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan); } // Only populate the title text view if the tab has been fully created. @@ -6285,7 +6022,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); } catch (Exception exception) { // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`. - pendingDialogsArrayList.add(new PendingDialog(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error))); + pendingDialogsArrayList.add(new PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error))); } } }