+ // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for `downloadUrl`.
+ String cookies = cookieManager.getCookie(downloadUrl);
+
+ // Add the cookies to `downloadRequest`. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookies);
+ }
+
+ // Get the file name from the dialog fragment.
+ EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
+ String fileName = downloadFileNameEditText.getText().toString();
+
+ // Specify the download location.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // External write permission granted.
+ // Download to the public download directory.
+ downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+ } else { // External write permission denied.
+ // Download to the app's external download directory.
+ downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
+ }
+
+ // Allow `MediaScanner` to index the download if it is a media file.
+ downloadRequest.allowScanningByMediaScanner();
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(downloadUrl);
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+
+ // Remove the lint warning below that `downloadManager` might be `null`.
+ assert downloadManager != null;
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest);
+ } else { // The download is not an HTTP or HTTPS URI.
+ Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
+ }
+ }
+
+ // Override `onBackPressed` to handle the navigation drawer and and the WebViews.
+ @Override
+ public void onBackPressed() {
+ // 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.
+ drawerLayout.closeDrawer(GravityCompat.START);
+ } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){ // The bookmarks drawer is open.
+ if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
+ // close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ } else { // A subfolder is displayed.
+ // Place the former parent folder in `currentFolder`.
+ currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
+
+ // Load the new folder.
+ loadBookmarksFolder();
+ }
+ } else if (displayingFullScreenVideo) { // A full screen video is shown.
+ // Get a handle for the layouts.
+ FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
+ RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
+ FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
+
+ // Re-enable the screen timeout.
+ fullScreenVideoFrameLayout.setKeepScreenOn(false);
+
+ // Unset the full screen video flag.
+ displayingFullScreenVideo = false;
+
+ // Remove all the views from the full screen video frame layout.
+ fullScreenVideoFrameLayout.removeAllViews();
+
+ // Hide the full screen video frame layout.
+ fullScreenVideoFrameLayout.setVisibility(View.GONE);
+
+ // Enable the sliding drawers.
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
+
+ // Show the main content relative layout.
+ mainContentRelativeLayout.setVisibility(View.VISIBLE);
+
+ // Apply the appropriate full screen mode flags.
+ if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
+ // Hide the app bar if specified.
+ if (hideAppBar) {
+ // Get handles for the views.
+ LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
+ ActionBar actionBar = getSupportActionBar();
+
+ // Remove the incorrect lint warning below that the action bar might be null.
+ assert actionBar != null;
+
+ // Hide the tab linear layout.
+ tabsLinearLayout.setVisibility(View.GONE);
+
+ // Hide the action bar.
+ actionBar.hide();
+ }
+
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
+
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ 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.
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ // Reload the ad for the free flavor if not in full screen mode.
+ if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
+ // Reload the ad.
+ AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
+ }
+ } else if (currentWebView.canGoBack()) { // There is at least one item in the current WebView history.
+ // Get the current web back forward list.
+ WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
+
+ // Get the previous entry URL.
+ String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
+
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView, previousUrl, false, false);
+
+ // Go back.
+ currentWebView.goBack();
+ } else if (tabLayout.getTabCount() > 1) { // There are at least two tabs.
+ // Close the current tab.
+ closeCurrentTab();
+ } else { // There isn't anything to do in Privacy Browser.
+ // Close Privacy Browser. `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
+ if (Build.VERSION.SDK_INT >= 21) {
+ finishAndRemoveTask();
+ } else {
+ finish();
+ }
+
+ // Manually kill Privacy Browser. Otherwise, it is glitchy when restarted.
+ System.exit(0);
+ }
+ }
+
+ // Process the results of a file browse.
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // Run the commands that correlate to the specified request code.
+ switch (requestCode) {
+ case FILE_UPLOAD_REQUEST_CODE:
+ // File uploads only work on API >= 21.
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Pass the file to the WebView.
+ fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
+ }
+ break;
+
+ case BROWSE_SAVE_WEBPAGE_IMAGE_REQUEST_CODE:
+ // Don't do anything if the user pressed back from the file picker.
+ if (resultCode == Activity.RESULT_OK) {
+ // Get a handle for the save dialog fragment.
+ DialogFragment saveWebpageImageDialogFragment= (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.save_as_image));
+
+ // Only update the file name if the dialog still exists.
+ if (saveWebpageImageDialogFragment != null) {
+ // Get a handle for the save webpage image dialog.
+ Dialog saveWebpageImageDialog = saveWebpageImageDialogFragment.getDialog();
+
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = saveWebpageImageDialog.findViewById(R.id.file_name_edittext);
+
+ // Instantiate the file name helper.
+ FileNameHelper fileNameHelper = new FileNameHelper();
+
+ // Convert the file name URI to a file name path.
+ String fileNamePath = fileNameHelper.convertUriToFileNamePath(data.getData());
+
+ // Set the file name path as the text of the file name edit text.
+ fileNameEditText.setText(fileNamePath);
+ }
+ }
+ break;
+ }
+ }
+
+ private void loadUrlFromTextBox() {
+ // Get a handle for the URL edit text.
+ EditText urlEditText = findViewById(R.id.url_edittext);
+
+ // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string.
+ String unformattedUrlString = urlEditText.getText().toString().trim();
+
+ // Initialize the formatted URL string.
+ String url = "";
+
+ // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search.
+ if (unformattedUrlString.startsWith("content://")) { // This is a Content URL.
+ // Load the entire content URL.
+ url = unformattedUrlString;
+ } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
+ unformattedUrlString.startsWith("file://")) { // This is a standard URL.
+ // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
+ if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
+ unformattedUrlString = "https://" + unformattedUrlString;
+ }
+
+ // Initialize `unformattedUrl`.
+ URL unformattedUrl = null;
+
+ // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
+ try {
+ unformattedUrl = new URL(unformattedUrlString);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+
+ // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
+ String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
+ String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
+ String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
+ String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
+ String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
+
+ // Build the URI.
+ Uri.Builder uri = new Uri.Builder();
+ uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
+
+ // Decode the URI as a UTF-8 string in.
+ try {
+ url = URLDecoder.decode(uri.build().toString(), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ // Do nothing. The formatted URL string will remain blank.
+ }
+ } else if (!unformattedUrlString.isEmpty()){ // This is not a URL, but rather a search string.
+ // Create an encoded URL String.
+ String encodedUrlString;
+
+ // Sanitize the search input.
+ try {
+ encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
+
+ // Add the base search URL.
+ url = searchURL + encodedUrlString;
+ }
+
+ // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
+ urlEditText.clearFocus();
+
+ // Make it so.
+ loadUrl(url);
+ }
+
+ private void loadUrl(String url) {
+ // Sanitize the URL.
+ url = sanitizeUrl(url);
+
+ // Apply the domain settings.
+ applyDomainSettings(currentWebView, url, true, false);
+
+ // Load the URL.
+ currentWebView.loadUrl(url, customHeaders);
+ }
+
+ public void findPreviousOnPage(View view) {
+ // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
+ currentWebView.findNext(false);
+ }
+
+ public void findNextOnPage(View view) {
+ // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
+ currentWebView.findNext(true);
+ }
+
+ public void closeFindOnPage(View view) {
+ // Get a handle for the views.
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
+ EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
+
+ // Delete the contents of `find_on_page_edittext`.
+ findOnPageEditText.setText(null);
+
+ // 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);
+
+ // Show the toolbar.
+ toolbar.setVisibility(View.VISIBLE);
+
+ // 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 keyboard.
+ inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
+ }
+
+ @Override
+ public void onSaveWebpageImage(DialogFragment dialogFragment) {
+ // Get a handle for the file name edit text.
+ EditText fileNameEditText = dialogFragment.getDialog().findViewById(R.id.file_name_edittext);
+
+ // Get the file path string.
+ saveWebsiteImageFilePath = fileNameEditText.getText().toString();
+
+ // Check to see if the storage permission is needed.
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // The storage permission has been granted.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } else { // The storage permission has not been granted.
+ // Get the external private directory `File`.
+ File externalPrivateDirectoryFile = getExternalFilesDir(null);
+
+ // Remove the incorrect lint error below that the file might be null.
+ assert externalPrivateDirectoryFile != null;
+
+ // Get the external private directory string.
+ String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
+
+ // Check to see if the file path is in the external private directory.
+ if (saveWebsiteImageFilePath.startsWith(externalPrivateDirectory)) { // The file path is in the external private directory.
+ // Save the webpage image.
+ new SaveWebpageImage(this, currentWebView).execute(saveWebsiteImageFilePath);
+ } else { // The file path is in a public directory.
+ // Check if the user has previously denied the storage permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // Show a dialog explaining the request first.
+ // Instantiate the storage permission alert dialog.
+ DialogFragment storagePermissionDialogFragment = new StoragePermissionDialog();
+
+ // Show the storage permission alert dialog. The permission will be requested when the dialog is closed.
+ storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
+ } else { // Show the permission request directly.
+ // Request the write external storage permission. The webpage image will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onCloseStoragePermissionDialog() {
+ // Request the write external storage permission. The webpage image will be saved when it finishes.
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, SAVE_WEBPAGE_IMAGE_REQUEST_CODE);
+ }
+
+ private void applyAppSettings() {
+ // Initialize the app if this is the first run. This is done here instead of in `onCreate()` to shorten the time that an unthemed background is displayed on app startup.
+ if (webViewDefaultUserAgent == null) {
+ initializeApp();
+ }
+
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // 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.
+ 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 warning below that the action bar might be null.
+ assert actionBar != null;
+
+ // Apply the proxy through Orbot settings.
+ applyProxyThroughOrbot(false);
+
+ // Set Do Not Track status.
+ if (doNotTrackEnabled) {
+ customHeaders.put("DNT", "1");
+ } else {
+ 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.
+ WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
+
+ // Get the fragment view.
+ View fragmentView = webViewTabFragment.getView();
+
+ // Only modify 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);
+
+ // Set the app bar scrolling.
+ nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
+ }
+ }
+
+ // Update the full screen browsing mode settings.
+ 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();
+ }
+
+ // Hide the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ AdHelper.hideAd(findViewById(R.id.adview));
+ }
+
+ // Remove the translucent status flag. This is necessary so the root frame layout can fill the entire screen.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ /* Hide the system bars.
+ * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
+ * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
+ * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
+ * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
+ */
+ 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 { // Privacy Browser is not in full screen browsing mode.
+ // 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 tab linear layout.
+ tabsLinearLayout.setVisibility(View.VISIBLE);
+
+ // Show the action bar.
+ actionBar.show();
+
+ // Show the banner ad in the free flavor.
+ if (BuildConfig.FLAVOR.contentEquals("free")) {
+ // Initialize the ads. If this isn't the first run, `loadAd()` will be automatically called instead.
+ AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getSupportFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
+ }
+
+ // Remove the `SYSTEM_UI` flags from the root frame layout.
+ rootFrameLayout.setSystemUiVisibility(0);
+
+ // Add the translucent status flag.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+ }
+
+ private void initializeApp() {
+ // Get a handle for the shared preferences.
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // Get the theme preference.
+ boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
+
+ // Get a handle for the input method.
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // Remove the lint warning below that the input method manager might be null.
+ assert inputMethodManager != null;
+
+ // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
+ redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
+ initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
+ finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
+
+ // Get handles for the URL views.
+ EditText urlEditText = findViewById(R.id.url_edittext);
+
+ // Remove the formatting from the URL edit text when the user is editing the text.
+ urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
+ if (hasFocus) { // The user is editing the URL text box.
+ // Remove the highlighting.
+ urlEditText.getText().removeSpan(redColorSpan);
+ urlEditText.getText().removeSpan(initialGrayColorSpan);
+ urlEditText.getText().removeSpan(finalGrayColorSpan);
+ } else { // The user has stopped editing the URL text box.
+ // Move to the beginning of the string.
+ urlEditText.setSelection(0);
+
+ // Reapply the highlighting.
+ highlightUrlText();
+ }
+ });
+
+ // Set the go button on the keyboard to load the URL in `urlTextBox`.
+ urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
+ // If the event is a key-down event on the `enter` button, load the URL.
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ // Load the URL into the mainWebView and consume the event.
+ loadUrlFromTextBox();
+
+ // If the enter key was pressed, consume the event.
+ return true;
+ } else {
+ // If any other key was pressed, do not consume the event.
+ return false;
+ }
+ });
+
+ // Initialize the Orbot status and the waiting for Orbot trackers.
+ orbotStatus = "unknown";
+ waitingForOrbot = false;
+
+ // Create an Orbot status `BroadcastReceiver`.
+ orbotStatusBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Store the content of the status message in `orbotStatus`.
+ orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
+
+ // If Privacy Browser is waiting on Orbot, load the website now that Orbot is connected.
+ if (orbotStatus.equals("ON") && waitingForOrbot) {
+ // Reset the waiting for Orbot status.
+ waitingForOrbot = false;
+
+ // Get the intent that started the app.
+ Intent launchingIntent = getIntent();
+
+ // Get the information from the intent.
+ String launchingIntentAction = launchingIntent.getAction();
+ Uri launchingIntentUriData = launchingIntent.getData();
+
+ // If the intent action is a web search, perform the search.
+ if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
+ // Create an encoded URL string.
+ String encodedUrlString;
+
+ // Sanitize the search input and convert it to a search.
+ try {
+ encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
+ } catch (UnsupportedEncodingException exception) {
+ encodedUrlString = "";
+ }
+
+ // Load the completed search URL.
+ loadUrl(searchURL + encodedUrlString);
+ } else if (launchingIntentUriData != null){ // Check to see if the intent contains a new URL.
+ // Load the URL from the intent.
+ loadUrl(launchingIntentUriData.toString());
+ } else { // The is no URL in the intent.
+ // Select the homepage based on the proxy through Orbot status.
+ if (proxyThroughOrbot) {
+ // Load the Tor homepage.
+ loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
+ } else {
+ // Load the normal homepage.
+ loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
+ }
+ }
+ }
+ }
+ };
+
+ // Register `orbotStatusBroadcastReceiver` on `this` context.
+ this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
+
+ // Get handles for views that need to be modified.
+ DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+ NavigationView navigationView = findViewById(R.id.navigationview);
+ TabLayout tabLayout = findViewById(R.id.tablayout);
+ SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
+ ViewPager webViewPager = findViewById(R.id.webviewpager);
+ ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
+ FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
+ FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
+ FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
+ EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
+
+ // Listen for touches on the navigation menu.
+ navigationView.setNavigationItemSelectedListener(this);
+
+ // Get handles for the navigation menu and the back and forward menu items. The menu is zero-based.
+ Menu navigationMenu = navigationView.getMenu();
+ MenuItem navigationBackMenuItem = navigationMenu.getItem(2);
+ MenuItem navigationForwardMenuItem = navigationMenu.getItem(3);
+ MenuItem navigationHistoryMenuItem = navigationMenu.getItem(4);
+ MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
+
+ // Update the web view pager every time a tab is modified.
+ webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ // Do nothing.
+ }
+
+ @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 or by creating a new tab.
+ if (tabLayout.getSelectedTabPosition() != position) {
+ // Create a handler to select the tab.
+ Handler selectTabHandler = new Handler();
+
+ // Create a runnable to select the tab.
+ Runnable selectTabRunnable = () -> {
+ // Get a handle for the tab.
+ TabLayout.Tab tab = tabLayout.getTabAt(position);
+
+ // Assert that the tab is not null.
+ assert tab != null;
+
+ // Select the tab.
+ tab.select();
+ };
+
+ // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated.
+ selectTabHandler.postDelayed(selectTabRunnable, 150);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // Do nothing.
+ }
+ });
+
+ // Display the View SSL Certificate dialog when the currently selected tab is reselected.
+ tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ // Select the same page in the view pager.
+ webViewPager.setCurrentItem(tab.getPosition());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ // Instantiate the View SSL Certificate dialog.
+ DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId());
+
+ // Display the View SSL Certificate dialog.
+ viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
+ }
+ });
+
+ // Set the bookmarks drawer resources according to the theme. This can't be done in the layout due to compatibility issues with the `DrawerLayout` support widget.
+ // The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21 and and `getResources().getColor()` must be used until the minimum API >= 23.
+ if (darkTheme) {
+ launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_dark));
+ createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_dark));
+ createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_dark));
+ bookmarksListView.setBackgroundColor(getResources().getColor(R.color.gray_850));
+ } else {
+ launchBookmarksActivityFab.setImageDrawable(getResources().getDrawable(R.drawable.bookmarks_light));
+ createBookmarkFolderFab.setImageDrawable(getResources().getDrawable(R.drawable.create_folder_light));
+ createBookmarkFab.setImageDrawable(getResources().getDrawable(R.drawable.create_bookmark_light));
+ bookmarksListView.setBackgroundColor(getResources().getColor(R.color.white));
+ }
+
+ // Set the launch bookmarks activity FAB to launch the bookmarks activity.
+ launchBookmarksActivityFab.setOnClickListener(v -> {
+ // Get a copy of the favorite icon bitmap.
+ Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
+
+ // Create a favorite icon byte array output stream.
+ ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
+ favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
+
+ // Convert the favorite icon byte array stream to a byte array.
+ byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
+
+ // Create an intent to launch the bookmarks activity.
+ Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
+
+ // Add the extra information to the intent.
+ bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
+ bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
+ bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
+ bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
+
+ // Make it so.
+ startActivity(bookmarksIntent);
+ });
+
+ // Set the create new bookmark folder FAB to display an alert dialog.
+ createBookmarkFolderFab.setOnClickListener(v -> {
+ // Create a create bookmark folder dialog.
+ DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
+
+ // Show the create bookmark folder dialog.
+ createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
+ });
+
+ // Set the create new bookmark FAB to display an alert dialog.
+ createBookmarkFab.setOnClickListener(view -> {
+ // Instantiate the create bookmark dialog.
+ DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
+
+ // Display the create bookmark dialog.
+ createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
+ });
+
+ // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
+ findOnPageEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing.
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // 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());
+ }
+ }
+ });
+
+ // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
+ findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
+ // Hide the soft keyboard.
+ inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
+
+ // Consume the event.
+ return true;
+ } else { // A different key was pressed.
+ // Do not consume the event.
+ return false;
+ }
+ });
+
+ // Implement swipe to refresh.
+ swipeRefreshLayout.setOnRefreshListener(() -> currentWebView.reload());
+
+ // 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) {
+ swipeRefreshLayout.setColorSchemeResources(R.color.blue_800);
+ swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_850);
+ } else {
+ swipeRefreshLayout.setColorSchemeResources(R.color.blue_500);
+ }
+
+ // `DrawerTitle` identifies the `DrawerLayouts` in accessibility mode.
+ drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
+ drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
+
+ // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
+ bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this, null, null, 0);
+
+ // Initialize `currentBookmarksFolder`. `""` is the home folder in the database.
+ currentBookmarksFolder = "";
+
+ // Load the home folder, which is `""` in the database.
+ loadBookmarksFolder();
+
+ bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
+ // Convert the id from long to int to match the format of the bookmarks database.
+ int databaseID = (int) id;
+
+ // Get the bookmark cursor for this ID and move it to the first row.
+ Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseID);
+ bookmarkCursor.moveToFirst();
+
+ // Act upon the bookmark according to the type.
+ if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
+ // Store the new folder name in `currentBookmarksFolder`.
+ currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
+
+ // Load the new folder.
+ loadBookmarksFolder();
+ } else { // The selected bookmark is not a folder.
+ // Load the bookmark URL.
+ loadUrl(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
+
+ // Close the bookmarks drawer.
+ drawerLayout.closeDrawer(GravityCompat.END);
+ }
+
+ // Close the `Cursor`.
+ bookmarkCursor.close();
+ });
+
+ bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
+ // Convert the database ID from `long` to `int`.
+ int databaseId = (int) id;
+
+ // Find out if the selected bookmark is a folder.
+ boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
+
+ if (isFolder) {
+ // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
+ oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
+
+ // Show the edit bookmark folder `AlertDialog` and name the instance `@string/edit_folder`.
+ DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
+ editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
+ } else {
+ // Show the edit bookmark `AlertDialog` and name the instance `@string/edit_bookmark`.
+ DialogFragment editBookmarkDialog = EditBookmarkDialog.bookmarkDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
+ editBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.edit_bookmark));
+ }
+
+ // Consume the event.
+ return true;
+ });
+
+ // Get the status bar pixel size.
+ int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);