2 * Copyright © 2017-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.activities;
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;
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;
53 import com.google.android.material.floatingactionbutton.FloatingActionButton;
54 import com.google.android.material.snackbar.Snackbar;
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;
62 import java.util.Objects;
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;
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;
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";
90 // Initialize the class variables.
91 private boolean restartAfterRotate;
92 private boolean domainSettingsDisplayedBeforeRotate;
93 private int domainSettingsDatabaseIdBeforeRotate;
94 private int domainSettingsScrollY = 0;
96 // Defile the class views.
97 private ListView domainsListView;
99 // `closeActivityAfterDismissingSnackbar` is used in `onOptionsItemSelected()`, and `onBackPressed()`.
100 private boolean closeActivityAfterDismissingSnackbar;
102 // The undelete snackbar is used in `onOptionsItemSelected()` and `onBackPressed()`.
103 private Snackbar undoDeleteSnackbar;
105 // `domainsDatabaseHelper` is used in `onCreate()`, `saveDomainSettings()`, and `onDestroy()`.
106 private static DomainsDatabaseHelper domainsDatabaseHelper;
108 // `addDomainFAB` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `onBackPressed()`.
109 private FloatingActionButton addDomainFAB;
111 // `deletedDomainPosition` is used in an inner and outer class in `onOptionsItemSelected()`.
112 private int deletedDomainPosition;
114 // `goDirectlyToDatabaseId` is used in `onCreate()` and `onCreateOptionsMenu()`.
115 private int goDirectlyToDatabaseId;
117 // `closeOnBack` is used in `onCreate()`, `onOptionsItemSelected()` and `onBackPressed()`.
118 private boolean closeOnBack;
120 // `coordinatorLayout` is use in `onCreate()`, `onOptionsItemSelected()`, and `onSaveInstanceState()`.
121 private View coordinatorLayout;
123 // `resources` is used in `onCreate()`, `onOptionsItemSelected()`, and `onSaveInstanceState()`.
124 private Resources resources;
127 protected void onCreate(Bundle savedInstanceState) {
128 // Get a handle for the shared preferences.
129 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
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);
135 // Disable screenshots if not allowed.
136 if (!allowScreenshots) {
137 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
141 setTheme(R.style.PrivacyBrowser);
143 // Run the default commands.
144 super.onCreate(savedInstanceState);
146 // Initialize the domains listview position.
147 domainsListViewPosition = 0;
149 // Extract the values from the saved instance state if it is not null.
150 if (savedInstanceState != null) {
151 domainsListViewPosition = savedInstanceState.getInt(LISTVIEW_POSITION);
152 restartAfterRotate = true;
153 domainSettingsDisplayedBeforeRotate = savedInstanceState.getBoolean(DOMAIN_SETTINGS_DISPLAYED);
154 domainSettingsDatabaseIdBeforeRotate = savedInstanceState.getInt(DOMAIN_SETTINGS_DATABASE_ID);
155 domainSettingsScrollY = savedInstanceState.getInt(DOMAIN_SETTINGS_SCROLL_Y);
158 // Get the launching intent
159 Intent intent = getIntent();
161 // Extract the domain to load if there is one. `-1` is the default value.
162 goDirectlyToDatabaseId = intent.getIntExtra("load_domain", -1);
164 // Get the status of close-on-back, which is true when the domains activity is called from the options menu.
165 closeOnBack = intent.getBooleanExtra("close_on_back", false);
167 // Get the current URL.
168 String currentUrl = intent.getStringExtra("current_url");
170 // Store the current SSL certificate information in class variables.
171 sslIssuedToCName = intent.getStringExtra("ssl_issued_to_cname");
172 sslIssuedToOName = intent.getStringExtra("ssl_issued_to_oname");
173 sslIssuedToUName = intent.getStringExtra("ssl_issued_to_uname");
174 sslIssuedByCName = intent.getStringExtra("ssl_issued_by_cname");
175 sslIssuedByOName = intent.getStringExtra("ssl_issued_by_oname");
176 sslIssuedByUName = intent.getStringExtra("ssl_issued_by_uname");
177 sslStartDateLong = intent.getLongExtra("ssl_start_date", 0);
178 sslEndDateLong = intent.getLongExtra("ssl_end_date", 0);
179 currentIpAddresses = intent.getStringExtra("current_ip_addresses");
183 setContentView(R.layout.domains_bottom_appbar);
185 setContentView(R.layout.domains_top_appbar);
188 // Populate the class variables.
189 coordinatorLayout = findViewById(R.id.domains_coordinatorlayout);
190 resources = getResources();
192 // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21.
193 final Toolbar toolbar = findViewById(R.id.domains_toolbar);
194 setSupportActionBar(toolbar);
196 // Get a handle for the action bar.
197 ActionBar actionBar = getSupportActionBar();
199 // Remove the incorrect lint warning that the action bar might be null.
200 assert actionBar != null;
202 // Set the back arrow on the action bar.
203 actionBar.setDisplayHomeAsUpEnabled(true);
205 // Initialize the database handler.
206 domainsDatabaseHelper = new DomainsDatabaseHelper(this);
208 // Determine if we are in two pane mode. `domain_settings_fragment_container` does not exist on devices with a width less than 900dp.
209 twoPanedMode = (findViewById(R.id.domain_settings_fragment_container) != null);
211 // Get a handle for the add domain floating action button.
212 addDomainFAB = findViewById(R.id.add_domain_fab);
214 // Configure the add domain floating action button.
215 addDomainFAB.setOnClickListener((View view) -> {
216 // Remove the incorrect warning below that the current URL might be null.
217 assert currentUrl != null;
219 // Create an add domain dialog.
220 DialogFragment addDomainDialog = AddDomainDialog.addDomain(currentUrl);
222 // Show the add domain dialog.
223 addDomainDialog.show(getSupportFragmentManager(), resources.getString(R.string.add_domain));
228 public boolean onCreateOptionsMenu(Menu menu) {
230 getMenuInflater().inflate(R.menu.domains_options_menu, menu);
232 // Store `deleteMenuItem` for future use.
233 deleteMenuItem = menu.findItem(R.id.delete_domain);
235 // Only display `deleteMenuItem` (initially) in two-paned mode.
236 deleteMenuItem.setVisible(twoPanedMode);
238 // Get a handle for the fragment manager.
239 FragmentManager fragmentManager = getSupportFragmentManager();
241 // Display the fragments. This must be done from `onCreateOptionsMenu()` instead of `onCreate()` because `populateDomainsListView()` needs `deleteMenuItem` to be inflated.
242 if (restartAfterRotate && domainSettingsDisplayedBeforeRotate) { // The device was rotated and domain settings were displayed previously.
243 if (twoPanedMode) { // The device is in two-paned mode.
244 // Reset `restartAfterRotate`.
245 restartAfterRotate = false;
247 // Display `DomainsListFragment`.
248 DomainsListFragment domainsListFragment = new DomainsListFragment();
249 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
250 fragmentManager.executePendingTransactions();
252 // Populate the list of domains. `domainSettingsDatabaseId` highlights the domain that was highlighted before the rotation.
253 populateDomainsListView(domainSettingsDatabaseIdBeforeRotate, domainsListViewPosition);
254 } else { // The device is in single-paned mode.
255 // Reset `restartAfterRotate`.
256 restartAfterRotate = false;
258 // Store the current domain database ID.
259 currentDomainDatabaseId = domainSettingsDatabaseIdBeforeRotate;
261 // Create an arguments bundle.
262 Bundle argumentsBundle = new Bundle();
264 // Add the domain settings arguments.
265 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
266 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
268 // Instantiate a new domain settings fragment.
269 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
271 // Add the arguments bundle to the domain settings fragment.
272 domainSettingsFragment.setArguments(argumentsBundle);
274 // Show the delete menu item.
275 deleteMenuItem.setVisible(true);
277 // Hide the add domain floating action button.
280 // Display the domain settings fragment.
281 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
283 } else { // The device was not rotated or, if it was, domain settings were not displayed previously.
284 if (goDirectlyToDatabaseId >=0) { // Load the indicated domain settings.
285 // Store the current domain database ID.
286 currentDomainDatabaseId = goDirectlyToDatabaseId;
288 if (twoPanedMode) { // The device is in two-paned mode.
289 // Display `DomainsListFragment`.
290 DomainsListFragment domainsListFragment = new DomainsListFragment();
291 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
292 fragmentManager.executePendingTransactions();
294 // Populate the list of domains. `domainSettingsDatabaseId` highlights the domain that was highlighted before the rotation.
295 populateDomainsListView(goDirectlyToDatabaseId, domainsListViewPosition);
296 } else { // The device is in single-paned mode.
297 // Create an arguments bundle.
298 Bundle argumentsBundle = new Bundle();
300 // Add the domain settings to arguments bundle.
301 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, goDirectlyToDatabaseId);
302 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
304 // Instantiate a new domain settings fragment.
305 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
307 // Add the arguments bundle to the domain settings fragment`.
308 domainSettingsFragment.setArguments(argumentsBundle);
310 // Show the delete menu item.
311 deleteMenuItem.setVisible(true);
313 // Hide the add domain floating action button.
316 // Display the domain settings fragment.
317 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
319 } else { // Highlight the first domain.
320 // Display `DomainsListFragment`.
321 DomainsListFragment domainsListFragment = new DomainsListFragment();
322 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
323 fragmentManager.executePendingTransactions();
325 // Populate the list of domains. `-1` highlights the first domain.
326 populateDomainsListView(-1, domainsListViewPosition);
335 public boolean onOptionsItemSelected(MenuItem menuItem) {
336 // Get the ID of the menu item that was selected.
337 int menuItemId = menuItem.getItemId();
339 // Get a handle for the fragment manager.
340 FragmentManager fragmentManager = getSupportFragmentManager();
342 // Run the command according to the selected menu item.
343 if (menuItemId == android.R.id.home) { // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
344 // Check if the device is in two-paned mode.
345 if (twoPanedMode) { // The device is in two-paned mode.
346 // Save the current domain settings if the domain settings fragment is displayed.
347 if (findViewById(R.id.domain_settings_scrollview) != null) {
348 saveDomainSettings(coordinatorLayout, resources);
351 // Dismiss the undo delete snackbar if it is shown.
352 if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
353 // Set the close flag.
354 closeActivityAfterDismissingSnackbar = true;
356 // Dismiss the snackbar.
357 undoDeleteSnackbar.dismiss();
360 NavUtils.navigateUpFromSameTask(this);
362 } else if (closeOnBack) { // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
363 // Save the current domain settings.
364 saveDomainSettings(coordinatorLayout, resources);
367 NavUtils.navigateUpFromSameTask(this);
368 } else if (findViewById(R.id.domain_settings_scrollview) != null) { // The device is in single-paned mode and the domain settings fragment is displayed.
369 // Save the current domain settings.
370 saveDomainSettings(coordinatorLayout, resources);
372 // Display the domains list fragment.
373 DomainsListFragment domainsListFragment = new DomainsListFragment();
374 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
375 fragmentManager.executePendingTransactions();
377 // Populate the list of domains. `-1` highlights the first domain if in two-paned mode. It has no effect in single-paned mode.
378 populateDomainsListView(-1, domainsListViewPosition);
380 // Show the add domain floating action button.
383 // Hide the delete menu item.
384 deleteMenuItem.setVisible(false);
385 } else { // The device is in single-paned mode and domains list fragment is displayed.
386 // Dismiss the undo delete `SnackBar` if it is shown.
387 if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
388 // Set the close flag.
389 closeActivityAfterDismissingSnackbar = true;
391 // Dismiss the snackbar.
392 undoDeleteSnackbar.dismiss();
395 NavUtils.navigateUpFromSameTask(this);
398 } else if (menuItemId == R.id.delete_domain) { // Delete.
399 // Get a handle for the activity.
400 Activity activity = this;
402 // Check to see if the domain settings were loaded directly for editing of this app in single-paned mode.
403 if (closeOnBack && !twoPanedMode) { // The activity should delete the domain settings and exit straight to the the main WebView activity.
404 // Delete the selected domain.
405 domainsDatabaseHelper.deleteDomain(currentDomainDatabaseId);
408 NavUtils.navigateUpFromSameTask(activity);
409 } else { // A snackbar should be shown before deleting the domain settings.
410 // Reset close-on-back, which otherwise can cause errors if the system attempts to save settings for a domain that no longer exists.
413 // Store a copy of the current domain database ID because it could change while the snackbar is displayed.
414 final int databaseIdToDelete = currentDomainDatabaseId;
416 // Update the fragments and menu items.
417 if (twoPanedMode) { // Two-paned mode.
418 // Store the deleted domain position, which is needed if undo is selected in the snackbar.
419 deletedDomainPosition = domainsListView.getCheckedItemPosition();
421 // Disable the options menu items.
422 deleteMenuItem.setEnabled(false);
423 deleteMenuItem.setIcon(R.drawable.delete_disabled);
425 // Remove the domain settings fragment.
426 fragmentManager.beginTransaction().remove(Objects.requireNonNull(fragmentManager.findFragmentById(R.id.domain_settings_fragment_container))).commit();
427 } else { // Single-paned mode.
428 // Display the domains list fragment.
429 DomainsListFragment domainsListFragment = new DomainsListFragment();
430 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
431 fragmentManager.executePendingTransactions();
433 // Show the add domain floating action button.
436 // Hide `deleteMenuItem`.
437 deleteMenuItem.setVisible(false);
440 // Get a cursor that does not show the domain to be deleted.
441 Cursor domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete);
443 // Setup the domains pending delete cursor adapter.
444 CursorAdapter domainsPendingDeleteCursorAdapter = new CursorAdapter(this, domainsPendingDeleteCursor, false) {
446 public View newView(Context context, Cursor cursor, ViewGroup parent) {
447 // Inflate the individual item layout.
448 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
452 public void bindView(View view, Context context, Cursor cursor) {
453 // Get the domain name string.
454 String domainNameString = cursor.getString(cursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME));
456 // Get a handle for the domain name text view.
457 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
459 // Display the domain name.
460 domainNameTextView.setText(domainNameString);
464 // Update the handle for the current domains list view.
465 domainsListView = findViewById(R.id.domains_listview);
467 // Update the list view.
468 domainsListView.setAdapter(domainsPendingDeleteCursorAdapter);
470 // Display a snackbar.
471 undoDeleteSnackbar = Snackbar.make(domainsListView, R.string.domain_deleted, Snackbar.LENGTH_LONG)
472 .setAction(R.string.undo, (View v) -> {
473 // Do nothing because everything will be handled by `onDismissed()` below.
475 .addCallback(new Snackbar.Callback() {
477 public void onDismissed(Snackbar snackbar, int event) {
478 // Run commands based on the event.
479 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) { // The user pushed the `Undo` button.
480 // Create an arguments bundle.
481 Bundle argumentsBundle = new Bundle();
483 // Store the domains settings in the arguments bundle.
484 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete);
485 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
487 // Instantiate a new domain settings fragment.
488 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
490 // Add the arguments bundle to the domain settings fragment.
491 domainSettingsFragment.setArguments(argumentsBundle);
493 // Display the correct fragments.
494 if (twoPanedMode) { // The device in in two-paned mode.
495 // Get a cursor with the current contents of the domains database.
496 Cursor undoDeleteDomainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
498 // Setup the domains cursor adapter.
499 CursorAdapter undoDeleteDomainsCursorAdapter = new CursorAdapter(getApplicationContext(), undoDeleteDomainsCursor, false) {
501 public View newView(Context context, Cursor cursor, ViewGroup parent) {
502 // Inflate the individual item layout.
503 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
507 public void bindView(View view, Context context, Cursor cursor) {
508 /// Get the domain name string.
509 String domainNameString = cursor.getString(cursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME));
511 // Get a handle for the domain name text view.
512 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
514 // Display the domain name.
515 domainNameTextView.setText(domainNameString);
519 // Update the domains list view.
520 domainsListView.setAdapter(undoDeleteDomainsCursorAdapter);
522 // Select the previously deleted domain in the list view.
523 domainsListView.setItemChecked(deletedDomainPosition, true);
525 // Display the domain settings fragment.
526 fragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
528 // Enable the options delete menu item.
529 deleteMenuItem.setEnabled(true);
531 // Set the delete menu item icon.
532 deleteMenuItem.setIcon(R.drawable.delete_enabled);
533 } else { // The device in in one-paned mode.
534 // Display the domain settings fragment.
535 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
537 // Hide the add domain floating action button.
540 // Show and enable the delete menu item.
541 deleteMenuItem.setVisible(true);
543 // Display the domain settings fragment.
544 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
546 } else { // The snackbar was dismissed without the undo button being pushed.
547 // Delete the selected domain.
548 domainsDatabaseHelper.deleteDomain(databaseIdToDelete);
550 // Enable the delete menu item if the system was waiting for a snackbar to be dismissed.
551 if (dismissingSnackbar) {
552 // Create a `Runnable` to enable the delete menu item.
553 Runnable enableDeleteMenuItemRunnable = () -> {
554 // Enable the delete menu item according to the display mode.
555 if (twoPanedMode) { // Two-paned mode.
556 // Enable the delete menu item.
557 deleteMenuItem.setEnabled(true);
559 // Set the delete menu item icon.
560 deleteMenuItem.setIcon(R.drawable.delete_enabled);
561 } else { // Single-paned mode.
562 // Show the delete menu item.
563 deleteMenuItem.setVisible(true);
566 // Reset the dismissing snackbar tracker.
567 dismissingSnackbar = false;
570 // Enable the delete menu icon after 100 milliseconds to make sure that the previous domain has been deleted from the database.
571 Handler handler = new Handler();
572 handler.postDelayed(enableDeleteMenuItemRunnable, 100);
575 // Close the activity if back was pressed.
576 if (closeActivityAfterDismissingSnackbar) {
578 NavUtils.navigateUpFromSameTask(activity);
584 // Show the Snackbar.
585 undoDeleteSnackbar.show();
589 // Consume the event.
594 protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
595 // Run the default commands.
596 super.onSaveInstanceState(savedInstanceState);
598 // Get a handle for the domain settings scrollview.
599 ScrollView domainSettingsScrollView = findViewById(R.id.domain_settings_scrollview);
601 // Check to see if the domain settings scrollview exists.
602 if (domainSettingsScrollView == null) { // The domain settings are not displayed.
603 // Store the domain settings status in the bundle.
604 savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, false);
605 savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, -1);
606 savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, 0);
607 } else { // The domain settings are displayed.
608 // Save any changes that have been made to the domain settings.
609 saveDomainSettings(coordinatorLayout, resources);
611 // Get the domain settings scroll Y.
612 int domainSettingsScrollY = domainSettingsScrollView.getScrollY();
614 // Store the domain settings status in the bundle.
615 savedInstanceState.putBoolean(DOMAIN_SETTINGS_DISPLAYED, true);
616 savedInstanceState.putInt(DOMAIN_SETTINGS_DATABASE_ID, DomainSettingsFragment.databaseId);
617 savedInstanceState.putInt(DOMAIN_SETTINGS_SCROLL_Y, domainSettingsScrollY);
620 // Check to see if the domains listview exists.
621 if (domainsListView != null) {
622 // Get the domains listview position.
623 int domainsListViewPosition = domainsListView.getFirstVisiblePosition();
625 // Store the listview position in the bundle.
626 savedInstanceState.putInt(LISTVIEW_POSITION, domainsListViewPosition);
630 // Control what the navigation bar back button does.
632 public void onBackPressed() {
633 // Get a handle for the fragment manager.
634 FragmentManager fragmentManager = getSupportFragmentManager();
636 if (twoPanedMode) { // The device is in two-paned mode.
637 // Save the current domain settings if the domain settings fragment is displayed.
638 if (findViewById(R.id.domain_settings_scrollview) != null) {
639 saveDomainSettings(coordinatorLayout, resources);
642 // Dismiss the undo delete SnackBar if it is shown.
643 if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
644 // Set the close flag.
645 closeActivityAfterDismissingSnackbar = true;
647 // Dismiss the snackbar.
648 undoDeleteSnackbar.dismiss();
650 // Pass `onBackPressed()` to the system.
651 super.onBackPressed();
653 } else if (closeOnBack) { // Go directly back to the main WebView activity because the domains activity was launched from the options menu.
654 // Save the current domain settings.
655 saveDomainSettings(coordinatorLayout, resources);
657 // Pass `onBackPressed()` to the system.
658 super.onBackPressed();
659 } else if (findViewById(R.id.domain_settings_scrollview) != null) { // The device is in single-paned mode and domain settings fragment is displayed.
660 // Save the current domain settings.
661 saveDomainSettings(coordinatorLayout, resources);
663 // Display the domains list fragment.
664 DomainsListFragment domainsListFragment = new DomainsListFragment();
665 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
666 fragmentManager.executePendingTransactions();
668 // Populate the list of domains. `-1` highlights the first domain if in two-paned mode. It has no effect in single-paned mode.
669 populateDomainsListView(-1, domainsListViewPosition);
671 // Show the add domain floating action button.
674 // Hide the delete menu item.
675 deleteMenuItem.setVisible(false);
676 } else { // The device is in single-paned mode and the domain list fragment is displayed.
677 // Dismiss the undo delete SnackBar if it is shown.
678 if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
679 // Set the close flag.
680 closeActivityAfterDismissingSnackbar = true;
682 // Dismiss the snackbar.
683 undoDeleteSnackbar.dismiss();
685 // Pass `onBackPressed()` to the system.
686 super.onBackPressed();
692 public void onAddDomain(@NonNull DialogFragment dialogFragment) {
693 // Dismiss the undo delete snackbar if it is currently displayed.
694 if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
695 undoDeleteSnackbar.dismiss();
698 // Remove the incorrect lint warning below that the dialog might be null.
699 assert dialogFragment.getDialog() != null;
701 // Get a handle for the domain name edit text.
702 EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
704 // Get the domain name string.
705 String domainNameString = domainNameEditText.getText().toString();
707 // Create the domain and store the database ID in `currentDomainDatabaseId`.
708 currentDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
710 // Display the newly created domain.
711 if (twoPanedMode) { // The device in in two-paned mode.
712 populateDomainsListView(currentDomainDatabaseId, 0);
713 } else { // The device is in single-paned mode.
714 // Hide the add domain floating action button.
717 // Show and enable the delete menu item.
718 DomainsActivity.deleteMenuItem.setVisible(true);
720 // Create an arguments bundle.
721 Bundle argumentsBundle = new Bundle();
723 // Add the domain settings to the arguments bundle. The scroll Y should always be `0` on a new domain.
724 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
725 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, 0);
727 // Instantiate a new domain settings fragment.
728 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
730 // Add the arguments bundle to the domain setting fragment.
731 domainSettingsFragment.setArguments(argumentsBundle);
733 // Display the domain settings fragment.
734 getSupportFragmentManager().beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
738 public void saveDomainSettings(View view, Resources resources) {
739 // Get handles for the domain settings.
740 EditText domainNameEditText = view.findViewById(R.id.domain_settings_name_edittext);
741 SwitchCompat javaScriptSwitch = view.findViewById(R.id.javascript_switch);
742 SwitchCompat cookiesSwitch = view.findViewById(R.id.cookies_switch);
743 SwitchCompat domStorageSwitch = view.findViewById(R.id.dom_storage_switch);
744 SwitchCompat formDataSwitch = view.findViewById(R.id.form_data_switch); // Form data can be removed once the minimum API >= 26.
745 SwitchCompat easyListSwitch = view.findViewById(R.id.easylist_switch);
746 SwitchCompat easyPrivacySwitch = view.findViewById(R.id.easyprivacy_switch);
747 SwitchCompat fanboysAnnoyanceSwitch = view.findViewById(R.id.fanboys_annoyance_list_switch);
748 SwitchCompat fanboysSocialBlockingSwitch = view.findViewById(R.id.fanboys_social_blocking_list_switch);
749 SwitchCompat ultraListSwitch = view.findViewById(R.id.ultralist_switch);
750 SwitchCompat ultraPrivacySwitch = view.findViewById(R.id.ultraprivacy_switch);
751 SwitchCompat blockAllThirdPartyRequestsSwitch = view.findViewById(R.id.block_all_third_party_requests_switch);
752 Spinner userAgentSpinner = view.findViewById(R.id.user_agent_spinner);
753 EditText customUserAgentEditText = view.findViewById(R.id.custom_user_agent_edittext);
754 Spinner fontSizeSpinner = view.findViewById(R.id.font_size_spinner);
755 EditText customFontSizeEditText = view.findViewById(R.id.custom_font_size_edittext);
756 Spinner swipeToRefreshSpinner = view.findViewById(R.id.swipe_to_refresh_spinner);
757 Spinner webViewThemeSpinner = view.findViewById(R.id.webview_theme_spinner);
758 Spinner wideViewportSpinner = view.findViewById(R.id.wide_viewport_spinner);
759 Spinner displayWebpageImagesSpinner = view.findViewById(R.id.display_webpage_images_spinner);
760 SwitchCompat pinnedSslCertificateSwitch = view.findViewById(R.id.pinned_ssl_certificate_switch);
761 RadioButton currentWebsiteCertificateRadioButton = view.findViewById(R.id.current_website_certificate_radiobutton);
762 SwitchCompat pinnedIpAddressesSwitch = view.findViewById(R.id.pinned_ip_addresses_switch);
763 RadioButton currentIpAddressesRadioButton = view.findViewById(R.id.current_ip_addresses_radiobutton);
765 // Extract the data for the domain settings.
766 String domainNameString = domainNameEditText.getText().toString();
767 boolean javaScript = javaScriptSwitch.isChecked();
768 boolean cookies = cookiesSwitch.isChecked();
769 boolean domStorage = domStorageSwitch.isChecked();
770 boolean formData = formDataSwitch.isChecked(); // Form data can be removed once the minimum API >= 26.
771 boolean easyList = easyListSwitch.isChecked();
772 boolean easyPrivacy = easyPrivacySwitch.isChecked();
773 boolean fanboysAnnoyance = fanboysAnnoyanceSwitch.isChecked();
774 boolean fanboysSocialBlocking = fanboysSocialBlockingSwitch.isChecked();
775 boolean ultraList = ultraListSwitch.isChecked();
776 boolean ultraPrivacy = ultraPrivacySwitch.isChecked();
777 boolean blockAllThirdPartyRequests = blockAllThirdPartyRequestsSwitch.isChecked();
778 int userAgentSwitchPosition = userAgentSpinner.getSelectedItemPosition();
779 int fontSizeSwitchPosition = fontSizeSpinner.getSelectedItemPosition();
780 int swipeToRefreshInt = swipeToRefreshSpinner.getSelectedItemPosition();
781 int webViewThemeInt = webViewThemeSpinner.getSelectedItemPosition();
782 int wideViewportInt = wideViewportSpinner.getSelectedItemPosition();
783 int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
784 boolean pinnedSslCertificate = pinnedSslCertificateSwitch.isChecked();
785 boolean pinnedIpAddress = pinnedIpAddressesSwitch.isChecked();
787 // Initialize the user agent name string.
788 String userAgentName;
790 // Set the user agent name.
791 switch (userAgentSwitchPosition) {
792 case MainWebViewActivity.DOMAINS_SYSTEM_DEFAULT_USER_AGENT:
793 // Set the user agent name to be `System default user agent`.
794 userAgentName = resources.getString(R.string.system_default_user_agent);
797 case MainWebViewActivity.DOMAINS_CUSTOM_USER_AGENT:
798 // Set the user agent name to be the custom user agent.
799 userAgentName = customUserAgentEditText.getText().toString();
803 // Get the array of user agent names.
804 String[] userAgentNameArray = resources.getStringArray(R.array.user_agent_names);
806 // Set the user agent name from the array. The domain spinner has one more entry than the name array, so the position must be decremented.
807 userAgentName = userAgentNameArray[userAgentSwitchPosition - 1];
810 // Initialize the font size integer. `0` indicates the system default font size.
813 // Use a custom font size if it is selected.
814 if (fontSizeSwitchPosition == 1) { // A custom font size is specified.
815 // Get the custom font size from the edit text.
816 fontSizeInt = Integer.parseInt(customFontSizeEditText.getText().toString());
819 // Save the domain settings.
820 domainsDatabaseHelper.updateDomain(DomainsActivity.currentDomainDatabaseId, domainNameString, javaScript, cookies, domStorage, formData, easyList, easyPrivacy,
821 fanboysAnnoyance, fanboysSocialBlocking, ultraList, ultraPrivacy, blockAllThirdPartyRequests, userAgentName, fontSizeInt, swipeToRefreshInt, webViewThemeInt, wideViewportInt,
822 displayWebpageImagesInt, pinnedSslCertificate, pinnedIpAddress);
824 // Update the pinned SSL certificate if a new one is checked.
825 if (currentWebsiteCertificateRadioButton.isChecked()) {
826 // Update the database.
827 domainsDatabaseHelper.updatePinnedSslCertificate(currentDomainDatabaseId, sslIssuedToCName, sslIssuedToOName, sslIssuedToUName, sslIssuedByCName, sslIssuedByOName, sslIssuedByUName,
828 sslStartDateLong, sslEndDateLong);
831 // Update the pinned IP addresses if new ones are checked.
832 if (currentIpAddressesRadioButton.isChecked()) {
833 // Update the database.
834 domainsDatabaseHelper.updatePinnedIpAddresses(currentDomainDatabaseId, currentIpAddresses);
838 private void populateDomainsListView(final int highlightedDomainDatabaseId, int domainsListViewPosition) {
839 // get a handle for the current `domains_listview`.
840 domainsListView = findViewById(R.id.domains_listview);
842 // Get a `Cursor` with the current contents of the domains database.
843 Cursor domainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
845 // Setup `domainsCursorAdapter` with `this` context. `false` disables `autoRequery`.
846 CursorAdapter domainsCursorAdapter = new CursorAdapter(getApplicationContext(), domainsCursor, false) {
848 public View newView(Context context, Cursor cursor, ViewGroup parent) {
849 // Inflate the individual item layout. `false` does not attach it to the root.
850 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
854 public void bindView(View view, Context context, Cursor cursor) {
855 // Set the domain name.
856 String domainNameString = cursor.getString(cursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME));
857 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
858 domainNameTextView.setText(domainNameString);
862 // Update the list view.
863 domainsListView.setAdapter(domainsCursorAdapter);
865 // Restore the scroll position.
866 domainsListView.setSelection(domainsListViewPosition);
868 // Display the domain settings in the second pane if operating in two pane mode and the database contains at least one domain.
869 if (DomainsActivity.twoPanedMode && (domainsCursor.getCount() > 0)) { // Two-paned mode is enabled and there is at least one domain.
870 // Initialize `highlightedDomainPosition`.
871 int highlightedDomainPosition = 0;
873 // Get the cursor position for the highlighted domain.
874 for (int i = 0; i < domainsCursor.getCount(); i++) {
875 // Move to position `i` in the cursor.
876 domainsCursor.moveToPosition(i);
878 // Get the database ID for this position.
879 int currentDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID));
881 // Set `highlightedDomainPosition` if the database ID for this matches `highlightedDomainDatabaseId`.
882 if (highlightedDomainDatabaseId == currentDatabaseId) {
883 highlightedDomainPosition = i;
887 // Select the highlighted domain.
888 domainsListView.setItemChecked(highlightedDomainPosition, true);
890 // Get the database ID for the highlighted domain.
891 domainsCursor.moveToPosition(highlightedDomainPosition);
892 currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID));
894 // Create an arguments bundle.
895 Bundle argumentsBundle = new Bundle();
897 // Store the domain settings in the arguments bundle.
898 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
899 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
901 // Instantiate a new domain settings fragment.
902 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
904 // Add the arguments bundle to the domain settings fragment.
905 domainSettingsFragment.setArguments(argumentsBundle);
907 // Display the domain settings fragment.
908 getSupportFragmentManager().beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
910 // Enable the delete options menu items.
911 deleteMenuItem.setEnabled(true);
913 // Set the delete icon.
914 deleteMenuItem.setIcon(R.drawable.delete_enabled);
915 } else if (twoPanedMode) { // Two-paned mode is enabled but there are no domains.
916 // Disable the options `MenuItems`.
917 deleteMenuItem.setEnabled(false);
918 deleteMenuItem.setIcon(R.drawable.delete_disabled);
923 public void dismissSnackbar() {
924 // Dismiss the undo delete snackbar if it is shown.
925 if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
926 // Dismiss the snackbar.
927 undoDeleteSnackbar.dismiss();
932 public void onDestroy() {
933 // Close the domains database helper.
934 domainsDatabaseHelper.close();
936 // Run the default commands.