X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FMainWebViewActivity.java;h=d397a619e3c0cd9a35a32aa8ef9c7a37f2fcaef6;hp=4050a01faca5283b636b76d9be83c0137594b9bb;hb=c0f2dcca77fffd804c90cee5103795fc3849112b;hpb=fa3b8b382eb5ed86c598e3b126d1ef5dd117c5be 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 4050a01f..d397a619 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -71,7 +71,6 @@ import android.webkit.CookieManager; import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.ValueCallback; -import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; @@ -96,6 +95,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.view.GravityCompat; @@ -105,6 +105,7 @@ import androidx.fragment.app.FragmentManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager.widget.ViewPager; +import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; @@ -124,7 +125,6 @@ import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog; import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog; import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog; import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; -import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog; import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog; @@ -154,17 +154,10 @@ import java.util.List; import java.util.Map; import java.util.Set; -// TODO. Store up reloads for tabs that are not visible. -// TODO. New tabs are white in dark mode. -// TODO. Hide the tabs in full screen mode. -// TODO. Find on page. -// TODO. Use TabLayout.setScrollPosition to scroll to new tabs. - // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21. public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener, - EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener, PinnedMismatchDialog.PinnedMismatchListener, - UrlHistoryDialog.UrlHistoryListener { + EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, WebViewTabFragment.NewTabListener { // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`. public static String orbotStatus; @@ -172,9 +165,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`. It is also used in `onCreate()`, `onResume()`, and `addTab()`. public static WebViewPagerAdapter webViewPagerAdapter; - // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is used in `onRestart()` - public static boolean reloadOnRestart; - // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`. They are used in `onRestart()`. public static boolean loadUrlOnRestart; public static String urlToLoadOnRestart; @@ -231,8 +221,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`. private boolean inFullScreenBrowsingMode; - // Hide app bar is used in `applyAppSettings()` and `initializeWebView()`. + // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`. private boolean hideAppBar; + private boolean scrollAppBar; + + // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`. + private boolean loadingNewIntent; // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, . private boolean reapplyDomainSettingsOnRestart; @@ -278,6 +272,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`. private ValueCallback fileChooserCallback; + // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`. + private int defaultProgressViewStartOffset; + private int defaultProgressViewEndOffset; + + // The swipe refresh layout top padding is used when exiting full screen browsing mode. It is used in an inner class in `initializeWebView()`. + private int swipeRefreshLayoutPaddingTop; + + // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`. + private boolean sanitizeGoogleAnalytics; + private boolean sanitizeFacebookClickIds; + private boolean sanitizeTwitterAmpRedirects; + // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`. private String downloadUrl; private String downloadContentDisposition; @@ -464,11 +470,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based. Menu navigationMenu = navigationView.getMenu(); - MenuItem navigationCloseTabMenuItem = navigationMenu.getItem(0); - MenuItem navigationBackMenuItem = navigationMenu.getItem(3); - MenuItem navigationForwardMenuItem = navigationMenu.getItem(4); - MenuItem navigationHistoryMenuItem = navigationMenu.getItem(5); - MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6); + MenuItem navigationBackMenuItem = navigationMenu.getItem(2); + MenuItem navigationForwardMenuItem = navigationMenu.getItem(3); + MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4); + MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5); // Initialize the web view pager adapter. webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager()); @@ -488,19 +493,31 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onPageSelected(int position) { + // Close the find on page bar if it is open. + closeFindOnPage(null); + // Set the current WebView. setCurrentWebView(position); - // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager. + // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled via swiping in the view pager or by creating a new tab. if (tabLayout.getSelectedTabPosition() != position) { - // Get a handle for the corresponding tab. - TabLayout.Tab correspondingTab = tabLayout.getTabAt(position); + // Create a handler to select the tab. + Handler selectTabHandler = new Handler(); + + // Create a runnable select the new tab. + Runnable selectTabRunnable = () -> { + // Get a handle for the tab. + TabLayout.Tab tab = tabLayout.getTabAt(position); - // Assert that the corresponding tab is not null. - assert correspondingTab != null; + // Assert that the tab is not null. + assert tab != null; - // Select the corresponding tab. - correspondingTab.select(); + // Select the tab. + tab.select(); + }; + + // Select the tab layout after 100 milliseconds, which leaves enough time for a new tab to be created. + selectTabHandler.postDelayed(selectTabRunnable, 100); } } @@ -609,8 +626,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void afterTextChanged(Editable s) { - // Search for the text in `mainWebView`. - currentWebView.findAllAsync(findOnPageEditText.getText().toString()); + // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null. + if (currentWebView != null) { + currentWebView.findAllAsync(findOnPageEditText.getText().toString()); + } } }); @@ -631,8 +650,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Implement swipe to refresh. swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload()); - // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. - swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayout.getProgressViewStartOffset() - 10, swipeRefreshLayout.getProgressViewEndOffset()); + // Store the default progress view offsets for use later in `initializeWebView()`. + defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset(); + defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset(); // Set the swipe to refresh color according to the theme. if (darkTheme) { @@ -750,7 +770,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } // Update the navigation menu items. - navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1); navigationBackMenuItem.setEnabled(currentWebView.canGoBack()); navigationForwardMenuItem.setEnabled(currentWebView.canGoForward()); navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward())); @@ -794,19 +813,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook String intentAction = intent.getAction(); Uri intentUriData = intent.getData(); - // Only process the URI if it contains data. If the user pressed the desktop icon after the app was already running the URI will be null. - if (intentUriData != null) { - // Sets the new intent as the activity intent, which replaces the one that originally started the app. - setIntent(intent); + // Determine if this is a web search. + boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)); - // Add a new tab. - addTab(null); + // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null. + if (intentUriData != null || isWebSearch) { + // Get the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Create a URL string. String url; // If the intent action is a web search, perform the search. - if ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH)) { + if (isWebSearch) { // Create an encoded URL string. String encodedUrlString; @@ -824,8 +843,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook url = intentUriData.toString(); } - // Load the URL. - loadUrl(url); + // Add a new tab if specified in the preferences. + if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) { // Load the URL in a new tab. + // Set the loading new intent flag. + loadingNewIntent = true; + + // Add a new tab. + addNewTab(url); + } else { // Load the URL in the current tab. + // Make it so. + loadUrl(url); + } // Get a handle for the drawer layout. DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); @@ -839,9 +867,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook if (drawerLayout.isDrawerVisible(GravityCompat.END)) { drawerLayout.closeDrawer(GravityCompat.END); } - - // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it. - currentWebView.requestFocus(); } } @@ -864,51 +889,43 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Apply the app settings if returning from the Settings activity. if (reapplyAppSettingsOnRestart) { + // Reset the reapply app settings on restart tracker. + reapplyAppSettingsOnRestart = false; + // Apply the app settings. applyAppSettings(); + } - // Reload the webpage to handle changes to night mode and displaying of images. - if (reloadOnRestart) { - // Reload the WebViews. - for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { - // Get the WebView tab fragment. - WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); + // Apply the domain settings if returning from the settings or domains activity. + if (reapplyDomainSettingsOnRestart) { + // Reset the reapply domain settings on restart tracker. + reapplyDomainSettingsOnRestart = false; - // Get the fragment view. - View fragmentView = webViewTabFragment.getView(); + // Reapply the domain settings for each tab. + for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { + // Get the WebView tab fragment. + WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i); - // Only reload the WebViews if they exist. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + // Get the fragment view. + View fragmentView = webViewTabFragment.getView(); - // Reload the WebView. This doesn't seem to work if for WebViews that aren't visible. - nestedScrollWebView.reload(); + // Only reload the WebViews if they exist. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Reset the current domain name so the domain settings will be reapplied. + nestedScrollWebView.resetCurrentDomainName(); + + // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings. + if (nestedScrollWebView.getUrl() != null) { + applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true); } } - - // Reset `reloadOnRestartBoolean`. - reloadOnRestart = false; } - - // Reset the return from settings flag. - reapplyAppSettingsOnRestart = false; - } - - // TODO apply to all the tabs. - // Apply the domain settings if returning from the Domains activity. - if (reapplyDomainSettingsOnRestart) { - // Reset the current domain name so the domain settings will be reapplied. - currentWebView.resetCurrentDomainName(); - - // Reapply the domain settings. - applyDomainSettings(currentWebView, currentWebView.getUrl(), false, true); // TODO. - - // Reset the reapply domain settings on restart tracker. - reapplyDomainSettingsOnRestart = false; } - // Load the URL on restart (used when loading a bookmark. + // Load the URL on restart (used when loading a bookmark). if (loadUrlOnRestart) { // Load the specified URL. loadUrl(urlToLoadOnRestart); @@ -1070,7 +1087,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Only show Ad Consent if this is the free flavor. adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free")); - // Get the shared preference values. + // Get the shared preferences. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Get the dark theme and app bar preferences.. @@ -1379,11 +1396,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reapply the domain settings on returning to `MainWebViewActivity`. reapplyDomainSettingsOnRestart = true; - // TODO. Move these to `putExtra`. The certificate can be stored as strings. - // Store the current SSL certificate and IP addresses in the domains activity. - DomainsActivity.currentSslCertificate = currentWebView.getCertificate(); - DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses(); - // Create an intent to launch the domains activity. Intent domainsIntent = new Intent(this, DomainsActivity.class); @@ -1392,6 +1404,38 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook domainsIntent.putExtra("close_on_back", true); domainsIntent.putExtra("current_url", currentWebView.getUrl()); + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); + + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } + + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } + // Make it so. startActivity(domainsIntent); } else { // Add a new domain. @@ -1408,11 +1452,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Create the domain and store the database ID. int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain); - // TODO. Move these to `putExtra`. The certificate can be stored as strings. - // Store the current SSL certificate and IP addresses in the domains activity. - DomainsActivity.currentSslCertificate = currentWebView.getCertificate(); - DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses(); - // Create an intent to launch the domains activity. Intent domainsIntent = new Intent(this, DomainsActivity.class); @@ -1421,6 +1460,38 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook domainsIntent.putExtra("close_on_back", true); domainsIntent.putExtra("current_url", currentWebView.getUrl()); + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); + + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } + + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } + // Make it so. startActivity(domainsIntent); } @@ -1882,6 +1953,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext); + // Set the minimum height of the find on page linear layout to match the toolbar. + findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight()); + // Hide the toolbar. toolbar.setVisibility(View.GONE); @@ -2002,195 +2076,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Run the commands that correspond to the selected menu item. switch (menuItemId) { - case R.id.close_tab: - // Get a handle for the tab layout and the view pager. - TabLayout tabLayout = findViewById(R.id.tablayout); - ViewPager webViewPager = findViewById(R.id.webviewpager); - - // Get the current tab number. - int currentTabNumber = tabLayout.getSelectedTabPosition(); - - // Delete the current tab. - tabLayout.removeTabAt(currentTabNumber); - - // Delete the current page. If the selected page number did not change during the delete, it will return true, meaning that the current WebView must be reset. - if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) { - setCurrentWebView(currentTabNumber); - } - break; - case R.id.clear_and_exit: - // Close the bookmarks cursor and database. - bookmarksCursor.close(); - bookmarksDatabaseHelper.close(); - - // Get the status of the clear everything preference. - boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); - - // Get a handle for the runtime. - Runtime runtime = Runtime.getRuntime(); - - // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, - // which links to `/data/data/com.stoutner.privacybrowser.standard`. - String privateDataDirectoryString = getApplicationInfo().dataDir; - - // Clear cookies. - if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) { - // The command to remove cookies changed slightly in API 21. - if (Build.VERSION.SDK_INT >= 21) { - CookieManager.getInstance().removeAllCookies(null); - } else { - CookieManager.getInstance().removeAllCookie(); - } - - // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run. - try { - // Two commands must be used because `Runtime.exec()` does not like `*`. - Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); - Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal"); - - // Wait until the processes have finished. - deleteCookiesProcess.waitFor(); - deleteCookiesJournalProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } - - // Clear DOM storage. - if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { - // Ask `WebStorage` to clear the DOM storage. - WebStorage webStorage = WebStorage.getInstance(); - webStorage.deleteAllData(); - - // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run. - try { - // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. - Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); - - // Multiple commands must be used because `Runtime.exec()` does not like `*`. - Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); - Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); - Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); - Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); - - // Wait until the processes have finished. - deleteLocalStorageProcess.waitFor(); - deleteIndexProcess.waitFor(); - deleteQuotaManagerProcess.waitFor(); - deleteQuotaManagerJournalProcess.waitFor(); - deleteDatabaseProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } - - // Clear form data if the API < 26. - if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) { - WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); - webViewDatabase.clearFormData(); - - // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run. - try { - // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly. - Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); - Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"}); - - // Wait until the processes have finished. - deleteWebDataProcess.waitFor(); - deleteWebDataJournalProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } - - // Clear the cache. - if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { - // Clear the cache from each WebView. - 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(); - - // Only clear the cache if the WebView exists. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - - // Clear the cache for this WebView. - nestedScrollWebView.clearCache(true); - } - } - - // Manually delete the cache directories. - try { - // Delete the main cache directory. - Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache"); - - // Delete the secondary `Service Worker` cache directory. - // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. - Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); - - // Wait until the processes have finished. - deleteCacheProcess.waitFor(); - deleteServiceWorkerProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } - - // Wipe out each WebView. - 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(); - - // Only wipe out the WebView if it exists. - if (fragmentView != null) { - // Get the nested scroll WebView from the tab fragment. - NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); - - // Clear SSL certificate preferences for this WebView. - nestedScrollWebView.clearSslPreferences(); - - // Clear the back/forward history for this WebView. - nestedScrollWebView.clearHistory(); - - // Destroy the internal state of `mainWebView`. - nestedScrollWebView.destroy(); - } - } - - // Clear the custom headers. - customHeaders.clear(); - - // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache. - // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. - if (clearEverything) { - try { - // Delete the folder. - Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); - - // Wait until the process has finished. - deleteAppWebviewProcess.waitFor(); - } catch (Exception exception) { - // Do nothing if an error is thrown. - } - } - - // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. - if (Build.VERSION.SDK_INT >= 21) { - finishAndRemoveTask(); - } else { - finish(); - } - - // Remove the terminated program from RAM. The status code is `0`. - System.exit(0); + // Clear and exit Privacy Browser. + clearAndExit(); break; case R.id.home: @@ -2231,11 +2119,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook break; case R.id.history: - // Get the `WebBackForwardList`. - WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList(); + // Instantiate the URL history dialog. + DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId()); - // Show the URL history dialog and name this instance `R.string.history`. - DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList); + // Show the URL history dialog. urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); break; @@ -2267,17 +2154,44 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the flag to reapply the domain settings on restart when returning from Domain Settings. reapplyDomainSettingsOnRestart = true; - // TODO. Move these to `putExtra`. The certificate can be stored as strings. - // Store the current SSL certificate and IP addresses in the domains activity. - DomainsActivity.currentSslCertificate = currentWebView.getCertificate(); - DomainsActivity.currentIpAddresses = currentWebView.getCurrentIpAddresses(); - // Launch the domains activity. Intent domainsIntent = new Intent(this, DomainsActivity.class); // Add the extra information to the intent. domainsIntent.putExtra("current_url", currentWebView.getUrl()); + // Get the current certificate. + SslCertificate sslCertificate = currentWebView.getCertificate(); + + // Check to see if the SSL certificate is populated. + if (sslCertificate != null) { + // Extract the certificate to strings. + String issuedToCName = sslCertificate.getIssuedTo().getCName(); + String issuedToOName = sslCertificate.getIssuedTo().getOName(); + String issuedToUName = sslCertificate.getIssuedTo().getUName(); + String issuedByCName = sslCertificate.getIssuedBy().getCName(); + String issuedByOName = sslCertificate.getIssuedBy().getOName(); + String issuedByUName = sslCertificate.getIssuedBy().getUName(); + long startDateLong = sslCertificate.getValidNotBeforeDate().getTime(); + long endDateLong = sslCertificate.getValidNotAfterDate().getTime(); + + // Add the certificate to the intent. + domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName); + domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName); + domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName); + domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName); + domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName); + domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName); + domainsIntent.putExtra("ssl_start_date", startDateLong); + domainsIntent.putExtra("ssl_end_date", endDateLong); + } + + // Check to see if the current IP addresses have been received. + if (currentWebView.hasCurrentIpAddresses()) { + // Add the current IP addresses to the intent. + domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses()); + } + // Make it so. startActivity(domainsIntent); break; @@ -2400,13 +2314,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the target URL as the title of the `ContextMenu`. menu.setHeaderTitle(linkUrl); - // Add a Load URL entry. + // Add an Open in New Tab entry. menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { - // Add a new tab. - addTab(null); - - // Load the URL. - loadUrl(linkUrl); + // Load the link URL in a new tab. + addNewTab(linkUrl); return false; }); @@ -2508,89 +2419,23 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook menu.add(R.string.cancel); break; - // `IMAGE_TYPE` is an image. + // `IMAGE_TYPE` is an image. `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. Privacy Browser processes them the same. case WebView.HitTestResult.IMAGE_TYPE: + case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: // Get the image URL. imageUrl = hitTestResult.getExtra(); - // Set the image URL as the title of the `ContextMenu`. + // Set the image URL as the title of the context menu. menu.setHeaderTitle(imageUrl); - // Add a View Image entry. - menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { - loadUrl(imageUrl); - return false; - }); - - // Add a Download Image entry. - menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> { - // Check if the download should be processed by an external app. - if (sharedPreferences.getBoolean("download_with_external_app", false)) { // Download with an external app. - openUrlWithExternalApp(imageUrl); - } else { // Download with Android's download manager. - // Check to see if the storage permission has already been granted. - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { // The storage permission needs to be requested. - // Store the image URL for use by `onRequestPermissionResult()`. - downloadImageUrl = imageUrl; - - // Show a dialog if the user has previously denied the permission. - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first. - // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE. - DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE); - - // Show the download location permission alert dialog. The permission will be requested when the dialog is closed. - downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location)); - } else { // Show the permission request directly. - // Request the permission. The download dialog will be launched by `onRequestPermissionResult(). - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE); - } - } else { // The storage permission has already been granted. - // Get a handle for the download image alert dialog. - DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - - // Show the download image alert dialog. - downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)); - } - } - return false; - }); - - // Add a Copy URL entry. - menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> { - // Save the image URL in a `ClipData`. - ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); - - // Set the `ClipData` as the clipboard's primary clip. - clipboardManager.setPrimaryClip(srcImageTypeClipData); - return false; - }); - - // Add an Open with App entry. - menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> { - openWithApp(imageUrl); - return false; - }); - - // Add an Open with Browser entry. - menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> { - openWithBrowser(imageUrl); + // Add an Open in New Tab entry. + menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> { + // Load the image URL in a new tab. + addNewTab(imageUrl); return false; }); - // Add a `Cancel` entry, which by default closes the `ContextMenu`. - menu.add(R.string.cancel); - break; - - - // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link. - case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: - // Get the image URL. - imageUrl = hitTestResult.getExtra(); - - // Set the image URL as the title of the `ContextMenu`. - menu.setHeaderTitle(imageUrl); - - // Add a `View Image` entry. + // Add a View Image entry. menu.add(R.string.view_image).setOnMenuItemClickListener(item -> { loadUrl(imageUrl); return false; @@ -3036,52 +2881,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } - @Override - public void onPinnedMismatchBack() { // TODO. Move this logic to the dialog. - if (currentWebView.canGoBack()) { // There is a back page in the history. - // Reset the current domain name so that navigation works if third-party requests are blocked. - currentWebView.resetCurrentDomainName(); - - // Set navigating history so that the domain settings are applied when the new URL is loaded. - currentWebView.setNavigatingHistory(true); - - // Go back. - currentWebView.goBack(); - } else { // There are no pages to go back to. - // Load a blank page - loadUrl(""); - } - } - - @Override - public void onPinnedMismatchProceed() { // TODO. Move this logic to the dialog. - // Do not check the pinned information for this domain again until the domain changes. - currentWebView.setIgnorePinnedDomainInformation(true); - } - - @Override - public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) { // TODO. Move this logic to the dialog. - // Reset the current domain name so that navigation works if third-party requests are blocked. - currentWebView.resetCurrentDomainName(); - - // Set navigating history so that the domain settings are applied when the new URL is loaded. - currentWebView.setNavigatingHistory(true); - - // Load the history entry. - currentWebView.goBackOrForward(moveBackOrForwardSteps); - } - - @Override - public void onClearHistory() { // TODO. Move this logic to the dialog. - // Clear the history. - currentWebView.clearHistory(); - } - // Override `onBackPressed` to handle the navigation drawer and and the WebView. @Override public void onBackPressed() { - // Get a handle for the drawer layout. + // Get a handle for the drawer layout and the tab layout. DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); + TabLayout tabLayout = findViewById(R.id.tablayout); if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open. // Close the navigation drawer. @@ -3106,9 +2911,15 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // 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. - // Pass `onBackPressed()` to the system. + // Run the default commands. super.onBackPressed(); + + // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted. + System.exit(0); } } @@ -3193,6 +3004,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } private void loadUrl(String url) { + // Sanitize the URL. + url = sanitizeUrl(url); + // Apply the domain settings. applyDomainSettings(currentWebView, url, true, false); @@ -3219,8 +3033,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Delete the contents of `find_on_page_edittext`. findOnPageEditText.setText(null); - // Clear the highlighted phrases. - currentWebView.clearMatches(); + // Clear the highlighted phrases if the WebView is not null. + if (currentWebView != null) { + currentWebView.clearMatches(); + } // Hide the find on page linear layout. findOnPageLinearLayout.setVisibility(View.GONE); @@ -3245,15 +3061,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Store the values from the shared preferences in variables. incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); + sanitizeGoogleAnalytics = sharedPreferences.getBoolean("google_analytics", true); + sanitizeFacebookClickIds = sharedPreferences.getBoolean("facebook_click_ids", true); + sanitizeTwitterAmpRedirects = sharedPreferences.getBoolean("twitter_amp_redirects", true); proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); hideAppBar = sharedPreferences.getBoolean("hide_app_bar", true); + scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); - // Get handles for the views that need to be modified. `getSupportActionBar()` must be used until the minimum API >= 21. + // Get handles for the views that need to be modified. FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); ActionBar actionBar = getSupportActionBar(); + Toolbar toolbar = findViewById(R.id.toolbar); + LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout); + LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - // Remove the incorrect lint warnings below that the action bar might be null. + // Remove the incorrect lint warning below that the action bar might be null. assert actionBar != null; // Apply the proxy through Orbot settings. @@ -3266,6 +3091,36 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook customHeaders.remove("DNT"); } + // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command. + CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams(); + AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams(); + AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams(); + + // Add the scrolling behavior to the layout parameters. + if (scrollAppBar) { + // Enable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP); + } else { + // Disable scrolling of the app bar. + swipeRefreshLayoutParams.setBehavior(null); + toolbarLayoutParams.setScrollFlags(0); + findOnPageLayoutParams.setScrollFlags(0); + tabsLayoutParams.setScrollFlags(0); + + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } + + // Apply the modified layout parameters. + swipeRefreshLayout.setLayoutParams(swipeRefreshLayoutParams); + toolbar.setLayoutParams(toolbarLayoutParams); + findOnPageLinearLayout.setLayoutParams(findOnPageLayoutParams); + tabsLinearLayout.setLayoutParams(tabsLayoutParams); + // Set the app bar scrolling for each WebView. for (int i = 0; i < webViewPagerAdapter.getCount(); i++) { // Get the WebView tab fragment. @@ -3280,7 +3135,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); // Set the app bar scrolling. - nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true)); + nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar); } } @@ -3288,8 +3143,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. // Update the visibility of the app bar, which might have changed in the settings. if (hideAppBar) { + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); + + // Hide the action bar. actionBar.hide(); } else { + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); + + // Show the action bar. actionBar.show(); } @@ -3313,7 +3176,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled. inFullScreenBrowsingMode = false; - // Show the app bar. + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); + + // Show the action bar. actionBar.show(); // Show the banner ad in the free flavor. @@ -3333,7 +3199,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled. @SuppressLint("SetJavaScriptEnabled") - private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetFavoriteIcon, boolean reloadWebsite) { + private boolean applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite) { // Store a copy of the current user agent to track changes for the return boolean. String initialUserAgent = nestedScrollWebView.getSettings().getUserAgentString(); @@ -3361,30 +3227,37 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.clearPinnedIpAddresses(); // Reset the favorite icon if specified. - if (resetFavoriteIcon) { + if (resetTab) { // Initialize the favorite icon. nestedScrollWebView.initializeFavoriteIcon(); + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); + // Get a handle for the tab layout. TabLayout tabLayout = findViewById(R.id.tablayout); - // Get the current tab. - TabLayout.Tab currentTab = tabLayout.getTabAt(tabLayout.getSelectedTabPosition()); // TODO. We need to get the tab for this WebView, which might not be the current tab. + // Get the corresponding tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition); - // Remove the warning below that the current tab might be null. - assert currentTab != null; + // Update the tab if it isn't null, which sometimes happens when restarting from the background. + if (tab != null) { + // Get the tab custom view. + View tabCustomView = tab.getCustomView(); - // Get the current tab custom view. - View currentTabCustomView = currentTab.getCustomView(); + // Remove the warning below that the tab custom view might be null. + assert tabCustomView != null; - // Remove the warning below that the current tab custom view might be null. - assert currentTabCustomView != null; + // Get the tab views. + ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview); + TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview); - // Get the current tab favorite icon image view. - ImageView currentTabFavoriteIconImageView = currentTabCustomView.findViewById(R.id.favorite_icon_imageview); + // Set the default favorite icon as the favorite icon for this tab. + tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true)); - // Set the default favorite icon as the favorite icon for this tab. - currentTabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true)); + // Set the loading title text. + tabTitleTextView.setText(R.string.loading); + } } // Initialize the database handler. The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`. @@ -3581,53 +3454,49 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook nestedScrollWebView.getSettings().setTextZoom(fontSize); } - // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. - // - if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading. - // Set the user agent. - if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent. - // Get the array position of the default user agent name. - int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); - - // Set the user agent according to the system default. - switch (defaultUserAgentArrayPosition) { - case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. - // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. - nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName); - break; - - case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: - // Set the user agent to `""`, which uses the default value. - nestedScrollWebView.getSettings().setUserAgentString(""); - break; - - case SETTINGS_CUSTOM_USER_AGENT: - // Set the default custom user agent. - nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); - break; - - default: - // Get the user agent string from the user agent data array - nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]); - } - } else { // Set the user agent according to the stored name. - // Get the array position of the user agent name. - int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName); - - switch (userAgentArrayPosition) { - case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent. - nestedScrollWebView.getSettings().setUserAgentString(userAgentName); - break; - - case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: - // Set the user agent to `""`, which uses the default value. - nestedScrollWebView.getSettings().setUserAgentString(""); - break; - - default: - // Get the user agent string from the user agent data array. - nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); - } + // Set the user agent. + if (userAgentName.equals(getString(R.string.system_default_user_agent))) { // Use the system default user agent. + // Get the array position of the default user agent name. + int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); + + // Set the user agent according to the system default. + switch (defaultUserAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. + // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. + nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName); + break; + + case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: + // Set the user agent to `""`, which uses the default value. + nestedScrollWebView.getSettings().setUserAgentString(""); + break; + + case SETTINGS_CUSTOM_USER_AGENT: + // Set the default custom user agent. + nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); + break; + + default: + // Get the user agent string from the user agent data array + nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]); + } + } else { // Set the user agent according to the stored name. + // Get the array position of the user agent name. + int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName); + + switch (userAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The user agent name contains a custom user agent. + nestedScrollWebView.getSettings().setUserAgentString(userAgentName); + break; + + case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: + // Set the user agent to `""`, which uses the default value. + nestedScrollWebView.getSettings().setUserAgentString(""); + break; + + default: + // Get the user agent string from the user agent data array. + nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); } } @@ -3725,33 +3594,29 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled); } - // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. - // - if (nestedScrollWebView.getProgress() == 100) { // A URL is not loading. - // Get the array position of the user agent name. - int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); + // Get the array position of the user agent name. + int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName); - // Set the user agent. - switch (userAgentArrayPosition) { - case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. - // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. - nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName); - break; + // Set the user agent. + switch (userAgentArrayPosition) { + case UNRECOGNIZED_USER_AGENT: // The default user agent name is not on the canonical list. + // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names. + nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName); + break; - case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: - // Set the user agent to `""`, which uses the default value. - nestedScrollWebView.getSettings().setUserAgentString(""); - break; + case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT: + // Set the user agent to `""`, which uses the default value. + nestedScrollWebView.getSettings().setUserAgentString(""); + break; - case SETTINGS_CUSTOM_USER_AGENT: - // Set the default custom user agent. - nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); - break; + case SETTINGS_CUSTOM_USER_AGENT: + // Set the default custom user agent. + nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value))); + break; - default: - // Get the user agent string from the user agent data array - nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); - } + default: + // Get the user agent string from the user agent data array + nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]); } // Set the loading of webpage images. @@ -3788,11 +3653,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value)); boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); - // Get a handle for the action bar. `getSupportActionBar()` must be used until the minimum API >= 21. - ActionBar actionBar = getSupportActionBar(); - - // Remove the incorrect lint warning later that the action bar might be null. - assert actionBar != null; + // Get a handle for the app bar layout. + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); // Set the homepage, search, and proxy options. if (proxyThroughOrbot) { // Set the Tor options. @@ -3806,11 +3668,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); - // Set the `appBar` background to indicate proxying through Orbot is enabled. + // Set the app bar background to indicate proxying through Orbot is enabled. if (darkTheme) { - actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30)); + appBarLayout.setBackgroundResource(R.color.dark_blue_30); } else { - actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50)); + appBarLayout.setBackgroundResource(R.color.blue_50); } // Check to see if Orbot is ready. @@ -3838,11 +3700,11 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Reset the proxy to default. The host is `""` and the port is `"0"`. OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); - // Set the default `appBar` background. + // Set the default app bar layout background. if (darkTheme) { - actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900)); + appBarLayout.setBackgroundResource(R.color.gray_900); } else { - actionBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100)); + appBarLayout.setBackgroundResource(R.color.gray_100); } // Reset `waitingForOrbot. @@ -4100,7 +3962,54 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook startActivity(openWithBrowserIntent); } + private String sanitizeUrl(String url) { + // Sanitize Google Analytics. + if (sanitizeGoogleAnalytics) { + // Remove `?utm_`. + if (url.contains("?utm_")) { + url = url.substring(0, url.indexOf("?utm_")); + } + + // Remove `&utm_`. + if (url.contains("&utm_")) { + url = url.substring(0, url.indexOf("&utm_")); + } + } + + // Sanitize Facebook Click IDs. + if (sanitizeFacebookClickIds) { + // Remove `?fbclid=`. + if (url.contains("?fbclid=")) { + url = url.substring(0, url.indexOf("?fbclid=")); + } + + // Remove `&fbclid=`. + if (url.contains("&fbclid=")) { + url = url.substring(0, url.indexOf("&fbclid=")); + } + } + + // Sanitize Twitter AMP redirects. + if (sanitizeTwitterAmpRedirects) { + // Remove `?amp=1`. + if (url.contains("?amp=1")) { + url = url.substring(0, url.indexOf("?amp=1")); + } + } + + // Return the sanitized URL. + return url; + } + public void addTab(View view) { + // Add a new tab with a blank URL. + addNewTab(""); + } + + private void addNewTab(String url) { + // Sanitize the URL. + url = sanitizeUrl(url); + // Get a handle for the tab layout and the view pager. TabLayout tabLayout = findViewById(R.id.tablayout); ViewPager webViewPager = findViewById(R.id.webviewpager); @@ -4118,10 +4027,221 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook assert newTab != null; // Set a custom view on the new tab. - newTab.setCustomView(R.layout.custom_tab_view); + newTab.setCustomView(R.layout.tab_custom_view); // Add the new WebView page. - webViewPagerAdapter.addPage(newTabNumber, webViewPager); + webViewPagerAdapter.addPage(newTabNumber, webViewPager, url); + } + + public void closeTab(View view) { + // Get a handle for the tab layout. + TabLayout tabLayout = findViewById(R.id.tablayout); + + // Run the command according to the number of tabs. + if (tabLayout.getTabCount() > 1) { // There is more than one tab open. + // Close the current tab. + closeCurrentTab(); + } else { // There is only one tab open. + clearAndExit(); + } + } + + private void closeCurrentTab() { + // Get handles for the views. + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); + TabLayout tabLayout = findViewById(R.id.tablayout); + ViewPager webViewPager = findViewById(R.id.webviewpager); + + // Get the current tab number. + int currentTabNumber = tabLayout.getSelectedTabPosition(); + + // Delete the current tab. + tabLayout.removeTabAt(currentTabNumber); + + // Delete the current page. If the selected page number did not change during the delete, it will return true, meaning that the current WebView must be reset. + if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) { + setCurrentWebView(currentTabNumber); + } + + // Expand the app bar if it is currently collapsed. + appBarLayout.setExpanded(true); + } + + private void clearAndExit() { + // Get a handle for the shared preferences. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Close the bookmarks cursor and database. + bookmarksCursor.close(); + bookmarksDatabaseHelper.close(); + + // Get the status of the clear everything preference. + boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); + + // Get a handle for the runtime. + Runtime runtime = Runtime.getRuntime(); + + // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, + // which links to `/data/data/com.stoutner.privacybrowser.standard`. + String privateDataDirectoryString = getApplicationInfo().dataDir; + + // Clear cookies. + if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) { + // The command to remove cookies changed slightly in API 21. + if (Build.VERSION.SDK_INT >= 21) { + CookieManager.getInstance().removeAllCookies(null); + } else { + CookieManager.getInstance().removeAllCookie(); + } + + // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // Two commands must be used because `Runtime.exec()` does not like `*`. + Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); + Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal"); + + // Wait until the processes have finished. + deleteCookiesProcess.waitFor(); + deleteCookiesJournalProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Clear DOM storage. + if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { + // Ask `WebStorage` to clear the DOM storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); + + // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // Multiple commands must be used because `Runtime.exec()` does not like `*`. + Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + + // Wait until the processes have finished. + deleteLocalStorageProcess.waitFor(); + deleteIndexProcess.waitFor(); + deleteQuotaManagerProcess.waitFor(); + deleteQuotaManagerJournalProcess.waitFor(); + deleteDatabaseProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Clear form data if the API < 26. + if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) { + WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); + webViewDatabase.clearFormData(); + + // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly. + Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); + Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"}); + + // Wait until the processes have finished. + deleteWebDataProcess.waitFor(); + deleteWebDataJournalProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Clear the cache. + if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { + // Clear the cache from each WebView. + 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(); + + // Only clear the cache if the WebView exists. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Clear the cache for this WebView. + nestedScrollWebView.clearCache(true); + } + } + + // Manually delete the cache directories. + try { + // Delete the main cache directory. + Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + + // Delete the secondary `Service Worker` cache directory. + // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly. + Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); + + // Wait until the processes have finished. + deleteCacheProcess.waitFor(); + deleteServiceWorkerProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Wipe out each WebView. + 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(); + + // Only wipe out the WebView if it exists. + if (fragmentView != null) { + // Get the nested scroll WebView from the tab fragment. + NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview); + + // Clear SSL certificate preferences for this WebView. + nestedScrollWebView.clearSslPreferences(); + + // Clear the back/forward history for this WebView. + nestedScrollWebView.clearHistory(); + + // Destroy the internal state of `mainWebView`. + nestedScrollWebView.destroy(); + } + } + + // Clear the custom headers. + customHeaders.clear(); + + // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache. + // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. + if (clearEverything) { + try { + // Delete the folder. + Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + + // Wait until the process has finished. + deleteAppWebviewProcess.waitFor(); + } catch (Exception exception) { + // Do nothing if an error is thrown. + } + } + + // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. + if (Build.VERSION.SDK_INT >= 21) { + finishAndRemoveTask(); + } else { + finish(); + } + + // Remove the terminated program from RAM. The status code is `0`. + System.exit(0); } private void setCurrentWebView(int pageNumber) { @@ -4173,23 +4293,43 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the privacy icons. `true` redraws the icons in the app bar. updatePrivacyIcons(true); - // Clear the focus from the URL text box. - urlEditText.clearFocus(); - // Get a handle for the input method manager. InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // Remove the lint warning below that the input method manager might be null. assert inputMethodManager != null; - // Hide the soft keyboard. - inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); + // Get the current URL. + String url = currentWebView.getUrl(); + + // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`. + if (!loadingNewIntent) { // A new intent is not being loaded. + if ((url == null) || url.equals("about:blank")) { // The WebView is blank. + // Display the hint in the URL edit text. + urlEditText.setText(""); + + // Request focus for the URL text box. + urlEditText.requestFocus(); + + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); + } else { // The WebView has a loaded URL. + // Clear the focus from the URL text box. + urlEditText.clearFocus(); + + // Hide the soft keyboard. + inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0); - // Display the current URL in the URL text box. - urlEditText.setText(currentWebView.getUrl()); + // Display the current URL in the URL text box. + urlEditText.setText(url); - // Highlight the URL text. - highlightUrlText(); + // Highlight the URL text. + highlightUrlText(); + } + } else { // A new intent is being loaded. + // Reset the loading new intent tracker. + loadingNewIntent = false; + } // Set the background to indicate the domain settings status. if (currentWebView.getDomainSettingsApplied()) { @@ -4206,17 +4346,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } @Override - public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar) { + public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) { // Get handles for the activity views. FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout); DrawerLayout drawerLayout = findViewById(R.id.drawerlayout); RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout); ActionBar actionBar = getSupportActionBar(); + LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout); EditText urlEditText = findViewById(R.id.url_edittext); TabLayout tabLayout = findViewById(R.id.tablayout); SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout); - // Remove the incorrect lint warnings below that the some of the views might be null. + // Remove the incorrect lint warning below that the action bar might be null. assert actionBar != null; // Get a handle for the activity @@ -4274,9 +4415,25 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Toggle the full screen browsing mode. if (inFullScreenBrowsingMode) { // Switch to full screen mode. + // Store the swipe refresh layout top padding. + swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop(); + // Hide the app bar if specified. if (hideAppBar) { + // Close the find on page bar if it is visible. + closeFindOnPage(null); + + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); + + // Hide the action bar. actionBar.hide(); + + // Check to see if app bar scrolling is disabled. + if (!scrollAppBar) { + // Remove the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + } } // Hide the banner ad in the free flavor. @@ -4296,9 +4453,18 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } else { // Switch to normal viewing mode. - // Show the app bar. + // Show the tab linear layout. + tabsLinearLayout.setVisibility(View.VISIBLE); + + // Show the action bar. actionBar.show(); + // Check to see if app bar scrolling is disabled. + if (!scrollAppBar) { + // Add the padding from the top of the swipe refresh layout. + swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0); + } + // Show the banner ad in the free flavor. if (BuildConfig.FLAVOR.contentEquals("free")) { // Reload the ad. @@ -4333,14 +4499,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook registerForContextMenu(nestedScrollWebView); // Allow the downloading of files. - nestedScrollWebView.setDownloadListener((String url, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { + nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> { // Check if the download should be processed by an external app. if (downloadWithExternalApp) { // Download with an external app. // Create a download intent. Not specifying the action type will display the maximum number of options. Intent downloadIntent = new Intent(); // Set the URI and the MIME type. Specifying `text/html` displays a good number of options. - downloadIntent.setDataAndType(Uri.parse(url), "text/html"); + downloadIntent.setDataAndType(Uri.parse(downloadUrl), "text/html"); // Flag the intent to open in a new task. downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -4353,7 +4519,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // The WRITE_EXTERNAL_STORAGE permission needs to be requested. // Store the variables for future use by `onRequestPermissionsResult()`. - downloadUrl = url; + this.downloadUrl = downloadUrl; downloadContentDisposition = contentDisposition; downloadContentLength = contentLength; @@ -4370,7 +4536,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } else { // The storage permission has already been granted. // Get a handle for the download file alert dialog. - DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); + DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, contentDisposition, contentLength); // Show the download file alert dialog. downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); @@ -4401,15 +4567,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }); - if (Build.VERSION.SDK_INT >= 23) { - nestedScrollWebView.setOnScrollChangeListener((View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) -> { - // Update the status of swipe to refresh if it is enabled. - if (nestedScrollWebView.getSwipeToRefresh()) { - // Only enable swipe to refresh if the WebView is scrolled to the top. - swipeRefreshLayout.setEnabled(scrollY == 0); - } - }); - } + // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView. + // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`. + nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> { + if (nestedScrollWebView.getSwipeToRefresh()) { + // Only enable swipe to refresh if the WebView is scrolled to the top. + swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0); + } + }); // Set the web chrome client. nestedScrollWebView.setWebChromeClient(new WebChromeClient() { @@ -4417,7 +4582,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onProgressChanged(WebView view, int progress) { // Inject the night mode CSS if night mode is enabled. - if (nestedScrollWebView.getNightMode()) { + if (nestedScrollWebView.getNightMode()) { // Night mode is enabled. // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links // used by WordPress. `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color. // `border: none` removes all borders, which can also be used to underline text. `a {color: #1565C0}` sets links to be a dark blue. @@ -4436,9 +4601,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } }; - // Displaying of `mainWebView` after 500 milliseconds. + // Display the WebView after 500 milliseconds. displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); }); + } else { // Night mode is disabled. + // Display the nested scroll WebView if night mode is disabled. + // Because of a race condition between `applyDomainSettings` and `onPageStarted`, + // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled. + nestedScrollWebView.setVisibility(View.VISIBLE); } // Update the progress bar. @@ -4452,13 +4622,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Hide the progress bar. progressBar.setVisibility(View.GONE); - // Display the nested scroll WebView if night mode is disabled. - // Because of a race condition between `applyDomainSettings` and `onPageStarted`, - // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled. - if (!nestedScrollWebView.getNightMode()) { - nestedScrollWebView.setVisibility(View.VISIBLE); - } - //Stop the swipe to refresh indicator if it is running swipeRefreshLayout.setRefreshing(false); } @@ -4591,6 +4754,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode. // Hide the app bar if specified. if (hideAppBar) { + // Hide the tab linear layout. + tabsLinearLayout.setVisibility(View.GONE); + + // Hide the action bar. actionBar.hide(); } @@ -4648,6 +4815,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24. @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { + // Sanitize the url. + url = sanitizeUrl(url); + if (url.startsWith("http")) { // Load the URL in Privacy Browser. // Apply the domain settings for the new URL. This doesn't do anything if the domain has not changed. boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false); @@ -4721,6 +4891,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Check requests against the block lists. The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21. @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + // Sanitize the URL. + url = sanitizeUrl(url); + // Get a handle for the navigation view. NavigationView navigationView = findViewById(R.id.navigationview); @@ -4728,16 +4901,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook Menu navigationMenu = navigationView.getMenu(); // Get a handle for the navigation requests menu item. The menu is 0 based. - MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6); - - // Get handles for the options menu items. - MenuItem blocklistsMenuItem = optionsMenu.findItem(R.id.blocklists); - MenuItem easyListMenuItem = optionsMenu.findItem(R.id.easylist); - MenuItem easyPrivacyMenuItem = optionsMenu.findItem(R.id.easyprivacy); - MenuItem fanboysAnnoyanceListMenuItem = optionsMenu.findItem(R.id.fanboys_annoyance_list); - MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list); - MenuItem ultraPrivacyMenuItem = optionsMenu.findItem(R.id.ultraprivacy); - MenuItem blockAllThirdPartyRequestsMenuItem = optionsMenu.findItem(R.id.block_all_third_party_requests); + MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5); // Create an empty web resource response to be used if the resource request is blocked. WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes())); @@ -4802,9 +4966,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook activity.runOnUiThread(() -> { // Update the menu item titles. navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + - getString(R.string.block_all_third_party_requests)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + + getString(R.string.block_all_third_party_requests)); + } }); } @@ -4833,8 +5001,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook activity.runOnUiThread(() -> { // Update the menu item titles. navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - ultraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy)); + } }); } @@ -4870,8 +5042,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook activity.runOnUiThread(() -> { // Update the menu item titles. navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - easyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist)); + } }); } @@ -4904,8 +5080,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook activity.runOnUiThread(() -> { // Update the menu item titles. navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - easyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy)); + } }); } @@ -4938,9 +5118,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook activity.runOnUiThread(() -> { // Update the menu item titles. navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - fanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + - getString(R.string.fanboys_annoyance_list)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + + getString(R.string.fanboys_annoyance_list)); + } }); } @@ -4971,9 +5155,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook activity.runOnUiThread(() -> { // Update the menu item titles. navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); - fanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + - getString(R.string.fanboys_social_blocking_list)); + + // Update the options menu if it has been populated. + if (optionsMenu != null) { + optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS)); + optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + + getString(R.string.fanboys_social_blocking_list)); + } }); } @@ -5012,9 +5200,31 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - // Get the theme preference. + // Get the preferences. + boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true); boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false); + // Get a handler for the app bar layout. + AppBarLayout appBarLayout = findViewById(R.id.appbar_layout); + + // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. + if (scrollAppBar) { + // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior. + swipeRefreshLayout.setPadding(0, 0, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset); + } else { + // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated. + int appBarHeight = appBarLayout.getHeight(); + + // The swipe refresh layout must be manually moved below the app bar layout. + swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0); + + // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels. + swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight); + } + // Reset the list of resource requests. nestedScrollWebView.clearResourceRequests(); @@ -5024,6 +5234,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. if (nestedScrollWebView.getNightMode()) { nestedScrollWebView.setVisibility(View.INVISIBLE); + } else { + nestedScrollWebView.setVisibility(View.VISIBLE); } // Hide the keyboard. @@ -5031,11 +5243,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Check to see if Privacy Browser is waiting on Orbot. if (!waitingForOrbot) { // Process the URL. - // Display the formatted URL text. - urlEditText.setText(url); + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - // Apply text highlighting to `urlTextBox`. - highlightUrlText(); + // Update the URL text bar if the page is currently selected. + if (tabLayout.getSelectedTabPosition() == currentPagePosition) { + // Clear the focus from the URL edit text. + urlEditText.clearFocus(); + + // Display the formatted URL text. + urlEditText.setText(url); + + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); + } // Reset the list of host IP addresses. nestedScrollWebView.clearCurrentIpAddresses(); @@ -5145,37 +5366,63 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook // Update the URL text box and apply domain settings if not waiting on Orbot. if (!waitingForOrbot) { - // Check to see if `WebView` has set `url` to be `about:blank`. - if (url.equals("about:blank")) { // The WebView is blank. - // Display the hint in the URL edit text. - urlEditText.setText(""); + // Get the current page position. + int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId()); - // Request focus for `urlTextBox`. - urlEditText.requestFocus(); + // Check the current website information against any pinned domain information if the current IP addresses have been loaded. + if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() && + !nestedScrollWebView.ignorePinnedDomainInformation()) { + CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView); + } + + // Get the current URL from the nested scroll WebView. This is more accurate than using the URL passed into the method, which is sometimes not the final one. + String currentUrl = nestedScrollWebView.getUrl(); - // Display the keyboard. - inputMethodManager.showSoftInput(urlEditText, 0); + // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text. + // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point. + // Probably some sort of race condition when Privacy Browser is being resumed. + if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) { + // Check to see if the URL is `about:blank`. + if (currentUrl.equals("about:blank")) { // The WebView is blank. + // Display the hint in the URL edit text. + urlEditText.setText(""); - // Hide the WebView, which causes the default background color to be displayed according to the theme. - nestedScrollWebView.setVisibility(View.GONE); + // Request focus for the URL text box. + urlEditText.requestFocus(); - // Apply the domain settings. This clears any settings from the previous domain. - applyDomainSettings(nestedScrollWebView, "", true, false); - } else { // The WebView has loaded a webpage. - // Only update the URL text box if the user is not typing in it. - if (!urlEditText.hasFocus()) { - // 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(nestedScrollWebView.getUrl()); + // Display the keyboard. + inputMethodManager.showSoftInput(urlEditText, 0); - // Apply text highlighting to `urlTextBox`. + // Hide the WebView, which causes the default background color to be displayed according to the theme. + nestedScrollWebView.setVisibility(View.INVISIBLE); + + // Apply the domain settings. This clears any settings from the previous domain. + applyDomainSettings(nestedScrollWebView, "", true, false); + } else { // The WebView has loaded a webpage. + // 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(currentUrl); + + // Apply text highlighting to the URL. highlightUrlText(); } } - // Check the current website information against any pinned domain information if the current IP addresses have been loaded. - if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() && - !nestedScrollWebView.ignorePinnedDomainInformation()) { - CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView); + // Get the current tab. + TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition); + + // Only populate the title text view if the tab has been fully created. + if (tab != null) { + // Get the custom view from the tab. + View tabView = tab.getCustomView(); + + // Remove the incorrect warning below that the current tab view might be null. + assert tabView != null; + + // Get the title text view from the tab. + TextView tabTitleTextView = tabView.findViewById(R.id.title_textview); + + // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history. + tabTitleTextView.setText(nestedScrollWebView.getTitle()); } } } @@ -5272,6 +5519,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook } } } + } else { // This is not the first tab. + // Apply the domain settings. + applyDomainSettings(nestedScrollWebView, url, false, false); + + // Load the URL. + nestedScrollWebView.loadUrl(url, customHeaders); + + // Display the keyboard if the URL is blank. + if (url.equals("")) { + inputMethodManager.showSoftInput(urlEditText, 0); + } } } } \ No newline at end of file