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