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;
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;
import java.util.Map;
import java.util.Set;
-// TODO. New tabs are white in dark mode.
-// TODO. Hide the tabs in full screen mode.
-// TODO. Find on page.
-
// 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,
// `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;
// `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
private ValueCallback<Uri[]> 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;
+
// The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
private String downloadUrl;
private String downloadContentDisposition;
// 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());
@Override
public void onPageSelected(int position) {
+ // Close the find on page bar if it is open.
+ closeFindOnPage(null);
+
// Set the current WebView.
setCurrentWebView(position);
@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());
+ }
}
});
// 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) {
}
// Update the navigation menu items.
- navigationCloseTabMenuItem.setEnabled(tabLayout.getTabCount() > 1);
navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
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;
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);
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();
}
}
// Reset the current domain name so the domain settings will be reapplied.
nestedScrollWebView.resetCurrentDomainName();
- // Reapply the domain settings.
- applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
+ // 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);
+ }
}
}
}
// 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..
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);
// 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:
// 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;
});
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;
// 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.
// Go back.
currentWebView.goBack();
- } else { // There is nothing else to do.
- // Load a blank website.
- loadUrl("");
+ } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs.
+ // Close the current tab.
+ closeCurrentTab();
+ } else { // There isn't anything to do in Privacy Browser.
+ // Run the default commands.
+ super.onBackPressed();
+
+ // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted.
+ System.exit(0);
}
}
}
private void loadUrl(String url) {
+ // Sanitize the URL.
+ url = sanitizeUrl(url);
+
// Apply the domain settings.
applyDomainSettings(currentWebView, url, true, false);
// 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);
// 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);
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.
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.
NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
// Set the app bar scrolling.
- nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
+ nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
}
}
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();
}
// 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.
// Get the corresponding tab.
TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
- // Remove the warning below that the tab might be null.
- assert tab != null;
-
- // Get the tab custom view.
- View tabCustomView = tab.getCustomView();
+ // 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();
- // Remove the warning below that the tab custom view might be null.
- assert tabCustomView != null;
+ // Remove the warning below that the tab custom view might be null.
+ assert tabCustomView != null;
- // Get the tab views.
- ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
- TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
+ // Get the tab views.
+ ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
+ TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
- // Set the default favorite icon as the favorite icon for this tab.
- tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
+ // Set the default favorite icon as the favorite icon for this tab.
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
- // Set the loading title text.
- tabTitleTextView.setText(R.string.loading);
+ // 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`.
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.
// 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.
// 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.
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_"));
+ }
+ }
+
+ // 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);
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) {
// 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();
- // Display the current URL in the URL text box.
- urlEditText.setText(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("");
- // Highlight the URL text.
- highlightUrlText();
+ // 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(url);
+
+ // 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()) {
}
@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
// 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.
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.
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);
// 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;
}
} 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));
}
});
- 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() {
@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.
// 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.
// 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);
}
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();
}
// 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);
Menu navigationMenu = navigationView.getMenu();
// Get a handle for the navigation requests menu item. The menu is 0 based.
- MenuItem navigationRequestsMenuItem = navigationMenu.getItem(6);
+ 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()));
@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();
// 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.
// 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);
// Get the current page position.
int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
- // Update the URL text bar if the page is currently selected.
- if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
+ // 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();
+
+ // 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 (url.equals("about:blank")) { // The WebView is blank.
+ if (currentUrl.equals("about:blank")) { // The WebView is blank.
// Display the hint in the URL edit text.
urlEditText.setText("");
// Display the keyboard.
inputMethodManager.showSoftInput(urlEditText, 0);
- // Hide the WebView, which causes the default background color to be displayed according to the theme. // TODO
- nestedScrollWebView.setVisibility(View.GONE);
+ // 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.
- // 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 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();
- }
+ // 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());
}
}
}
}
}
}
+ } 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