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