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