Implement IP Address Pinning. https://redmine.stoutner.com/issues/212
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / DomainsActivity.java
1 /*
2  * Copyright © 2017-2019 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.activities;
21
22 import android.app.Activity;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.res.Resources;
26 import android.database.Cursor;
27 import android.net.http.SslCertificate;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.support.design.widget.FloatingActionButton;
31 import android.support.design.widget.Snackbar;
32 import android.support.v4.app.FragmentManager;
33 import android.support.v4.app.NavUtils;
34 import android.support.v7.app.ActionBar;
35 import android.support.v7.app.AppCompatActivity;
36 // `AppCompatDialogFragment` is required instead of `DialogFragment` or an error is produced on API <=22.
37 import android.support.v7.app.AppCompatDialogFragment;
38 import android.support.v7.widget.Toolbar;
39 import android.view.Menu;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.WindowManager;
44 import android.widget.CursorAdapter;
45 import android.widget.EditText;
46 import android.widget.ListView;
47 import android.widget.RadioButton;
48 import android.widget.Spinner;
49 import android.widget.Switch;
50 import android.widget.TextView;
51
52 import com.stoutner.privacybrowser.R;
53 import com.stoutner.privacybrowser.dialogs.AddDomainDialog;
54 import com.stoutner.privacybrowser.fragments.DomainSettingsFragment;
55 import com.stoutner.privacybrowser.fragments.DomainsListFragment;
56 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
57
58 import java.util.Objects;
59
60 public class DomainsActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener {
61     // `twoPanedMode` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreate()`, `onCreateOptionsMenu()`, and `populateDomainsListView()`.
62     public static boolean twoPanedMode;
63
64     // `databaseId` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreateOptionsMenu()`, `saveDomainSettings()` and `populateDomainsListView()`.
65     public static int currentDomainDatabaseId;
66
67     // `deleteMenuItem` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `onBackPressed()`.
68     public static MenuItem deleteMenuItem;
69
70     // `undoDeleteSnackbar` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onOptionsItemSelected()` and `onBackPressed()`.
71     public static Snackbar undoDeleteSnackbar;
72
73     // `dismissingSnackbar` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onOptionsItemSelected()`.
74     public static boolean dismissingSnackbar;
75
76     // `closeActivityAfterDismissingSnackbar` is used in `onOptionsItemSelected()`, and `onBackPressed()`.
77     private boolean closeActivityAfterDismissingSnackbar;
78
79     // `context` is used in `onCreate()`, `onOptionsItemSelected()`, and `onAddDomain()`.
80     private Context context;
81
82     // `supportFragmentManager` is used in `onCreate()` and `onCreateOptionsMenu()`.
83     private FragmentManager supportFragmentManager;
84
85     // `domainsDatabaseHelper` is used in `onCreate()`, `saveDomainSettings()`, and `onDestroy()`.
86     private static DomainsDatabaseHelper domainsDatabaseHelper;
87
88     // `domainsListView` is used in `onCreate()` and `populateDomainsList()`.
89     private ListView domainsListView;
90
91     // `addDomainFAB` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `onBackPressed()`.
92     private FloatingActionButton addDomainFAB;
93
94     // `deletedDomainPosition` is used in an inner and outer class in `onOptionsItemSelected()`.
95     private int deletedDomainPosition;
96
97     // `restartAfterRotate` is used in `onCreate()` and `onCreateOptionsMenu()`.
98     private boolean restartAfterRotate;
99
100     // `domainSettingsDisplayedBeforeRotate` is used in `onCreate()` and `onCreateOptionsMenu()`.
101     private boolean domainSettingsDisplayedBeforeRotate;
102
103     // `domainSettingsDatabaseIdBeforeRotate` is used in `onCreate()` and `onCreateOptionsMenu()`.
104     private int domainSettingsDatabaseIdBeforeRotate;
105
106     // `goDirectlyToDatabaseId` is used in `onCreate()` and `onCreateOptionsMenu()`.
107     private int goDirectlyToDatabaseId;
108
109     // `closeOnBack` is used in `onCreate()`, `onOptionsItemSelected()` and `onBackPressed()`.
110     private boolean closeOnBack;
111
112     // `coordinatorLayout` is use in `onCreate()`, `onOptionsItemSelected()`, and `onSaveInstanceState()`.
113     private View coordinatorLayout;
114
115     // `resources` is used in `onCreate()`, `onOptionsItemSelected()`, and `onSaveInstanceState()`.
116     private Resources resources;
117
118     @Override
119     protected void onCreate(Bundle savedInstanceState) {
120         // Disable screenshots if not allowed.
121         if (!MainWebViewActivity.allowScreenshots) {
122             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
123         }
124
125         // Set the activity theme.
126         if (MainWebViewActivity.darkTheme) {
127             setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
128         } else {
129             setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
130         }
131
132         // Run the default commands.
133         super.onCreate(savedInstanceState);
134
135         // Extract the values from `savedInstanceState` if it is not `null`.
136         if (savedInstanceState != null) {
137             restartAfterRotate = true;
138             domainSettingsDisplayedBeforeRotate = savedInstanceState.getBoolean("domainSettingsDisplayed");
139             domainSettingsDatabaseIdBeforeRotate = savedInstanceState.getInt("domainSettingsDatabaseId");
140         }
141
142         // Get the launching intent
143         Intent intent = getIntent();
144
145         // Extract the domain to load if there is one.  `-1` is the default value.
146         goDirectlyToDatabaseId = intent.getIntExtra("loadDomain", -1);
147
148         // Get the status of close-on-back, which is true when the domains activity is called from the options menu.
149         closeOnBack = intent.getBooleanExtra("closeOnBack", false);
150
151         // Set the content view.
152         setContentView(R.layout.domains_coordinatorlayout);
153
154         // Populate the class variables.
155         coordinatorLayout = findViewById(R.id.domains_coordinatorlayout);
156         resources = getResources();
157         context = this;
158         supportFragmentManager = getSupportFragmentManager();
159
160         // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21.
161         final Toolbar domainsAppBar = findViewById(R.id.domains_toolbar);
162         setSupportActionBar(domainsAppBar);
163
164         // Display the home arrow on the support action bar.
165         ActionBar appBar = getSupportActionBar();
166         assert appBar != null;// This assert removes the incorrect warning in Android Studio on the following line that `appBar` might be null.
167         appBar.setDisplayHomeAsUpEnabled(true);
168
169         // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
170         domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
171
172         // Determine if we are in two pane mode.  `domain_settings_fragment_container` does not exist on devices with a width less than 900dp.
173         twoPanedMode = (findViewById(R.id.domain_settings_fragment_container) != null);
174
175         // Configure `addDomainFAB`.
176         addDomainFAB = findViewById(R.id.add_domain_fab);
177         addDomainFAB.setOnClickListener((View view) -> {
178             // Show the add domain `AlertDialog`.
179             AppCompatDialogFragment addDomainDialog = new AddDomainDialog();
180             addDomainDialog.show(supportFragmentManager, resources.getString(R.string.add_domain));
181         });
182     }
183
184     @Override
185     public boolean onCreateOptionsMenu(Menu menu) {
186         // Inflate the menu.
187         getMenuInflater().inflate(R.menu.domains_options_menu, menu);
188
189         // Store `deleteMenuItem` for future use.
190         deleteMenuItem = menu.findItem(R.id.delete_domain);
191
192         // Only display `deleteMenuItem` (initially) in two-paned mode.
193         deleteMenuItem.setVisible(twoPanedMode);
194
195         // Display the fragments.  This must be done from `onCreateOptionsMenu()` instead of `onCreate()` because `populateDomainsListView()` needs `deleteMenuItem` to be inflated.
196         if (restartAfterRotate && domainSettingsDisplayedBeforeRotate) {  // The device was rotated and domain settings were displayed previously.
197             if (twoPanedMode) {  // The device is in two-paned mode.
198                 // Reset `restartAfterRotate`.
199                 restartAfterRotate = false;
200
201                 // Display `DomainsListFragment`.
202                 DomainsListFragment domainsListFragment = new DomainsListFragment();
203                 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
204                 supportFragmentManager.executePendingTransactions();
205
206                 // Populate the list of domains.  `domainSettingsDatabaseId` highlights the domain that was highlighted before the rotation.
207                 populateDomainsListView(domainSettingsDatabaseIdBeforeRotate);
208             } else {  // The device is in single-paned mode.
209                 // Reset `restartAfterRotate`.
210                 restartAfterRotate = false;
211
212                 // Store the current domain database ID.
213                 currentDomainDatabaseId = domainSettingsDatabaseIdBeforeRotate;
214
215                 // Add `currentDomainDatabaseId` to `argumentsBundle`.
216                 Bundle argumentsBundle = new Bundle();
217                 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
218
219                 // Add `argumentsBundle` to `domainSettingsFragment`.
220                 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
221                 domainSettingsFragment.setArguments(argumentsBundle);
222
223                 // Show `deleteMenuItem`.
224                 deleteMenuItem.setVisible(true);
225
226                 // Hide `add_domain_fab`.
227                 addDomainFAB.hide();
228
229                 // Display `domainSettingsFragment`.
230                 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
231             }
232         } else {  // The device was not rotated or, if it was, domain settings were not displayed previously.
233             if (goDirectlyToDatabaseId >=0) {  // Load the indicated domain settings.
234                 // Store the current domain database ID.
235                 currentDomainDatabaseId = goDirectlyToDatabaseId;
236
237                 if (twoPanedMode) {  // The device is in two-paned mode.
238                     // Display `DomainsListFragment`.
239                     DomainsListFragment domainsListFragment = new DomainsListFragment();
240                     supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
241                     supportFragmentManager.executePendingTransactions();
242
243                     // Populate the list of domains.  `domainSettingsDatabaseId` highlights the domain that was highlighted before the rotation.
244                     populateDomainsListView(goDirectlyToDatabaseId);
245                 } else {  // The device is in single-paned mode.
246                     // Add the domain ID to be loaded to `argumentsBundle`.
247                     Bundle argumentsBundle = new Bundle();
248                     argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, goDirectlyToDatabaseId);
249
250                     // Add `argumentsBundle` to `domainSettingsFragment`.
251                     DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
252                     domainSettingsFragment.setArguments(argumentsBundle);
253
254                     // Show `deleteMenuItem`.
255                     deleteMenuItem.setVisible(true);
256
257                     // Hide `add_domain_fab`.
258                     addDomainFAB.hide();
259
260                     // Display `domainSettingsFragment`.
261                     supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
262                 }
263             } else {  // Highlight the first domain.
264                 // Display `DomainsListFragment`.
265                 DomainsListFragment domainsListFragment = new DomainsListFragment();
266                 supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
267                 supportFragmentManager.executePendingTransactions();
268
269                 // Populate the list of domains.  `-1` highlights the first domain.
270                 populateDomainsListView(-1);
271             }
272         }
273
274         // Success!
275         return true;
276     }
277
278     @Override
279     public boolean onOptionsItemSelected(MenuItem menuItem) {
280         // Get the ID of the `MenuItem` that was selected.
281         int menuItemID = menuItem.getItemId();
282
283         switch (menuItemID) {
284             case android.R.id.home:  // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
285                 if (twoPanedMode) {  // The device is in two-paned mode.
286                     // Save the current domain settings if the domain settings fragment is displayed.
287                     if (findViewById(R.id.domain_settings_scrollview) != null) {
288                         saveDomainSettings(coordinatorLayout, resources);
289                     }
290
291                     // Dismiss the undo delete `SnackBar` if it is shown.
292                     if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
293                         // Set the close flag.
294                         closeActivityAfterDismissingSnackbar = true;
295
296                         // Dismiss the snackbar.
297                         undoDeleteSnackbar.dismiss();
298                     } else {
299                         // Go home.
300                         NavUtils.navigateUpFromSameTask(this);
301                     }
302                 } else if (closeOnBack) {  // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
303                     // Save the current domain settings.
304                     saveDomainSettings(coordinatorLayout, resources);
305
306                     // Go home.
307                     NavUtils.navigateUpFromSameTask(this);
308                 } else if (findViewById(R.id.domain_settings_scrollview) != null) {  // The device is in single-paned mode and the domain settings fragment is displayed.
309                     // Save the current domain settings.
310                     saveDomainSettings(coordinatorLayout, resources);
311
312                     // Display the domains list fragment.
313                     DomainsListFragment domainsListFragment = new DomainsListFragment();
314                     supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
315                     supportFragmentManager.executePendingTransactions();
316
317                     // Populate the list of domains.  `-1` highlights the first domain if in two-paned mode.  It has no effect in single-paned mode.
318                     populateDomainsListView(-1);
319
320                     // Show the add domain FAB.
321                     addDomainFAB.show();
322
323                     // Hide the delete menu item.
324                     deleteMenuItem.setVisible(false);
325                 } else {  // The device is in single-paned mode and `DomainsListFragment` is displayed.
326                     // Dismiss the undo delete `SnackBar` if it is shown.
327                     if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
328                         // Set the close flag.
329                         closeActivityAfterDismissingSnackbar = true;
330
331                         // Dismiss the snackbar.
332                         undoDeleteSnackbar.dismiss();
333                     } else {
334                         // Go home.
335                         NavUtils.navigateUpFromSameTask(this);
336                     }
337                 }
338                 break;
339
340             case R.id.delete_domain:
341                 // Reset close-on-back, which otherwise can cause errors if the system attempts to save settings for a domain that no longer exists.
342                 closeOnBack = false;
343
344                 // Store a copy of `currentDomainDatabaseId` because it could change while the `Snackbar` is displayed.
345                 final int databaseIdToDelete = currentDomainDatabaseId;
346
347                 // Update the fragments and menu items.
348                 if (twoPanedMode) {  // Two-paned mode.
349                     // Store the deleted domain position, which is needed if `Undo` is selected in the `Snackbar`.
350                     deletedDomainPosition = domainsListView.getCheckedItemPosition();
351
352                     // Disable the options `MenuItems`.
353                     deleteMenuItem.setEnabled(false);
354                     deleteMenuItem.setIcon(R.drawable.delete_blue);
355
356                     // Remove the domain settings fragment.
357                     supportFragmentManager.beginTransaction().remove(Objects.requireNonNull(supportFragmentManager.findFragmentById(R.id.domain_settings_fragment_container))).commit();
358                 } else {  // Single-paned mode.
359                     // Display `DomainsListFragment`.
360                     DomainsListFragment domainsListFragment = new DomainsListFragment();
361                     supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
362                     supportFragmentManager.executePendingTransactions();
363
364                     // Show the add domain FAB.
365                     addDomainFAB.show();
366
367                     // Hide `deleteMenuItem`.
368                     deleteMenuItem.setVisible(false);
369                 }
370
371                 // Get a `Cursor` that does not show the domain to be deleted.
372                 Cursor domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete);
373
374                 // Setup `domainsPendingDeleteCursorAdapter` with `this` context.  `false` disables `autoRequery`.
375                 CursorAdapter domainsPendingDeleteCursorAdapter = new CursorAdapter(this, domainsPendingDeleteCursor, false) {
376                     @Override
377                     public View newView(Context context, Cursor cursor, ViewGroup parent) {
378                         // Inflate the individual item layout.  `false` does not attach it to the root.
379                         return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
380                     }
381
382                     @Override
383                     public void bindView(View view, Context context, Cursor cursor) {
384                         // Set the domain name.
385                         String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
386                         TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
387                         domainNameTextView.setText(domainNameString);
388                     }
389                 };
390
391                 // Update the handle for the current `domains_listview`.
392                 domainsListView = findViewById(R.id.domains_listview);
393
394                 // Update the `ListView`.
395                 domainsListView.setAdapter(domainsPendingDeleteCursorAdapter);
396
397                 // Get a handle for the activity.
398                 Activity activity = this;
399
400                 // Display a `Snackbar`.
401                 undoDeleteSnackbar = Snackbar.make(domainsListView, R.string.domain_deleted, Snackbar.LENGTH_LONG)
402                         .setAction(R.string.undo, (View v) -> {
403                             // Do nothing because everything will be handled by `onDismissed()` below.
404                         })
405                         .addCallback(new Snackbar.Callback() {
406                             @Override
407                             public void onDismissed(Snackbar snackbar, int event) {
408                                 switch (event) {
409                                     // The user pushed the `Undo` button.
410                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
411                                         // Store `databaseId` in `argumentsBundle`.
412                                         Bundle argumentsBundle = new Bundle();
413                                         argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete);
414
415                                         // Add `argumentsBundle` to `domainSettingsFragment`.
416                                         DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
417                                         domainSettingsFragment.setArguments(argumentsBundle);
418
419                                         // Display the correct fragments.
420                                         if (twoPanedMode) {  // The device in in two-paned mode.
421                                             // Get a `Cursor` with the current contents of the domains database.
422                                             Cursor undoDeleteDomainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
423
424                                             // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
425                                             CursorAdapter undoDeleteDomainsCursorAdapter = new CursorAdapter(context, undoDeleteDomainsCursor, false) {
426                                                 @Override
427                                                 public View newView(Context context, Cursor cursor, ViewGroup parent) {
428                                                     // Inflate the individual item layout.  `false` does not attach it to the root.
429                                                     return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
430                                                 }
431
432                                                 @Override
433                                                 public void bindView(View view, Context context, Cursor cursor) {
434                                                     // Set the domain name.
435                                                     String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
436                                                     TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
437                                                     domainNameTextView.setText(domainNameString);
438                                                 }
439                                             };
440
441                                             // Update the `ListView`.
442                                             domainsListView.setAdapter(undoDeleteDomainsCursorAdapter);
443                                             // Select the previously deleted domain in `domainsListView`.
444                                             domainsListView.setItemChecked(deletedDomainPosition, true);
445
446                                             // Display `domainSettingsFragment`.
447                                             supportFragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
448
449                                             // Enable the options `MenuItems`.
450                                             deleteMenuItem.setEnabled(true);
451                                             deleteMenuItem.setIcon(R.drawable.delete_light);
452                                         } else {  // The device in in one-paned mode.
453                                             // Display `domainSettingsFragment`.
454                                             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
455
456                                             // Hide the add domain FAB.
457                                             addDomainFAB.hide();
458
459                                             // Show and enable `deleteMenuItem`.
460                                             deleteMenuItem.setVisible(true);
461
462                                             // Display `domainSettingsFragment`.
463                                             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
464                                         }
465                                         break;
466
467                                     // The `Snackbar` was dismissed without the `Undo` button being pushed.
468                                     default:
469                                         // Delete the selected domain.
470                                         domainsDatabaseHelper.deleteDomain(databaseIdToDelete);
471
472                                         // Enable the delete menu item if the system was waiting for a snackbar to be dismissed.
473                                         if (dismissingSnackbar) {
474                                             // Create a `Runnable` to enable the delete menu item.
475                                             Runnable enableDeleteMenuItemRunnable = () -> {
476                                                 // Enable `deleteMenuItem` according to the display mode.
477                                                 if (twoPanedMode) {  // Two-paned mode.
478                                                     // Enable `deleteMenuItem`.
479                                                     deleteMenuItem.setEnabled(true);
480
481                                                     // Set the delete icon according to the theme.
482                                                     if (MainWebViewActivity.darkTheme) {
483                                                         deleteMenuItem.setIcon(R.drawable.delete_dark);
484                                                     } else {
485                                                         deleteMenuItem.setIcon(R.drawable.delete_light);
486                                                     }
487                                                 } else {  // Single-paned mode.
488                                                     // Show `deleteMenuItem`.
489                                                     deleteMenuItem.setVisible(true);
490                                                 }
491
492                                                 // Reset `dismissingSnackbar`.
493                                                 dismissingSnackbar = false;
494                                             };
495
496                                             // Enable the delete menu icon after 100 milliseconds to make sure that the previous domain has been deleted from the database.
497                                             Handler handler = new Handler();
498                                             handler.postDelayed(enableDeleteMenuItemRunnable, 100);
499                                         }
500
501                                         // Close the activity if back was pressed.
502                                         if (closeActivityAfterDismissingSnackbar) {
503                                             // Go home.
504                                             NavUtils.navigateUpFromSameTask(activity);
505                                         }
506
507                                         break;
508                                 }
509                             }
510                         });
511
512                 // Show the Snackbar.
513                 undoDeleteSnackbar.show();
514                 break;
515         }
516
517         // Consume the event.
518         return true;
519     }
520
521     @Override
522     protected void onSaveInstanceState(Bundle outState) {
523         // Store the current `DomainSettingsFragment` state in `outState`.
524         if (findViewById(R.id.domain_settings_scrollview) != null) {  // `DomainSettingsFragment` is displayed.
525             // Save any changes that have been made to the domain settings.
526             saveDomainSettings(coordinatorLayout, resources);
527
528             // Store `DomainSettingsDisplayed`.
529             outState.putBoolean("domainSettingsDisplayed", true);
530             outState.putInt("domainSettingsDatabaseId", DomainSettingsFragment.databaseId);
531         } else {  // `DomainSettingsFragment` is not displayed.
532             outState.putBoolean("domainSettingsDisplayed", false);
533             outState.putInt("domainSettingsDatabaseId", -1);
534         }
535
536         super.onSaveInstanceState(outState);
537     }
538
539     // Control what the navigation bar back button does.
540     @Override
541     public void onBackPressed() {
542         if (twoPanedMode) {  // The device is in two-paned mode.
543             // Save the current domain settings if the domain settings fragment is displayed.
544             if (findViewById(R.id.domain_settings_scrollview) != null) {
545                 saveDomainSettings(coordinatorLayout, resources);
546             }
547
548             // Dismiss the undo delete SnackBar if it is shown.
549             if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
550                 // Set the close flag.
551                 closeActivityAfterDismissingSnackbar = true;
552
553                 // Dismiss the snackbar.
554                 undoDeleteSnackbar.dismiss();
555             } else {
556                 // Pass `onBackPressed()` to the system.
557                 super.onBackPressed();
558             }
559         } else if (closeOnBack) {  // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
560             // Save the current domain settings.
561             saveDomainSettings(coordinatorLayout, resources);
562
563             // Pass `onBackPressed()` to the system.
564             super.onBackPressed();
565         } else if (findViewById(R.id.domain_settings_scrollview) != null) {  // The device is in single-paned mode and domain settings fragment is displayed.
566             // Save the current domain settings.
567             saveDomainSettings(coordinatorLayout, resources);
568
569             // Display the domains list fragment.
570             DomainsListFragment domainsListFragment = new DomainsListFragment();
571             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
572             supportFragmentManager.executePendingTransactions();
573
574             // Populate the list of domains.  `-1` highlights the first domain if in two-paned mode.  It has no effect in single-paned mode.
575             populateDomainsListView(-1);
576
577             // Show the add domain FAB.
578             addDomainFAB.show();
579
580             // Hide the delete menu item.
581             deleteMenuItem.setVisible(false);
582         } else {  // The device is in single-paned mode and the domain list fragment is displayed.
583             // Dismiss the undo delete SnackBar if it is shown.
584             if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
585                 // Set the close flag.
586                 closeActivityAfterDismissingSnackbar = true;
587
588                 // Dismiss the snackbar.
589                 undoDeleteSnackbar.dismiss();
590             } else {
591                 // Pass `onBackPressed()` to the system.
592                 super.onBackPressed();
593             }
594         }
595     }
596
597     @Override
598     public void onAddDomain(AppCompatDialogFragment dialogFragment) {
599         // Dismiss the undo delete snackbar if it is currently displayed.
600         if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
601             undoDeleteSnackbar.dismiss();
602         }
603
604         // Get the new domain name String from the dialog fragment.
605         EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
606         String domainNameString = domainNameEditText.getText().toString();
607
608         // Create the domain and store the database ID in `currentDomainDatabaseId`.
609         currentDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
610
611         // Display the newly created domain.
612         if (twoPanedMode) {  // The device in in two-paned mode.
613             populateDomainsListView(currentDomainDatabaseId);
614         } else {  // The device is in single-paned mode.
615             // Hide the add domain FAB.
616             addDomainFAB.hide();
617
618             // Show and enable `deleteMenuItem`.
619             DomainsActivity.deleteMenuItem.setVisible(true);
620
621             // Add `currentDomainDatabaseId` to `argumentsBundle`.
622             Bundle argumentsBundle = new Bundle();
623             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
624
625             // Add `argumentsBundle` to `domainSettingsFragment`.
626             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
627             domainSettingsFragment.setArguments(argumentsBundle);
628
629             // Display `domainSettingsFragment`.
630             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
631         }
632     }
633
634     public void saveDomainSettings(View view, Resources resources) {
635         // Get handles for the domain settings.
636         EditText domainNameEditText = view.findViewById(R.id.domain_settings_name_edittext);
637         Switch javaScriptSwitch = view.findViewById(R.id.javascript_switch);
638         Switch firstPartyCookiesSwitch = view.findViewById(R.id.first_party_cookies_switch);
639         Switch thirdPartyCookiesSwitch = view.findViewById(R.id.third_party_cookies_switch);
640         Switch domStorageSwitch = view.findViewById(R.id.dom_storage_switch);
641         Switch formDataSwitch = view.findViewById(R.id.form_data_switch);  // Form data can be removed once the minimum API >= 26.
642         Switch easyListSwitch = view.findViewById(R.id.easylist_switch);
643         Switch easyPrivacySwitch = view.findViewById(R.id.easyprivacy_switch);
644         Switch fanboysAnnoyanceSwitch = view.findViewById(R.id.fanboys_annoyance_list_switch);
645         Switch fanboysSocialBlockingSwitch = view.findViewById(R.id.fanboys_social_blocking_list_switch);
646         Switch ultraPrivacySwitch = view.findViewById(R.id.ultraprivacy_switch);
647         Switch blockAllThirdPartyRequestsSwitch = view.findViewById(R.id.block_all_third_party_requests_switch);
648         Spinner userAgentSpinner = view.findViewById(R.id.user_agent_spinner);
649         EditText customUserAgentEditText = view.findViewById(R.id.custom_user_agent_edittext);
650         Spinner fontSizeSpinner = view.findViewById(R.id.font_size_spinner);
651         Spinner swipeToRefreshSpinner = view.findViewById(R.id.swipe_to_refresh_spinner);
652         Spinner displayWebpageImagesSpinner = view.findViewById(R.id.display_webpage_images_spinner);
653         Spinner nightModeSpinner = view.findViewById(R.id.night_mode_spinner);
654         Switch pinnedSslCertificateSwitch = view.findViewById(R.id.pinned_ssl_certificate_switch);
655         RadioButton currentWebsiteCertificateRadioButton = view.findViewById(R.id.current_website_certificate_radiobutton);
656         Switch pinnedIpAddressesSwitch = view.findViewById(R.id.pinned_ip_addresses_switch);
657         RadioButton currentIpAddressesRadioButton = view.findViewById(R.id.current_ip_addresses_radiobutton);
658         TextView currentIpAddressesTextView = view.findViewById(R.id.current_ip_addresses_textview);
659
660         // Extract the data for the domain settings.
661         String domainNameString = domainNameEditText.getText().toString();
662         boolean javaScriptEnabled = javaScriptSwitch.isChecked();
663         boolean firstPartyCookiesEnabled = firstPartyCookiesSwitch.isChecked();
664         boolean thirdPartyCookiesEnabled = thirdPartyCookiesSwitch.isChecked();
665         boolean domStorageEnabled  = domStorageSwitch.isChecked();
666         boolean formDataEnabled = formDataSwitch.isChecked();  // Form data can be removed once the minimum API >= 26.
667         boolean easyListEnabled = easyListSwitch.isChecked();
668         boolean easyPrivacyEnabled = easyPrivacySwitch.isChecked();
669         boolean fanboysAnnoyanceEnabled = fanboysAnnoyanceSwitch.isChecked();
670         boolean fanboysSocialBlockingEnabled = fanboysSocialBlockingSwitch.isChecked();
671         boolean ultraPrivacyEnabled = ultraPrivacySwitch.isChecked();
672         boolean blockAllThirdPartyRequests = blockAllThirdPartyRequestsSwitch.isChecked();
673         int userAgentPosition = userAgentSpinner.getSelectedItemPosition();
674         int fontSizePosition = fontSizeSpinner.getSelectedItemPosition();
675         int swipeToRefreshInt = swipeToRefreshSpinner.getSelectedItemPosition();
676         int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
677         int nightModeInt = nightModeSpinner.getSelectedItemPosition();
678         boolean pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked();
679         boolean pinnedIpAddress = pinnedIpAddressesSwitch.isChecked();
680
681         // Initialize the user agent name string.
682         String userAgentName;
683
684         // Set the user agent name.
685         switch (userAgentPosition) {
686             case MainWebViewActivity.DOMAINS_SYSTEM_DEFAULT_USER_AGENT:
687                 // Set the user agent name to be `System default user agent`.
688                 userAgentName = resources.getString(R.string.system_default_user_agent);
689                 break;
690
691             case MainWebViewActivity.DOMAINS_CUSTOM_USER_AGENT:
692                 // Set the user agent name to be the custom user agent.
693                 userAgentName = customUserAgentEditText.getText().toString();
694                 break;
695
696             default:
697                 // Get the array of user agent names.
698                 String[] userAgentNameArray = resources.getStringArray(R.array.user_agent_names);
699
700                 // Set the user agent name from the array.  The domain spinner has one more entry than the name array, so the position must be decremented.
701                 userAgentName = userAgentNameArray[userAgentPosition - 1];
702         }
703
704         // Get the font size integer.
705         int fontSizeInt = Integer.parseInt(resources.getStringArray(R.array.domain_settings_font_size_entry_values)[fontSizePosition]);
706
707         // Save the domain settings.
708         domainsDatabaseHelper.updateDomain(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScriptEnabled, firstPartyCookiesEnabled, thirdPartyCookiesEnabled,
709                     domStorageEnabled, formDataEnabled, easyListEnabled, easyPrivacyEnabled, fanboysAnnoyanceEnabled, fanboysSocialBlockingEnabled, ultraPrivacyEnabled, blockAllThirdPartyRequests,
710                     userAgentName, fontSizeInt, swipeToRefreshInt, nightModeInt, displayWebpageImagesInt, pinnedSslCertificate, pinnedIpAddress);
711
712         // Update the pinned SSL certificate if a new one is checked.
713         if (currentWebsiteCertificateRadioButton.isChecked()) {
714             // Get the current website SSL certificate.
715             SslCertificate currentWebsiteSslCertificate = MainWebViewActivity.sslCertificate;
716
717             // Store the values from the SSL certificate.
718             String issuedToCommonName = currentWebsiteSslCertificate.getIssuedTo().getCName();
719             String issuedToOrganization = currentWebsiteSslCertificate.getIssuedTo().getOName();
720             String issuedToOrganizationalUnit = currentWebsiteSslCertificate.getIssuedTo().getUName();
721             String issuedByCommonName = currentWebsiteSslCertificate.getIssuedBy().getCName();
722             String issuedByOrganization = currentWebsiteSslCertificate.getIssuedBy().getOName();
723             String issuedByOrganizationalUnit = currentWebsiteSslCertificate.getIssuedBy().getUName();
724             long startDateLong = currentWebsiteSslCertificate.getValidNotBeforeDate().getTime();
725             long endDateLong = currentWebsiteSslCertificate.getValidNotAfterDate().getTime();
726
727             // Update the database.
728             domainsDatabaseHelper.updatePinnedSslCertificate(currentDomainDatabaseId, issuedToCommonName, issuedToOrganization, issuedToOrganizationalUnit, issuedByCommonName, issuedByOrganization,
729                     issuedByOrganizationalUnit, startDateLong, endDateLong);
730         }
731
732         // Update the pinned IP addresses if new ones are checked.
733         if (currentIpAddressesRadioButton.isChecked()) {
734             // Get the current IP addresses.
735             String currentIpAddresses = currentIpAddressesTextView.getText().toString();
736
737             // Update the database.
738             domainsDatabaseHelper.updatePinnedIpAddresses(currentDomainDatabaseId, currentIpAddresses);
739         }
740     }
741
742     private void populateDomainsListView(final int highlightedDomainDatabaseId) {
743         // get a handle for the current `domains_listview`.
744         domainsListView = findViewById(R.id.domains_listview);
745
746         // Get a `Cursor` with the current contents of the domains database.
747         Cursor domainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
748
749         // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
750         CursorAdapter domainsCursorAdapter = new CursorAdapter(context, domainsCursor, false) {
751             @Override
752             public View newView(Context context, Cursor cursor, ViewGroup parent) {
753                 // Inflate the individual item layout.  `false` does not attach it to the root.
754                 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
755             }
756
757             @Override
758             public void bindView(View view, Context context, Cursor cursor) {
759                 // Set the domain name.
760                 String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
761                 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
762                 domainNameTextView.setText(domainNameString);
763             }
764         };
765
766         // Update the `ListView`.
767         domainsListView.setAdapter(domainsCursorAdapter);
768
769         // Display the domain settings in the second pane if operating in two pane mode and the database contains at least one domain.
770         if (DomainsActivity.twoPanedMode && (domainsCursor.getCount() > 0)) {  // Two-paned mode is enabled and there is at least one domain.
771             // Initialize `highlightedDomainPosition`.
772             int highlightedDomainPosition = 0;
773
774             // Get the cursor position for the highlighted domain.
775             for (int i = 0; i < domainsCursor.getCount(); i++) {
776                 // Move to position `i` in the cursor.
777                 domainsCursor.moveToPosition(i);
778
779                 // Get the database ID for this position.
780                 int currentDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
781
782                 // Set `highlightedDomainPosition` if the database ID for this matches `highlightedDomainDatabaseId`.
783                 if (highlightedDomainDatabaseId == currentDatabaseId) {
784                     highlightedDomainPosition = i;
785                 }
786             }
787
788             // Select the highlighted domain.
789             domainsListView.setItemChecked(highlightedDomainPosition, true);
790
791             // Get the `databaseId` for the highlighted domain.
792             domainsCursor.moveToPosition(highlightedDomainPosition);
793             currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
794
795             // Store `databaseId` in `argumentsBundle`.
796             Bundle argumentsBundle = new Bundle();
797             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
798
799             // Add `argumentsBundle` to `domainSettingsFragment`.
800             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
801             domainSettingsFragment.setArguments(argumentsBundle);
802
803             // Display `domainSettingsFragment`.
804             supportFragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
805
806             // Enable the options `MenuItems`.
807             deleteMenuItem.setEnabled(true);
808
809             // Set the delete icon according to the theme.
810             if (MainWebViewActivity.darkTheme) {
811                 deleteMenuItem.setIcon(R.drawable.delete_dark);
812             } else {
813                 deleteMenuItem.setIcon(R.drawable.delete_light);
814             }
815         } else if (twoPanedMode) {  // Two-paned mode is enabled but there are no domains.
816             // Disable the options `MenuItems`.
817             deleteMenuItem.setEnabled(false);
818             deleteMenuItem.setIcon(R.drawable.delete_blue);
819         }
820     }
821
822     @Override
823     public void onDestroy() {
824         // Close the domains database helper.
825         domainsDatabaseHelper.close();
826
827         // Run the default commands.
828         super.onDestroy();
829     }
830 }