Implement context menus for links and images. Fixes https://redmine.stoutner.com...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / MainWebViewActivity.java
1 /**
2  * Copyright 2015-2016 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser;
21
22 import android.annotation.SuppressLint;
23 import android.app.DialogFragment;
24 import android.app.DownloadManager;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.SharedPreferences;
28 import android.content.res.Configuration;
29 import android.graphics.Bitmap;
30 import android.graphics.drawable.BitmapDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.net.Uri;
33 import android.net.http.SslCertificate;
34 import android.net.http.SslError;
35 import android.os.Build;
36 import android.os.Bundle;
37 import android.preference.PreferenceManager;
38 import android.print.PrintDocumentAdapter;
39 import android.print.PrintManager;
40 import android.support.annotation.NonNull;
41 import android.support.design.widget.NavigationView;
42 import android.support.design.widget.Snackbar;
43 import android.support.v4.app.ActivityCompat;
44 import android.support.v4.content.ContextCompat;
45 import android.support.v4.view.GravityCompat;
46 import android.support.v4.widget.DrawerLayout;
47 import android.support.v4.widget.SwipeRefreshLayout;
48 import android.support.v7.app.ActionBar;
49 import android.support.v7.app.ActionBarDrawerToggle;
50 import android.support.v7.app.AppCompatActivity;
51 import android.support.v7.app.AppCompatDialogFragment;
52 import android.support.v7.widget.Toolbar;
53 import android.text.Editable;
54 import android.text.TextWatcher;
55 import android.util.Patterns;
56 import android.view.ContextMenu;
57 import android.view.KeyEvent;
58 import android.view.Menu;
59 import android.view.MenuItem;
60 import android.view.View;
61 import android.view.inputmethod.InputMethodManager;
62 import android.webkit.CookieManager;
63 import android.webkit.DownloadListener;
64 import android.webkit.SslErrorHandler;
65 import android.webkit.WebChromeClient;
66 import android.webkit.WebStorage;
67 import android.webkit.WebView;
68 import android.webkit.WebViewClient;
69 import android.webkit.WebViewDatabase;
70 import android.widget.EditText;
71 import android.widget.FrameLayout;
72 import android.widget.ImageView;
73 import android.widget.LinearLayout;
74 import android.widget.ProgressBar;
75 import android.widget.RelativeLayout;
76 import android.widget.TextView;
77
78 import java.io.UnsupportedEncodingException;
79 import java.net.MalformedURLException;
80 import java.net.URL;
81 import java.net.URLEncoder;
82 import java.util.HashMap;
83 import java.util.Map;
84
85 // We need to use AppCompatActivity from android.support.v7.app.AppCompatActivity to have access to the SupportActionBar until the minimum API is >= 21.
86 public class MainWebViewActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, CreateHomeScreenShortcut.CreateHomeScreenSchortcutListener,
87         SslCertificateError.SslCertificateErrorListener, DownloadFile.DownloadFileListener, DownloadImage.DownloadImageListener {
88
89     // `appBar` is public static so it can be accessed from `OrbotProxyHelper`.
90     // It is also used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
91     public static ActionBar appBar;
92
93     // `favoriteIcon` is public static so it can be accessed from `CreateHomeScreenShortcut`, `BookmarksActivity`, `CreateBookmark`, `CreateBookmarkFolder`, and `EditBookmark`.
94     // It is also used in `onCreate()` and `onCreateHomeScreenShortcutCreate()`.
95     public static Bitmap favoriteIcon;
96
97     // `formattedUrlString` is public static so it can be accessed from `BookmarksActivity`.
98     // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onCreateHomeScreenShortcutCreate()`, and `loadUrlFromTextBox()`.
99     public static String formattedUrlString;
100
101     // `sslCertificate` is public static so it can be accessed from `ViewSslCertificate`.  It is also used in `onCreate()`.
102     public static SslCertificate sslCertificate;
103
104
105     // 'mainWebView' is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, and `loadUrlFromTextBox()`.
106     private WebView mainWebView;
107
108     // `swipeRefreshLayout` is used in `onCreate()`, `onPrepareOptionsMenu`, and `onRestart()`.
109     private SwipeRefreshLayout swipeRefreshLayout;
110
111     // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, and `onRestart()`.
112     private CookieManager cookieManager;
113
114     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrlFromTextBox()`.
115     private final Map<String, String> customHeaders = new HashMap<>();
116
117     // `javaScriptEnabled` is also used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `applySettings()`.
118     // It is `Boolean` instead of `boolean` because `applySettings()` needs to know if it is `null`.
119     private Boolean javaScriptEnabled;
120
121     // `firstPartyCookiesEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applySettings()`.
122     private boolean firstPartyCookiesEnabled;
123
124     // `thirdPartyCookiesEnabled` used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applySettings()`.
125     private boolean thirdPartyCookiesEnabled;
126
127     // `domStorageEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applySettings()`.
128     private boolean domStorageEnabled;
129
130     // `saveFormDataEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applySettings()`.
131     private boolean saveFormDataEnabled;
132
133     // `swipeToRefreshEnabled` is used in `onPrepareOptionsMenu()` and `applySettings()`.
134     private boolean swipeToRefreshEnabled;
135
136     // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applySettings()`.
137     private String homepage;
138
139     // `javaScriptDisabledSearchURL` is used in `loadURLFromTextBox()` and `applySettings()`.
140     private String javaScriptDisabledSearchURL;
141
142     // `javaScriptEnabledSearchURL` is used in `loadURLFromTextBox()` and `applySettings()`.
143     private String javaScriptEnabledSearchURL;
144
145     // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`.
146     private Menu mainMenu;
147
148     // `drawerToggle` is used in `onCreate()`, `onPostCreate()`, `onConfigurationChanged()`, `onNewIntent()`, and `onNavigationItemSelected()`.
149     private ActionBarDrawerToggle drawerToggle;
150
151     // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, and `onBackPressed()`.
152     private DrawerLayout drawerLayout;
153
154     // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, and `loadUrlFromTextBox()`.
155     private EditText urlTextBox;
156
157     // `adView` is used in `onCreate()` and `onConfigurationChanged()`.
158     private View adView;
159
160     // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`.
161     private SslErrorHandler sslErrorHandler;
162
163     // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
164     private EditText findOnPageEditText;
165
166     // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
167     private InputMethodManager inputMethodManager;
168
169     @Override
170     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.  The whole premise of Privacy Browser is built around an understanding of these dangers.
171     @SuppressLint("SetJavaScriptEnabled")
172     protected void onCreate(Bundle savedInstanceState) {
173         super.onCreate(savedInstanceState);
174         setContentView(R.layout.main_coordinatorlayout);
175
176         // Get a handle for `inputMethodManager`.
177         inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
178
179         // We need to use the SupportActionBar from android.support.v7.app.ActionBar until the minimum API is >= 21.
180         Toolbar supportAppBar = (Toolbar) findViewById(R.id.appBar);
181         setSupportActionBar(supportAppBar);
182         appBar = getSupportActionBar();
183
184         // This is needed to get rid of the Android Studio warning that appBar might be null.
185         assert appBar != null;
186
187         // Add the custom url_app_bar layout, which shows the favoriteIcon, urlTextBar, and progressBar.
188         appBar.setCustomView(R.layout.url_app_bar);
189         appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
190
191         // Set the "go" button on the keyboard to load the URL in urlTextBox.
192         urlTextBox = (EditText) appBar.getCustomView().findViewById(R.id.urlTextBox);
193         urlTextBox.setOnKeyListener(new View.OnKeyListener() {
194             @Override
195             public boolean onKey(View v, int keyCode, KeyEvent event) {
196                 // If the event is a key-down event on the `enter` button, load the URL.
197                 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
198                     // Load the URL into the mainWebView and consume the event.
199                     try {
200                         loadUrlFromTextBox();
201                     } catch (UnsupportedEncodingException e) {
202                         e.printStackTrace();
203                     }
204                     // If the enter key was pressed, consume the event.
205                     return true;
206                 } else {
207                     // If any other key was pressed, do not consume the event.
208                     return false;
209                 }
210             }
211         });
212
213         // Get handles for `fullScreenVideoFrameLayout`, `mainWebView`, and `find_on_page_edittext`.
214         final FrameLayout fullScreenVideoFrameLayout = (FrameLayout) findViewById(R.id.fullScreenVideoFrameLayout);
215         mainWebView = (WebView) findViewById(R.id.mainWebView);
216         findOnPageEditText = (EditText) findViewById(R.id.find_on_page_edittext);
217
218         // Update `findOnPageCountTextView`.
219         mainWebView.setFindListener(new WebView.FindListener() {
220             // Get a handle for `findOnPageCountTextView`.
221             TextView findOnPageCountTextView = (TextView) findViewById(R.id.find_on_page_count_textview);
222
223             @Override
224             public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
225                 if ((isDoneCounting) && (numberOfMatches == 0)) {  // There are no matches.
226                     // Set `findOnPageCountTextView` to `0/0`.
227                     findOnPageCountTextView.setText(R.string.zero_of_zero);
228                 } else if (isDoneCounting) {  // There are matches.
229                     // `activeMatchOrdinal` is zero-based.
230                     int activeMatch = activeMatchOrdinal + 1;
231
232                     // Set `findOnPageCountTextView`.
233                     findOnPageCountTextView.setText(activeMatch + "/" + numberOfMatches);
234                 }
235             }
236         });
237
238         // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
239         findOnPageEditText.addTextChangedListener(new TextWatcher() {
240             @Override
241             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
242                 // Do nothing.
243             }
244
245             @Override
246             public void onTextChanged(CharSequence s, int start, int before, int count) {
247                 // Do nothing.
248             }
249
250             @Override
251             public void afterTextChanged(Editable s) {
252                 // Search for the text in `mainWebView`.
253                 mainWebView.findAllAsync(findOnPageEditText.getText().toString());
254             }
255         });
256
257         // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
258         findOnPageEditText.setOnKeyListener(new View.OnKeyListener() {
259             @Override
260             public boolean onKey(View v, int keyCode, KeyEvent event) {
261                 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
262                     // Hide the soft keyboard.  `0` indicates no additional flags.
263                     inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
264
265                     // Consume the event.
266                     return true;
267                 } else {  // A different key was pressed.
268                     // Do not consume the event.
269                     return false;
270                 }
271             }
272         });
273
274         // Implement swipe to refresh
275         swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
276         swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
277         swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
278             @Override
279             public void onRefresh() {
280                 mainWebView.reload();
281             }
282         });
283
284         // Create the navigation drawer.
285         drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout);
286         // The DrawerTitle identifies the drawer in accessibility mode.
287         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
288
289         // Listen for touches on the navigation menu.
290         final NavigationView navigationView = (NavigationView) findViewById(R.id.navigationView);
291         navigationView.setNavigationItemSelectedListener(this);
292
293         // drawerToggle creates the hamburger icon at the start of the AppBar.
294         drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, supportAppBar, R.string.open_navigation, R.string.close_navigation);
295
296         mainWebView.setWebViewClient(new WebViewClient() {
297             // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
298             // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24.
299             @SuppressWarnings("deprecation")
300             @Override
301             public boolean shouldOverrideUrlLoading(WebView view, String url) {
302                 // Use an external email program if the link begins with "mailto:".
303                 if (url.startsWith("mailto:")) {
304                     // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
305                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
306
307                     // Parse the url and set it as the data for the `Intent`.
308                     emailIntent.setData(Uri.parse(url));
309
310                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
311                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
312
313                     // Make it so.
314                     startActivity(emailIntent);
315                     return true;
316                 } else {  // Load the URL in Privacy Browser.
317                     mainWebView.loadUrl(url, customHeaders);
318                     return true;
319                 }
320             }
321
322             // Update the URL in urlTextBox when the page starts to load.
323             @Override
324             public void onPageStarted(WebView view, String url, Bitmap favicon) {
325                 // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded.
326                 formattedUrlString = url;
327
328                 // Display the loading URL is the URL text box.
329                 urlTextBox.setText(url);
330             }
331
332             // Update formattedUrlString and urlTextBox.  It is necessary to do this after the page finishes loading because the final URL can change during load.
333             @Override
334             public void onPageFinished(WebView view, String url) {
335                 formattedUrlString = url;
336
337                 // Only update urlTextBox if the user is not typing in it.
338                 if (!urlTextBox.hasFocus()) {
339                     urlTextBox.setText(formattedUrlString);
340                 }
341
342                 // Store the SSL certificate so it can be accessed from `ViewSslCertificate`.
343                 sslCertificate = mainWebView.getCertificate();
344             }
345
346             // Handle SSL Certificate errors.
347             @Override
348             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
349                 // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`.
350                 sslErrorHandler = handler;
351
352                 // Display the SSL error `AlertDialog`.
353                 AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateError.displayDialog(error);
354                 sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.ssl_certificate_error));
355             }
356         });
357
358         mainWebView.setWebChromeClient(new WebChromeClient() {
359             // Update the progress bar when a page is loading.
360             @Override
361             public void onProgressChanged(WebView view, int progress) {
362                 ProgressBar progressBar = (ProgressBar) appBar.getCustomView().findViewById(R.id.progressBar);
363                 progressBar.setProgress(progress);
364                 if (progress < 100) {
365                     progressBar.setVisibility(View.VISIBLE);
366                 } else {
367                     progressBar.setVisibility(View.GONE);
368
369                     //Stop the `SwipeToRefresh` indicator if it is running
370                     swipeRefreshLayout.setRefreshing(false);
371                 }
372             }
373
374             // Set the favorite icon when it changes.
375             @Override
376             public void onReceivedIcon(WebView view, Bitmap icon) {
377                 // Save a copy of the favorite icon for use if a shortcut is added to the home screen.
378                 favoriteIcon = icon;
379
380                 // Place the favorite icon in the appBar.
381                 ImageView imageViewFavoriteIcon = (ImageView) appBar.getCustomView().findViewById(R.id.favoriteIcon);
382                 imageViewFavoriteIcon.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
383             }
384
385             // Enter full screen video
386             @Override
387             public void onShowCustomView(View view, CustomViewCallback callback) {
388                 appBar.hide();
389
390                 // Show the fullScreenVideoFrameLayout.
391                 fullScreenVideoFrameLayout.addView(view);
392                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
393
394                 // Hide the mainWebView.
395                 mainWebView.setVisibility(View.GONE);
396
397                 // Hide the ad if this is the free flavor.
398                 BannerAd.hideAd(adView);
399
400                 /* SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bars on the bottom or right of the screen.
401                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar across the top of the screen.
402                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the navigation and status bars ghosted overlays and automatically rehides them.
403                  */
404                 view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
405             }
406
407             // Exit full screen video
408             public void onHideCustomView() {
409                 appBar.show();
410
411                 // Show the mainWebView.
412                 mainWebView.setVisibility(View.VISIBLE);
413
414                 // Show the ad if this is the free flavor.
415                 BannerAd.showAd(adView);
416
417                 // Hide the fullScreenVideoFrameLayout.
418                 fullScreenVideoFrameLayout.removeAllViews();
419                 fullScreenVideoFrameLayout.setVisibility(View.GONE);
420             }
421         });
422
423         // Register `mainWebView` for a context menu.  This is used to see link targets and download images.
424         registerForContextMenu(mainWebView);
425
426         // Allow the downloading of files.
427         mainWebView.setDownloadListener(new DownloadListener() {
428             @Override
429             public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
430                 // Show the `DownloadFile` `AlertDialog` and name this instance `@string/download`.
431                 AppCompatDialogFragment downloadFileDialogFragment = DownloadFile.fromUrl(url, contentDisposition, contentLength);
432                 downloadFileDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download));
433             }
434         });
435
436         // Allow pinch to zoom.
437         mainWebView.getSettings().setBuiltInZoomControls(true);
438
439         // Hide zoom controls.
440         mainWebView.getSettings().setDisplayZoomControls(false);
441
442         // Initialize cookieManager.
443         cookieManager = CookieManager.getInstance();
444
445         // Replace the header that `WebView` creates for `X-Requested-With` with a null value.  The default value is the application ID (com.stoutner.privacybrowser.standard).
446         customHeaders.put("X-Requested-With", "");
447
448         // Initialize the default preference values the first time the program is run.
449         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
450
451         // Apply the settings from the shared preferences.
452         applySettings();
453
454         // Get the intent information that started the app.
455         final Intent intent = getIntent();
456
457         if (intent.getData() != null) {
458             // Get the intent data and convert it to a string.
459             final Uri intentUriData = intent.getData();
460             formattedUrlString = intentUriData.toString();
461         }
462
463         // If formattedUrlString is null assign the homepage to it.
464         if (formattedUrlString == null) {
465             formattedUrlString = homepage;
466         }
467
468         // Load the initial website.
469         mainWebView.loadUrl(formattedUrlString, customHeaders);
470
471         // If the favorite icon is null, load the default.
472         if (favoriteIcon == null) {
473             // We have to use `ContextCompat` until API >= 21.
474             Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world);
475             BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
476             favoriteIcon = favoriteIconBitmapDrawable.getBitmap();
477         }
478
479         // Initialize AdView for the free flavor and request an ad.  If this is not the free flavor BannerAd.requestAd() does nothing.
480         adView = findViewById(R.id.adView);
481         BannerAd.requestAd(adView);
482     }
483
484
485     @Override
486     protected void onNewIntent(Intent intent) {
487         // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity.
488         setIntent(intent);
489
490         if (intent.getData() != null) {
491             // Get the intent data and convert it to a string.
492             final Uri intentUriData = intent.getData();
493             formattedUrlString = intentUriData.toString();
494         }
495
496         // Close the navigation drawer if it is open.
497         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
498             drawerLayout.closeDrawer(GravityCompat.START);
499         }
500
501         // Load the website.
502         mainWebView.loadUrl(formattedUrlString, customHeaders);
503
504         // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
505         mainWebView.requestFocus();
506     }
507
508     @Override
509     public boolean onCreateOptionsMenu(Menu menu) {
510         // Inflate the menu; this adds items to the action bar if it is present.
511         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
512
513         // Set mainMenu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons`.
514         mainMenu = menu;
515
516         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
517         updatePrivacyIcons(false);
518
519         // Get handles for the menu items.
520         MenuItem toggleFirstPartyCookies = menu.findItem(R.id.toggleFirstPartyCookies);
521         MenuItem toggleThirdPartyCookies = menu.findItem(R.id.toggleThirdPartyCookies);
522         MenuItem toggleDomStorage = menu.findItem(R.id.toggleDomStorage);
523         MenuItem toggleSaveFormData = menu.findItem(R.id.toggleSaveFormData);
524
525         // Only display third-Party Cookies if SDK >= 21
526         toggleThirdPartyCookies.setVisible(Build.VERSION.SDK_INT >= 21);
527
528         // Get the shared preference values.  `this` references the current context.
529         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
530
531         // Set the status of the additional app bar icons.  The default is `false`.
532         if (sharedPreferences.getBoolean("display_additional_app_bar_icons", false)) {
533             toggleFirstPartyCookies.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
534             toggleDomStorage.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
535             toggleSaveFormData.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
536         } else { //Do not display the additional icons.
537             toggleFirstPartyCookies.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
538             toggleDomStorage.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
539             toggleSaveFormData.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
540         }
541
542         return true;
543     }
544
545     @Override
546     public boolean onPrepareOptionsMenu(Menu menu) {
547         // Get handles for the menu items.
548         MenuItem toggleFirstPartyCookies = menu.findItem(R.id.toggleFirstPartyCookies);
549         MenuItem toggleThirdPartyCookies = menu.findItem(R.id.toggleThirdPartyCookies);
550         MenuItem toggleDomStorage = menu.findItem(R.id.toggleDomStorage);
551         MenuItem toggleSaveFormData = menu.findItem(R.id.toggleSaveFormData);
552         MenuItem clearCookies = menu.findItem(R.id.clearCookies);
553         MenuItem clearFormData = menu.findItem(R.id.clearFormData);
554         MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
555
556         // Set the status of the menu item checkboxes.
557         toggleFirstPartyCookies.setChecked(firstPartyCookiesEnabled);
558         toggleThirdPartyCookies.setChecked(thirdPartyCookiesEnabled);
559         toggleDomStorage.setChecked(domStorageEnabled);
560         toggleSaveFormData.setChecked(saveFormDataEnabled);
561
562         // Enable third-party cookies if first-party cookies are enabled.
563         toggleThirdPartyCookies.setEnabled(firstPartyCookiesEnabled);
564
565         // Enable DOM Storage if JavaScript is enabled.
566         toggleDomStorage.setEnabled(javaScriptEnabled);
567
568         // Enable Clear Cookies if there are any.
569         clearCookies.setEnabled(cookieManager.hasCookies());
570
571         // Enable Clear Form Data is there is any.
572         WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
573         clearFormData.setEnabled(mainWebViewDatabase.hasFormData());
574
575         // Only show `Refresh` if `swipeToRefresh` is disabled.
576         refreshMenuItem.setVisible(!swipeToRefreshEnabled);
577
578         // Initialize font size variables.
579         int fontSize = mainWebView.getSettings().getTextZoom();
580         String fontSizeTitle;
581         MenuItem selectedFontSizeMenuItem;
582
583         // Prepare the font size title and current size menu item.
584         switch (fontSize) {
585             case 50:
586                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.fifty_percent);
587                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeFiftyPercent);
588                 break;
589
590             case 75:
591                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.seventy_five_percent);
592                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeSeventyFivePercent);
593                 break;
594
595             case 100:
596                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_percent);
597                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredPercent);
598                 break;
599
600             case 125:
601                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_twenty_five_percent);
602                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredTwentyFivePercent);
603                 break;
604
605             case 150:
606                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_fifty_percent);
607                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredFiftyPercent);
608                 break;
609
610             case 175:
611                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_seventy_five_percent);
612                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredSeventyFivePercent);
613                 break;
614
615             case 200:
616                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.two_hundred_percent);
617                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeTwoHundredPercent);
618                 break;
619
620             default:
621                 fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_percent);
622                 selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredPercent);
623                 break;
624         }
625
626         // Set the font size title and select the current size menu item.
627         MenuItem fontSizeMenuItem = menu.findItem(R.id.fontSize);
628         fontSizeMenuItem.setTitle(fontSizeTitle);
629         selectedFontSizeMenuItem.setChecked(true);
630
631         // Run all the other default commands.
632         super.onPrepareOptionsMenu(menu);
633
634         // `return true` displays the menu.
635         return true;
636     }
637
638     @Override
639     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
640     @SuppressLint("SetJavaScriptEnabled")
641     // removeAllCookies is deprecated, but it is required for API < 21.
642     @SuppressWarnings("deprecation")
643     public boolean onOptionsItemSelected(MenuItem menuItem) {
644         int menuItemId = menuItem.getItemId();
645
646         // Set the commands that relate to the menu entries.
647         switch (menuItemId) {
648             case R.id.toggleJavaScript:
649                 // Switch the status of javaScriptEnabled.
650                 javaScriptEnabled = !javaScriptEnabled;
651
652                 // Apply the new JavaScript status.
653                 mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
654
655                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
656                 updatePrivacyIcons(true);
657
658                 // Display a `Snackbar`.
659                 if (javaScriptEnabled) {  // JavaScrip is enabled.
660                     Snackbar.make(findViewById(R.id.mainWebView), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
661                 } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled, but first-party cookies are enabled.
662                     Snackbar.make(findViewById(R.id.mainWebView), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
663                 } else {  // Privacy mode.
664                     Snackbar.make(findViewById(R.id.mainWebView), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
665                 }
666
667                 // Reload the WebView.
668                 mainWebView.reload();
669                 return true;
670
671             case R.id.toggleFirstPartyCookies:
672                 // Switch the status of firstPartyCookiesEnabled.
673                 firstPartyCookiesEnabled = !firstPartyCookiesEnabled;
674
675                 // Update the menu checkbox.
676                 menuItem.setChecked(firstPartyCookiesEnabled);
677
678                 // Apply the new cookie status.
679                 cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
680
681                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
682                 updatePrivacyIcons(true);
683
684                 // Display a `Snackbar`.
685                 if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
686                     Snackbar.make(findViewById(R.id.mainWebView), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
687                 } else if (javaScriptEnabled){  // JavaScript is still enabled.
688                     Snackbar.make(findViewById(R.id.mainWebView), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
689                 } else {  // Privacy mode.
690                     Snackbar.make(findViewById(R.id.mainWebView), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
691                 }
692
693                 // Reload the WebView.
694                 mainWebView.reload();
695                 return true;
696
697             case R.id.toggleThirdPartyCookies:
698                 if (Build.VERSION.SDK_INT >= 21) {
699                     // Switch the status of thirdPartyCookiesEnabled.
700                     thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled;
701
702                     // Update the menu checkbox.
703                     menuItem.setChecked(thirdPartyCookiesEnabled);
704
705                     // Apply the new cookie status.
706                     cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
707
708                     // Display a `Snackbar`.
709                     if (thirdPartyCookiesEnabled) {
710                         Snackbar.make(findViewById(R.id.mainWebView), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
711                     } else {
712                         Snackbar.make(findViewById(R.id.mainWebView), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
713                     }
714
715                     // Reload the WebView.
716                     mainWebView.reload();
717                 } // Else do nothing because SDK < 21.
718                 return true;
719
720             case R.id.toggleDomStorage:
721                 // Switch the status of domStorageEnabled.
722                 domStorageEnabled = !domStorageEnabled;
723
724                 // Update the menu checkbox.
725                 menuItem.setChecked(domStorageEnabled);
726
727                 // Apply the new DOM Storage status.
728                 mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
729
730                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
731                 updatePrivacyIcons(true);
732
733                 // Display a `Snackbar`.
734                 if (domStorageEnabled) {
735                     Snackbar.make(findViewById(R.id.mainWebView), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
736                 } else {
737                     Snackbar.make(findViewById(R.id.mainWebView), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
738                 }
739
740                 // Reload the WebView.
741                 mainWebView.reload();
742                 return true;
743
744             case R.id.toggleSaveFormData:
745                 // Switch the status of saveFormDataEnabled.
746                 saveFormDataEnabled = !saveFormDataEnabled;
747
748                 // Update the menu checkbox.
749                 menuItem.setChecked(saveFormDataEnabled);
750
751                 // Apply the new form data status.
752                 mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
753
754                 // Display a `Snackbar`.
755                 if (saveFormDataEnabled) {
756                     Snackbar.make(findViewById(R.id.mainWebView), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
757                 } else {
758                     Snackbar.make(findViewById(R.id.mainWebView), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
759                 }
760
761                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
762                 updatePrivacyIcons(true);
763
764                 // Reload the WebView.
765                 mainWebView.reload();
766                 return true;
767
768             case R.id.clearCookies:
769                 if (Build.VERSION.SDK_INT < 21) {
770                     cookieManager.removeAllCookie();
771                 } else {
772                     cookieManager.removeAllCookies(null);
773                 }
774                 Snackbar.make(findViewById(R.id.mainWebView), R.string.cookies_deleted, Snackbar.LENGTH_SHORT).show();
775                 return true;
776
777             case R.id.clearDomStorage:
778                 WebStorage webStorage = WebStorage.getInstance();
779                 webStorage.deleteAllData();
780                 Snackbar.make(findViewById(R.id.mainWebView), R.string.dom_storage_deleted, Snackbar.LENGTH_SHORT).show();
781                 return true;
782
783             case R.id.clearFormData:
784                 WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this);
785                 mainWebViewDatabase.clearFormData();
786                 mainWebView.reload();
787                 return true;
788
789             case R.id.fontSizeFiftyPercent:
790                 mainWebView.getSettings().setTextZoom(50);
791                 return true;
792
793             case R.id.fontSizeSeventyFivePercent:
794                 mainWebView.getSettings().setTextZoom(75);
795                 return true;
796
797             case R.id.fontSizeOneHundredPercent:
798                 mainWebView.getSettings().setTextZoom(100);
799                 return true;
800
801             case R.id.fontSizeOneHundredTwentyFivePercent:
802                 mainWebView.getSettings().setTextZoom(125);
803                 return true;
804
805             case R.id.fontSizeOneHundredFiftyPercent:
806                 mainWebView.getSettings().setTextZoom(150);
807                 return true;
808
809             case R.id.fontSizeOneHundredSeventyFivePercent:
810                 mainWebView.getSettings().setTextZoom(175);
811                 return true;
812
813             case R.id.fontSizeTwoHundredPercent:
814                 mainWebView.getSettings().setTextZoom(200);
815                 return true;
816
817             case R.id.find_on_page:
818                 // Hide the URL app bar.
819                 Toolbar appBarToolbar = (Toolbar) findViewById(R.id.appBar);
820                 appBarToolbar.setVisibility(View.GONE);
821
822                 // Show the Find on Page `RelativeLayout`.
823                 LinearLayout findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout);
824                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
825
826                 // Display the keyboard.  We have to wait 200 ms before running the command to work around a bug in Android.
827                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
828                 findOnPageEditText.postDelayed(new Runnable()
829                 {
830                     @Override
831                     public void run()
832                     {
833                         // Set the focus on `findOnPageEditText`.
834                         findOnPageEditText.requestFocus();
835
836                         // Display the keyboard.
837                         inputMethodManager.showSoftInput(findOnPageEditText, 0);
838                     }
839                 }, 200);
840                 return true;
841
842             case R.id.share:
843                 Intent shareIntent = new Intent();
844                 shareIntent.setAction(Intent.ACTION_SEND);
845                 shareIntent.putExtra(Intent.EXTRA_TEXT, urlTextBox.getText().toString());
846                 shareIntent.setType("text/plain");
847                 startActivity(Intent.createChooser(shareIntent, "Share URL"));
848                 return true;
849
850             case R.id.addToHomescreen:
851                 // Show the `CreateHomeScreenShortcut` `AlertDialog` and name this instance `@string/create_shortcut`.
852                 AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcut();
853                 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.create_shortcut));
854
855                 //Everything else will be handled by `CreateHomeScreenShortcut` and the associated listener below.
856                 return true;
857
858             case R.id.print:
859                 // Get a `PrintManager` instance.
860                 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
861
862                 // Convert `mainWebView` to `printDocumentAdapter`.
863                 PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter();
864
865                 // Print the document.  The print attributes are `null`.
866                 printManager.print(getResources().getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
867                 return true;
868
869             case R.id.refresh:
870                 mainWebView.reload();
871                 return true;
872
873             default:
874                 // Don't consume the event.
875                 return super.onOptionsItemSelected(menuItem);
876         }
877     }
878
879     // removeAllCookies is deprecated, but it is required for API < 21.
880     @SuppressWarnings("deprecation")
881     @Override
882     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
883         int menuItemId = menuItem.getItemId();
884
885         switch (menuItemId) {
886             case R.id.home:
887                 mainWebView.loadUrl(homepage, customHeaders);
888                 break;
889
890             case R.id.back:
891                 if (mainWebView.canGoBack()) {
892                     mainWebView.goBack();
893                 }
894                 break;
895
896             case R.id.forward:
897                 if (mainWebView.canGoForward()) {
898                     mainWebView.goForward();
899                 }
900                 break;
901
902             case R.id.bookmarks:
903                 // Launch BookmarksActivity.
904                 Intent bookmarksIntent = new Intent(this, BookmarksActivity.class);
905                 startActivity(bookmarksIntent);
906                 break;
907
908             case R.id.downloads:
909                 // Launch the system Download Manager.
910                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
911
912                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
913                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
914
915                 startActivity(downloadManagerIntent);
916                 break;
917
918             case R.id.settings:
919                 // Launch `SettingsActivity`.
920                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
921                 startActivity(settingsIntent);
922                 break;
923
924             case R.id.guide:
925                 // Launch `GuideActivity`.
926                 Intent guideIntent = new Intent(this, GuideActivity.class);
927                 startActivity(guideIntent);
928                 break;
929
930             case R.id.about:
931                 // Launch `AboutActivity`.
932                 Intent aboutIntent = new Intent(this, AboutActivity.class);
933                 startActivity(aboutIntent);
934                 break;
935
936             case R.id.clearAndExit:
937                 // Clear cookies.  The commands changed slightly in API 21.
938                 if (Build.VERSION.SDK_INT >= 21) {
939                     cookieManager.removeAllCookies(null);
940                 } else {
941                     cookieManager.removeAllCookie();
942                 }
943
944                 // Clear DOM storage.
945                 WebStorage domStorage = WebStorage.getInstance();
946                 domStorage.deleteAllData();
947
948                 // Clear form data.
949                 WebViewDatabase formData = WebViewDatabase.getInstance(this);
950                 formData.clearFormData();
951
952                 // Clear cache.  The argument of "true" includes disk files.
953                 mainWebView.clearCache(true);
954
955                 // Clear the back/forward history.
956                 mainWebView.clearHistory();
957
958                 // Clear any SSL certificate preferences.
959                 mainWebView.clearSslPreferences();
960
961                 // Clear `formattedUrlString`.
962                 formattedUrlString = null;
963
964                 // Clear `customHeaders`.
965                 customHeaders.clear();
966
967                 // Detach all views from `mainWebViewRelativeLayout`.
968                 RelativeLayout mainWebViewRelativeLayout = (RelativeLayout) findViewById(R.id.mainWebViewRelativeLayout);
969                 mainWebViewRelativeLayout.removeAllViews();
970
971                 // Destroy the internal state of `mainWebView`.
972                 mainWebView.destroy();
973
974                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
975                 if (Build.VERSION.SDK_INT >= 21) {
976                     finishAndRemoveTask();
977                 } else {
978                     finish();
979                 }
980
981                 // Remove the terminated program from RAM.  The status code is `0`.
982                 System.exit(0);
983                 break;
984
985             default:
986                 break;
987         }
988
989         // Close the navigation drawer.
990         drawerLayout.closeDrawer(GravityCompat.START);
991         return true;
992     }
993
994     @Override
995     public void onPostCreate(Bundle savedInstanceState) {
996         super.onPostCreate(savedInstanceState);
997
998         // Sync the state of the DrawerToggle after onRestoreInstanceState has finished.
999         drawerToggle.syncState();
1000     }
1001
1002     @Override
1003     public void onConfigurationChanged(Configuration newConfig) {
1004         super.onConfigurationChanged(newConfig);
1005
1006         // Reload the ad if this is the free flavor.
1007         BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id));
1008
1009         // Reinitialize the adView variable, as the View will have been removed and re-added in the free flavor by BannerAd.reloadAfterRotate().
1010         adView = findViewById(R.id.adView);
1011
1012         // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:  https://code.google.com/p/android/issues/detail?id=20493#c8
1013         // ActivityCompat.invalidateOptionsMenu(this);
1014     }
1015
1016     @Override
1017     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
1018         // Store the `HitTestResult`.
1019         final WebView.HitTestResult hitTestResult = mainWebView.getHitTestResult();
1020
1021         // Create strings.
1022         final String imageUrl;
1023         final String linkUrl;
1024
1025         switch (hitTestResult.getType()) {
1026             // `SRC_ANCHOR_TYPE` is a link.
1027             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
1028                 // Get the target URL.
1029                 linkUrl = hitTestResult.getExtra();
1030
1031                 // Set the target URL as the title of the `ContextMenu`.
1032                 menu.setHeaderTitle(linkUrl);
1033
1034                 // Add a `Load URL` button.
1035                 menu.add(R.string.load_url).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1036                     @Override
1037                     public boolean onMenuItemClick(MenuItem item) {
1038                         mainWebView.loadUrl(linkUrl, customHeaders);
1039                         return false;
1040                     }
1041                 });
1042
1043                 // Add a `Cancel` button, which by default closes the `ContextMenu`.
1044                 menu.add(R.string.cancel);
1045                 break;
1046
1047             case WebView.HitTestResult.EMAIL_TYPE:
1048                 // Get the target URL.
1049                 linkUrl = hitTestResult.getExtra();
1050
1051                 // Set the target URL as the title of the `ContextMenu`.
1052                 menu.setHeaderTitle(linkUrl);
1053
1054                 // Add a `Write Email` button.
1055                 menu.add(R.string.write_email).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1056                     @Override
1057                     public boolean onMenuItemClick(MenuItem item) {
1058                         // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
1059                         Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
1060
1061                         // Parse the url and set it as the data for the `Intent`.
1062                         emailIntent.setData(Uri.parse("mailto:" + linkUrl));
1063
1064                         // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
1065                         emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1066
1067                         // Make it so.
1068                         startActivity(emailIntent);
1069                         return false;
1070                     }
1071                 });
1072
1073                 // Add a `Cancel` button, which by default closes the `ContextMenu`.
1074                 menu.add(R.string.cancel);
1075                 break;
1076
1077             // `IMAGE_TYPE` is an image.
1078             case WebView.HitTestResult.IMAGE_TYPE:
1079                 // Get the image URL.
1080                 imageUrl = hitTestResult.getExtra();
1081
1082                 // Set the image URL as the title of the `ContextMenu`.
1083                 menu.setHeaderTitle(imageUrl);
1084
1085                 // Add a `View Image` button.
1086                 menu.add(R.string.view_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1087                     @Override
1088                     public boolean onMenuItemClick(MenuItem item) {
1089                         mainWebView.loadUrl(imageUrl, customHeaders);
1090                         return false;
1091                     }
1092                 });
1093
1094                 // Add a `Download Image` button.
1095                 menu.add(R.string.download_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1096                     @Override
1097                     public boolean onMenuItemClick(MenuItem item) {
1098                         // Show the `DownloadImage` `AlertDialog` and name this instance `@string/download`.
1099                         AppCompatDialogFragment downloadImageDialogFragment = DownloadImage.imageUrl(imageUrl);
1100                         downloadImageDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download));
1101                         return false;
1102                     }
1103                 });
1104
1105                 // Add a `Cancel` button, which by default closes the `ContextMenu`.
1106                 menu.add(R.string.cancel);
1107                 break;
1108
1109
1110             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
1111             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
1112                 // Get the image URL.
1113                 imageUrl = hitTestResult.getExtra();
1114
1115                 // Set the image URL as the title of the `ContextMenu`.
1116                 menu.setHeaderTitle(imageUrl);
1117
1118                 // Add a `View Image` button.
1119                 menu.add(R.string.view_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1120                     @Override
1121                     public boolean onMenuItemClick(MenuItem item) {
1122                         mainWebView.loadUrl(imageUrl, customHeaders);
1123                         return false;
1124                     }
1125                 });
1126
1127                 // Add a `Download Image` button.
1128                 menu.add(R.string.download_image).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1129                     @Override
1130                     public boolean onMenuItemClick(MenuItem item) {
1131                         // Show the `DownloadImage` `AlertDialog` and name this instance `@string/download`.
1132                         AppCompatDialogFragment downloadImageDialogFragment = DownloadImage.imageUrl(imageUrl);
1133                         downloadImageDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download));
1134                         return false;
1135                     }
1136                 });
1137
1138                 // Add a `Cancel` button, which by default closes the `ContextMenu`.
1139                 menu.add(R.string.cancel);
1140                 break;
1141         }
1142     }
1143
1144     @Override
1145     public void onCreateHomeScreenShortcut(AppCompatDialogFragment dialogFragment) {
1146         // Get shortcutNameEditText from the alert dialog.
1147         EditText shortcutNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.shortcut_name_edittext);
1148
1149         // Create the bookmark shortcut based on formattedUrlString.
1150         Intent bookmarkShortcut = new Intent();
1151         bookmarkShortcut.setAction(Intent.ACTION_VIEW);
1152         bookmarkShortcut.setData(Uri.parse(formattedUrlString));
1153
1154         // Place the bookmark shortcut on the home screen.
1155         Intent placeBookmarkShortcut = new Intent();
1156         placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.INTENT", bookmarkShortcut);
1157         placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.NAME", shortcutNameEditText.getText().toString());
1158         placeBookmarkShortcut.putExtra("android.intent.extra.shortcut.ICON", favoriteIcon);
1159         placeBookmarkShortcut.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
1160         sendBroadcast(placeBookmarkShortcut);
1161     }
1162
1163     @Override
1164     public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) {
1165         // Get a handle for the system `DOWNLOAD_SERVICE`.
1166         DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
1167
1168         // Parse `imageUrl`.
1169         DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
1170
1171         // Get the file name from `dialogFragment`.
1172         EditText downloadImageNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_image_name);
1173         String imageName = downloadImageNameEditText.getText().toString();
1174
1175         // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`.
1176         if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `imageName`.
1177             downloadRequest.setDestinationInExternalFilesDir(this, "/", imageName);
1178         } else { // Only set the title using `imageName`.
1179             downloadRequest.setTitle(imageName);
1180         }
1181
1182         // Allow `MediaScanner` to index the download if it is a media file.
1183         downloadRequest.allowScanningByMediaScanner();
1184
1185         // Add the URL as the description for the download.
1186         downloadRequest.setDescription(imageUrl);
1187
1188         // Show the download notification after the download is completed.
1189         downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
1190
1191         // Initiate the download.
1192         downloadManager.enqueue(downloadRequest);
1193     }
1194
1195     @Override
1196     public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) {
1197         // Get a handle for the system `DOWNLOAD_SERVICE`.
1198         DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
1199
1200         // Parse `downloadUrl`.
1201         DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
1202
1203         // Get the file name from `dialogFragment`.
1204         EditText downloadFileNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_file_name);
1205         String fileName = downloadFileNameEditText.getText().toString();
1206
1207         // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`.
1208         if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `fileName`.
1209             downloadRequest.setDestinationInExternalFilesDir(this, "/", fileName);
1210         } else { // Only set the title using `fileName`.
1211             downloadRequest.setTitle(fileName);
1212         }
1213
1214         // Allow `MediaScanner` to index the download if it is a media file.
1215         downloadRequest.allowScanningByMediaScanner();
1216
1217         // Add the URL as the description for the download.
1218         downloadRequest.setDescription(downloadUrl);
1219
1220         // Show the download notification after the download is completed.
1221         downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
1222
1223         // Initiate the download.
1224         downloadManager.enqueue(downloadRequest);
1225     }
1226
1227     public void viewSslCertificate(View view) {
1228         // Show the `ViewSslCertificate` `AlertDialog` and name this instance `@string/view_ssl_certificate`.
1229         DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificate();
1230         viewSslCertificateDialogFragment.show(getFragmentManager(), getResources().getString(R.string.view_ssl_certificate));
1231     }
1232
1233     @Override
1234     public void onSslErrorCancel() {
1235         sslErrorHandler.cancel();
1236     }
1237
1238     @Override
1239     public void onSslErrorProceed() {
1240         sslErrorHandler.proceed();
1241     }
1242
1243     // Override onBackPressed to handle the navigation drawer and mainWebView.
1244     @Override
1245     public void onBackPressed() {
1246         final WebView mainWebView = (WebView) findViewById(R.id.mainWebView);
1247
1248         // Close the navigation drawer if it is available.  GravityCompat.START is the drawer on the left on Left-to-Right layout text.
1249         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
1250             drawerLayout.closeDrawer(GravityCompat.START);
1251         } else {
1252             // Load the previous URL if available.
1253             if (mainWebView.canGoBack()) {
1254                 mainWebView.goBack();
1255             } else {
1256                 // Pass onBackPressed to the system.
1257                 super.onBackPressed();
1258             }
1259         }
1260     }
1261
1262     @Override
1263     public void onPause() {
1264         // We need to pause the adView or it will continue to consume resources in the background on the free flavor.
1265         BannerAd.pauseAd(adView);
1266
1267         super.onPause();
1268     }
1269
1270     @Override
1271     public void onResume() {
1272         super.onResume();
1273
1274         // We need to resume the adView for the free flavor.
1275         BannerAd.resumeAd(adView);
1276     }
1277
1278     @Override
1279     public void onRestart() {
1280         super.onRestart();
1281
1282         // Apply the settings from shared preferences, which might have been changed in `SettingsActivity`.
1283         applySettings();
1284
1285         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1286         updatePrivacyIcons(true);
1287
1288     }
1289
1290     private void loadUrlFromTextBox() throws UnsupportedEncodingException {
1291         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
1292         String unformattedUrlString = urlTextBox.getText().toString().trim();
1293
1294         URL unformattedUrl = null;
1295         Uri.Builder formattedUri = new Uri.Builder();
1296
1297         // Check to see if unformattedUrlString is a valid URL.  Otherwise, convert it into a Duck Duck Go search.
1298         if (Patterns.WEB_URL.matcher(unformattedUrlString).matches()) {
1299             // Add http:// at the beginning if it is missing.  Otherwise the app will segfault.
1300             if (!unformattedUrlString.startsWith("http")) {
1301                 unformattedUrlString = "http://" + unformattedUrlString;
1302             }
1303
1304             // 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.
1305             try {
1306                 unformattedUrl = new URL(unformattedUrlString);
1307             } catch (MalformedURLException e) {
1308                 e.printStackTrace();
1309             }
1310
1311             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if .get was called on a null value.
1312             final String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
1313             final String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
1314             final String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
1315             final String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
1316             final String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
1317
1318             formattedUri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
1319             formattedUrlString = formattedUri.build().toString();
1320         } else {
1321             // Sanitize the search input and convert it to a DuckDuckGo search.
1322             final String encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
1323
1324             // Use the correct search URL.
1325             if (javaScriptEnabled) {  // JavaScript is enabled.
1326                 formattedUrlString = javaScriptEnabledSearchURL + encodedUrlString;
1327             } else { // JavaScript is disabled.
1328                 formattedUrlString = javaScriptDisabledSearchURL + encodedUrlString;
1329             }
1330         }
1331
1332         mainWebView.loadUrl(formattedUrlString, customHeaders);
1333
1334         // Hides the keyboard so we can see the webpage.  `0` indicates no additional flags.
1335         inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
1336     }
1337
1338     public void findPreviousOnPage(View view) {
1339         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
1340         mainWebView.findNext(false);
1341     }
1342
1343     public void findNextOnPage(View view) {
1344         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
1345         mainWebView.findNext(true);
1346     }
1347
1348     public void closeFindOnPage(View view) {
1349         // Delete the contents of `find_on_page_edittext`.
1350         findOnPageEditText.setText(null);
1351
1352         // Clear the highlighted phrases.
1353         mainWebView.clearMatches();
1354
1355         // Hide the Find on Page `RelativeLayout`.
1356         LinearLayout findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout);
1357         findOnPageLinearLayout.setVisibility(View.GONE);
1358
1359         // Show the URL app bar.
1360         Toolbar appBarToolbar = (Toolbar) findViewById(R.id.appBar);
1361         appBarToolbar.setVisibility(View.VISIBLE);
1362
1363         // Hides the keyboard so we can see the webpage.  `0` indicates no additional flags.
1364         inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
1365     }
1366
1367     private void applySettings() {
1368         // Get the shared preference values.  `this` references the current context.
1369         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1370
1371         // Store the values from `sharedPreferences` in variables.
1372         String userAgentString = sharedPreferences.getString("user_agent", "Default user agent");
1373         String customUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0");
1374         String javaScriptDisabledSearchString = sharedPreferences.getString("javascript_disabled_search", "https://duckduckgo.com/html/?q=");
1375         String javaScriptDisabledCustomSearchString = sharedPreferences.getString("javascript_disabled_search_custom_url", "");
1376         String javaScriptEnabledSearchString = sharedPreferences.getString("javascript_enabled_search", "https://duckduckgo.com/?q=");
1377         String javaScriptEnabledCustomSearchString = sharedPreferences.getString("javascript_enabled_search_custom_url", "");
1378         String homepageString = sharedPreferences.getString("homepage", "https://www.duckduckgo.com");
1379         String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100");
1380         swipeToRefreshEnabled = sharedPreferences.getBoolean("swipe_to_refresh_enabled", false);
1381         boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", true);
1382         boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
1383
1384         // Because they can be modified on-the-fly by the user, these default settings are only applied when the program first runs.
1385         if (javaScriptEnabled == null) {  // If `javaScriptEnabled` is null the program is just starting.
1386             // Get the values from `sharedPreferences`.
1387             javaScriptEnabled = sharedPreferences.getBoolean("javascript_enabled", false);
1388             firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies_enabled", false);
1389             thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies_enabled", false);
1390             domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false);
1391             saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false);
1392
1393             // Apply the default settings.
1394             mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled);
1395             cookieManager.setAcceptCookie(firstPartyCookiesEnabled);
1396             mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled);
1397             mainWebView.getSettings().setSaveFormData(saveFormDataEnabled);
1398             mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
1399
1400             // Set third-party cookies status if API >= 21.
1401             if (Build.VERSION.SDK_INT >= 21) {
1402                 cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled);
1403             }
1404         }
1405
1406         // Apply the other settings from `sharedPreferences`.
1407         homepage = homepageString;
1408         swipeRefreshLayout.setEnabled(swipeToRefreshEnabled);
1409
1410         // Set the user agent initial status.
1411         switch (userAgentString) {
1412             case "Default user agent":
1413                 // Set the user agent to `""`, which uses the default value.
1414                 mainWebView.getSettings().setUserAgentString("");
1415                 break;
1416
1417             case "Custom user agent":
1418                 // Set the custom user agent.
1419                 mainWebView.getSettings().setUserAgentString(customUserAgentString);
1420                 break;
1421
1422             default:
1423                 // Use the selected user agent.
1424                 mainWebView.getSettings().setUserAgentString(userAgentString);
1425                 break;
1426         }
1427
1428         // Set JavaScript disabled search.
1429         if (javaScriptDisabledSearchString.equals("Custom URL")) {  // Get the custom URL string.
1430             javaScriptDisabledSearchURL = javaScriptDisabledCustomSearchString;
1431         } else {  // Use the string from the pre-built list.
1432             javaScriptDisabledSearchURL = javaScriptDisabledSearchString;
1433         }
1434
1435         // Set JavaScript enabled search.
1436         if (javaScriptEnabledSearchString.equals("Custom URL")) {  // Get the custom URL string.
1437             javaScriptEnabledSearchURL = javaScriptEnabledCustomSearchString;
1438         } else {  // Use the string from the pre-built list.
1439             javaScriptEnabledSearchURL = javaScriptEnabledSearchString;
1440         }
1441
1442         // Set Do Not Track status.
1443         if (doNotTrackEnabled) {
1444             customHeaders.put("DNT", "1");
1445         } else {
1446             customHeaders.remove("DNT");
1447         }
1448
1449         // Set Orbot proxy status.
1450         if (proxyThroughOrbot) {
1451             // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
1452             OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
1453         } else {  // Reset the proxy to default.  The host is `""` and the port is `"0"`.
1454             OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
1455         }
1456     }
1457
1458     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
1459         // Get handles for the icons.
1460         MenuItem privacyIcon = mainMenu.findItem(R.id.toggleJavaScript);
1461         MenuItem firstPartyCookiesIcon = mainMenu.findItem(R.id.toggleFirstPartyCookies);
1462         MenuItem domStorageIcon = mainMenu.findItem(R.id.toggleDomStorage);
1463         MenuItem formDataIcon = mainMenu.findItem(R.id.toggleSaveFormData);
1464
1465         // Update `privacyIcon`.
1466         if (javaScriptEnabled) {  // JavaScript is enabled.
1467             privacyIcon.setIcon(R.drawable.javascript_enabled);
1468         } else if (firstPartyCookiesEnabled) {  // JavaScript is disabled but cookies are enabled.
1469             privacyIcon.setIcon(R.drawable.warning);
1470         } else {  // All the dangerous features are disabled.
1471             privacyIcon.setIcon(R.drawable.privacy_mode);
1472         }
1473
1474         // Update `firstPartyCookiesIcon`.
1475         if (firstPartyCookiesEnabled) {  // First-party cookies are enabled.
1476             firstPartyCookiesIcon.setIcon(R.drawable.cookies_enabled);
1477         } else {  // First-party cookies are disabled.
1478             firstPartyCookiesIcon.setIcon(R.drawable.cookies_disabled);
1479         }
1480
1481         // Update `domStorageIcon`.
1482         if (javaScriptEnabled && domStorageEnabled) {  // Both JavaScript and DOM storage are enabled.
1483             domStorageIcon.setIcon(R.drawable.dom_storage_enabled);
1484         } else if (javaScriptEnabled) {  // JavaScript is enabled but DOM storage is disabled.
1485             domStorageIcon.setIcon(R.drawable.dom_storage_disabled);
1486         } else {  // JavaScript is disabled, so DOM storage is ghosted.
1487             domStorageIcon.setIcon(R.drawable.dom_storage_ghosted);
1488         }
1489
1490         // Update `formDataIcon`.
1491         if (saveFormDataEnabled) {  // Form data is enabled.
1492             formDataIcon.setIcon(R.drawable.form_data_enabled);
1493         } else {  // Form data is disabled.
1494             formDataIcon.setIcon(R.drawable.form_data_disabled);
1495         }
1496
1497         // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.  `this` references the current activity.
1498         if (runInvalidateOptionsMenu) {
1499             ActivityCompat.invalidateOptionsMenu(this);
1500         }
1501     }
1502 }