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