]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java
Remove third-party cookies. https://redmine.stoutner.com/issues/601
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / DomainsActivity.java
1 /*
2  * Copyright © 2017-2021 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         // Run the command according to the selected menu item.
339         if (menuItemId == android.R.id.home) {  // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
340             // Check if the device is in two-paned mode.
341             if (twoPanedMode) {  // The device is in two-paned mode.
342                 // Save the current domain settings if the domain settings fragment is displayed.
343                 if (findViewById(R.id.domain_settings_scrollview) != null) {
344                     saveDomainSettings(coordinatorLayout, resources);
345                 }
346
347                 // Dismiss the undo delete snackbar if it is shown.
348                 if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
349                     // Set the close flag.
350                     closeActivityAfterDismissingSnackbar = true;
351
352                     // Dismiss the snackbar.
353                     undoDeleteSnackbar.dismiss();
354                 } else {
355                     // Go home.
356                     NavUtils.navigateUpFromSameTask(this);
357                 }
358             } else if (closeOnBack) {  // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
359                 // Save the current domain settings.
360                 saveDomainSettings(coordinatorLayout, resources);
361
362                 // Go home.
363                 NavUtils.navigateUpFromSameTask(this);
364             } else if (findViewById(R.id.domain_settings_scrollview) != null) {  // The device is in single-paned mode and the domain settings fragment is displayed.
365                 // Save the current domain settings.
366                 saveDomainSettings(coordinatorLayout, resources);
367
368                 // Display the domains list fragment.
369                 DomainsListFragment domainsListFragment = new DomainsListFragment();
370                 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
371                 fragmentManager.executePendingTransactions();
372
373                 // Populate the list of domains.  `-1` highlights the first domain if in two-paned mode.  It has no effect in single-paned mode.
374                 populateDomainsListView(-1, domainsListViewPosition);
375
376                 // Show the add domain floating action button.
377                 addDomainFAB.show();
378
379                 // Hide the delete menu item.
380                 deleteMenuItem.setVisible(false);
381             } else {  // The device is in single-paned mode and domains list fragment is displayed.
382                 // Dismiss the undo delete `SnackBar` if it is shown.
383                 if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
384                     // Set the close flag.
385                     closeActivityAfterDismissingSnackbar = true;
386
387                     // Dismiss the snackbar.
388                     undoDeleteSnackbar.dismiss();
389                 } else {
390                     // Go home.
391                     NavUtils.navigateUpFromSameTask(this);
392                 }
393             }
394         } else if (menuItemId == R.id.delete_domain) {  // Delete.
395             // Get a handle for the activity.
396             Activity activity = this;
397
398             // Check to see if the domain settings were loaded directly for editing of this app in single-paned mode.
399             if (closeOnBack && !twoPanedMode) {  // The activity should delete the domain settings and exit straight to the the main WebView activity.
400                 // Delete the selected domain.
401                 domainsDatabaseHelper.deleteDomain(currentDomainDatabaseId);
402
403                 // Go home.
404                 NavUtils.navigateUpFromSameTask(activity);
405             } else {  // A snackbar should be shown before deleting the domain settings.
406                 // Reset close-on-back, which otherwise can cause errors if the system attempts to save settings for a domain that no longer exists.
407                 closeOnBack = false;
408
409                 // Store a copy of the current domain database ID because it could change while the snackbar is displayed.
410                 final int databaseIdToDelete = currentDomainDatabaseId;
411
412                 // Update the fragments and menu items.
413                 if (twoPanedMode) {  // Two-paned mode.
414                     // Store the deleted domain position, which is needed if undo is selected in the snackbar.
415                     deletedDomainPosition = domainsListView.getCheckedItemPosition();
416
417                     // Disable the options menu items.
418                     deleteMenuItem.setEnabled(false);
419                     deleteMenuItem.setIcon(R.drawable.delete_disabled);
420
421                     // Remove the domain settings fragment.
422                     fragmentManager.beginTransaction().remove(Objects.requireNonNull(fragmentManager.findFragmentById(R.id.domain_settings_fragment_container))).commit();
423                 } else {  // Single-paned mode.
424                     // Display the domains list fragment.
425                     DomainsListFragment domainsListFragment = new DomainsListFragment();
426                     fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
427                     fragmentManager.executePendingTransactions();
428
429                     // Show the add domain floating action button.
430                     addDomainFAB.show();
431
432                     // Hide `deleteMenuItem`.
433                     deleteMenuItem.setVisible(false);
434                 }
435
436                 // Get a cursor that does not show the domain to be deleted.
437                 Cursor domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete);
438
439                 // Setup the domains pending delete cursor adapter.
440                 CursorAdapter domainsPendingDeleteCursorAdapter = new CursorAdapter(this, domainsPendingDeleteCursor, false) {
441                     @Override
442                     public View newView(Context context, Cursor cursor, ViewGroup parent) {
443                         // Inflate the individual item layout.
444                         return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
445                     }
446
447                     @Override
448                     public void bindView(View view, Context context, Cursor cursor) {
449                         // Get the domain name string.
450                         String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
451
452                         // Get a handle for the domain name text view.
453                         TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
454
455                         // Display the domain name.
456                         domainNameTextView.setText(domainNameString);
457                     }
458                 };
459
460                 // Update the handle for the current domains list view.
461                 domainsListView = findViewById(R.id.domains_listview);
462
463                 // Update the list view.
464                 domainsListView.setAdapter(domainsPendingDeleteCursorAdapter);
465
466                 // Display a snackbar.
467                 undoDeleteSnackbar = Snackbar.make(domainsListView, R.string.domain_deleted, Snackbar.LENGTH_LONG)
468                         .setAction(R.string.undo, (View v) -> {
469                             // Do nothing because everything will be handled by `onDismissed()` below.
470                         })
471                         .addCallback(new Snackbar.Callback() {
472                             @Override
473                             public void onDismissed(Snackbar snackbar, int event) {
474                                 // Run commands based on the event.
475                                 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The user pushed the `Undo` button.
476                                     // Create an arguments bundle.
477                                     Bundle argumentsBundle = new Bundle();
478
479                                     // Store the domains settings in the arguments bundle.
480                                     argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete);
481                                     argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
482
483                                     // Instantiate a new domain settings fragment.
484                                     DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
485
486                                     // Add the arguments bundle to the domain settings fragment.
487                                     domainSettingsFragment.setArguments(argumentsBundle);
488
489                                     // Display the correct fragments.
490                                     if (twoPanedMode) {  // The device in in two-paned mode.
491                                         // Get a cursor with the current contents of the domains database.
492                                         Cursor undoDeleteDomainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
493
494                                         // Setup the domains cursor adapter.
495                                         CursorAdapter undoDeleteDomainsCursorAdapter = new CursorAdapter(getApplicationContext(), undoDeleteDomainsCursor, false) {
496                                             @Override
497                                             public View newView(Context context, Cursor cursor, ViewGroup parent) {
498                                                 // Inflate the individual item layout.
499                                                 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
500                                             }
501
502                                             @Override
503                                             public void bindView(View view, Context context, Cursor cursor) {
504                                                 /// Get the domain name string.
505                                                 String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
506
507                                                 // Get a handle for the domain name text view.
508                                                 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
509
510                                                 // Display the domain name.
511                                                 domainNameTextView.setText(domainNameString);
512                                             }
513                                         };
514
515                                         // Update the domains list view.
516                                         domainsListView.setAdapter(undoDeleteDomainsCursorAdapter);
517
518                                         // Select the previously deleted domain in the list view.
519                                         domainsListView.setItemChecked(deletedDomainPosition, true);
520
521                                         // Display the domain settings fragment.
522                                         fragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
523
524                                         // Enable the options delete menu item.
525                                         deleteMenuItem.setEnabled(true);
526
527                                         // Get the current theme status.
528                                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
529
530                                         // Set the delete menu item icon according to the theme.
531                                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
532                                             deleteMenuItem.setIcon(R.drawable.delete_night);
533                                         } else {
534                                             deleteMenuItem.setIcon(R.drawable.delete_day);
535                                         }
536                                     } else {  // The device in in one-paned mode.
537                                         // Display the domain settings fragment.
538                                         fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
539
540                                         // Hide the add domain floating action button.
541                                         addDomainFAB.hide();
542
543                                         // Show and enable the delete menu item.
544                                         deleteMenuItem.setVisible(true);
545
546                                         // Display the domain settings fragment.
547                                         fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
548                                     }
549                                 } else {  // The snackbar was dismissed without the undo button being pushed.
550                                     // Delete the selected domain.
551                                     domainsDatabaseHelper.deleteDomain(databaseIdToDelete);
552
553                                     // Enable the delete menu item if the system was waiting for a snackbar to be dismissed.
554                                     if (dismissingSnackbar) {
555                                         // Create a `Runnable` to enable the delete menu item.
556                                         Runnable enableDeleteMenuItemRunnable = () -> {
557                                             // Enable the delete menu item according to the display mode.
558                                             if (twoPanedMode) {  // Two-paned mode.
559                                                 // Enable the delete menu item.
560                                                 deleteMenuItem.setEnabled(true);
561
562                                                 // Get the current theme status.
563                                                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
564
565                                                 // Set the delete menu item icon according to the theme.
566                                                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
567                                                     deleteMenuItem.setIcon(R.drawable.delete_night);
568                                                 } else {
569                                                     deleteMenuItem.setIcon(R.drawable.delete_day);
570                                                 }
571                                             } else {  // Single-paned mode.
572                                                 // Show the delete menu item.
573                                                 deleteMenuItem.setVisible(true);
574                                             }
575
576                                             // Reset the dismissing snackbar tracker.
577                                             dismissingSnackbar = false;
578                                         };
579
580                                         // Enable the delete menu icon after 100 milliseconds to make sure that the previous domain has been deleted from the database.
581                                         Handler handler = new Handler();
582                                         handler.postDelayed(enableDeleteMenuItemRunnable, 100);
583                                     }
584
585                                     // Close the activity if back was pressed.
586                                     if (closeActivityAfterDismissingSnackbar) {
587                                         // Go home.
588                                         NavUtils.navigateUpFromSameTask(activity);
589                                     }
590                                 }
591                             }
592                         });
593
594                 // Show the Snackbar.
595                 undoDeleteSnackbar.show();
596             }
597         }
598
599         // Consume the event.
600         return true;
601     }
602
603     @Override
604     protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
605         // Run the default commands.
606         super.onSaveInstanceState(savedInstanceState);
607
608         // Get a handle for the domain settings scrollview.
609         ScrollView domainSettingsScrollView = findViewById(R.id.domain_settings_scrollview);
610
611         // Check to see if the domain settings scrollview exists.
612         if (domainSettingsScrollView == null) {  // The domain settings are not displayed.
613             // Store the domain settings status in the bundle.
614             savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, false);
615             savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, -1);
616             savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, 0);
617         } else {  // The domain settings are displayed.
618             // Save any changes that have been made to the domain settings.
619             saveDomainSettings(coordinatorLayout, resources);
620
621             // Get the domain settings scroll Y.
622             int domainSettingsScrollY = domainSettingsScrollView.getScrollY();
623
624             // Store the domain settings status in the bundle.
625             savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, true);
626             savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, DomainSettingsFragment.databaseId);
627             savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, domainSettingsScrollY);
628         }
629
630         // Check to see if the domains listview exists.
631         if (domainsListView != null) {
632             // Get the domains listview position.
633             int domainsListViewPosition = domainsListView.getFirstVisiblePosition();
634
635             // Store the listview position in the bundle.
636             savedInstanceState.putInt(LISTVIEW_POSITION, domainsListViewPosition);
637         }
638     }
639
640     // Control what the navigation bar back button does.
641     @Override
642     public void onBackPressed() {
643         // Get a handle for the fragment manager.
644         FragmentManager fragmentManager = getSupportFragmentManager();
645
646         if (twoPanedMode) {  // The device is in two-paned mode.
647             // Save the current domain settings if the domain settings fragment is displayed.
648             if (findViewById(R.id.domain_settings_scrollview) != null) {
649                 saveDomainSettings(coordinatorLayout, resources);
650             }
651
652             // Dismiss the undo delete SnackBar if it is shown.
653             if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
654                 // Set the close flag.
655                 closeActivityAfterDismissingSnackbar = true;
656
657                 // Dismiss the snackbar.
658                 undoDeleteSnackbar.dismiss();
659             } else {
660                 // Pass `onBackPressed()` to the system.
661                 super.onBackPressed();
662             }
663         } else if (closeOnBack) {  // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
664             // Save the current domain settings.
665             saveDomainSettings(coordinatorLayout, resources);
666
667             // Pass `onBackPressed()` to the system.
668             super.onBackPressed();
669         } else if (findViewById(R.id.domain_settings_scrollview) != null) {  // The device is in single-paned mode and domain settings fragment is displayed.
670             // Save the current domain settings.
671             saveDomainSettings(coordinatorLayout, resources);
672
673             // Display the domains list fragment.
674             DomainsListFragment domainsListFragment = new DomainsListFragment();
675             fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
676             fragmentManager.executePendingTransactions();
677
678             // Populate the list of domains.  `-1` highlights the first domain if in two-paned mode.  It has no effect in single-paned mode.
679             populateDomainsListView(-1, domainsListViewPosition);
680
681             // Show the add domain floating action button.
682             addDomainFAB.show();
683
684             // Hide the delete menu item.
685             deleteMenuItem.setVisible(false);
686         } else {  // The device is in single-paned mode and the domain list fragment is displayed.
687             // Dismiss the undo delete SnackBar if it is shown.
688             if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
689                 // Set the close flag.
690                 closeActivityAfterDismissingSnackbar = true;
691
692                 // Dismiss the snackbar.
693                 undoDeleteSnackbar.dismiss();
694             } else {
695                 // Pass `onBackPressed()` to the system.
696                 super.onBackPressed();
697             }
698         }
699     }
700
701     @Override
702     public void onAddDomain(@NonNull DialogFragment dialogFragment) {
703         // Dismiss the undo delete snackbar if it is currently displayed.
704         if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
705             undoDeleteSnackbar.dismiss();
706         }
707
708         // Remove the incorrect lint warning below that the dialog might be null.
709         assert dialogFragment.getDialog() != null;
710
711         // Get a handle for the domain name edit text.
712         EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
713
714         // Get the domain name string.
715         String domainNameString = domainNameEditText.getText().toString();
716
717         // Create the domain and store the database ID in `currentDomainDatabaseId`.
718         currentDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
719
720         // Display the newly created domain.
721         if (twoPanedMode) {  // The device in in two-paned mode.
722             populateDomainsListView(currentDomainDatabaseId, 0);
723         } else {  // The device is in single-paned mode.
724             // Hide the add domain floating action button.
725             addDomainFAB.hide();
726
727             // Show and enable the delete menu item.
728             DomainsActivity.deleteMenuItem.setVisible(true);
729
730             // Create an arguments bundle.
731             Bundle argumentsBundle = new Bundle();
732
733             // Add the domain settings to the arguments bundle.  The scroll Y should always be `0` on a new domain.
734             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
735             argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, 0);
736
737             // Instantiate a new domain settings fragment.
738             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
739
740             // Add the arguments bundle to the domain setting fragment.
741             domainSettingsFragment.setArguments(argumentsBundle);
742
743             // Display the domain settings fragment.
744             getSupportFragmentManager().beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
745         }
746     }
747
748     public void saveDomainSettings(View view, Resources resources) {
749         // Get handles for the domain settings.
750         EditText domainNameEditText = view.findViewById(R.id.domain_settings_name_edittext);
751         SwitchCompat javaScriptSwitch = view.findViewById(R.id.javascript_switch);
752         SwitchCompat cookiesSwitch = view.findViewById(R.id.cookies_switch);
753         SwitchCompat domStorageSwitch = view.findViewById(R.id.dom_storage_switch);
754         SwitchCompat formDataSwitch = view.findViewById(R.id.form_data_switch);  // Form data can be removed once the minimum API >= 26.
755         SwitchCompat easyListSwitch = view.findViewById(R.id.easylist_switch);
756         SwitchCompat easyPrivacySwitch = view.findViewById(R.id.easyprivacy_switch);
757         SwitchCompat fanboysAnnoyanceSwitch = view.findViewById(R.id.fanboys_annoyance_list_switch);
758         SwitchCompat fanboysSocialBlockingSwitch = view.findViewById(R.id.fanboys_social_blocking_list_switch);
759         SwitchCompat ultraListSwitch = view.findViewById(R.id.ultralist_switch);
760         SwitchCompat ultraPrivacySwitch = view.findViewById(R.id.ultraprivacy_switch);
761         SwitchCompat blockAllThirdPartyRequestsSwitch = view.findViewById(R.id.block_all_third_party_requests_switch);
762         Spinner userAgentSpinner = view.findViewById(R.id.user_agent_spinner);
763         EditText customUserAgentEditText = view.findViewById(R.id.custom_user_agent_edittext);
764         Spinner fontSizeSpinner = view.findViewById(R.id.font_size_spinner);
765         EditText customFontSizeEditText = view.findViewById(R.id.custom_font_size_edittext);
766         Spinner swipeToRefreshSpinner = view.findViewById(R.id.swipe_to_refresh_spinner);
767         Spinner webViewThemeSpinner = view.findViewById(R.id.webview_theme_spinner);
768         Spinner wideViewportSpinner = view.findViewById(R.id.wide_viewport_spinner);
769         Spinner displayWebpageImagesSpinner = view.findViewById(R.id.display_webpage_images_spinner);
770         SwitchCompat pinnedSslCertificateSwitch = view.findViewById(R.id.pinned_ssl_certificate_switch);
771         RadioButton currentWebsiteCertificateRadioButton = view.findViewById(R.id.current_website_certificate_radiobutton);
772         SwitchCompat pinnedIpAddressesSwitch = view.findViewById(R.id.pinned_ip_addresses_switch);
773         RadioButton currentIpAddressesRadioButton = view.findViewById(R.id.current_ip_addresses_radiobutton);
774
775         // Extract the data for the domain settings.
776         String domainNameString = domainNameEditText.getText().toString();
777         boolean javaScript = javaScriptSwitch.isChecked();
778         boolean cookies = cookiesSwitch.isChecked();
779         boolean domStorage  = domStorageSwitch.isChecked();
780         boolean formData = formDataSwitch.isChecked();  // Form data can be removed once the minimum API >= 26.
781         boolean easyList = easyListSwitch.isChecked();
782         boolean easyPrivacy = easyPrivacySwitch.isChecked();
783         boolean fanboysAnnoyance = fanboysAnnoyanceSwitch.isChecked();
784         boolean fanboysSocialBlocking = fanboysSocialBlockingSwitch.isChecked();
785         boolean ultraList = ultraListSwitch.isChecked();
786         boolean ultraPrivacy = ultraPrivacySwitch.isChecked();
787         boolean blockAllThirdPartyRequests = blockAllThirdPartyRequestsSwitch.isChecked();
788         int userAgentSwitchPosition = userAgentSpinner.getSelectedItemPosition();
789         int fontSizeSwitchPosition = fontSizeSpinner.getSelectedItemPosition();
790         int swipeToRefreshInt = swipeToRefreshSpinner.getSelectedItemPosition();
791         int webViewThemeInt = webViewThemeSpinner.getSelectedItemPosition();
792         int wideViewportInt = wideViewportSpinner.getSelectedItemPosition();
793         int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
794         boolean pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked();
795         boolean pinnedIpAddress = pinnedIpAddressesSwitch.isChecked();
796
797         // Initialize the user agent name string.
798         String userAgentName;
799
800         // Set the user agent name.
801         switch (userAgentSwitchPosition) {
802             case MainWebViewActivity.DOMAINS_SYSTEM_DEFAULT_USER_AGENT:
803                 // Set the user agent name to be `System default user agent`.
804                 userAgentName = resources.getString(R.string.system_default_user_agent);
805                 break;
806
807             case MainWebViewActivity.DOMAINS_CUSTOM_USER_AGENT:
808                 // Set the user agent name to be the custom user agent.
809                 userAgentName = customUserAgentEditText.getText().toString();
810                 break;
811
812             default:
813                 // Get the array of user agent names.
814                 String[] userAgentNameArray = resources.getStringArray(R.array.user_agent_names);
815
816                 // 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.
817                 userAgentName = userAgentNameArray[userAgentSwitchPosition - 1];
818         }
819
820         // Initialize the font size integer.  `0` indicates the system default font size.
821         int fontSizeInt = 0;
822
823         // Use a custom font size if it is selected.
824         if (fontSizeSwitchPosition == 1) {  // A custom font size is specified.
825             // Get the custom font size from the edit text.
826             fontSizeInt = Integer.parseInt(customFontSizeEditText.getText().toString());
827         }
828
829         // Save the domain settings.
830         domainsDatabaseHelper.updateDomain(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScript, cookies, domStorage, formData, easyList, easyPrivacy,
831                 fanboysAnnoyance, fanboysSocialBlocking, ultraList, ultraPrivacy, blockAllThirdPartyRequests, userAgentName, fontSizeInt, swipeToRefreshInt, webViewThemeInt, wideViewportInt,
832                 displayWebpageImagesInt, pinnedSslCertificate, pinnedIpAddress);
833
834         // Update the pinned SSL certificate if a new one is checked.
835         if (currentWebsiteCertificateRadioButton.isChecked()) {
836             // Update the database.
837             domainsDatabaseHelper.updatePinnedSslCertificate(currentDomainDatabaseId, sslIssuedToCName, sslIssuedToOName, sslIssuedToUName, sslIssuedByCName, sslIssuedByOName, sslIssuedByUName,
838                     sslStartDateLong, sslEndDateLong);
839         }
840
841         // Update the pinned IP addresses if new ones are checked.
842         if (currentIpAddressesRadioButton.isChecked()) {
843             // Update the database.
844             domainsDatabaseHelper.updatePinnedIpAddresses(currentDomainDatabaseId, currentIpAddresses);
845         }
846     }
847
848     private void populateDomainsListView(final int highlightedDomainDatabaseId, int domainsListViewPosition) {
849         // get a handle for the current `domains_listview`.
850         domainsListView = findViewById(R.id.domains_listview);
851
852         // Get a `Cursor` with the current contents of the domains database.
853         Cursor domainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
854
855         // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
856         CursorAdapter domainsCursorAdapter = new CursorAdapter(getApplicationContext(), domainsCursor, false) {
857             @Override
858             public View newView(Context context, Cursor cursor, ViewGroup parent) {
859                 // Inflate the individual item layout.  `false` does not attach it to the root.
860                 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
861             }
862
863             @Override
864             public void bindView(View view, Context context, Cursor cursor) {
865                 // Set the domain name.
866                 String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
867                 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
868                 domainNameTextView.setText(domainNameString);
869             }
870         };
871
872         // Update the list view.
873         domainsListView.setAdapter(domainsCursorAdapter);
874
875         // Restore the scroll position.
876         domainsListView.setSelection(domainsListViewPosition);
877
878         // Display the domain settings in the second pane if operating in two pane mode and the database contains at least one domain.
879         if (DomainsActivity.twoPanedMode && (domainsCursor.getCount() > 0)) {  // Two-paned mode is enabled and there is at least one domain.
880             // Initialize `highlightedDomainPosition`.
881             int highlightedDomainPosition = 0;
882
883             // Get the cursor position for the highlighted domain.
884             for (int i = 0; i < domainsCursor.getCount(); i++) {
885                 // Move to position `i` in the cursor.
886                 domainsCursor.moveToPosition(i);
887
888                 // Get the database ID for this position.
889                 int currentDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
890
891                 // Set `highlightedDomainPosition` if the database ID for this matches `highlightedDomainDatabaseId`.
892                 if (highlightedDomainDatabaseId == currentDatabaseId) {
893                     highlightedDomainPosition = i;
894                 }
895             }
896
897             // Select the highlighted domain.
898             domainsListView.setItemChecked(highlightedDomainPosition, true);
899
900             // Get the database ID for the highlighted domain.
901             domainsCursor.moveToPosition(highlightedDomainPosition);
902             currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
903
904             // Create an arguments bundle.
905             Bundle argumentsBundle = new Bundle();
906
907             // Store the domain settings in the arguments bundle.
908             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
909             argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
910
911             // Instantiate a new domain settings fragment.
912             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
913
914             // Add the arguments bundle to the domain settings fragment.
915             domainSettingsFragment.setArguments(argumentsBundle);
916
917             // Display the domain settings fragment.
918             getSupportFragmentManager().beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
919
920             // Enable the delete options menu items.
921             deleteMenuItem.setEnabled(true);
922
923             // Get the current theme status.
924             int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
925
926             // Set the delete icon according to the theme.
927             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
928                 deleteMenuItem.setIcon(R.drawable.delete_night);
929             } else {
930                 deleteMenuItem.setIcon(R.drawable.delete_day);
931             }
932         } else if (twoPanedMode) {  // Two-paned mode is enabled but there are no domains.
933             // Disable the options `MenuItems`.
934             deleteMenuItem.setEnabled(false);
935             deleteMenuItem.setIcon(R.drawable.delete_disabled);
936         }
937     }
938
939     @Override
940     public void dismissSnackbar() {
941         // Dismiss the undo delete snackbar if it is shown.
942         if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
943             // Dismiss the snackbar.
944             undoDeleteSnackbar.dismiss();
945         }
946     }
947
948     @Override
949     public void onDestroy() {
950         // Close the domains database helper.
951         domainsDatabaseHelper.close();
952
953         // Run the default commands.
954         super.onDestroy();
955     }
956 }