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