Refactor Domains with one activity and two fragments. https://redmine.stoutner.com...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / DomainsActivity.java
1 /*
2  * Copyright © 2017 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.content.Context;
23 import android.database.Cursor;
24 import android.os.Bundle;
25 import android.support.design.widget.FloatingActionButton;
26 import android.support.design.widget.Snackbar;
27 import android.support.v4.app.FragmentManager;
28 import android.support.v4.app.NavUtils;
29 import android.support.v7.app.ActionBar;
30 import android.support.v7.app.AppCompatActivity;
31 import android.support.v7.app.AppCompatDialogFragment;
32 import android.support.v7.widget.Toolbar;
33 import android.view.Menu;
34 import android.view.MenuItem;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.CursorAdapter;
38 import android.widget.EditText;
39 import android.widget.ListView;
40 import android.widget.Spinner;
41 import android.widget.Switch;
42 import android.widget.TextView;
43
44 import com.stoutner.privacybrowser.R;
45 import com.stoutner.privacybrowser.dialogs.AddDomainDialog;
46 import com.stoutner.privacybrowser.fragments.DomainSettingsFragment;
47 import com.stoutner.privacybrowser.fragments.DomainsListFragment;
48 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
49
50 public class DomainsActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener {
51     // `twoPanedMode` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreate()` and `populateDomainsListView()`.
52     public static boolean twoPanedMode;
53
54     // `databaseId` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `saveDomainSettings()` and `populateDomainsListView()`.
55     public static int currentDomainDatabaseId;
56
57     // `domainSettingsFragmentDisplayed` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
58     public static boolean domainSettingsFragmentDisplayed;
59
60     // `deleteMenuItem` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `onBackPressed()`.
61     public static MenuItem deleteMenuItem;
62
63     // `context` is used in `onCreate()`, `onOptionsItemSelected()`, and `onAddDomain()`.
64     private Context context;
65
66     // `supportFragmentManager` is used in `onCreate()`.
67     private FragmentManager supportFragmentManager;
68
69     // `domainsDatabaseHelper` is used in `onCreate()` and `saveDomainSettings()`.
70     private static DomainsDatabaseHelper domainsDatabaseHelper;
71
72     // `domainsListView` is used in `onCreate()` and `populateDomainsList()`.
73     private ListView domainsListView;
74
75     // `addDomainFAB` is used in `onCreate()`, `onOptionsItemSelected()`, and `onBackPressed()`.
76     private FloatingActionButton addDomainFAB;
77
78     @Override
79     protected void onCreate(Bundle savedInstanceState) {
80         // Set the activity theme.
81         if (MainWebViewActivity.darkTheme) {
82             setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
83         } else {
84             setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
85         }
86
87         // Run the default commands.
88         super.onCreate(savedInstanceState);
89
90         // Set the content view.
91         setContentView(R.layout.domains_coordinatorlayout);
92
93         // Get a handle for the context.
94         context = this;
95
96         // Get a handle for the fragment manager.
97         supportFragmentManager = getSupportFragmentManager();
98
99         // We need to use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21.
100         final Toolbar domainsAppBar = (Toolbar) findViewById(R.id.domains_toolbar);
101         setSupportActionBar(domainsAppBar);
102
103         // Display the home arrow on `SupportActionBar`.
104         ActionBar appBar = getSupportActionBar();
105         assert appBar != null;// This assert removes the incorrect warning in Android Studio on the following line that `appBar` might be null.
106         appBar.setDisplayHomeAsUpEnabled(true);
107
108         // Initialize the database handler.  The two `nulls` do not specify the database name or a `CursorFactory`.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
109         domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
110
111         // Determine if we are in two pane mode.  `domain_settings_fragment_container` does not exist on devices with a width less than 900dp.
112         twoPanedMode = (findViewById(R.id.domain_settings_fragment_container) != null);
113
114         // Display `DomainsListFragment`.
115         DomainsListFragment domainsListFragment = new DomainsListFragment();
116         supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
117         supportFragmentManager.executePendingTransactions();
118
119         // Configure `addDomainFAB`.
120         addDomainFAB = (FloatingActionButton) findViewById(R.id.add_domain_fab);
121         addDomainFAB.setOnClickListener(new View.OnClickListener() {
122             @Override
123             public void onClick(View view) {
124                 // Show the `AddDomainDialog` `AlertDialog` and name the instance `@string/add_domain`.
125                 AppCompatDialogFragment addDomainDialog = new AddDomainDialog();
126                 addDomainDialog.show(supportFragmentManager, getResources().getString(R.string.add_domain));
127             }
128         });
129     }
130
131     @Override
132     public boolean onCreateOptionsMenu(Menu menu) {
133         // Inflate the menu.
134         getMenuInflater().inflate(R.menu.domains_options_menu, menu);
135
136         // Store `deleteMenuItem` for future use.
137         deleteMenuItem = menu.findItem(R.id.delete_domain);
138
139         // Only display `deleteMenuItem` (initially) in two-paned mode.
140         deleteMenuItem.setVisible(twoPanedMode);
141
142         // Populate the list of domains.  We have to do this from `onCreateOptionsMenu()` instead of `onCreate()` because `populateDomainsListView()` needs the `deleteMenuItem` to be inflated.
143         populateDomainsListView();
144
145         // Success!
146         return true;
147     }
148
149     @Override
150     public boolean onOptionsItemSelected(MenuItem menuItem) {
151         // Get the ID of the `MenuItem` that was selected.
152         int menuItemID = menuItem.getItemId();
153
154         switch (menuItemID) {
155             case android.R.id.home:  // The home arrow is identified as `android.R.id.home`, not just `R.id.home`.
156                 if (twoPanedMode) {  // The device is in two-paned mode.
157                     // If there is at least one domain, save the current domain settings.
158                     if (domainsListView.getCount() > 0) {
159                         saveDomainSettings();
160                     }
161
162                     // Go home.
163                     NavUtils.navigateUpFromSameTask(this);
164                 } else if (domainSettingsFragmentDisplayed) {  // The device is in single-paned mode and `DomainSettingsFragment` is displayed.
165                     // Save the current domain settings.
166                     saveDomainSettings();
167
168                     // Display `DomainsListFragment`.
169                     DomainsListFragment domainsListFragment = new DomainsListFragment();
170                     supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
171                     supportFragmentManager.executePendingTransactions();
172
173                     // Populate the list of domains.
174                     populateDomainsListView();
175
176                     // Update `domainSettingsFragmentDisplayed`.
177                     domainSettingsFragmentDisplayed = false;
178
179                     // Display `addDomainFAB`.
180                     addDomainFAB.setVisibility(View.VISIBLE);
181
182                     // Hide `deleteMenuItem`.
183                     deleteMenuItem.setVisible(false);
184                 } else {  // The device is in single-paned mode and `DomainsListFragment` is displayed.
185                     // Go home.
186                     NavUtils.navigateUpFromSameTask(this);
187                 }
188                 break;
189
190             case R.id.delete_domain:
191                 // Store a copy of `currentDomainDatabaseId` because it could change while the `Snackbar` is displayed.
192                 final int databaseIdToDelete = currentDomainDatabaseId;
193
194                 // Store the deleted domain position, which is needed if `Undo` is selected in the `Snackbar`.
195                 final int deletedDomainPosition = domainsListView.getCheckedItemPosition();
196
197                 // Update the fragments and menu items.
198                 if (twoPanedMode) {  // Two-paned mode.
199                     // Disable the options `MenuItems`.
200                     deleteMenuItem.setEnabled(false);
201                     deleteMenuItem.setIcon(R.drawable.delete_blue);
202
203                     // Remove the domain settings fragment.
204                     supportFragmentManager.beginTransaction().remove(supportFragmentManager.findFragmentById(R.id.domain_settings_fragment_container)).commit();
205                 } else {  // Single-paned mode.
206                     // Display `DomainsListFragment`.
207                     DomainsListFragment domainsListFragment = new DomainsListFragment();
208                     supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
209                     supportFragmentManager.executePendingTransactions();
210
211                     // Update `domainSettingsFragmentDisplayed`.
212                     domainSettingsFragmentDisplayed = false;
213
214                     // Display `addDomainFAB`.
215                     addDomainFAB.setVisibility(View.VISIBLE);
216
217                     // Hide `deleteMenuItem`.
218                     deleteMenuItem.setVisible(false);
219                 }
220
221                 // Get a `Cursor` that does not show the domain to be deleted.
222                 Cursor domainsPendingDeleteCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomainExcept(databaseIdToDelete);
223
224                 // Setup `domainsPendingDeleteCursorAdapter` with `this` context.  `false` disables `autoRequery`.
225                 CursorAdapter domainsPendingDeleteCursorAdapter = new CursorAdapter(this, domainsPendingDeleteCursor, false) {
226                     @Override
227                     public View newView(Context context, Cursor cursor, ViewGroup parent) {
228                         // Inflate the individual item layout.  `false` does not attach it to the root.
229                         return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
230                     }
231
232                     @Override
233                     public void bindView(View view, Context context, Cursor cursor) {
234                         // Set the domain name.
235                         String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
236                         TextView domainNameTextView = (TextView) view.findViewById(R.id.domain_name_textview);
237                         domainNameTextView.setText(domainNameString);
238                     }
239                 };
240
241                 // Update the handle for the current `domains_listview`.
242                 domainsListView = (ListView) findViewById(R.id.domains_listview);
243
244                 // Update the `ListView`.
245                 domainsListView.setAdapter(domainsPendingDeleteCursorAdapter);
246
247                 // Display a `Snackbar`.
248                 Snackbar.make(domainsListView, R.string.domain_deleted, Snackbar.LENGTH_LONG)
249                         .setAction(R.string.undo, new View.OnClickListener() {
250                             @Override
251                             public void onClick(View v) {
252                                 // Do nothing because everything will be handled by `onDismissed()` below.
253                             }
254                         })
255                         .addCallback(new Snackbar.Callback() {
256                             @Override
257                             public void onDismissed(Snackbar snackbar, int event) {
258                                 switch (event) {
259                                     // The user pushed the `Undo` button.
260                                     case Snackbar.Callback.DISMISS_EVENT_ACTION:
261                                         // Store `databaseId` in `argumentsBundle`.
262                                         Bundle argumentsBundle = new Bundle();
263                                         argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, databaseIdToDelete);
264
265                                         // Add `argumentsBundle` to `domainSettingsFragment`.
266                                         DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
267                                         domainSettingsFragment.setArguments(argumentsBundle);
268
269                                         // Display the correct fragments.
270                                         if (twoPanedMode) {  // The device in in two-paned mode.
271                                             // Get a `Cursor` with the current contents of the domains database.
272                                             Cursor undoDeleteDomainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
273
274                                             // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
275                                             CursorAdapter undoDeleteDomainsCursorAdapter = new CursorAdapter(context, undoDeleteDomainsCursor, false) {
276                                                 @Override
277                                                 public View newView(Context context, Cursor cursor, ViewGroup parent) {
278                                                     // Inflate the individual item layout.  `false` does not attach it to the root.
279                                                     return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
280                                                 }
281
282                                                 @Override
283                                                 public void bindView(View view, Context context, Cursor cursor) {
284                                                     // Set the domain name.
285                                                     String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
286                                                     TextView domainNameTextView = (TextView) view.findViewById(R.id.domain_name_textview);
287                                                     domainNameTextView.setText(domainNameString);
288                                                 }
289                                             };
290
291                                             // Update the `ListView`.
292                                             domainsListView.setAdapter(undoDeleteDomainsCursorAdapter);
293                                             // Select the previously deleted domain in `domainsListView`.
294                                             domainsListView.setItemChecked(deletedDomainPosition, true);
295
296                                             // Display `domainSettingsFragment`.
297                                             supportFragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
298
299                                             // Enable the options `MenuItems`.
300                                             deleteMenuItem.setEnabled(true);
301                                             deleteMenuItem.setIcon(R.drawable.delete_light);
302                                         } else {  // The device in in one-paned mode.
303                                             // Display `domainSettingsFragment`.
304                                             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
305
306                                             // Hide `add_domain_fab`.
307                                             FloatingActionButton addDomainFAB = (FloatingActionButton) findViewById(R.id.add_domain_fab);
308                                             addDomainFAB.setVisibility(View.GONE);
309
310                                             // Show and enable `deleteMenuItem`.
311                                             deleteMenuItem.setVisible(true);
312
313                                             // Set `domainSettingsFragmentDisplayed`.
314                                             domainSettingsFragmentDisplayed = true;
315
316                                             // Display `domainSettingsFragment`.
317                                             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainSettingsFragment).commit();
318                                         }
319                                         break;
320
321                                     // The `Snackbar` was dismissed without the `Undo` button being pushed.
322                                     default:
323                                         // Delete the selected domain.
324                                         domainsDatabaseHelper.deleteDomain(databaseIdToDelete);
325                                         break;
326                                 }
327                             }
328                         })
329                         .show();
330                 break;
331         }
332
333         // Consume the event.
334         return true;
335     }
336
337     // Control what the navigation bar back button does.
338     @Override
339     public void onBackPressed() {
340         if (twoPanedMode) {  // The device is in two-paned mode.
341             // If there is at least one domain, save the current domain settings.
342             if (domainsListView.getCount() > 0) {
343                 saveDomainSettings();
344             }
345
346             // Go home.
347             NavUtils.navigateUpFromSameTask(this);
348         } else if (domainSettingsFragmentDisplayed) {  // The device is in single-paned mode and `DomainSettingsFragment` is displayed.
349             // Save the current domain settings.
350             saveDomainSettings();
351
352             // Display `DomainsListFragment`.
353             DomainsListFragment domainsListFragment = new DomainsListFragment();
354             supportFragmentManager.beginTransaction().replace(R.id.domains_listview_fragment_container, domainsListFragment).commit();
355             supportFragmentManager.executePendingTransactions();
356
357             // Populate the list of domains.
358             populateDomainsListView();
359
360             // Update `domainSettingsFragmentDisplayed`.
361             domainSettingsFragmentDisplayed = false;
362
363             // Display `addDomainFAB`.
364             addDomainFAB.setVisibility(View.VISIBLE);
365
366             // Hide `deleteMenuItem`.
367             deleteMenuItem.setVisible(false);
368         } else {  // The device is in single-paned mode and `DomainsListFragment` is displayed.
369             // Pass `onBackPressed()` to the system.
370             super.onBackPressed();
371         }
372     }
373
374     @Override
375     public void onAddDomain(AppCompatDialogFragment dialogFragment) {
376         // Get the `domainNameEditText` from `dialogFragment` and extract the string.
377         EditText domainNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
378         String domainNameString = domainNameEditText.getText().toString();
379
380         // Create the domain.
381         domainsDatabaseHelper.addDomain(domainNameString);
382
383         // Populate the `ListView`.
384         populateDomainsListView();
385     }
386
387     private void saveDomainSettings() {
388         // Get handles for the domain settings.
389         EditText domainNameEditText = (EditText) findViewById(R.id.domain_settings_name_edittext);
390         Switch javaScriptEnabledSwitch = (Switch) findViewById(R.id.domain_settings_javascript_switch);
391         Switch firstPartyCookiesEnabledSwitch = (Switch) findViewById(R.id.domain_settings_first_party_cookies_switch);
392         Switch thirdPartyCookiesEnabledSwitch = (Switch) findViewById(R.id.domain_settings_third_party_cookies_switch);
393         Switch domStorageEnabledSwitch = (Switch) findViewById(R.id.domain_settings_dom_storage_switch);
394         Switch formDataEnabledSwitch = (Switch) findViewById(R.id.domain_settings_form_data_switch);
395         Spinner userAgentSpinner = (Spinner) findViewById(R.id.domain_settings_user_agent_spinner);
396         EditText customUserAgentEditText = (EditText) findViewById(R.id.domain_settings_custom_user_agent_edittext);
397         Spinner fontSizeSpinner = (Spinner) findViewById(R.id.domain_settings_font_size_spinner);
398         Spinner displayWebpageImagesSpinner = (Spinner) findViewById(R.id.domain_settings_display_webpage_images_spinner);
399
400         // Extract the data for the domain settings.
401         String domainNameString = domainNameEditText.getText().toString();
402         boolean javaScriptEnabledBoolean = javaScriptEnabledSwitch.isChecked();
403         boolean firstPartyCookiesEnabledBoolean = firstPartyCookiesEnabledSwitch.isChecked();
404         boolean thirdPartyCookiesEnabledBoolean = thirdPartyCookiesEnabledSwitch.isChecked();
405         boolean domStorageEnabledEnabledBoolean  = domStorageEnabledSwitch.isChecked();
406         boolean formDataEnabledBoolean = formDataEnabledSwitch.isChecked();
407         int userAgentPositionInt = userAgentSpinner.getSelectedItemPosition();
408         int fontSizePositionInt = fontSizeSpinner.getSelectedItemPosition();
409         int displayWebpageImagesInt = displayWebpageImagesSpinner.getSelectedItemPosition();
410
411         // Get the data for the `Spinners` from the entry values string arrays.
412         String userAgentString = getResources().getStringArray(R.array.domain_settings_user_agent_entry_values)[userAgentPositionInt];
413         int fontSizeInt = Integer.parseInt(getResources().getStringArray(R.array.domain_settings_font_size_entry_values)[fontSizePositionInt]);
414
415         // Check to see if we are using a custom user agent.
416         if (userAgentString.equals("Custom user agent")) {
417             // Set `userAgentString` to the custom user agent string.
418             userAgentString = customUserAgentEditText.getText().toString();
419         }
420
421         // Save the domain settings.
422         domainsDatabaseHelper.saveDomain(currentDomainDatabaseId, domainNameString, javaScriptEnabledBoolean, firstPartyCookiesEnabledBoolean, thirdPartyCookiesEnabledBoolean, domStorageEnabledEnabledBoolean, formDataEnabledBoolean, userAgentString, fontSizeInt,
423                 displayWebpageImagesInt);
424     }
425
426     private void populateDomainsListView() {
427         // get a handle for the current `domains_listview`.
428         domainsListView = (ListView) findViewById(R.id.domains_listview);
429
430         // Get a `Cursor` with the current contents of the domains database.
431         Cursor domainsCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
432
433         // Setup `domainsCursorAdapter` with `this` context.  `false` disables `autoRequery`.
434         CursorAdapter domainsCursorAdapter = new CursorAdapter(context, domainsCursor, false) {
435             @Override
436             public View newView(Context context, Cursor cursor, ViewGroup parent) {
437                 // Inflate the individual item layout.  `false` does not attach it to the root.
438                 return getLayoutInflater().inflate(R.layout.domain_name_linearlayout, parent, false);
439             }
440
441             @Override
442             public void bindView(View view, Context context, Cursor cursor) {
443                 // Set the domain name.
444                 String domainNameString = cursor.getString(cursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME));
445                 TextView domainNameTextView = (TextView) view.findViewById(R.id.domain_name_textview);
446                 domainNameTextView.setText(domainNameString);
447             }
448         };
449
450         // Update the `ListView`.
451         domainsListView.setAdapter(domainsCursorAdapter);
452
453         // Display the domain settings in the second pane if operating in two pane mode and the database contains at least one domain.
454         if (DomainsActivity.twoPanedMode && (domainsCursor.getCount() > 0)) {  // Two-paned mode is enabled and there is at least one domain.
455             // Select the first domain.
456             domainsListView.setItemChecked(0, true);
457
458             // Get the `databaseId` for the first domain.
459             domainsCursor.moveToPosition(0);
460             currentDomainDatabaseId = domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper._ID));
461
462             // Store `databaseId` in `argumentsBundle`.
463             Bundle argumentsBundle = new Bundle();
464             argumentsBundle.putInt(DomainSettingsFragment.DATABASE_ID, currentDomainDatabaseId);
465
466             // Add `argumentsBundle` to `domainSettingsFragment`.
467             DomainSettingsFragment domainSettingsFragment = new DomainSettingsFragment();
468             domainSettingsFragment.setArguments(argumentsBundle);
469
470             // Display `domainSettingsFragment`.
471             supportFragmentManager.beginTransaction().replace(R.id.domain_settings_fragment_container, domainSettingsFragment).commit();
472
473             // Enable the options `MenuItems`.
474             deleteMenuItem.setEnabled(true);
475
476             // Set the delete icon according to the theme.
477             if (MainWebViewActivity.darkTheme) {
478                 deleteMenuItem.setIcon(R.drawable.delete_dark);
479             } else {
480                 deleteMenuItem.setIcon(R.drawable.delete_light);
481             }
482         } else if (twoPanedMode) {  // Two-paned mode is enabled but there are no domains.
483             // Disable the options `MenuItems`.
484             deleteMenuItem.setEnabled(false);
485             deleteMenuItem.setIcon(R.drawable.delete_blue);
486         }
487     }
488 }