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);
140 // Run the default commands.
141 super.onCreate(savedInstanceState);
143 // Initialize the domains listview position.
144 domainsListViewPosition = 0;
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);
155 // Get the launching intent
156 Intent intent = getIntent();
158 // Extract the domain to load if there is one. `-1` is the default value.
159 goDirectlyToDatabaseId = intent.getIntExtra("load_domain", -1);
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);
164 // Get the current URL.
165 String currentUrl = intent.getStringExtra("current_url");
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");
180 setContentView(R.layout.domains_bottom_appbar);
182 setContentView(R.layout.domains_top_appbar);
185 // Populate the class variables.
186 coordinatorLayout = findViewById(R.id.domains_coordinatorlayout);
187 resources = getResources();
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);
193 // Get a handle for the action bar.
194 ActionBar actionBar = getSupportActionBar();
196 // Remove the incorrect lint warning that the action bar might be null.
197 assert actionBar != null;
199 // Set the back arrow on the action bar.
200 actionBar.setDisplayHomeAsUpEnabled(true);
202 // Initialize the database handler.
203 domainsDatabaseHelper = new DomainsDatabaseHelper(this);
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);
208 // Get a handle for the add domain floating action button.
209 addDomainFAB = findViewById(R.id.add_domain_fab);
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;
216 // Create an add domain dialog.
217 DialogFragment addDomainDialog = AddDomainDialog.addDomain(currentUrl);
219 // Show the add domain dialog.
220 addDomainDialog.show(getSupportFragmentManager(), resources.getString(R.string.add_domain));
225 public boolean onCreateOptionsMenu(Menu menu) {
227 getMenuInflater().inflate(R.menu.domains_options_menu, menu);
229 // Store `deleteMenuItem` for future use.
230 deleteMenuItem = menu.findItem(R.id.delete_domain);
232 // Only display `deleteMenuItem` (initially) in two-paned mode.
233 deleteMenuItem.setVisible(twoPanedMode);
235 // Get a handle for the fragment manager.
236 FragmentManager fragmentManager = getSupportFragmentManager();
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;
244 // Display `DomainsListFragment`.
245 DomainsListFragment domainsListFragment = new DomainsListFragment();
246 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
247 fragmentManager.executePendingTransactions();
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;
255 // Store the current domain database ID.
256 currentDomainDatabaseId = domainSettingsDatabaseIdBeforeRotate;
258 // Create an arguments bundle.
259 Bundle argumentsBundle = new Bundle();
261 // Add the domain settings arguments.
262 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
263 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
265 // Instantiate a new domain settings fragment.
266 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
268 // Add the arguments bundle to the domain settings fragment.
269 domainSettingsFragment.setArguments(argumentsBundle);
271 // Show the delete menu item.
272 deleteMenuItem.setVisible(true);
274 // Hide the add domain floating action button.
277 // Display the domain settings fragment.
278 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
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;
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();
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();
297 // Add the domain settings to arguments bundle.
298 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, goDirectlyToDatabaseId);
299 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
301 // Instantiate a new domain settings fragment.
302 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
304 // Add the arguments bundle to the domain settings fragment`.
305 domainSettingsFragment.setArguments(argumentsBundle);
307 // Show the delete menu item.
308 deleteMenuItem.setVisible(true);
310 // Hide the add domain floating action button.
313 // Display the domain settings fragment.
314 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
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();
322 // Populate the list of domains. `-1` highlights the first domain.
323 populateDomainsListView(-1, domainsListViewPosition);
332 public boolean onOptionsItemSelected(MenuItem menuItem) {
333 // Get the ID of the menu item that was selected.
334 int menuItemId = menuItem.getItemId();
336 // Get a handle for the fragment manager.
337 FragmentManager fragmentManager = getSupportFragmentManager();
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);
348 // Dismiss the undo delete snackbar if it is shown.
349 if (undoDeleteSnackbar != null && undoDeleteSnackbar.isShown()) {
350 // Set the close flag.
351 closeActivityAfterDismissingSnackbar = true;
353 // Dismiss the snackbar.
354 undoDeleteSnackbar.dismiss();
357 NavUtils.navigateUpFromSameTask(this);
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);
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);
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();
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);
377 // Show the add domain floating action button.
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;
388 // Dismiss the snackbar.
389 undoDeleteSnackbar.dismiss();
392 NavUtils.navigateUpFromSameTask(this);
395 } else if (menuItemId == R.id.delete_domain) { // Delete.
396 // Get a handle for the activity.
397 Activity activity = this;
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);
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.
410 // Store a copy of the current domain database ID because it could change while the snackbar is displayed.
411 final int databaseIdToDelete = currentDomainDatabaseId;
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();
418 // Disable the options menu items.
419 deleteMenuItem.setEnabled(false);
420 deleteMenuItem.setIcon(R.drawable.delete_disabled);
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();
430 // Show the add domain floating action button.
433 // Hide `deleteMenuItem`.
434 deleteMenuItem.setVisible(false);
437 // Get a cursor that does not show the domain to be deleted.
438 Cursor domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete);
440 // Setup the domains pending delete cursor adapter.
441 CursorAdapter domainsPendingDeleteCursorAdapter = new CursorAdapter(this, domainsPendingDeleteCursor, false) {
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);
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));
453 // Get a handle for the domain name text view.
454 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
456 // Display the domain name.
457 domainNameTextView.setText(domainNameString);
461 // Update the handle for the current domains list view.
462 domainsListView = findViewById(R.id.domains_listview);
464 // Update the list view.
465 domainsListView.setAdapter(domainsPendingDeleteCursorAdapter);
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.
472 .addCallback(new Snackbar.Callback() {
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();
480 // Store the domains settings in the arguments bundle.
481 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete);
482 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
484 // Instantiate a new domain settings fragment.
485 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
487 // Add the arguments bundle to the domain settings fragment.
488 domainSettingsFragment.setArguments(argumentsBundle);
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();
495 // Setup the domains cursor adapter.
496 CursorAdapter undoDeleteDomainsCursorAdapter = new CursorAdapter(getApplicationContext(), undoDeleteDomainsCursor, false) {
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);
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));
508 // Get a handle for the domain name text view.
509 TextView domainNameTextView = view.findViewById(R.id.domain_name_textview);
511 // Display the domain name.
512 domainNameTextView.setText(domainNameString);
516 // Update the domains list view.
517 domainsListView.setAdapter(undoDeleteDomainsCursorAdapter);
519 // Select the previously deleted domain in the list view.
520 domainsListView.setItemChecked(deletedDomainPosition, true);
522 // Display the domain settings fragment.
523 fragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
525 // Enable the options delete menu item.
526 deleteMenuItem.setEnabled(true);
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();
534 // Hide the add domain floating action button.
537 // Show and enable the delete menu item.
538 deleteMenuItem.setVisible(true);
540 // Display the domain settings fragment.
541 fragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
543 } else { // The snackbar was dismissed without the undo button being pushed.
544 // Delete the selected domain.
545 domainsDatabaseHelper.deleteDomain(databaseIdToDelete);
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);
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);
563 // Reset the dismissing snackbar tracker.
564 dismissingSnackbar = false;
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);
572 // Close the activity if back was pressed.
573 if (closeActivityAfterDismissingSnackbar) {
575 NavUtils.navigateUpFromSameTask(activity);
581 // Show the Snackbar.
582 undoDeleteSnackbar.show();
586 // Consume the event.
591 protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
592 // Run the default commands.
593 super.onSaveInstanceState(savedInstanceState);
595 // Get a handle for the domain settings scrollview.
596 ScrollView domainSettingsScrollView = findViewById(R.id.domain_settings_scrollview);
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);
608 // Get the domain settings scroll Y.
609 int domainSettingsScrollY = domainSettingsScrollView.getScrollY();
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);
617 // Check to see if the domains listview exists.
618 if (domainsListView != null) {
619 // Get the domains listview position.
620 int domainsListViewPosition = domainsListView.getFirstVisiblePosition();
622 // Store the listview position in the bundle.
623 savedInstanceState.putInt(LISTVIEW_POSITION, domainsListViewPosition);
627 // Control what the navigation bar back button does.
629 public void onBackPressed() {
630 // Get a handle for the fragment manager.
631 FragmentManager fragmentManager = getSupportFragmentManager();
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);
639 // Dismiss the undo delete SnackBar if it is shown.
640 if ((undoDeleteSnackbar != null) && undoDeleteSnackbar.isShown()) {
641 // Set the close flag.
642 closeActivityAfterDismissingSnackbar = true;
644 // Dismiss the snackbar.
645 undoDeleteSnackbar.dismiss();
647 // Pass `onBackPressed()` to the system.
648 super.onBackPressed();
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);
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);
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();
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);
668 // Show the add domain floating action button.
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;
679 // Dismiss the snackbar.
680 undoDeleteSnackbar.dismiss();
682 // Pass `onBackPressed()` to the system.
683 super.onBackPressed();
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();
695 // Remove the incorrect lint warning below that the dialog might be null.
696 assert dialogFragment.getDialog() != null;
698 // Get a handle for the domain name edit text.
699 EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
701 // Get the domain name string.
702 String domainNameString = domainNameEditText.getText().toString();
704 // Create the domain and store the database ID in `currentDomainDatabaseId`.
705 currentDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
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.
714 // Show and enable the delete menu item.
715 DomainsActivity.deleteMenuItem.setVisible(true);
717 // Create an arguments bundle.
718 Bundle argumentsBundle = new Bundle();
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);
724 // Instantiate a new domain settings fragment.
725 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
727 // Add the arguments bundle to the domain setting fragment.
728 domainSettingsFragment.setArguments(argumentsBundle);
730 // Display the domain settings fragment.
731 getSupportFragmentManager().beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
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);
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();
786 // Initialize the user agent name string.
787 String userAgentName;
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);
796 case MainWebViewActivity.DOMAINS_CUSTOM_USER_AGENT:
797 // Set the user agent name to be the custom user agent.
798 userAgentName = customUserAgentEditText.getText().toString();
802 // Get the array of user agent names.
803 String[] userAgentNameArray = resources.getStringArray(R.array.user_agent_names);
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];
809 // Initialize the font size integer. `0` indicates the system default font size.
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());
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);
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);
830 // Update the pinned IP addresses if new ones are checked.
831 if (currentIpAddressesRadioButton.isChecked()) {
832 // Update the database.
833 domainsDatabaseHelper.updatePinnedIpAddresses(currentDomainDatabaseId, currentIpAddresses);
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);
841 // Get a `Cursor` with the current contents of the domains database.
842 Cursor domainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
844 // Setup `domainsCursorAdapter` with `this` context. `false` disables `autoRequery`.
845 CursorAdapter domainsCursorAdapter = new CursorAdapter(getApplicationContext(), domainsCursor, false) {
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);
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);
861 // Update the list view.
862 domainsListView.setAdapter(domainsCursorAdapter);
864 // Restore the scroll position.
865 domainsListView.setSelection(domainsListViewPosition);
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;
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);
877 // Get the database ID for this position.
878 int currentDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID));
880 // Set `highlightedDomainPosition` if the database ID for this matches `highlightedDomainDatabaseId`.
881 if (highlightedDomainDatabaseId == currentDatabaseId) {
882 highlightedDomainPosition = i;
886 // Select the highlighted domain.
887 domainsListView.setItemChecked(highlightedDomainPosition, true);
889 // Get the database ID for the highlighted domain.
890 domainsCursor.moveToPosition(highlightedDomainPosition);
891 currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID));
893 // Create an arguments bundle.
894 Bundle argumentsBundle = new Bundle();
896 // Store the domain settings in the arguments bundle.
897 argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
898 argumentsBundle.putInt(DomainSettingsFragment.SCROLL_Y, domainSettingsScrollY);
900 // Instantiate a new domain settings fragment.
901 DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
903 // Add the arguments bundle to the domain settings fragment.
904 domainSettingsFragment.setArguments(argumentsBundle);
906 // Display the domain settings fragment.
907 getSupportFragmentManager().beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
909 // Enable the delete options menu items.
910 deleteMenuItem.setEnabled(true);
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);
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();
931 public void onDestroy() {
932 // Close the domains database helper.
933 domainsDatabaseHelper.close();
935 // Run the default commands.