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