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