]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java
Save and restore the app state. https://redmine.stoutner.com/issues/461
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / DomainsActivity.java
1 /*
2  * Copyright © 2017-2020 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.activities;
21
22 import android.app.Activity;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.database.Cursor;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.preference.PreferenceManager;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.WindowManager;
37 import android.widget.CursorAdapter;
38 import android.widget.EditText;
39 import android.widget.ListView;
40 import android.widget.RadioButton;
41 import android.widget.ScrollView;
42 import android.widget.Spinner;
43 import android.widget.TextView;
44
45 import androidx.annotation.NonNull;
46 import androidx.appcompat.app.ActionBar;
47 import androidx.appcompat.app.AppCompatActivity;
48 import androidx.appcompat.widget.SwitchCompat;
49 import androidx.appcompat.widget.Toolbar;
50 import androidx.core.app.NavUtils;
51 import androidx.fragment.app.DialogFragment;
52 import androidx.fragment.app.FragmentManager;
53
54 import com.google.android.material.floatingactionbutton.FloatingActionButton;
55 import com.google.android.material.snackbar.Snackbar;
56
57 import com.stoutner.privacybrowser.R;
58 import com.stoutner.privacybrowser.dialogs.AddDomainDialog;
59 import com.stoutner.privacybrowser.fragments.DomainSettingsFragment;
60 import com.stoutner.privacybrowser.fragments.DomainsListFragment;
61 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
62
63 import java.util.Objects;
64
65 public class DomainsActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener, DomainsListFragment.DismissSnackbarInterface {
66     // Define the public static variables.
67     public static int domainsListViewPosition;
68     public static boolean twoPanedMode;
69     public static int currentDomainDatabaseId;
70     public static MenuItem deleteMenuItem;
71     public static boolean dismissingSnackbar;
72
73     // The SSL certificate and IP address information are accessed from `DomainSettingsFragment` and `saveDomainSettings()`.
74     public static String sslIssuedToCName;
75     public static String sslIssuedToOName;
76     public static String sslIssuedToUName;
77     public static String sslIssuedByCName;
78     public static String sslIssuedByOName;
79     public static String sslIssuedByUName;
80     public static long sslStartDateLong;
81     public static long sslEndDateLong;
82     public static String currentIpAddresses;
83
84
85     // Initialize the class constants.
86     private final String LISTVIEW_POSITION = "listview_position";
87     private final String DOMAIN_SETTINGS_DISPLAYED = "domain_settings_displayed";
88     private final String DOMAIN_SETTINGS_DATABASE_ID = "domain_settings_database_is";
89     private final String DOMAIN_SETTINGS_SCROLL_Y = "domain_settings_scroll_y";
90
91     // Initialize the class variables.
92     private boolean restartAfterRotate;
93     private boolean domainSettingsDisplayedBeforeRotate;
94     private int domainSettingsDatabaseIdBeforeRotate;
95     private int domainSettingsScrollY = 0;
96
97     // Defile the class views.
98     private ListView domainsListView;
99
100     // `closeActivityAfterDismissingSnackbar` is used in `onOptionsItemSelected()`, and `onBackPressed()`.
101     private boolean closeActivityAfterDismissingSnackbar;
102
103     // The undelete snackbar is used in `onOptionsItemSelected()` and `onBackPressed()`.
104     private Snackbar undoDeleteSnackbar;
105
106     // `domainsDatabaseHelper` is used in `onCreate()`, `saveDomainSettings()`, and `onDestroy()`.
107     private static DomainsDatabaseHelper domainsDatabaseHelper;
108
109     // `addDomainFAB` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `onBackPressed()`.
110     private FloatingActionButton addDomainFAB;
111
112     // `deletedDomainPosition` is used in an inner and outer class in `onOptionsItemSelected()`.
113     private int deletedDomainPosition;
114
115     // `goDirectlyToDatabaseId` is used in `onCreate()` and `onCreateOptionsMenu()`.
116     private int goDirectlyToDatabaseId;
117
118     // `closeOnBack` is used in `onCreate()`, `onOptionsItemSelected()` and `onBackPressed()`.
119     private boolean closeOnBack;
120
121     // `coordinatorLayout` is use in `onCreate()`, `onOptionsItemSelected()`, and `onSaveInstanceState()`.
122     private View coordinatorLayout;
123
124     // `resources` is used in `onCreate()`, `onOptionsItemSelected()`, and `onSaveInstanceState()`.
125     private Resources resources;
126
127     @Override
128     protected void onCreate(Bundle savedInstanceState) {
129         // Get a handle for the shared preferences.
130         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
131
132         // Get the screenshot preference.
133         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
134
135         // Disable screenshots if not allowed.
136         if (!allowScreenshots) {
137             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
138         }
139
140         // Set the theme.
141         setTheme(R.style.PrivacyBrowser);
142
143         // Run the default commands.
144         super.onCreate(savedInstanceState);
145
146         // Initialize the domains listview position.
147         domainsListViewPosition = 0;
148
149         // Extract the values from the saved instance state if it is not null.
150         if (savedInstanceState != null) {
151             domainsListViewPosition = savedInstanceState.getInt(LISTVIEW_POSITION);
152             restartAfterRotate = true;
153             domainSettingsDisplayedBeforeRotate = savedInstanceState.getBoolean(DOMAIN_SETTINGS_DISPLAYED);
154             domainSettingsDatabaseIdBeforeRotate = savedInstanceState.getInt(DOMAIN_SETTINGS_DATABASE_ID);
155             domainSettingsScrollY = savedInstanceState.getInt(DOMAIN_SETTINGS_SCROLL_Y);
156         }
157
158         // Get the launching intent
159         Intent intent = getIntent();
160
161         // Extract the domain to load if there is one.  `-1` is the default value.
162         goDirectlyToDatabaseId = intent.getIntExtra("load_domain", -1);
163
164         // Get the status of close-on-back, which is true when the domains activity is called from the options menu.
165         closeOnBack = intent.getBooleanExtra("close_on_back", false);
166
167         // Get the current URL.
168         String currentUrl = intent.getStringExtra("current_url");
169
170         // Store the current SSL certificate information in class variables.
171         sslIssuedToCName = intent.getStringExtra("ssl_issued_to_cname");
172         sslIssuedToOName = intent.getStringExtra("ssl_issued_to_oname");
173         sslIssuedToUName = intent.getStringExtra("ssl_issued_to_uname");
174         sslIssuedByCName = intent.getStringExtra("ssl_issued_by_cname");
175         sslIssuedByOName = intent.getStringExtra("ssl_issued_by_oname");
176         sslIssuedByUName = intent.getStringExtra("ssl_issued_by_uname");
177         sslStartDateLong = intent.getLongExtra("ssl_start_date", 0);
178         sslEndDateLong = intent.getLongExtra("ssl_end_date", 0);
179         currentIpAddresses = intent.getStringExtra("current_ip_addresses");
180
181         // Set the content view.
182         setContentView(R.layout.domains_coordinatorlayout);
183
184         // Populate the class variables.
185         coordinatorLayout = findViewById(R.id.domains_coordinatorlayout);
186         resources = getResources();
187
188         // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21.
189         final Toolbar toolbar = findViewById(R.id.domains_toolbar);
190         setSupportActionBar(toolbar);
191
192         // Get a handle for the action bar.
193         ActionBar actionBar = getSupportActionBar();
194
195         // Remove the incorrect lint warning that the action bar might be null.
196         assert actionBar != null;
197
198         // Set the back arrow on the action bar.
199         actionBar.setDisplayHomeAsUpEnabled(true);
200
201         // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
202         domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
203
204         // Determine if we are in two pane mode.  `domain_settings_fragment_container` does not exist on devices with a width less than 900dp.
205         twoPanedMode = (findViewById(R.id.domain_settings_fragment_container) != null);
206
207         // Get a handle for the add domain floating action button.
208         addDomainFAB = findViewById(R.id.add_domain_fab);
209
210         // Configure the add domain floating action button.
211         addDomainFAB.setOnClickListener((View view) -> {
212             // Remove the incorrect warning below that the current URL might be null.
213             assert currentUrl != null;
214
215             // Create an add domain dialog.
216             DialogFragment addDomainDialog = AddDomainDialog.addDomain(currentUrl);
217
218             // Show the add domain dialog.
219             addDomainDialog.show(getSupportFragmentManager(), resources.getString(R.string.add_domain));
220         });
221     }
222
223     @Override
224     public boolean onCreateOptionsMenu(Menu menu) {
225         // Inflate the menu.
226         getMenuInflater().inflate(R.menu.domains_options_menu, menu);
227
228         // Store `deleteMenuItem` for future use.
229         deleteMenuItem = menu.findItem(R.id.delete_domain);
230
231         // Only display `deleteMenuItem` (initially) in two-paned mode.
232         deleteMenuItem.setVisible(twoPanedMode);
233
234         // Get a handle for the fragment manager.
235         FragmentManager fragmentManager = getSupportFragmentManager();
236
237         // Display the fragments.  This must be done from `onCreateOptionsMenu()` instead of `onCreate()` because `populateDomainsListView()` needs `deleteMenuItem` to be inflated.
238         if (restartAfterRotate && domainSettingsDisplayedBeforeRotate) {  // The device was rotated and domain settings were displayed previously.
239             if (twoPanedMode) {  // The device is in two-paned mode.
240                 // Reset `restartAfterRotate`.
241                 restartAfterRotate = false;
242
243                 // Display `DomainsListFragment`.
244                 DomainsListFragment domainsListFragment = new DomainsListFragment();
245                 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
246                 fragmentManager.executePendingTransactions();
247
248                 // Populate the list of domains.  `domainSettingsDatabaseId` highlights the domain that was highlighted before the rotation.
249                 populateDomainsListView(domainSettingsDatabaseIdBeforeRotate, domainsListViewPosition);
250             } else {  // The device is in single-paned mode.
251                 // Reset `restartAfterRotate`.
252                 restartAfterRotate = false;
253
254                 // Store the current domain database ID.
255                 currentDomainDatabaseId = domainSettingsDatabaseIdBeforeRotate;
256
257                 // Create an arguments bundle.
258                 Bundle argumentsBundle = new Bundle();
259
260                 // Add the domain settings arguments.
261                 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
262                 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
263
264                 // Instantiate a new domain settings fragment.
265                 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
266
267                 // Add the arguments bundle to the domain settings fragment.
268                 domainSettingsFragment.setArguments(argumentsBundle);
269
270                 // Show the delete menu item.
271                 deleteMenuItem.setVisible(true);
272
273                 // Hide the add domain floating action button.
274                 addDomainFAB.hide();
275
276                 // Display the domain settings fragment.
277                 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
278             }
279         } else {  // The device was not rotated or, if it was, domain settings were not displayed previously.
280             if (goDirectlyToDatabaseId >=0) {  // Load the indicated domain settings.
281                 // Store the current domain database ID.
282                 currentDomainDatabaseId = goDirectlyToDatabaseId;
283
284                 if (twoPanedMode) {  // The device is in two-paned mode.
285                     // Display `DomainsListFragment`.
286                     DomainsListFragment domainsListFragment = new DomainsListFragment();
287                     fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
288                     fragmentManager.executePendingTransactions();
289
290                     // Populate the list of domains.  `domainSettingsDatabaseId` highlights the domain that was highlighted before the rotation.
291                     populateDomainsListView(goDirectlyToDatabaseId, domainsListViewPosition);
292                 } else {  // The device is in single-paned mode.
293                     // Create an arguments bundle.
294                     Bundle argumentsBundle = new Bundle();
295
296                     // Add the domain settings to arguments bundle.
297                     argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, goDirectlyToDatabaseId);
298                     argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
299
300                     // Instantiate a new domain settings fragment.
301                     DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
302
303                     // Add the arguments bundle to the domain settings fragment`.
304                     domainSettingsFragment.setArguments(argumentsBundle);
305
306                     // Show the delete menu item.
307                     deleteMenuItem.setVisible(true);
308
309                     // Hide the add domain floating action button.
310                     addDomainFAB.hide();
311
312                     // Display the domain settings fragment.
313                     fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
314                 }
315             } else {  // Highlight the first domain.
316                 // Display `DomainsListFragment`.
317                 DomainsListFragment domainsListFragment = new DomainsListFragment();
318                 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
319                 fragmentManager.executePendingTransactions();
320
321                 // Populate the list of domains.  `-1` highlights the first domain.
322                 populateDomainsListView(-1, domainsListViewPosition);
323             }
324         }
325
326         // Success!
327         return true;
328     }
329
330     @Override
331     public boolean onOptionsItemSelected(MenuItem menuItem) {
332         // Get the ID of the menu item that was selected.
333         int menuItemID = menuItem.getItemId();
334
335         // Get a handle for the fragment manager.
336         FragmentManager fragmentManager = getSupportFragmentManager();
337
338         switch (menuItemID) {
339             case android.R.id.home:  // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
340                 if (twoPanedMode) {  // The device is in two-paned mode.
341                     // Save the current domain settings if the domain settings fragment is displayed.
342                     if (findViewById(R.id.domain_settings_scrollview) != null) {
343                         saveDomainSettings(coordinatorLayout, resources);
344                     }
345
346                     // Dismiss the undo delete `SnackBar` if it is shown.
347                     if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
348                         // Set the close flag.
349                         closeActivityAfterDismissingSnackbar = true;
350
351                         // Dismiss the snackbar.
352                         undoDeleteSnackbar.dismiss();
353                     } else {
354                         // Go home.
355                         NavUtils.navigateUpFromSameTask(this);
356                     }
357                 } else if (closeOnBack) {  // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
358                     // Save the current domain settings.
359                     saveDomainSettings(coordinatorLayout, resources);
360
361                     // Go home.
362                     NavUtils.navigateUpFromSameTask(this);
363                 } else if (findViewById(R.id.domain_settings_scrollview) != null) {  // The device is in single-paned mode and the domain settings fragment is displayed.
364                     // Save the current domain settings.
365                     saveDomainSettings(coordinatorLayout, resources);
366
367                     // Display the domains list fragment.
368                     DomainsListFragment domainsListFragment = new DomainsListFragment();
369                     fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
370                     fragmentManager.executePendingTransactions();
371
372                     // Populate the list of domains.  `-1` highlights the first domain if in two-paned mode.  It has no effect in single-paned mode.
373                     populateDomainsListView(-1, domainsListViewPosition);
374
375                     // Show the add domain floating action button.
376                     addDomainFAB.show();
377
378                     // Hide the delete menu item.
379                     deleteMenuItem.setVisible(false);
380                 } else {  // The device is in single-paned mode and `DomainsListFragment` is displayed.
381                     // Dismiss the undo delete `SnackBar` if it is shown.
382                     if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
383                         // Set the close flag.
384                         closeActivityAfterDismissingSnackbar = true;
385
386                         // Dismiss the snackbar.
387                         undoDeleteSnackbar.dismiss();
388                     } else {
389                         // Go home.
390                         NavUtils.navigateUpFromSameTask(this);
391                     }
392                 }
393                 break;
394
395             case R.id.delete_domain:
396                 // Reset close-on-back, which otherwise can cause errors if the system attempts to save settings for a domain that no longer exists.
397                 closeOnBack = false;
398
399                 // Store a copy of `currentDomainDatabaseId` because it could change while the `Snackbar` is displayed.
400                 final int databaseIdToDelete = currentDomainDatabaseId;
401
402                 // Update the fragments and menu items.
403                 if (twoPanedMode) {  // Two-paned mode.
404                     // Store the deleted domain position, which is needed if `Undo` is selected in the `Snackbar`.
405                     deletedDomainPosition = domainsListView.getCheckedItemPosition();
406
407                     // Disable the options `MenuItems`.
408                     deleteMenuItem.setEnabled(false);
409                     deleteMenuItem.setIcon(R.drawable.delete_disabled);
410
411                     // Remove the domain settings fragment.
412                     fragmentManager.beginTransaction().remove(Objects.requireNonNull(fragmentManager.findFragmentById(R.id.domain_settings_fragment_container))).commit();
413                 } else {  // Single-paned mode.
414                     // Display `DomainsListFragment`.
415                     DomainsListFragment domainsListFragment = new DomainsListFragment();
416                     fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
417                     fragmentManager.executePendingTransactions();
418
419                     // Show the add domain floating action button.
420                     addDomainFAB.show();
421
422                     // Hide `deleteMenuItem`.
423                     deleteMenuItem.setVisible(false);
424                 }
425
426                 // Get a `Cursor` that does not show the domain to be deleted.
427                 Cursor domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete);
428
429                 // Setup `domainsPendingDeleteCursorAdapter` with `this` context.  `false` disables `autoRequery`.
430                 CursorAdapter domainsPendingDeleteCursorAdapter = new CursorAdapter(this, domainsPendingDeleteCursor, false) {
431                     @Override
432                     public View newView(Context context, Cursor cursor, ViewGroup parent) {
433                         // Inflate the individual item layout.  `false` does not attach it to the root.
434                         return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
435                     }
436
437                     @Override
438                     public void bindView(View view, Context context, Cursor cursor) {
439                         // Set the domain name.
440                         String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
441                         TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
442                         domainNameTextView.setText(domainNameString);
443                     }
444                 };
445
446                 // Update the handle for the current `domains_listview`.
447                 domainsListView = findViewById(R.id.domains_listview);
448
449                 // Update the `ListView`.
450                 domainsListView.setAdapter(domainsPendingDeleteCursorAdapter);
451
452                 // Get a handle for the activity.
453                 Activity activity = this;
454
455                 // Display a snackbar.
456                 undoDeleteSnackbar = Snackbar.make(domainsListView, R.string.domain_deleted, Snackbar.LENGTH_LONG)
457                         .setAction(R.string.undo, (View v) -> {
458                             // Do nothing because everything will be handled by `onDismissed()` below.
459                         })
460                         .addCallback(new Snackbar.Callback() {
461                             @Override
462                             public void onDismissed(Snackbar snackbar, int event) {
463                                 // Run commands based on the event.
464                                 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The user pushed the `Undo` button.
465                                     // Create an arguments bundle.
466                                     Bundle argumentsBundle = new Bundle();
467
468                                     // Store the domains settings in the arguments bundle.
469                                     argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete);
470                                     argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
471
472                                     // Instantiate a new domain settings fragment.
473                                     DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
474
475                                     // Add the arguments bundle to the domain settings fragment.
476                                     domainSettingsFragment.setArguments(argumentsBundle);
477
478                                     // Display the correct fragments.
479                                     if (twoPanedMode) {  // The device in in two-paned mode.
480                                         // Get a `Cursor` with the current contents of the domains database.
481                                         Cursor undoDeleteDomainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
482
483                                         // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
484                                         CursorAdapter undoDeleteDomainsCursorAdapter = new CursorAdapter(getApplicationContext(), undoDeleteDomainsCursor, false) {
485                                             @Override
486                                             public View newView(Context context, Cursor cursor, ViewGroup parent) {
487                                                 // Inflate the individual item layout.  `false` does not attach it to the root.
488                                                 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
489                                             }
490
491                                             @Override
492                                             public void bindView(View view, Context context, Cursor cursor) {
493                                                 // Set the domain name.
494                                                 String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
495                                                 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
496                                                 domainNameTextView.setText(domainNameString);
497                                             }
498                                         };
499
500                                         // Update the domains list view.
501                                         domainsListView.setAdapter(undoDeleteDomainsCursorAdapter);
502
503                                         // Select the previously deleted domain in the list view.
504                                         domainsListView.setItemChecked(deletedDomainPosition, true);
505
506                                         // Display the domain settings fragment.
507                                         fragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
508
509                                         // Enable the options delete menu item.
510                                         deleteMenuItem.setEnabled(true);
511
512                                         // Get the current theme status.
513                                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
514
515                                         // Set the delete menu item icon according to the theme.
516                                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
517                                             deleteMenuItem.setIcon(R.drawable.delete_night);
518                                         } else {
519                                             deleteMenuItem.setIcon(R.drawable.delete_day);
520                                         }
521                                     } else {  // The device in in one-paned mode.
522                                         // Display the domain settings fragment.
523                                         fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
524
525                                         // Hide the add domain floating action button.
526                                         addDomainFAB.hide();
527
528                                         // Show and enable the delete menu item.
529                                         deleteMenuItem.setVisible(true);
530
531                                         // Display the domain settings fragment.
532                                         fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
533                                     }
534                                 } else {  // The snackbar was dismissed without the undo button being pushed.
535                                     // Delete the selected domain.
536                                     domainsDatabaseHelper.deleteDomain(databaseIdToDelete);
537
538                                     // Enable the delete menu item if the system was waiting for a snackbar to be dismissed.
539                                     if (dismissingSnackbar) {
540                                         // Create a `Runnable` to enable the delete menu item.
541                                         Runnable enableDeleteMenuItemRunnable = () -> {
542                                             // Enable the delete menu item according to the display mode.
543                                             if (twoPanedMode) {  // Two-paned mode.
544                                                 // Enable the delete menu item.
545                                                 deleteMenuItem.setEnabled(true);
546
547                                                 // Get the current theme status.
548                                                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
549
550                                                 // Set the delete menu item icon according to the theme.
551                                                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
552                                                     deleteMenuItem.setIcon(R.drawable.delete_night);
553                                                 } else {
554                                                     deleteMenuItem.setIcon(R.drawable.delete_day);
555                                                 }
556                                             } else {  // Single-paned mode.
557                                                 // Show the delete menu item.
558                                                 deleteMenuItem.setVisible(true);
559                                             }
560
561                                             // Reset the dismissing snackbar tracker.
562                                             dismissingSnackbar = false;
563                                         };
564
565                                         // Enable the delete menu icon after 100 milliseconds to make sure that the previous domain has been deleted from the database.
566                                         Handler handler = new Handler();
567                                         handler.postDelayed(enableDeleteMenuItemRunnable, 100);
568                                     }
569
570                                     // Close the activity if back was pressed.
571                                     if (closeActivityAfterDismissingSnackbar) {
572                                         // Go home.
573                                         NavUtils.navigateUpFromSameTask(activity);
574                                     }
575                                 }
576                             }
577                         });
578
579                 // Show the Snackbar.
580                 undoDeleteSnackbar.show();
581                 break;
582         }
583
584         // Consume the event.
585         return true;
586     }
587
588     @Override
589     protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
590         // Run the default commands.
591         super.onSaveInstanceState(savedInstanceState);
592
593         // Get a handle for the domain settings scrollview.
594         ScrollView domainSettingsScrollView = findViewById(R.id.domain_settings_scrollview);
595
596         // Check to see if the domain settings scrollview exists.
597         if (domainSettingsScrollView == null) {  // The domain settings are not displayed.
598             // Store the domain settings status in the bundle.
599             savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, false);
600             savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, -1);
601             savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, 0);
602         } else {  // The domain settings are displayed.
603             // Save any changes that have been made to the domain settings.
604             saveDomainSettings(coordinatorLayout, resources);
605
606             // Get the domain settings scroll Y.
607             int domainSettingsScrollY = domainSettingsScrollView.getScrollY();
608
609             // Store the domain settings status in the bundle.
610             savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, true);
611             savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, DomainSettingsFragment.databaseId);
612             savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, domainSettingsScrollY);
613         }
614
615         // Check to see if the domains listview exists.
616         if (domainsListView != null) {
617             // Get the domains listview position.
618             int domainsListViewPosition = domainsListView.getFirstVisiblePosition();
619
620             // Store the listview position in the bundle.
621             savedInstanceState.putInt(LISTVIEW_POSITION, domainsListViewPosition);
622         }
623     }
624
625     // Control what the navigation bar back button does.
626     @Override
627     public void onBackPressed() {
628         // Get a handle for the fragment manager.
629         FragmentManager fragmentManager = getSupportFragmentManager();
630
631         if (twoPanedMode) {  // The device is in two-paned mode.
632             // Save the current domain settings if the domain settings fragment is displayed.
633             if (findViewById(R.id.domain_settings_scrollview) != null) {
634                 saveDomainSettings(coordinatorLayout, resources);
635             }
636
637             // Dismiss the undo delete SnackBar if it is shown.
638             if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
639                 // Set the close flag.
640                 closeActivityAfterDismissingSnackbar = true;
641
642                 // Dismiss the snackbar.
643                 undoDeleteSnackbar.dismiss();
644             } else {
645                 // Pass `onBackPressed()` to the system.
646                 super.onBackPressed();
647             }
648         } else if (closeOnBack) {  // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
649             // Save the current domain settings.
650             saveDomainSettings(coordinatorLayout, resources);
651
652             // Pass `onBackPressed()` to the system.
653             super.onBackPressed();
654         } else if (findViewById(R.id.domain_settings_scrollview) != null) {  // The device is in single-paned mode and domain settings fragment is displayed.
655             // Save the current domain settings.
656             saveDomainSettings(coordinatorLayout, resources);
657
658             // Display the domains list fragment.
659             DomainsListFragment domainsListFragment = new DomainsListFragment();
660             fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
661             fragmentManager.executePendingTransactions();
662
663             // Populate the list of domains.  `-1` highlights the first domain if in two-paned mode.  It has no effect in single-paned mode.
664             populateDomainsListView(-1, domainsListViewPosition);
665
666             // Show the add domain floating action button.
667             addDomainFAB.show();
668
669             // Hide the delete menu item.
670             deleteMenuItem.setVisible(false);
671         } else {  // The device is in single-paned mode and the domain list fragment is displayed.
672             // Dismiss the undo delete SnackBar if it is shown.
673             if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
674                 // Set the close flag.
675                 closeActivityAfterDismissingSnackbar = true;
676
677                 // Dismiss the snackbar.
678                 undoDeleteSnackbar.dismiss();
679             } else {
680                 // Pass `onBackPressed()` to the system.
681                 super.onBackPressed();
682             }
683         }
684     }
685
686     @Override
687     public void onAddDomain(@NonNull DialogFragment dialogFragment) {
688         // Dismiss the undo delete snackbar if it is currently displayed.
689         if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
690             undoDeleteSnackbar.dismiss();
691         }
692
693         // Remove the incorrect lint warning below that the dialog might be null.
694         assert dialogFragment.getDialog() != null;
695
696         // Get a handle for the domain name edit text.
697         EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
698
699         // Get the domain name string.
700         String domainNameString = domainNameEditText.getText().toString();
701
702         // Create the domain and store the database ID in `currentDomainDatabaseId`.
703         currentDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
704
705         // Display the newly created domain.
706         if (twoPanedMode) {  // The device in in two-paned mode.
707             populateDomainsListView(currentDomainDatabaseId, 0);
708         } else {  // The device is in single-paned mode.
709             // Hide the add domain floating action button.
710             addDomainFAB.hide();
711
712             // Show and enable the delete menu item.
713             DomainsActivity.deleteMenuItem.setVisible(true);
714
715             // Create an arguments bundle.
716             Bundle argumentsBundle = new Bundle();
717
718             // Add the domain settings to the arguments bundle.  The scroll Y should always be `0` on a new domain.
719             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
720             argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, 0);
721
722             // Instantiate a new domain settings fragment.
723             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
724
725             // Add the arguments bundle to the domain setting fragment.
726             domainSettingsFragment.setArguments(argumentsBundle);
727
728             // Display the domain settings fragment.
729             getSupportFragmentManager().beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
730         }
731     }
732
733     public void saveDomainSettings(View view, Resources resources) {
734         // Get handles for the domain settings.
735         EditText domainNameEditText = view.findViewById(R.id.domain_settings_name_edittext);
736         SwitchCompat javaScriptSwitch = view.findViewById(R.id.javascript_switch);
737         SwitchCompat firstPartyCookiesSwitch = view.findViewById(R.id.first_party_cookies_switch);
738         SwitchCompat thirdPartyCookiesSwitch = view.findViewById(R.id.third_party_cookies_switch);
739         SwitchCompat domStorageSwitch = view.findViewById(R.id.dom_storage_switch);
740         SwitchCompat formDataSwitch = view.findViewById(R.id.form_data_switch);  // Form data can be removed once the minimum API >= 26.
741         SwitchCompat easyListSwitch = view.findViewById(R.id.easylist_switch);
742         SwitchCompat easyPrivacySwitch = view.findViewById(R.id.easyprivacy_switch);
743         SwitchCompat fanboysAnnoyanceSwitch = view.findViewById(R.id.fanboys_annoyance_list_switch);
744         SwitchCompat fanboysSocialBlockingSwitch = view.findViewById(R.id.fanboys_social_blocking_list_switch);
745         SwitchCompat ultraListSwitch = view.findViewById(R.id.ultralist_switch);
746         SwitchCompat ultraPrivacySwitch = view.findViewById(R.id.ultraprivacy_switch);
747         SwitchCompat blockAllThirdPartyRequestsSwitch = view.findViewById(R.id.block_all_third_party_requests_switch);
748         Spinner userAgentSpinner = view.findViewById(R.id.user_agent_spinner);
749         EditText customUserAgentEditText = view.findViewById(R.id.custom_user_agent_edittext);
750         Spinner fontSizeSpinner = view.findViewById(R.id.font_size_spinner);
751         EditText customFontSizeEditText = view.findViewById(R.id.custom_font_size_edittext);
752         Spinner swipeToRefreshSpinner = view.findViewById(R.id.swipe_to_refresh_spinner);
753         Spinner webViewThemeSpinner = view.findViewById(R.id.webview_theme_spinner);
754         Spinner wideViewportSpinner = view.findViewById(R.id.wide_viewport_spinner);
755         Spinner displayWebpageImagesSpinner = view.findViewById(R.id.display_webpage_images_spinner);
756         SwitchCompat pinnedSslCertificateSwitch = view.findViewById(R.id.pinned_ssl_certificate_switch);
757         RadioButton currentWebsiteCertificateRadioButton = view.findViewById(R.id.current_website_certificate_radiobutton);
758         SwitchCompat pinnedIpAddressesSwitch = view.findViewById(R.id.pinned_ip_addresses_switch);
759         RadioButton currentIpAddressesRadioButton = view.findViewById(R.id.current_ip_addresses_radiobutton);
760
761         // Extract the data for the domain settings.
762         String domainNameString = domainNameEditText.getText().toString();
763         boolean javaScript = javaScriptSwitch.isChecked();
764         boolean firstPartyCookies = firstPartyCookiesSwitch.isChecked();
765         boolean thirdPartyCookies = thirdPartyCookiesSwitch.isChecked();
766         boolean domStorage  = domStorageSwitch.isChecked();
767         boolean formData = formDataSwitch.isChecked();  // Form data can be removed once the minimum API >= 26.
768         boolean easyList = easyListSwitch.isChecked();
769         boolean easyPrivacy = easyPrivacySwitch.isChecked();
770         boolean fanboysAnnoyance = fanboysAnnoyanceSwitch.isChecked();
771         boolean fanboysSocialBlocking = fanboysSocialBlockingSwitch.isChecked();
772         boolean ultraList = ultraListSwitch.isChecked();
773         boolean ultraPrivacy = ultraPrivacySwitch.isChecked();
774         boolean blockAllThirdPartyRequests = blockAllThirdPartyRequestsSwitch.isChecked();
775         int userAgentSwitchPosition = userAgentSpinner.getSelectedItemPosition();
776         int fontSizeSwitchPosition = fontSizeSpinner.getSelectedItemPosition();
777         int swipeToRefreshInt = swipeToRefreshSpinner.getSelectedItemPosition();
778         int webViewThemeInt = webViewThemeSpinner.getSelectedItemPosition();
779         int wideViewportInt = wideViewportSpinner.getSelectedItemPosition();
780         int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
781         boolean pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked();
782         boolean pinnedIpAddress = pinnedIpAddressesSwitch.isChecked();
783
784         // Initialize the user agent name string.
785         String userAgentName;
786
787         // Set the user agent name.
788         switch (userAgentSwitchPosition) {
789             case MainWebViewActivity.DOMAINS_SYSTEM_DEFAULT_USER_AGENT:
790                 // Set the user agent name to be `System default user agent`.
791                 userAgentName = resources.getString(R.string.system_default_user_agent);
792                 break;
793
794             case MainWebViewActivity.DOMAINS_CUSTOM_USER_AGENT:
795                 // Set the user agent name to be the custom user agent.
796                 userAgentName = customUserAgentEditText.getText().toString();
797                 break;
798
799             default:
800                 // Get the array of user agent names.
801                 String[] userAgentNameArray = resources.getStringArray(R.array.user_agent_names);
802
803                 // 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.
804                 userAgentName = userAgentNameArray[userAgentSwitchPosition - 1];
805         }
806
807         // Initialize the font size integer.  `0` indicates the system default font size.
808         int fontSizeInt = 0;
809
810         // Use a custom font size if it is selected.
811         if (fontSizeSwitchPosition == 1) {  // A custom font size is specified.
812             // Get the custom font size from the edit text.
813             fontSizeInt = Integer.parseInt(customFontSizeEditText.getText().toString());
814         }
815
816         // Save the domain settings.
817         domainsDatabaseHelper.updateDomain(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScript, firstPartyCookies, thirdPartyCookies, domStorage, formData, easyList, easyPrivacy,
818                 fanboysAnnoyance, fanboysSocialBlocking, ultraList, ultraPrivacy, blockAllThirdPartyRequests, userAgentName, fontSizeInt, swipeToRefreshInt, webViewThemeInt, wideViewportInt,
819                 displayWebpageImagesInt, pinnedSslCertificate, pinnedIpAddress);
820
821         // Update the pinned SSL certificate if a new one is checked.
822         if (currentWebsiteCertificateRadioButton.isChecked()) {
823             // Update the database.
824             domainsDatabaseHelper.updatePinnedSslCertificate(currentDomainDatabaseId, sslIssuedToCName, sslIssuedToOName, sslIssuedToUName, sslIssuedByCName, sslIssuedByOName, sslIssuedByUName,
825                     sslStartDateLong, sslEndDateLong);
826         }
827
828         // Update the pinned IP addresses if new ones are checked.
829         if (currentIpAddressesRadioButton.isChecked()) {
830             // Update the database.
831             domainsDatabaseHelper.updatePinnedIpAddresses(currentDomainDatabaseId, currentIpAddresses);
832         }
833     }
834
835     private void populateDomainsListView(final int highlightedDomainDatabaseId, int domainsListViewPosition) {
836         // get a handle for the current `domains_listview`.
837         domainsListView = findViewById(R.id.domains_listview);
838
839         // Get a `Cursor` with the current contents of the domains database.
840         Cursor domainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
841
842         // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
843         CursorAdapter domainsCursorAdapter = new CursorAdapter(getApplicationContext(), domainsCursor, false) {
844             @Override
845             public View newView(Context context, Cursor cursor, ViewGroup parent) {
846                 // Inflate the individual item layout.  `false` does not attach it to the root.
847                 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
848             }
849
850             @Override
851             public void bindView(View view, Context context, Cursor cursor) {
852                 // Set the domain name.
853                 String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
854                 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
855                 domainNameTextView.setText(domainNameString);
856             }
857         };
858
859         // Update the list view.
860         domainsListView.setAdapter(domainsCursorAdapter);
861
862         // Restore the scroll position.
863         domainsListView.setSelection(domainsListViewPosition);
864
865         // Display the domain settings in the second pane if operating in two pane mode and the database contains at least one domain.
866         if (DomainsActivity.twoPanedMode && (domainsCursor.getCount() > 0)) {  // Two-paned mode is enabled and there is at least one domain.
867             // Initialize `highlightedDomainPosition`.
868             int highlightedDomainPosition = 0;
869
870             // Get the cursor position for the highlighted domain.
871             for (int i = 0; i < domainsCursor.getCount(); i++) {
872                 // Move to position `i` in the cursor.
873                 domainsCursor.moveToPosition(i);
874
875                 // Get the database ID for this position.
876                 int currentDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
877
878                 // Set `highlightedDomainPosition` if the database ID for this matches `highlightedDomainDatabaseId`.
879                 if (highlightedDomainDatabaseId == currentDatabaseId) {
880                     highlightedDomainPosition = i;
881                 }
882             }
883
884             // Select the highlighted domain.
885             domainsListView.setItemChecked(highlightedDomainPosition, true);
886
887             // Get the database ID for the highlighted domain.
888             domainsCursor.moveToPosition(highlightedDomainPosition);
889             currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
890
891             // Create an arguments bundle.
892             Bundle argumentsBundle = new Bundle();
893
894             // Store the domain settings in the arguments bundle.
895             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
896             argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
897
898             // Instantiate a new domain settings fragment.
899             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
900
901             // Add the arguments bundle to the domain settings fragment.
902             domainSettingsFragment.setArguments(argumentsBundle);
903
904             // Display the domain settings fragment.
905             getSupportFragmentManager().beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
906
907             // Enable the delete options menu items.
908             deleteMenuItem.setEnabled(true);
909
910             // Get the current theme status.
911             int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
912
913             // Set the delete icon according to the theme.
914             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
915                 deleteMenuItem.setIcon(R.drawable.delete_night);
916             } else {
917                 deleteMenuItem.setIcon(R.drawable.delete_day);
918             }
919         } else if (twoPanedMode) {  // Two-paned mode is enabled but there are no domains.
920             // Disable the options `MenuItems`.
921             deleteMenuItem.setEnabled(false);
922             deleteMenuItem.setIcon(R.drawable.delete_disabled);
923         }
924     }
925
926     @Override
927     public void dismissSnackbar() {
928         // Dismiss the undo delete snackbar if it is shown.
929         if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
930             // Dismiss the snackbar.
931             undoDeleteSnackbar.dismiss();
932         }
933     }
934
935     @Override
936     public void onDestroy() {
937         // Close the domains database helper.
938         domainsDatabaseHelper.close();
939
940         // Run the default commands.
941         super.onDestroy();
942     }
943 }