Add import and export of settings. https://redmine.stoutner.com/issues/23
authorSoren Stoutner <soren@stoutner.com>
Thu, 25 Oct 2018 18:48:11 +0000 (11:48 -0700)
committerSoren Stoutner <soren@stoutner.com>
Thu, 25 Oct 2018 18:48:11 +0000 (11:48 -0700)
44 files changed:
.idea/assetWizardSettings.xml
.idea/dictionaries/soren.xml
app/build.gradle
app/src/free/java/com/stoutner/privacybrowser/dialogs/AdConsentDialog.java
app/src/free/java/com/stoutner/privacybrowser/helpers/AdHelper.java
app/src/free/res/layout/main_webview.xml
app/src/free/res/values/strings.xml
app/src/main/AndroidManifest.xml
app/src/main/assets/de/about_licenses_dark.html
app/src/main/assets/de/about_licenses_light.html
app/src/main/assets/en/about_licenses_dark.html
app/src/main/assets/en/about_licenses_light.html
app/src/main/assets/es/about_licenses_dark.html
app/src/main/assets/es/about_licenses_light.html
app/src/main/assets/it/about_licenses_dark.html
app/src/main/assets/it/about_licenses_light.html
app/src/main/assets/ru/about_licenses_dark.html
app/src/main/assets/ru/about_licenses_light.html
app/src/main/assets/shared_images/import_export_dark.png [new file with mode: 0644]
app/src/main/assets/shared_images/import_export_light.png [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/BookmarksDatabaseViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/DomainsActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/RequestsActivity.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/DownloadLocationPermissionDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/ImportExportStoragePermissionDialog.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/helpers/BookmarksDatabaseHelper.java
app/src/main/java/com/stoutner/privacybrowser/helpers/DomainsDatabaseHelper.java
app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java [new file with mode: 0644]
app/src/main/res/drawable/images_disabled_dark.xml
app/src/main/res/drawable/images_disabled_light.xml
app/src/main/res/drawable/images_enabled_dark.xml
app/src/main/res/drawable/images_enabled_light.xml
app/src/main/res/drawable/import_export_dark.xml [new file with mode: 0644]
app/src/main/res/drawable/import_export_light.xml [new file with mode: 0644]
app/src/main/res/layout/import_export_coordinatorlayout.xml [new file with mode: 0644]
app/src/main/res/layout/requests_coordinatorlayout.xml
app/src/main/res/menu/webview_navigation_menu.xml
app/src/main/res/values/strings.xml
app/src/standard/java/com/stoutner/privacybrowser/helpers/AdHelper.java
build.gradle
gradle/wrapper/gradle-wrapper.properties

index 58477f9..9f20b9b 100644 (file)
@@ -68,7 +68,7 @@
                                 <PersistentState>
                                   <option name="values">
                                     <map>
-                                      <entry key="url" value="jar:file:/home/soren/Android/android-studio/plugins/android/lib/android.jar!/images/material_design_icons/av/ic_new_releases_black_24dp.xml" />
+                                      <entry key="url" value="jar:file:/home/soren/Android/android-studio/plugins/android/lib/android.jar!/images/material_design_icons/communication/ic_import_export_black_24dp.xml" />
                                     </map>
                                   </option>
                                 </PersistentState>
@@ -78,9 +78,8 @@
                         </option>
                         <option name="values">
                           <map>
-                            <entry key="assetSourceType" value="FILE" />
                             <entry key="autoMirrored" value="true" />
-                            <entry key="outputName" value="bookmarks_light" />
+                            <entry key="outputName" value="import_export" />
                             <entry key="sourceFile" value="$USER_HOME$/ownCloud/Android/Privacy Browser/Icons/Icons/bookmarks_light.svg" />
                           </map>
                         </option>
index 5bed189..3a9ec86 100644 (file)
@@ -54,6 +54,7 @@
       <w>enableformdata</w>
       <w>enablejavascript</w>
       <w>enablethirdpartycookies</w>
+      <w>externalstorage</w>
       <w>exynos</w>
       <w>fanboy</w>
       <w>fanboys</w>
       <w>snackbar</w>
       <w>snackbars</w>
       <w>softkeyboard</w>
+      <w>sqlite</w>
       <w>sslenddate</w>
       <w>sslissuedbycommonname</w>
       <w>sslissuedbyorganization</w>
index 25eacaf..88ecb08 100644 (file)
@@ -21,7 +21,7 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 28
-    buildToolsVersion '28.0.2'
+    buildToolsVersion '28.0.3'
 
     defaultConfig {
         minSdkVersion 19
@@ -71,10 +71,10 @@ android {
 
 dependencies {
     implementation fileTree(include: ['*.jar'], dir: 'libs')
-    implementation 'com.android.support:design:28.0.0-rc02'
+    implementation 'com.android.support:design:28.0.0'
 
     // Only compile Firebase ads for the free flavor.
-    freeImplementation 'com.google.firebase:firebase-ads:15.0.1'
+    freeImplementation 'com.google.firebase:firebase-ads:17.0.0'
 
     // Only compile the consent library for the free flavor.  It is used to comply with the GDPR in Europe.
     freeImplementation 'com.google.android.ads.consent:consent-library:1.0.6'
index 6b771d6..8808923 100644 (file)
@@ -81,7 +81,7 @@ public class AdConsentDialog extends DialogFragment {
             consentInformation.setTagForUnderAgeOfConsent(true);
 
             // Load an ad.
-            AdHelper.loadAd(getActivity().findViewById(R.id.adview), getActivity().getApplicationContext(), getString(R.string.ad_id));
+            AdHelper.loadAd(getActivity().findViewById(R.id.adview), getActivity().getApplicationContext(), getString(R.string.ad_unit_id));
         });
 
         // Return the alert dialog.
index a4f5b03..2411207 100644 (file)
@@ -40,10 +40,10 @@ import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
 public class AdHelper {
     private static boolean initialized;
 
-    public static void initializeAds (View view, Context applicationContext, FragmentManager fragmentManager, String adId) {
+    public static void initializeAds (View view, Context applicationContext, FragmentManager fragmentManager, String googleAppId, String adUnitId) {
         if (!initialized) {  // This is the first run.
             // Initialize mobile ads.
-            MobileAds.initialize(applicationContext, adId);
+            MobileAds.initialize(applicationContext, googleAppId);
 
             // Store the publisher ID in a string array.
             String[] publisherIds = {"pub-5962503714887045"};
@@ -62,7 +62,7 @@ public class AdHelper {
                         consentInformation.setTagForUnderAgeOfConsent(true);
 
                         // Load an ad.
-                        loadAd(view, applicationContext, adId);
+                        loadAd(view, applicationContext, adUnitId);
                     }
                 }
 
@@ -72,7 +72,7 @@ public class AdHelper {
                     consentInformation.setTagForUnderAgeOfConsent(true);
 
                     // Load an ad.
-                    loadAd(view, applicationContext, adId);
+                    loadAd(view, applicationContext, adUnitId);
                 }
             });
 
@@ -80,11 +80,11 @@ public class AdHelper {
             initialized = true;
         } else {  // Ads have previously been initialized.
             // Load an ad.
-            loadAd(view, applicationContext, adId);
+            loadAd(view, applicationContext, adUnitId);
         }
     }
 
-    public static void loadAd (View view, Context applicationContext, String adId) {
+    public static void loadAd (View view, Context applicationContext, String adUnitId) {
         // Cast the generic view to an AdView.
         AdView adView = (AdView) view;
 
@@ -98,7 +98,7 @@ public class AdHelper {
         // Setup the new AdView.  This is necessary because the size of the banner ad can change on rotate.
         adView = new AdView(applicationContext);
         adView.setAdSize(AdSize.SMART_BANNER);
-        adView.setAdUnitId(adId);
+        adView.setAdUnitId(adUnitId);
         adView.setId(R.id.adview);
         adView.setLayoutParams(adViewLayoutParameters);
 
@@ -111,6 +111,8 @@ public class AdHelper {
 
         // Request a new ad.
         AdRequest adRequest = new AdRequest.Builder().addNetworkExtrasBundle(AdMobAdapter.class, adSettingsBundle).build();
+        // Pixel 2 XL test ads.
+        // AdRequest adRequest = new AdRequest.Builder().addTestDevice("137D42984218CEECDFD11927BB7D6416").addNetworkExtrasBundle(AdMobAdapter.class, adSettingsBundle).build();
         adView.loadAd(adRequest);
     }
 
index fb80533..49c7ed8 100644 (file)
@@ -39,7 +39,7 @@
         android:layout_centerHorizontal="true"
         android:layout_alignParentBottom="true"
         ads:adSize="SMART_BANNER"
-        ads:adUnitId="@string/ad_id" >
+        ads:adUnitId="@string/ad_unit_id" >
     </com.google.android.gms.ads.AdView>
 
     <android.support.v4.widget.SwipeRefreshLayout
index f0005d8..9aae5fc 100644 (file)
     <string name="privacy_browser">Privacy Browser Free</string>
 
     <!-- Ad Control. -->
-    <string name="ad_id" translatable="false">ca-app-pub-5962503714887045/2738552414</string>
+    <string name="google_app_id" translatable="false">ca-app-pub-5962503714887045~2738552414</string>
+    <string name="ad_unit_id" translatable="false">ca-app-pub-5962503714887045/2738552414</string>
 
     <!-- Test Ad Control.
-    <string name="ad_id" translatable="false">ca-app-pub-3940256099942544/6300978111</string>
-    -->
+    <string name="google_app_id" translatable="false">ca-app-pub-3940256099942544~3347511713</string>
+    <string name="ad_unit_id" translatable="false">ca-app-pub-3940256099942544/6300978111</string> -->
 
     <!-- Ad Consent. -->
     <string name="ad_consent_text">Privacy Browser Free displays a banner ad on the bottom of the screen.
index fc0dc95..028a7e3 100644 (file)
     <!-- Required to create home screen shortcuts. -->
     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
 
-    <!-- Required to save files to the public download folder. -->
+    <!-- Required to import settings from external storage. -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <!-- Required to export settings and save files to public storage. -->
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
 
             android:name="android.webkit.WebView.EnableSafeBrowsing"
             android:value="false" />
 
+        <!-- Specify the Application ID used by the ads in the free flavor. -->
+        <meta-data
+            android:name="com.google.android.gms.ads.APPLICATION_ID"
+            android:value="@string/google_app_id" />
+
         <!-- The theme has to be defined here or an ugly title bar is displayed when the app launches.
              `android:configChanges="orientation|screenSize"` makes the activity not reload when the orientation changes, which preserves scroll location in the WebView.
              `android:launchMode="singleTask"` makes the app launch in a new task instead of inside the task of the program that sends it an intent.
             android:persistableMode="persistNever"
             tools:ignore="UnusedAttribute" />
 
+        <!-- `android:configChanges="orientation|screenSize"` makes the activity not reload when the orientation changes.
+             `android:persistableMode="persistNever"` removes Privacy Browser from the recent apps list on a device reboot.
+             `tools:ignore="unusedAttribute"` removes the lint warning that `persistableMode` does not apply to API < 21. -->
+        <activity
+            android:name=".activities.ImportExportActivity"
+            android:label="@string/import_export"
+            android:parentActivityName=".activities.MainWebViewActivity"
+            android:configChanges="orientation|screenSize"
+            android:screenOrientation="fullUser"
+            android:persistableMode="persistNever"
+            tools:ignore="UnusedAttribute" />
+
         <!-- `android:configChanges="orientation|screenSize"` makes the activity not reload when the orientation changes.
              `android:persistableMode="persistNever"` removes Privacy Browser from the recent apps list on a device reboot.
              `tools:ignore="unusedAttribute"` removes the lint warning that `persistableMode` does not apply to API < 21. -->
index 0d1cccb..a2c3052 100644 (file)
         <p><img class="icon" src="../shared_images/home_dark.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_dark.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_dark.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_dark.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_dark.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_dark.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_dark.png"> language.</p>
index 460cc40..5e7008d 100644 (file)
         <p><img class="icon" src="../shared_images/home_light.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_light.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_light.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_light.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_light.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_light.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_light.png"> language.</p>
index ff72afd..453f701 100644 (file)
         <p><img class="icon" src="../shared_images/home_dark.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_dark.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_dark.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_dark.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_dark.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_dark.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_dark.png"> language.</p>
index ba326c7..63c3af1 100644 (file)
         <p><img class="icon" src="../shared_images/home_light.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_light.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_light.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_light.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_light.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_light.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_light.png"> language.</p>
index 4c005c0..a857022 100644 (file)
         <p><img class="icon" src="../shared_images/home_dark.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_dark.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_dark.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_dark.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_dark.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_dark.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_dark.png"> language.</p>
index 3a70f45..5a8eadb 100644 (file)
         <p><img class="icon" src="../shared_images/home_light.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_light.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_light.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_light.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_light.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_light.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_light.png"> language.</p>
index 2cba635..bffcdc2 100644 (file)
         <p><img class="icon" src="../shared_images/home_dark.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_dark.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_dark.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_dark.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_dark.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_dark.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_dark.png"> language.</p>
index dcc575c..f8cf92d 100644 (file)
         <p><img class="icon" src="../shared_images/home_light.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_light.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_light.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_light.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_light.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_light.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_light.png"> language.</p>
index e2819da..b5fe7ae 100644 (file)
         <p><img class="icon" src="../shared_images/home_dark.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_dark.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_dark.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_dark.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_dark.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_dark.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_dark.png"> language.</p>
index 6a419a6..264f0d5 100644 (file)
         <p><img class="icon" src="../shared_images/home_light.png"> home.</p>
         <p><img class="icon" src="../shared_images/image_light.png"> image.</p>
         <p><img class="icon" src="../shared_images/import_contacts_light.png"> import_contacts.</p>
+        <p><img class="icon" src="../shared_images/import_export_light.png"> import_export.</p>
         <p><img class="icon" src="../shared_images/important_devices_light.png"> important_devices.</p>
         <p><img class="icon" src="../shared_images/info_outline_light.png"> info_outline.</p>
         <p><img class="icon" src="../shared_images/language_light.png"> language.</p>
diff --git a/app/src/main/assets/shared_images/import_export_dark.png b/app/src/main/assets/shared_images/import_export_dark.png
new file mode 100644 (file)
index 0000000..aae1e03
Binary files /dev/null and b/app/src/main/assets/shared_images/import_export_dark.png differ
diff --git a/app/src/main/assets/shared_images/import_export_light.png b/app/src/main/assets/shared_images/import_export_light.png
new file mode 100644 (file)
index 0000000..2b70cd7
Binary files /dev/null and b/app/src/main/assets/shared_images/import_export_light.png differ
index d556b30..d48fe2b 100644 (file)
@@ -66,22 +66,25 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma
         EditBookmarkFolderDialog.EditBookmarkFolderListener, MoveToFolderDialog.MoveToFolderListener {
 
     // `currentFolder` is public static so it can be accessed from `MoveToFolderDialog`.
-    // It is used in `onCreate`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, and `loadFolder()`.
+    // It is used in `onCreate`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`,
+    // and `loadFolder()`.
     public static String currentFolder;
 
-    // `checkedItemIds` is public static so it can be accessed from `MoveToFolderDialog`.  It is also used in `onCreate()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, and `updateMoveIcons()`.
+    // `checkedItemIds` is public static so it can be accessed from `MoveToFolderDialog`.  It is also used in `onCreate()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`,
+    // and `updateMoveIcons()`.
     public static long[] checkedItemIds;
 
 
-    // `bookmarksDatabaseHelper` is used in `onCreate()`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `deleteBookmarkFolderContents()`,
-    // and `loadFolder().
+    // `bookmarksDatabaseHelper` is used in `onCreate()`, `onOptionsItemSelected()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`,
+    // `onMoveToFolder()`, `deleteBookmarkFolderContents()`, `loadFolder()`, and `onDestroy()`.
     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
 
-    // `bookmarksListView` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `updateMoveIcons()`, `scrollBookmarks()`,
-    // and `loadFolder()`.
+    // `bookmarksListView` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`,
+    // `updateMoveIcons()`, `scrollBookmarks()`, and `loadFolder()`.
     private ListView bookmarksListView;
 
-    // `bookmarksCursor` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `deleteBookmarkFolderContents()`, and `loadFolder()`.
+    // `bookmarksCursor` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, `deleteBookmarkFolderContents()`,
+    // `loadFolder()`, and `onDestroy()`.
     private Cursor bookmarksCursor;
 
     // `bookmarksCursorAdapter` is used in `onCreate(), `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, `onMoveToFolder()`, and `onLoadFolder()`.
@@ -1072,4 +1075,14 @@ public class BookmarksActivity extends AppCompatActivity implements CreateBookma
             appBar.setTitle(currentFolder);
         }
     }
+
+    @Override
+    public void onDestroy() {
+        // Close the bookmarks cursor and database.
+        bookmarksCursor.close();
+        bookmarksDatabaseHelper.close();
+
+        // Run the default commands.
+        super.onDestroy();
+    }
 }
\ No newline at end of file
index 006af07..620715a 100644 (file)
@@ -60,10 +60,10 @@ public class BookmarksDatabaseViewActivity extends AppCompatActivity implements
     private static final int ALL_FOLDERS_DATABASE_ID = -2;
     private static final int HOME_FOLDER_DATABASE_ID = -1;
 
-    // `bookmarksDatabaseHelper` is used in `onCreate()` and `updateBookmarksListView()`.
+    // `bookmarksDatabaseHelper` is used in `onCreate()`, `updateBookmarksListView()`, and `onDestroy()`.
     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
 
-    // `bookmarksCursor` is used in `onCreate()`, `updateBookmarksListView()`, `onSaveBookmark()`, and `onSaveBookmarkFolder()`.
+    // `bookmarksCursor` is used in `onCreate()`, `updateBookmarksListView()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`, and `onDestroy()`.
     private Cursor bookmarksCursor;
 
     // `bookmarksCursorAdapter` is used in `onCreate()`, `onSaveBookmark()`, `onSaveBookmarkFolder()`.
@@ -435,4 +435,14 @@ public class BookmarksDatabaseViewActivity extends AppCompatActivity implements
         // Update the `ListView`.
         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
     }
+
+    @Override
+    public void onDestroy() {
+        // Close the bookmarks cursor and database.
+        bookmarksCursor.close();
+        bookmarksDatabaseHelper.close();
+
+        // Run the default commands.
+        super.onDestroy();
+    }
 }
\ No newline at end of file
index 479b47a..1d9a62a 100644 (file)
@@ -55,6 +55,8 @@ import com.stoutner.privacybrowser.fragments.DomainSettingsFragment;
 import com.stoutner.privacybrowser.fragments.DomainsListFragment;
 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
 
+import java.util.Objects;
+
 public class DomainsActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener {
     // `twoPanedMode` is public static so it can be accessed from `DomainsListFragment`.  It is also used in `onCreate()`, `onCreateOptionsMenu()`, and `populateDomainsListView()`.
     public static boolean twoPanedMode;
@@ -80,7 +82,7 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
     // `supportFragmentManager` is used in `onCreate()` and `onCreateOptionsMenu()`.
     private FragmentManager supportFragmentManager;
 
-    // `domainsDatabaseHelper` is used in `onCreate()` and `saveDomainSettings()`.
+    // `domainsDatabaseHelper` is used in `onCreate()`, `saveDomainSettings()`, and `onDestroy()`.
     private static DomainsDatabaseHelper domainsDatabaseHelper;
 
     // `domainsListView` is used in `onCreate()` and `populateDomainsList()`.
@@ -159,12 +161,12 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
         final Toolbar domainsAppBar = findViewById(R.id.domains_toolbar);
         setSupportActionBar(domainsAppBar);
 
-        // Display the home arrow on `SupportActionBar`.
+        // Display the home arrow on the support action bar.
         ActionBar appBar = getSupportActionBar();
         assert appBar != null;// This assert removes the incorrect warning in Android Studio on the following line that `appBar` might be null.
         appBar.setDisplayHomeAsUpEnabled(true);
 
-        // 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`.
+        // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
         domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
 
         // Determine if we are in two pane mode.  `domain_settings_fragment_container` does not exist on devices with a width less than 900dp.
@@ -352,7 +354,7 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
                     deleteMenuItem.setIcon(R.drawable.delete_blue);
 
                     // Remove the domain settings fragment.
-                    supportFragmentManager.beginTransaction().remove(supportFragmentManager.findFragmentById(R.id.domain_settings_fragment_container)).commit();
+                    supportFragmentManager.beginTransaction().remove(Objects.requireNonNull(supportFragmentManager.findFragmentById(R.id.domain_settings_fragment_container))).commit();
                 } else {  // Single-paned mode.
                     // Display `DomainsListFragment`.
                     DomainsListFragment domainsListFragment = new DomainsListFragment();
@@ -813,4 +815,13 @@ public class DomainsActivity extends AppCompatActivity implements AddDomainDialo
             deleteMenuItem.setIcon(R.drawable.delete_blue);
         }
     }
+
+    @Override
+    public void onDestroy() {
+        // Close the domains database helper.
+        domainsDatabaseHelper.close();
+
+        // Run the default commands.
+        super.onDestroy();
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
new file mode 100644 (file)
index 0000000..705a662
--- /dev/null
@@ -0,0 +1,490 @@
+/*
+ * Copyright © 2018 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.activities;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.dialogs.ImportExportStoragePermissionDialog;
+import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
+
+import java.io.File;
+
+public class ImportExportActivity extends AppCompatActivity implements ImportExportStoragePermissionDialog.ImportExportStoragePermissionDialogListener {
+    private final static int EXPORT_FILE_PICKER_REQUEST_CODE = 1;
+    private final static int IMPORT_FILE_PICKER_REQUEST_CODE = 2;
+    private final static int EXPORT_REQUEST_CODE = 3;
+    private final static int IMPORT_REQUEST_CODE = 4;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        // Disable screenshots if not allowed.
+        if (!MainWebViewActivity.allowScreenshots) {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
+        }
+
+        // Set the activity theme.
+        if (MainWebViewActivity.darkTheme) {
+            setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
+        } else {
+            setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
+        }
+
+        // Run the default commands.
+        super.onCreate(savedInstanceState);
+
+        // Set the content view.
+        setContentView(R.layout.import_export_coordinatorlayout);
+
+        // Use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21.
+        Toolbar importExportAppBar = findViewById(R.id.import_export_toolbar);
+        setSupportActionBar(importExportAppBar);
+
+        // Display the home arrow on the support action bar.
+        ActionBar appBar = getSupportActionBar();
+        assert appBar != null;// This assert removes the incorrect warning in Android Studio on the following line that `appBar` might be null.
+        appBar.setDisplayHomeAsUpEnabled(true);
+
+        // Get handles for the views that need to be modified.
+        EditText exportFileEditText = findViewById(R.id.export_file_edittext);
+        Button exportButton = findViewById(R.id.export_button);
+        EditText importFileEditText = findViewById(R.id.import_file_edittext);
+        Button importButton = findViewById(R.id.import_button);
+        TextView storagePermissionTextView = findViewById(R.id.import_export_storage_permission_textview);
+
+        // Initially disable the buttons.
+        exportButton.setEnabled(false);
+        importButton.setEnabled(false);
+
+        // Enable the export button when the export file EditText isn't empty.
+        exportFileEditText.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+                // Do nothing.
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                // Do nothing.
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                exportButton.setEnabled(!exportFileEditText.getText().toString().isEmpty());
+            }
+        });
+
+        // Enable the import button when the export file EditText isn't empty.
+        importFileEditText.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+                // Do nothing.
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                // Do nothing.
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                importButton.setEnabled(!importFileEditText.getText().toString().isEmpty());
+            }
+        });
+
+        // Set the default download file path if the storage permission has not been granted.
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
+            // Create a string for the external private path.
+            String EXTERNAL_PRIVATE_PATH = getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/Privacy Browser Backup";
+
+            // Set the default path.
+            exportFileEditText.setText(EXTERNAL_PRIVATE_PATH);
+            importFileEditText.setText(EXTERNAL_PRIVATE_PATH);
+        }
+
+        // Hide the storage permissions TextView on API < 23 as permissions on older devices are automatically granted.
+        if (Build.VERSION.SDK_INT < 23) {
+            storagePermissionTextView.setVisibility(View.GONE);
+        }
+    }
+
+    public void exportBrowse(View view) {
+        // Create the file picker intent.
+        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+
+        // Set the intent MIME type to include all files.
+        intent.setType("*/*");
+
+        // Set the initial export file name.
+        intent.putExtra(Intent.EXTRA_TITLE, getString(R.string.privacy_browser_backup));
+
+        // Set the initial directory if API >= 26.
+        if (Build.VERSION.SDK_INT >= 26) {
+            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
+        }
+
+        // Specify that a file that can be opened is requested.
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+        // Launch the file picker.
+        startActivityForResult(intent, EXPORT_FILE_PICKER_REQUEST_CODE);
+    }
+
+    public void onClickExport(View view) {
+        // Check to see if the storage permission has been granted.
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // Storage permission granted.
+            // Export the settings.
+            exportSettings();
+        } else {  // Storage permission not granted.
+            // Get a handle for the export file EditText.
+            EditText exportFileEditText = findViewById(R.id.export_file_edittext);
+
+            // Get the export file string.
+            String exportFileString = exportFileEditText.getText().toString();
+
+            // Get the external private directory `File`.
+            File externalPrivateDirectoryFile = getApplicationContext().getExternalFilesDir(null);
+
+            // Remove the lint error below that the `File` might be null.
+            assert externalPrivateDirectoryFile != null;
+
+            // Get the external private directory string.
+            String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
+
+            // Check to see if the export file path is in the external private directory.
+            if (exportFileString.startsWith(externalPrivateDirectory)) {  // The export path is in the external private directory.
+                // Export the settings.
+                exportSettings();
+            } else {  // The export path is in a public directory.
+                // Check if the user has previously denied the storage permission.
+                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
+                    // Instantiate the storage permission alert dialog and set the type to EXPORT_SETTINGS.
+                    DialogFragment importExportStoragePermissionDialogFragment = ImportExportStoragePermissionDialog.type(ImportExportStoragePermissionDialog.EXPORT_SETTINGS);
+
+                    // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
+                    importExportStoragePermissionDialogFragment.show(getFragmentManager(), getString(R.string.storage_permission));
+                } else {  // Show the permission request directly.
+                    // Request the storage permission.  The export will be run when it finishes.
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXPORT_REQUEST_CODE);
+                }
+            }
+        }
+    }
+
+    public void importBrowse(View view) {
+        // Create the file picker intent.
+        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+
+        // Set the intent MIME type to include all files.
+        intent.setType("*/*");
+
+        // Set the initial directory if API >= 26.
+        if (Build.VERSION.SDK_INT >= 26) {
+            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
+        }
+
+        // Specify that a file that can be opened is requested.
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+        // Launch the file picker.
+        startActivityForResult(intent, IMPORT_FILE_PICKER_REQUEST_CODE);
+    }
+
+    public void onClickImport(View view) {
+        // Check to see if the storage permission has been granted.
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // Storage permission granted.
+            // Import the settings.
+            importSettings();
+        } else {  // Storage permission not granted.
+            // Get a handle for the import file EditText.
+            EditText importFileEditText = findViewById(R.id.import_file_edittext);
+
+            // Get the import file string.
+            String importFileString = importFileEditText.getText().toString();
+
+            // Get the external private directory `File`.
+            File externalPrivateDirectoryFile = getApplicationContext().getExternalFilesDir(null);
+
+            // Remove the lint error below that `File` might be null.
+            assert externalPrivateDirectoryFile != null;
+
+            // Get the external private directory string.
+            String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
+
+            // Check to see if the import file path is in the external private directory.
+            if (importFileString.startsWith(externalPrivateDirectory)) {  // The import path is in the external private directory.
+                // Import the settings.
+                importSettings();
+            } else {  // The import path is in a public directory.
+                // Check if the user has previously denied the storage permission.
+                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
+                    // Instantiate the storage permission alert dialog and set the type to IMPORT_SETTINGS.
+                    DialogFragment importExportStoragePermissionDialogFragment = ImportExportStoragePermissionDialog.type(ImportExportStoragePermissionDialog.IMPORT_SETTINGS);
+
+                    // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
+                    importExportStoragePermissionDialogFragment.show(getFragmentManager(), getString(R.string.storage_permission));
+                } else {  // Show the permission request directly.
+                    // Request the storage permission.  The export will be run when it finishes.
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, IMPORT_REQUEST_CODE);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        // Don't do anything if the user pressed back from the file picker.
+        if (resultCode == Activity.RESULT_OK) {
+            // Run the commands for the specific request code.
+            switch (requestCode) {
+                case EXPORT_FILE_PICKER_REQUEST_CODE:
+                    // Get a handle for the export file EditText.
+                    EditText exportFileEditText = findViewById(R.id.export_file_edittext);
+
+                    // Get the selected export file.
+                    Uri exportUri = data.getData();
+
+                    // Remove the lint warning that the export URI might be null.
+                    assert exportUri != null;
+
+                    // Get the raw export path.
+                    String rawExportPath = exportUri.getPath();
+
+                    // Remove the warning that the raw export path might be null.
+                    assert rawExportPath != null;
+
+                    // Check to see if the rawExportPath includes a valid storage location.
+                    if (rawExportPath.contains(":")) {  // The path is valid.
+                        // Split the path into the initial content uri and the path information.
+                        String exportContentPath = rawExportPath.substring(0, rawExportPath.indexOf(":"));
+                        String exportFilePath = rawExportPath.substring(rawExportPath.indexOf(":") + 1);
+
+                        // Create the export path string.
+                        String exportPath;
+
+                        // Construct the export path.
+                        switch (exportContentPath) {
+                            // The documents home has a special content path.
+                            case "/document/home":
+                                exportPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + exportFilePath;
+                                break;
+
+                            // Everything else for the primary user should be in `/document/primary`.
+                            case "/document/primary":
+                                exportPath = Environment.getExternalStorageDirectory() + "/" + exportFilePath;
+                                break;
+
+                            // Just in case, catch everything else and place it in the external storage directory.
+                            default:
+                                exportPath = Environment.getExternalStorageDirectory() + "/" + exportFilePath;
+                                break;
+                        }
+
+                        // Set the export file URI as the text for the export file EditText.
+                        exportFileEditText.setText(exportPath);
+                    } else {  // The path is invalid.
+                        Snackbar.make(exportFileEditText, rawExportPath + " + " + getString(R.string.invalid_location), Snackbar.LENGTH_INDEFINITE).show();
+                    }
+                    break;
+
+                case IMPORT_FILE_PICKER_REQUEST_CODE:
+                    // Get a handle for the import file EditText.
+                    EditText importFileEditText = findViewById(R.id.import_file_edittext);
+
+                    // Get the selected import file.
+                    Uri importUri = data.getData();
+
+                    // Remove the lint warning that the import URI might be null.
+                    assert importUri != null;
+
+                    // Get the raw import path.
+                    String rawImportPath = importUri.getPath();
+
+                    // Remove the warning that the raw import path might be null.
+                    assert rawImportPath != null;
+
+                    // Check to see if the rawExportPath includes a valid storage location.
+                    if (rawImportPath.contains(":")) {  // The path is valid.
+                        // Split the path into the initial content uri and the path information.
+                        String importContentPath = rawImportPath.substring(0, rawImportPath.indexOf(":"));
+                        String importFilePath = rawImportPath.substring(rawImportPath.indexOf(":") + 1);
+
+                        // Create the export path string.
+                        String importPath;
+
+                        // Construct the export path.
+                        switch (importContentPath) {
+                            // The documents folder has a special content path.
+                            case "/document/home":
+                                importPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + importFilePath;
+                                break;
+
+                            // Everything else for the primary user should be in `/document/primary`.
+                            case "/document/primary":
+                                importPath = Environment.getExternalStorageDirectory() + "/" + importFilePath;
+                                break;
+
+                            // Just in case, catch everything else and place it in the external storage directory.
+                            default:
+                                importPath = Environment.getExternalStorageDirectory() + "/" + importFilePath;
+                                break;
+                        }
+
+                        // Set the export file URI as the text for the export file EditText.
+                        importFileEditText.setText(importPath);
+                    } else {  // The path is invalid.
+                        Snackbar.make(importFileEditText, rawImportPath + " + " + getString(R.string.invalid_location), Snackbar.LENGTH_INDEFINITE).show();
+                    }
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void onCloseImportExportStoragePermissionDialog(int type) {
+        // Request the storage permission based on the button that was pressed.
+        switch (type) {
+            case ImportExportStoragePermissionDialog.EXPORT_SETTINGS:
+                // Request the storage permission.  The export will be run when it finishes.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXPORT_REQUEST_CODE);
+                break;
+
+            case ImportExportStoragePermissionDialog.IMPORT_SETTINGS:
+                // Request the storage permission.  The import will be run when it finishes.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, IMPORT_REQUEST_CODE);
+                break;
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        switch (requestCode) {
+            case EXPORT_REQUEST_CODE:
+                // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
+                if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
+                    // Export the settings.
+                    exportSettings();
+                } else {  // The storage permission was not granted.
+                    // Get a handle for the export file EditText.
+                    EditText exportFileEditText = findViewById(R.id.export_file_edittext);
+
+                    // Display an error snackbar.
+                    Snackbar.make(exportFileEditText, getString(R.string.cannot_export), Snackbar.LENGTH_LONG).show();
+                }
+                break;
+
+            case IMPORT_REQUEST_CODE:
+                // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
+                if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
+                    // Import the settings.
+                    importSettings();
+                } else {  // The storage permission was not granted.
+                    // Get a handle for the import file EditText.
+                    EditText importFileEditText = findViewById(R.id.import_file_edittext);
+
+                    // Display an error snackbar.
+                    Snackbar.make(importFileEditText, getString(R.string.cannot_import), Snackbar.LENGTH_LONG).show();
+                }
+                break;
+        }
+    }
+
+    private void exportSettings() {
+        // Get a handle for the export file EditText.
+        EditText exportFileEditText = findViewById(R.id.export_file_edittext);
+
+        // Get the export file string.
+        String exportFileString = exportFileEditText.getText().toString();
+
+        // Set the export file.
+        File exportFile = new File(exportFileString);
+
+        // Instantiate the import export database helper.
+        ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
+
+        // Export the unencrypted file.
+        String exportStatus = importExportDatabaseHelper.exportUnencrypted(exportFile, getApplicationContext());
+
+        // Show a disposition snackbar.
+        if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
+            Snackbar.make(exportFileEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
+        } else {
+            Snackbar.make(exportFileEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
+        }
+    }
+
+    private void importSettings() {
+        // Get a handle for the import file EditText.
+        EditText importFileEditText = findViewById(R.id.import_file_edittext);
+
+        // Get the import file string.
+        String importFileString = importFileEditText.getText().toString();
+
+        // Set the import file.
+        File importFile = new File(importFileString);
+
+        // Instantiate the import export database helper.
+        ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
+
+        // Import the unencrypted file.
+        String importStatus = importExportDatabaseHelper.importUnencrypted(importFile, getApplicationContext());
+
+        // Respond to the import disposition.
+        if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {  // The import was successful.
+            // Create an intent to restart Privacy Browser.
+            Intent restartIntent = getParentActivityIntent();
+
+            // Assert that the intent is not null to remove the lint error below.
+            assert restartIntent != null;
+
+            // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
+            restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+            // Make it so.
+            startActivity(restartIntent);
+        } else {  // The import was not successful.
+            // Display a snack bar with the import error.
+            Snackbar.make(importFileEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
+        }
+    }
+}
\ No newline at end of file
index 230fc49..f2a422a 100644 (file)
@@ -202,13 +202,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // The request items are public static so they can be accessed by `BlockListHelper`, `RequestsArrayAdapter`, and `ViewRequestsDialog`.  They are also used in `onCreate()` and `onPrepareOptionsMenu()`.
     public static List<String[]> resourceRequests;
     public static String[] whiteListResultStringArray;
-    int blockedRequests;
-    int easyListBlockedRequests;
-    int easyPrivacyBlockedRequests;
-    int fanboysAnnoyanceListBlockedRequests;
-    int fanboysSocialBlockingListBlockedRequests;
-    int ultraPrivacyBlockedRequests;
-    int thirdPartyBlockedRequests;
+    private int blockedRequests;
+    private int easyListBlockedRequests;
+    private int easyPrivacyBlockedRequests;
+    private int fanboysAnnoyanceListBlockedRequests;
+    private int fanboysSocialBlockingListBlockedRequests;
+    private int ultraPrivacyBlockedRequests;
+    private int thirdPartyBlockedRequests;
 
     public final static int REQUEST_DISPOSITION = 0;
     public final static int REQUEST_URL = 1;
@@ -353,7 +353,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private MenuItem fanboysAnnoyanceListMenuItem;
     private MenuItem fanboysSocialBlockingListMenuItem;
     private MenuItem ultraPrivacyMenuItem;
-    private MenuItem blockAllThirdParyRequestsMenuItem;
+    private MenuItem blockAllThirdPartyRequestsMenuItem;
 
     // The blocklist variables are used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
     private boolean easyListEnabled;
@@ -404,7 +404,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `ignorePinnedSslCertificateForDomain` is used in `onCreate()`, `onSslMismatchProceed()`, and `applyDomainSettings()`.
     private boolean ignorePinnedSslCertificate;
 
-    // `orbotStatusBroadcastReciever` is used in `onCreate()` and `onDestroy()`.
+    // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
     private BroadcastReceiver orbotStatusBroadcastReceiver;
 
     // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyAppSettings()`.
@@ -469,7 +469,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `pinnedDomainSslCertificate` is used in `onCreate()` and `applyDomainSettings()`.
     private boolean pinnedDomainSslCertificate;
 
-    // `bookmarksDatabaseHelper` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
+    // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
+    // and `loadBookmarksFolder()`.
     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
 
     // `bookmarksListView` is used in `onCreate()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, and `loadBookmarksFolder()`.
@@ -478,7 +479,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // `bookmarksTitleTextView` is used in `onCreate()` and `loadBookmarksFolder()`.
     private TextView bookmarksTitleTextView;
 
-    // `bookmarksCursor` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
+    // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
     private Cursor bookmarksCursor;
 
     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
@@ -728,7 +729,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         // Show the `BannerAd` in the free flavor.
                         if (BuildConfig.FLAVOR.contentEquals("free")) {
                             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
-                            AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_id));
+                            AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
                         }
 
                         // Remove the translucent navigation bar flag if it is set.
@@ -949,10 +950,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
                             "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
                             "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;}'; parent.appendChild(style)})()", value -> {
-                                // Initialize a `Handler` to display `mainWebView`.
+                                // Initialize a handler to display `mainWebView`.
                                 Handler displayWebViewHandler = new Handler();
 
-                                // Setup a `Runnable` to display `mainWebView` after a delay to allow the CSS to be applied.
+                                // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
                                 Runnable displayWebViewRunnable = () -> {
                                     // Only display `mainWebView` if the progress bar is one.  This prevents the display of the `WebView` while it is still loading.
                                     if (progressBar.getVisibility() == View.GONE) {
@@ -960,7 +961,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                                     }
                                 };
 
-                                // Use `displayWebViewHandler` to delay the displaying of `mainWebView` for 500 milliseconds.
+                                // Displaying of `mainWebView` after 500 milliseconds.
                                 displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
                             });
                 }
@@ -1097,7 +1098,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Show the `BannerAd` in the free flavor.
                     if (BuildConfig.FLAVOR.contentEquals("free")) {
                         // Initialize the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
-                        AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.ad_id));
+                        AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
                     }
 
                     // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`.
@@ -1116,7 +1117,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Show the ad if this is the free flavor.
                 if (BuildConfig.FLAVOR.contentEquals("free")) {
                     // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
-                    AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_id));
+                    AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
                 }
             }
 
@@ -1154,7 +1155,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Show a dialog if the user has previously denied the permission.
                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
-                    // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
+                    // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
                     DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
 
                     // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
@@ -1163,7 +1164,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
                 }
-            } else {  // The WRITE_EXTERNAL_STORAGE permission has already been granted.
+            } else {  // The storage permission has already been granted.
                 // Get a handle for the download file alert dialog.
                 AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
 
@@ -1400,7 +1401,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     activity.runOnUiThread(() -> {
                         navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
                         blocklistsMenuItem.setTitle(getString(R.string.requests) + " - " + blockedRequests);
-                        blockAllThirdParyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
+                        blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
                     });
 
                     // Add the request to the log.
@@ -1526,7 +1527,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Add the request to the log because it hasn't been processed by any of the previous checks.
                 if (whiteListResultStringArray != null ) {  // The request was processed by a whitelist.
                     resourceRequests.add(whiteListResultStringArray);
-                } else {  // The request didn't match any blocklist entry.  Log it as a defult request.
+                } else {  // The request didn't match any blocklist entry.  Log it as a default request.
                     resourceRequests.add(new String[]{String.valueOf(REQUEST_DEFAULT), url});
                 }
 
@@ -1960,6 +1961,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Unregister the Orbot status broadcast receiver.
         this.unregisterReceiver(orbotStatusBroadcastReceiver);
 
+        // Close the bookmarks cursor and database.
+        bookmarksCursor.close();
+        bookmarksDatabaseHelper.close();
+
         // Run the default commands.
         super.onDestroy();
     }
@@ -1988,7 +1993,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
         fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
         ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
-        blockAllThirdParyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
+        blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
 
         // Only display third-party cookies if API >= 21
@@ -2070,7 +2075,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         fanboysAnnoyanceListMenuItem.setChecked(fanboysAnnoyanceListEnabled);
         fanboysSocialBlockingListMenuItem.setChecked(fanboysSocialBlockingListEnabled);
         ultraPrivacyMenuItem.setChecked(ultraPrivacyEnabled);
-        blockAllThirdParyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
+        blockAllThirdPartyRequestsMenuItem.setChecked(blockAllThirdPartyRequests);
         swipeToRefreshMenuItem.setChecked(swipeRefreshLayout.isEnabled());
         displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically());
         nightModeMenuItem.setChecked(nightMode);
@@ -2123,7 +2128,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         fanboysAnnoyanceListMenuItem.setTitle(fanboysAnnoyanceListBlockedRequests + " - " + getString(R.string.fanboys_annoyance_list));
         fanboysSocialBlockingListMenuItem.setTitle(fanboysSocialBlockingListBlockedRequests + " - " + getString(R.string.fanboys_social_blocking_list));
         ultraPrivacyMenuItem.setTitle(ultraPrivacyBlockedRequests + " - " + getString(R.string.ultraprivacy));
-        blockAllThirdParyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
+        blockAllThirdPartyRequestsMenuItem.setTitle(thirdPartyBlockedRequests + " - " + getString(R.string.block_all_third_party_requests));
 
         // Get the current user agent.
         String currentUserAgent = mainWebView.getSettings().getUserAgentString();
@@ -2908,6 +2913,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 startActivity(settingsIntent);
                 break;
 
+            case R.id.import_export:
+                // Launch the import/export activity.
+                Intent importExportIntent = new Intent (this, ImportExportActivity.class);
+                startActivity(importExportIntent);
+                break;
+
             case R.id.guide:
                 // Launch `GuideActivity`.
                 Intent guideIntent = new Intent(this, GuideActivity.class);
@@ -2921,6 +2932,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 break;
 
             case R.id.clearAndExit:
+                // Close the bookmarks cursor and database.
+                bookmarksCursor.close();
+                bookmarksDatabaseHelper.close();
+
                 // Get a handle for `sharedPreferences`.  `this` references the current context.
                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
 
@@ -3059,7 +3074,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Reload the ad for the free flavor if we not in full screen mode.
         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
-            AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_id));
+            AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
         }
 
         // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
@@ -3120,7 +3135,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                         // Show a dialog if the user has previously denied the permission.
                         if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
-                            // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
+                            // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
                             DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
 
                             // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
@@ -3205,7 +3220,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                         // Show a dialog if the user has previously denied the permission.
                         if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
-                            // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+                            // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
                             DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
 
                             // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
@@ -3264,7 +3279,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                         // Show a dialog if the user has previously denied the permission.
                         if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
-                            // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+                            // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
                             DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
 
                             // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
@@ -3417,7 +3432,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         switch (requestCode) {
             case DOWNLOAD_FILE_REQUEST_CODE:
                 // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
@@ -3997,7 +4012,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Show the `BannerAd` in the free flavor.
             if (BuildConfig.FLAVOR.contentEquals("free")) {
                 // Initialize the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
-                AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.ad_id));
+                AdHelper.initializeAds(findViewById(R.id.adview), getApplicationContext(), getFragmentManager(), getString(R.string.google_app_id), getString(R.string.ad_unit_id));
             }
 
             // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`.
@@ -4364,7 +4379,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent));
             }
 
-            // Close `domainsDatabaseHelper`.
+            // Close the domains database helper.
             domainsDatabaseHelper.close();
 
             // Remove the `onTheFlyDisplayImagesSet` flag and set the display webpage images mode.  `true` indicates that custom domain settings are applied.
index 4b58053..a62708a 100644 (file)
@@ -45,7 +45,7 @@ import java.util.List;
 
 public class RequestsActivity extends AppCompatActivity implements ViewRequestDialog.ViewRequestListener {
     // The list view is used in `onCreate()` and `launchViewRequestDialog()`.
-    private ListView resourceRequestsListView;
+    private ListView requestsListView;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -68,12 +68,12 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
         setContentView(R.layout.requests_coordinatorlayout);
 
         // Use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21.
-        Toolbar blockListsAppBar = findViewById(R.id.blocklists_toolbar);
-        setSupportActionBar(blockListsAppBar);
+        Toolbar requestsAppBar = findViewById(R.id.requests_toolbar);
+        setSupportActionBar(requestsAppBar);
 
         // Get a handle for the app bar and the list view.
         ActionBar appBar = getSupportActionBar();
-        resourceRequestsListView = findViewById(R.id.resource_requests_listview);
+        requestsListView = findViewById(R.id.requests_listview);
 
         // Remove the incorrect lint warning that `appBar` might be null.
         assert appBar != null;
@@ -165,7 +165,7 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
                         ArrayAdapter<String[]> allResourceRequestsArrayAdapter = new RequestsArrayAdapter(getApplicationContext(), allResourceRequests);
 
                         // Display the adapter in the list view.
-                        resourceRequestsListView.setAdapter(allResourceRequestsArrayAdapter);
+                        requestsListView.setAdapter(allResourceRequestsArrayAdapter);
                         break;
 
                     case 1:  // Default requests.
@@ -173,7 +173,7 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
                         ArrayAdapter<String[]> defaultResourceRequestsArrayAdapter = new RequestsArrayAdapter(getApplicationContext(), defaultResourceRequests);
 
                         // Display the adapter in the list view.
-                        resourceRequestsListView.setAdapter(defaultResourceRequestsArrayAdapter);
+                        requestsListView.setAdapter(defaultResourceRequestsArrayAdapter);
                         break;
 
                     case 2:  // Allowed requests.
@@ -181,7 +181,7 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
                         ArrayAdapter<String[]> allowedResourceRequestsArrayAdapter = new RequestsArrayAdapter(getApplicationContext(), allowedResourceRequests);
 
                         // Display the adapter in the list view.
-                        resourceRequestsListView.setAdapter(allowedResourceRequestsArrayAdapter);
+                        requestsListView.setAdapter(allowedResourceRequestsArrayAdapter);
                         break;
 
                     case 3:  // Third-party requests.
@@ -189,7 +189,7 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
                         ArrayAdapter<String[]> thirdPartyResourceRequestsArrayAdapter = new RequestsArrayAdapter(getApplicationContext(), thirdPartyResourceRequests);
 
                         //Display the adapter in the list view.
-                        resourceRequestsListView.setAdapter(thirdPartyResourceRequestsArrayAdapter);
+                        requestsListView.setAdapter(thirdPartyResourceRequestsArrayAdapter);
                         break;
 
                     case 4:  // Blocked requests.
@@ -197,7 +197,7 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
                         ArrayAdapter<String[]> blockedResourceRequestsArrayAdapter = new RequestsArrayAdapter(getApplicationContext(), blockedResourceRequests);
 
                         // Display the adapter in the list view.
-                        resourceRequestsListView.setAdapter(blockedResourceRequestsArrayAdapter);
+                        requestsListView.setAdapter(blockedResourceRequestsArrayAdapter);
                         break;
                 }
             }
@@ -212,10 +212,10 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
         ArrayAdapter<String[]> resourceRequestsArrayAdapter = new RequestsArrayAdapter(getApplicationContext(), allResourceRequests);
 
         // Populate the list view with the resource requests adapter.
-        resourceRequestsListView.setAdapter(resourceRequestsArrayAdapter);
+        requestsListView.setAdapter(resourceRequestsArrayAdapter);
 
         // Listen for taps on entries in the list view.
-        resourceRequestsListView.setOnItemClickListener((AdapterView<?> parent, View view, int position, long id) -> {
+        requestsListView.setOnItemClickListener((AdapterView<?> parent, View view, int position, long id) -> {
             // Display the view request dialog.  The list view is 0 based, so the position must be incremented by 1.
             launchViewRequestDialog(position + 1);
         });
@@ -235,10 +235,10 @@ public class RequestsActivity extends AppCompatActivity implements ViewRequestDi
 
     private void launchViewRequestDialog(int id) {
         // Determine if this is the last request in the list.
-        boolean isLastRequest = (id == resourceRequestsListView.getCount());
+        boolean isLastRequest = (id == requestsListView.getCount());
 
         // Get the string array for the selected resource request.  The resource requests list view is zero based.
-        String[] selectedRequestStringArray = (String[]) resourceRequestsListView.getItemAtPosition(id - 1);
+        String[] selectedRequestStringArray = (String[]) requestsListView.getItemAtPosition(id - 1);
 
         // Remove the warning that `selectedRequest` might be null.
         assert selectedRequestStringArray != null;
index 117f000..a232058 100644 (file)
@@ -31,10 +31,11 @@ import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
 
 public class DownloadLocationPermissionDialog extends DialogFragment {
+    // The constants are used to differentiate between the two download types.
     public static final int DOWNLOAD_FILE = 1;
     public static final int DOWNLOAD_IMAGE = 2;
 
-    // `downloadLocationPermissionDialogListener` is used in `onAttach()` and `onCreateDialog()`.
+    // The listener is used in `onAttach()` and `onCreateDialog()`.
     private DownloadLocationPermissionDialogListener downloadLocationPermissionDialogListener;
 
     // The public interface is used to send information back to the parent activity.
@@ -47,7 +48,7 @@ public class DownloadLocationPermissionDialog extends DialogFragment {
         // Run the default commands.
         super.onAttach(context);
 
-        // Get a handle for `DownloadLocationPermissionDialogListener` from the launching context.
+        // Get a handle for the listener from the launching context.
         downloadLocationPermissionDialogListener = (DownloadLocationPermissionDialogListener) context;
     }
 
@@ -56,9 +57,9 @@ public class DownloadLocationPermissionDialog extends DialogFragment {
         Bundle argumentsBundle = new Bundle();
 
         // Store the download type in the bundle.
-        argumentsBundle.putInt("Download_Type", type);
+        argumentsBundle.putInt("download_type", type);
 
-        // Add the arguments bundle to this instance of `DownloadLocationPermissionDialog`.
+        // Add the arguments bundle to this instance of the dialog.
         DownloadLocationPermissionDialog thisDownloadLocationPermissionDialog = new DownloadLocationPermissionDialog();
         thisDownloadLocationPermissionDialog.setArguments(argumentsBundle);
         return thisDownloadLocationPermissionDialog;
@@ -66,8 +67,8 @@ public class DownloadLocationPermissionDialog extends DialogFragment {
 
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        // Store the download type in the local class variable.
-        int downloadType = getArguments().getInt("Download_Type");
+        // Store the download type in a local variable.
+        int downloadType = getArguments().getInt("download_type");
 
         // Use a builder to create the alert dialog.
         AlertDialog.Builder dialogBuilder;
@@ -81,19 +82,19 @@ public class DownloadLocationPermissionDialog extends DialogFragment {
             dialogBuilder.setIcon(R.drawable.downloads_light);
         }
 
-        // Set an `onClick` listener on the negative button.  Using `null` as the listener closes the dialog without doing anything else.
-        dialogBuilder.setNegativeButton(R.string.ok, (DialogInterface dialog, int which) -> {
-            // Inform the parent activity that the dialog was closed.
-            downloadLocationPermissionDialogListener.onCloseDownloadLocationPermissionDialog(downloadType);
-        });
-
         // Set the title.
         dialogBuilder.setTitle(R.string.download_location);
 
         // Set the text.
         dialogBuilder.setMessage(R.string.download_location_message);
 
-        // Create an alert dialog from the alert dialog builder.
+        // Set an `onClick` listener on the negative button.
+        dialogBuilder.setNegativeButton(R.string.ok, (DialogInterface dialog, int which) -> {
+            // Inform the parent activity that the dialog was closed.
+            downloadLocationPermissionDialogListener.onCloseDownloadLocationPermissionDialog(downloadType);
+        });
+
+        // Create an alert dialog from the builder.
         final AlertDialog alertDialog = dialogBuilder.create();
 
         // Disable screenshots if not allowed.
@@ -105,7 +106,7 @@ public class DownloadLocationPermissionDialog extends DialogFragment {
             alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
         }
 
-        // `onCreateDialog` requires the return of an `AlertDialog`.
+        // `onCreateDialog()` requires the return of an `AlertDialog`.
         return alertDialog;
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/ImportExportStoragePermissionDialog.java b/app/src/main/java/com/stoutner/privacybrowser/dialogs/ImportExportStoragePermissionDialog.java
new file mode 100644 (file)
index 0000000..d5b9390
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright © 2018 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.activities.MainWebViewActivity;
+
+public class ImportExportStoragePermissionDialog extends DialogFragment {
+    // The constants are used to differentiate between the two commands.
+    public static final int EXPORT_SETTINGS = 1;
+    public static final int IMPORT_SETTINGS = 2;
+
+    // The listener is used in `onAttach()` and `onCreateDialog()`.
+    private ImportExportStoragePermissionDialogListener importExportStoragePermissionDialogListener;
+
+    // The public interface is used to send information back to the parent activity.
+    public interface ImportExportStoragePermissionDialogListener {
+        void onCloseImportExportStoragePermissionDialog(int type);
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        // Run the default commands.
+        super.onAttach(context);
+
+        // Get a handle for the listener from the launching context.
+        importExportStoragePermissionDialogListener = (ImportExportStoragePermissionDialogListener) context;
+    }
+
+    public static ImportExportStoragePermissionDialog type(int type) {
+        // Create an arguments bundle.
+        Bundle argumentsBundle = new Bundle();
+
+        // Store the download type in the bundle.
+        argumentsBundle.putInt("type", type);
+
+        // Add the arguments bundle to this instance of the dialog.
+        ImportExportStoragePermissionDialog thisImportExportStoragePermissionDialog = new ImportExportStoragePermissionDialog();
+        thisImportExportStoragePermissionDialog.setArguments(argumentsBundle);
+        return thisImportExportStoragePermissionDialog;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        // Store the download type in a local variable.
+        int type = getArguments().getInt("type");
+
+        // Use a builder to create the alert dialog.
+        AlertDialog.Builder dialogBuilder;
+
+        // Set the style and the icon according to the theme.
+        if (MainWebViewActivity.darkTheme) {
+            dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
+            dialogBuilder.setIcon(R.drawable.import_export_dark);
+        } else {
+            dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogLight);
+            dialogBuilder.setIcon(R.drawable.import_export_light);
+        }
+
+        // Set the title.
+        dialogBuilder.setTitle(R.string.storage_permission);
+
+        // Set the text.
+        dialogBuilder.setMessage(R.string.storage_permission_message);
+
+        // Set an `onClick` listener on the negative button.
+        dialogBuilder.setNegativeButton(R.string.ok, (DialogInterface dialog, int which) -> {
+            // Inform the parent activity that the dialog was closed.
+            importExportStoragePermissionDialogListener.onCloseImportExportStoragePermissionDialog(type);
+        });
+
+        // Create an alert dialog from the builder.
+        final AlertDialog alertDialog = dialogBuilder.create();
+
+        // Disable screenshots if not allowed.
+        if (!MainWebViewActivity.allowScreenshots) {
+            // Remove the warning below that `getWindow()` might be null.
+            assert alertDialog.getWindow() != null;
+
+            // Disable screenshots.
+            alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
+        }
+
+        // `onCreateDialog()` requires the return of an `AlertDialog`.
+        return alertDialog;
+    }
+}
index e152b14..ac70f2c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2016-2017 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2016-2018 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -28,8 +28,8 @@ import android.database.sqlite.SQLiteOpenHelper;
 
 public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
     private static final int SCHEMA_VERSION = 1;
-    private static final String BOOKMARKS_DATABASE = "bookmarks.db";
-    private static final String BOOKMARKS_TABLE = "bookmarks";
+    static final String BOOKMARKS_DATABASE = "bookmarks.db";
+    static final String BOOKMARKS_TABLE = "bookmarks";
 
     public static final String _ID = "_id";
     public static final String BOOKMARK_NAME = "bookmarkname";
@@ -39,6 +39,15 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
     public static final String IS_FOLDER = "isfolder";
     public static final String FAVORITE_ICON = "favoriteicon";
 
+    static final String CREATE_BOOKMARKS_TABLE = "CREATE TABLE " + BOOKMARKS_TABLE + " (" +
+            _ID + " INTEGER PRIMARY KEY, " +
+            BOOKMARK_NAME + " TEXT, " +
+            BOOKMARK_URL + " TEXT, " +
+            PARENT_FOLDER + " TEXT, " +
+            DISPLAY_ORDER + " INTEGER, " +
+            IS_FOLDER + " BOOLEAN, " +
+            FAVORITE_ICON + " BLOB)";
+
     // Initialize the database.  The lint warnings for the unused parameters are suppressed.
     public BookmarksDatabaseHelper(Context context, @SuppressWarnings("UnusedParameters") String name, SQLiteDatabase.CursorFactory cursorFactory, @SuppressWarnings("UnusedParameters") int version) {
         super(context, BOOKMARKS_DATABASE, cursorFactory, SCHEMA_VERSION);
@@ -46,17 +55,7 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
 
     @Override
     public void onCreate(SQLiteDatabase bookmarksDatabase) {
-        // Setup the SQL string to create the `bookmarks` table.
-        final String CREATE_BOOKMARKS_TABLE = "CREATE TABLE " + BOOKMARKS_TABLE + " (" +
-                _ID + " integer primary key, " +
-                BOOKMARK_NAME + " text, " +
-                BOOKMARK_URL + " text, " +
-                PARENT_FOLDER + " text, " +
-                DISPLAY_ORDER + " integer, " +
-                IS_FOLDER + " boolean, " +
-                FAVORITE_ICON + " blob);";
-
-        // Create the `bookmarks` table.
+        // Create the bookmarks table.
         bookmarksDatabase.execSQL(CREATE_BOOKMARKS_TABLE);
     }
 
@@ -67,7 +66,7 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
 
     // Create a bookmark.
     public void createBookmark(String bookmarkName, String bookmarkURL, String parentFolder, int displayOrder, byte[] favoriteIcon) {
-        // We need to store the bookmark data in a `ContentValues`.
+        // Store the bookmark data in a `ContentValues`.
         ContentValues bookmarkContentValues = new ContentValues();
 
         // ID is created automatically.
@@ -88,6 +87,18 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
         bookmarksDatabase.close();
     }
 
+    // Create a bookmark from content values.
+    void createBookmark(ContentValues contentValues) {
+        // Get a writable database.
+        SQLiteDatabase bookmarksDatabase = this.getWritableDatabase();
+
+        // Insert a new row.  The second argument is `null`, which makes it so that a completely null row cannot be created.
+        bookmarksDatabase.insert(BOOKMARKS_TABLE, null, contentValues);
+
+        // Close the database handle.
+        bookmarksDatabase.close();
+    }
+
     // Create a folder.
     public void createFolder(String folderName, String parentFolder, byte[] favoriteIcon) {
         ContentValues bookmarkContentValues = new ContentValues();
@@ -272,11 +283,10 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
         // Get a readable database handle.
         SQLiteDatabase bookmarksDatabase = this.getReadableDatabase();
 
-        // Get everything in `BOOKMARKS_TABLE`.
+        // Get everything in in the bookmarks table.
         final String GET_ALL_BOOKMARKS = "SELECT * FROM " + BOOKMARKS_TABLE;
 
-        // Return the results as a Cursor.  The second argument is `null` because there are no selectionArgs.
-        // We can't close the Cursor because we need to use it in the parent activity.
+        // Return the results as a Cursor.  The second argument is `null` because there are no selectionArgs.  The Cursor cannot be closed because it is used in the parent activity.
         return bookmarksDatabase.rawQuery(GET_ALL_BOOKMARKS, null);
     }
 
@@ -288,11 +298,11 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
         // SQL escape `folderName`.
         folderName = DatabaseUtils.sqlEscapeString(folderName);
 
-        // Get everything in the `BOOKMARKS_TABLE` with `folderName` as the `PARENT_FOLDER`.
+        // Get everything in the bookmarks table with `folderName` as the `PARENT_FOLDER`.
         final String GET_ALL_BOOKMARKS = "SELECT * FROM " + BOOKMARKS_TABLE +
                 " WHERE " + PARENT_FOLDER + " = " + folderName;
 
-        // Return the results as a `Cursor`.  The second argument is `null` because there are no `selectionArgs`.  We can't close the `Cursor` because we need to use it in the parent activity.
+        // Return the results as a `Cursor`.  The second argument is `null` because there are no `selectionArgs`.  The Cursor cannot be closed because it is used in the parent activity.
         return bookmarksDatabase.rawQuery(GET_ALL_BOOKMARKS, null);
     }
 
@@ -309,7 +319,7 @@ public class BookmarksDatabaseHelper extends SQLiteOpenHelper {
                 " WHERE " + PARENT_FOLDER + " = " + folderName +
                 " ORDER BY " + DISPLAY_ORDER + " ASC";
 
-        // Return the results as a `Cursor`.  The second argument is `null` because there are no `selectionArgs`.  We can't close the `Cursor` because we need to use it in the parent activity.
+        // Return the results as a `Cursor`.  The second argument is `null` because there are no `selectionArgs`.  The Cursor cannot be closed because it is used in the parent activity.
         return bookmarksDatabase.rawQuery(GET_ALL_BOOKMARKS, null);
     }
 
index 0af0390..2ed01cb 100644 (file)
@@ -29,8 +29,8 @@ import android.preference.PreferenceManager;
 
 public class DomainsDatabaseHelper extends SQLiteOpenHelper {
     private static final int SCHEMA_VERSION = 8;
-    private static final String DOMAINS_DATABASE = "domains.db";
-    private static final String DOMAINS_TABLE = "domains";
+    static final String DOMAINS_DATABASE = "domains.db";
+    static final String DOMAINS_TABLE = "domains";
 
     public static final String _ID = "_id";
     public static final String DOMAIN_NAME = "domainname";
@@ -75,6 +75,35 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
     public static final int DISPLAY_WEBPAGE_IMAGES_ENABLED = 1;
     public static final int DISPLAY_WEBPAGE_IMAGES_DISABLED = 2;
 
+    static final String CREATE_DOMAINS_TABLE = "CREATE TABLE " + DOMAINS_TABLE + " (" +
+            _ID + " INTEGER PRIMARY KEY, " +
+            DOMAIN_NAME + " TEXT, " +
+            ENABLE_JAVASCRIPT + " BOOLEAN, " +
+            ENABLE_FIRST_PARTY_COOKIES + " BOOLEAN, " +
+            ENABLE_THIRD_PARTY_COOKIES + " BOOLEAN, " +
+            ENABLE_DOM_STORAGE + " BOOLEAN, " +
+            ENABLE_FORM_DATA + " BOOLEAN, " +
+            ENABLE_EASYLIST + " BOOLEAN, " +
+            ENABLE_EASYPRIVACY + " BOOLEAN, " +
+            ENABLE_FANBOYS_ANNOYANCE_LIST + " BOOLEAN, " +
+            ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST + " BOOLEAN, " +
+            ENABLE_ULTRAPRIVACY + " BOOLEAN, " +
+            BLOCK_ALL_THIRD_PARTY_REQUESTS + " BOOLEAN, " +
+            USER_AGENT + " TEXT, " +
+            FONT_SIZE + " INTEGER, " +
+            SWIPE_TO_REFRESH + " INTEGER, " +
+            NIGHT_MODE + " INTEGER, " +
+            DISPLAY_IMAGES + " INTEGER, " +
+            PINNED_SSL_CERTIFICATE + " BOOLEAN, " +
+            SSL_ISSUED_TO_COMMON_NAME + " TEXT, " +
+            SSL_ISSUED_TO_ORGANIZATION + " TEXT, " +
+            SSL_ISSUED_TO_ORGANIZATIONAL_UNIT + " TEXT, " +
+            SSL_ISSUED_BY_COMMON_NAME + " TEXT, " +
+            SSL_ISSUED_BY_ORGANIZATION + " TEXT, " +
+            SSL_ISSUED_BY_ORGANIZATIONAL_UNIT + " TEXT, " +
+            SSL_START_DATE + " INTEGER, " +
+            SSL_END_DATE + " INTEGER)";
+
     private final Context appContext;
 
     // Initialize the database.  The lint warnings for the unused parameters are suppressed.
@@ -87,37 +116,7 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
 
     @Override
     public void onCreate(SQLiteDatabase domainsDatabase) {
-        // Setup the SQL string to create the `domains` table.
-        String CREATE_DOMAINS_TABLE = "CREATE TABLE " + DOMAINS_TABLE + " (" +
-                _ID + " INTEGER PRIMARY KEY, " +
-                DOMAIN_NAME + " TEXT, " +
-                ENABLE_JAVASCRIPT + " BOOLEAN, " +
-                ENABLE_FIRST_PARTY_COOKIES + " BOOLEAN, " +
-                ENABLE_THIRD_PARTY_COOKIES + " BOOLEAN, " +
-                ENABLE_DOM_STORAGE + " BOOLEAN, " +
-                ENABLE_FORM_DATA + " BOOLEAN, " +
-                ENABLE_EASYLIST + " BOOLEAN, " +
-                ENABLE_EASYPRIVACY + " BOOLEAN, " +
-                ENABLE_FANBOYS_ANNOYANCE_LIST + " BOOLEAN, " +
-                ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST + " BOOLEAN, " +
-                ENABLE_ULTRAPRIVACY + " BOOLEAN, " +
-                BLOCK_ALL_THIRD_PARTY_REQUESTS + " BOOLEAN, " +
-                USER_AGENT + " TEXT, " +
-                FONT_SIZE + " INTEGER, " +
-                SWIPE_TO_REFRESH + " INTEGER, " +
-                NIGHT_MODE + " INTEGER, " +
-                DISPLAY_IMAGES + " INTEGER, " +
-                PINNED_SSL_CERTIFICATE + " BOOLEAN, " +
-                SSL_ISSUED_TO_COMMON_NAME + " TEXT, " +
-                SSL_ISSUED_TO_ORGANIZATION + " TEXT, " +
-                SSL_ISSUED_TO_ORGANIZATIONAL_UNIT + " TEXT, " +
-                SSL_ISSUED_BY_COMMON_NAME + " TEXT, " +
-                SSL_ISSUED_BY_ORGANIZATION + " TEXT, " +
-                SSL_ISSUED_BY_ORGANIZATIONAL_UNIT + " TEXT, " +
-                SSL_START_DATE + " INTEGER, " +
-                SSL_END_DATE + " INTEGER);";
-
-        // Make it so.
+        // Create the domains table.
         domainsDatabase.execSQL(CREATE_DOMAINS_TABLE);
     }
 
@@ -213,11 +212,19 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         }
     }
 
+    Cursor getCompleteCursorOrderedByDomain() {
+        // Get a readable database handle.
+        SQLiteDatabase domainsDatabase = this.getReadableDatabase();
+
+        // Return everything in the domains table ordered by the domain name.  The second argument is `null` because there are no `selectionArgs`.
+        return domainsDatabase.rawQuery("SELECT * FROM " + DOMAINS_TABLE + " ORDER BY " + DOMAIN_NAME + " ASC", null);
+    }
+
     public Cursor getDomainNameCursorOrderedByDomain() {
         // Get a readable database handle.
         SQLiteDatabase domainsDatabase = this.getReadableDatabase();
 
-        // Get everything in `DOMAINS_TABLE` ordered by `DOMAIN_NAME`.
+        // Get everything in the domains table ordered by the domain name.
         String GET_CURSOR_ORDERED_BY_DOMAIN = "SELECT " + _ID + ", " + DOMAIN_NAME +
                 " FROM " + DOMAINS_TABLE +
                 " ORDER BY " + DOMAIN_NAME + " ASC";
@@ -313,6 +320,17 @@ public class DomainsDatabaseHelper extends SQLiteOpenHelper {
         return newDomainDatabaseId;
     }
 
+    void addDomain(ContentValues contentValues) {
+        // Get a writable database handle.
+        SQLiteDatabase domainsDatabase = this.getWritableDatabase();
+
+        // Add the new domain.
+        domainsDatabase.insert(DOMAINS_TABLE, null, contentValues);
+
+        // Close the database handle.
+        domainsDatabase.close();
+    }
+
     public void updateDomainExceptCertificate(int databaseId, String domainName, boolean javaScriptEnabled, boolean firstPartyCookiesEnabled, boolean thirdPartyCookiesEnabled, boolean domStorageEnabled,
                                               boolean formDataEnabled, boolean easyListEnabled, boolean easyPrivacyEnabled, boolean fanboysAnnoyanceEnabled, boolean fanboysSocialBlockingEnabled,
                                               boolean ultraPrivacyEnabled, boolean blockAllThirdPartyRequests, String userAgent, int fontSize, int swipeToRefresh, int nightMode, int displayImages,
diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java b/app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java
new file mode 100644 (file)
index 0000000..3388700
--- /dev/null
@@ -0,0 +1,498 @@
+/*
+ * Copyright © 2018 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.helpers;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.preference.PreferenceManager;
+
+import java.io.File;
+
+public class ImportExportDatabaseHelper {
+    public static final String EXPORT_SUCCESSFUL = "Export Successful";
+    public static final String IMPORT_SUCCESSFUL = "Import Successful";
+
+    private static final int SCHEMA_VERSION = 1;
+    private static final String PREFERENCES_TABLE = "preferences";
+
+    // The preferences constants.
+    private static final String _ID = "_id";
+    private static final String JAVASCRIPT = "javascript";
+    private static final String FIRST_PARTY_COOKIES = "first_party_cookies";
+    private static final String THIRD_PARTY_COOKIES = "third_party_cookies";
+    private static final String DOM_STORAGE = "dom_storage";
+    private static final String SAVE_FORM_DATA = "save_form_data";
+    private static final String USER_AGENT = "user_agent";
+    private static final String CUSTOM_USER_AGENT = "custom_user_agent";
+    private static final String INCOGNITO_MODE = "incognito_mode";
+    private static final String DO_NOT_TRACK = "do_not_track";
+    private static final String ALLOW_SCREENSHOTS = "allow_screenshots";
+    private static final String EASYLIST = "easylist";
+    private static final String EASYPRIVACY = "easyprivacy";
+    private static final String FANBOYS_ANNOYANCE_LIST = "fanboys_annoyance_list";
+    private static final String FANBOYS_SOCIAL_BLOCKING_LIST = "fanboys_social_blocking_list";
+    private static final String ULTRAPRIVACY = "ultraprivacy";
+    private static final String BLOCK_ALL_THIRD_PARTY_REQUESTS = "block_all_third_party_requests";
+    private static final String PROXY_THROUGH_ORBOT = "proxy_through_orbot";
+    private static final String TOR_HOMEPAGE = "tor_homepage";
+    private static final String TOR_SEARCH = "tor_search";
+    private static final String TOR_SEARCH_CUSTOM_URL = "tor_search_custom_url";
+    private static final String SEARCH = "search";
+    private static final String SEARCH_CUSTOM_URL = "search_custom_url";
+    private static final String FULL_SCREEN_BROWSING_MODE = "full_screen_browsing_mode";
+    private static final String HIDE_SYSTEM_BARS = "hide_system_bars";
+    private static final String TRANSLUCENT_NAVIGATION_BAR = "translucent_navigation_bar";
+    private static final String CLEAR_EVERYTHING = "clear_everything";
+    private static final String CLEAR_COOKIES = "clear_cookies";
+    private static final String CLEAR_DOM_STORAGE = "clear_dom_storage";
+    private static final String CLEAR_FORM_DATA = "clear_form_data";
+    private static final String CLEAR_CACHE = "clear_cache";
+    private static final String HOMEPAGE = "homepage";
+    private static final String DEFAULT_FONT_SIZE = "default_font_size";
+    private static final String SWIPE_TO_REFRESH = "swipe_to_refresh";
+    private static final String DISPLAY_ADDITIONAL_APP_BAR_ICONS = "display_additional_app_bar_icons";
+    private static final String DARK_THEME = "dark_theme";
+    private static final String NIGHT_MODE = "night_mode";
+    private static final String DISPLAY_WEBPAGE_IMAGES = "display_webpage_images";
+
+    public String exportUnencrypted(File databaseFile, Context context) {
+        try {
+            // Delete the current file if it exists.
+            if (databaseFile.exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                databaseFile.delete();
+            }
+
+            // Create the export database.
+            SQLiteDatabase exportDatabase = SQLiteDatabase.openOrCreateDatabase(databaseFile, null);
+
+            // Set the export database version number.
+            exportDatabase.setVersion(SCHEMA_VERSION);
+
+            // Create the export database domains table.
+            exportDatabase.execSQL(DomainsDatabaseHelper.CREATE_DOMAINS_TABLE);
+
+            // Open the domains database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+            DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
+
+            // Get a full domains database cursor.
+            Cursor domainsCursor = domainsDatabaseHelper.getCompleteCursorOrderedByDomain();
+
+            // Move to the first domain.
+            domainsCursor.moveToFirst();
+
+            // Copy the data from the domains cursor into the export database.
+            for (int i = 0; i < domainsCursor.getCount(); i++) {
+                // Extract the record from the cursor and store the data in a ContentValues.
+                ContentValues domainsContentValues = new ContentValues();
+                domainsContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)));
+                domainsContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)));
+                domainsContentValues.put(DomainsDatabaseHelper.USER_AGENT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)));
+                domainsContentValues.put(DomainsDatabaseHelper.FONT_SIZE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)));
+                domainsContentValues.put(DomainsDatabaseHelper.NIGHT_MODE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)));
+                domainsContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)));
+                domainsContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT, domainsCursor.getString(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, domainsCursor.getLong(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, domainsCursor.getLong(domainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+
+                // Insert the record into the export database.
+                exportDatabase.insert(DomainsDatabaseHelper.DOMAINS_TABLE, null, domainsContentValues);
+
+                // Advance to the next record.
+                domainsCursor.moveToNext();
+            }
+
+            // Close the domains database.
+            domainsCursor.close();
+            domainsDatabaseHelper.close();
+
+
+            // Create the export database bookmarks table.
+            exportDatabase.execSQL(BookmarksDatabaseHelper.CREATE_BOOKMARKS_TABLE);
+
+            // Open the bookmarks database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
+            BookmarksDatabaseHelper bookmarksDatabaseHelper = new BookmarksDatabaseHelper(context, null, null, 0);
+
+            // Get a full bookmarks cursor.
+            Cursor bookmarksCursor = bookmarksDatabaseHelper.getAllBookmarksCursor();
+
+            // Move to the first bookmark.
+            bookmarksCursor.moveToFirst();
+
+            // Copy the data from the bookmarks cursor into the export database.
+            for (int i = 0; i < bookmarksCursor.getCount(); i++) {
+                // Extract the record from the cursor and store the data in a ContentValues.
+                ContentValues bookmarksContentValues = new ContentValues();
+                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, bookmarksCursor.getString(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, bookmarksCursor.getBlob(bookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)));
+
+                // Insert the record into the export database.
+                exportDatabase.insert(BookmarksDatabaseHelper.BOOKMARKS_TABLE, null, bookmarksContentValues);
+
+                // Advance to the next record.
+                bookmarksCursor.moveToNext();
+            }
+
+            // Close the bookmarks database.
+            bookmarksCursor.close();
+            bookmarksDatabaseHelper.close();
+
+
+            // Prepare the preferences table SQL creation string.
+            String CREATE_PREFERENCES_TABLE = "CREATE TABLE " + PREFERENCES_TABLE + " (" +
+                    _ID + " INTEGER PRIMARY KEY, " +
+                    JAVASCRIPT + " BOOLEAN, " +
+                    FIRST_PARTY_COOKIES + " BOOLEAN, " +
+                    THIRD_PARTY_COOKIES + " BOOLEAN, " +
+                    DOM_STORAGE + " BOOLEAN, " +
+                    SAVE_FORM_DATA + " BOOLEAN, " +
+                    USER_AGENT + " TEXT, " +
+                    CUSTOM_USER_AGENT + " TEXT, " +
+                    INCOGNITO_MODE + " BOOLEAN, " +
+                    DO_NOT_TRACK + " BOOLEAN, " +
+                    ALLOW_SCREENSHOTS + " BOOLEAN, " +
+                    EASYLIST + " BOOLEAN, " +
+                    EASYPRIVACY + " BOOLEAN, " +
+                    FANBOYS_ANNOYANCE_LIST + " BOOLEAN, " +
+                    FANBOYS_SOCIAL_BLOCKING_LIST + " BOOLEAN, " +
+                    ULTRAPRIVACY + " BOOLEAN, " +
+                    BLOCK_ALL_THIRD_PARTY_REQUESTS + " BOOLEAN, " +
+                    PROXY_THROUGH_ORBOT + " BOOLEAN, " +
+                    TOR_HOMEPAGE + " TEXT, " +
+                    TOR_SEARCH + " TEXT, " +
+                    TOR_SEARCH_CUSTOM_URL + " TEXT, " +
+                    SEARCH + " TEXT, " +
+                    SEARCH_CUSTOM_URL + " TEXT, " +
+                    FULL_SCREEN_BROWSING_MODE + " BOOLEAN, " +
+                    HIDE_SYSTEM_BARS + " BOOLEAN, " +
+                    TRANSLUCENT_NAVIGATION_BAR + " BOOLEAN, " +
+                    CLEAR_EVERYTHING + " BOOLEAN, " +
+                    CLEAR_COOKIES + " BOOLEAN, " +
+                    CLEAR_DOM_STORAGE + " BOOLEAN, " +
+                    CLEAR_FORM_DATA + " BOOLEAN, " +
+                    CLEAR_CACHE + " BOOLEAN, " +
+                    HOMEPAGE + " TEXT, " +
+                    DEFAULT_FONT_SIZE + " TEXT, " +
+                    SWIPE_TO_REFRESH + " BOOLEAN, " +
+                    DISPLAY_ADDITIONAL_APP_BAR_ICONS + " BOOLEAN, " +
+                    DARK_THEME + " BOOLEAN, " +
+                    NIGHT_MODE + " BOOLEAN, " +
+                    DISPLAY_WEBPAGE_IMAGES + " BOOLEAN)";
+
+            // Create the export database preferences table.
+            exportDatabase.execSQL(CREATE_PREFERENCES_TABLE);
+
+            // Get a handle for the shared preference.
+            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+            // Create a ContentValues with the preferences information.
+            ContentValues preferencesContentValues = new ContentValues();
+            preferencesContentValues.put(JAVASCRIPT, sharedPreferences.getBoolean("javascript_enabled", false));
+            preferencesContentValues.put(FIRST_PARTY_COOKIES, sharedPreferences.getBoolean("first_party_cookies_enabled", false));
+            preferencesContentValues.put(THIRD_PARTY_COOKIES, sharedPreferences.getBoolean("third_party_cookies_enabled", false));
+            preferencesContentValues.put(DOM_STORAGE, sharedPreferences.getBoolean("dom_storage_enabled", false));
+            preferencesContentValues.put(SAVE_FORM_DATA, sharedPreferences.getBoolean("save_form_data_enabled", false));  // Save form data can be removed once the minimum API >= 26.
+            preferencesContentValues.put(USER_AGENT, sharedPreferences.getString("user_agent", "Privacy Browser"));
+            preferencesContentValues.put(CUSTOM_USER_AGENT, sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"));
+            preferencesContentValues.put(INCOGNITO_MODE, sharedPreferences.getBoolean("incognito_mode", false));
+            preferencesContentValues.put(DO_NOT_TRACK, sharedPreferences.getBoolean("do_not_track", false));
+            preferencesContentValues.put(ALLOW_SCREENSHOTS, sharedPreferences.getBoolean("allow_screenshots", false));
+            preferencesContentValues.put(EASYLIST, sharedPreferences.getBoolean("easylist", true));
+            preferencesContentValues.put(EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
+            preferencesContentValues.put(FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboy_annoyance_list", true));
+            preferencesContentValues.put(FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboy_social_blocking_list", true));
+            preferencesContentValues.put(ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
+            preferencesContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
+            preferencesContentValues.put(PROXY_THROUGH_ORBOT, sharedPreferences.getBoolean("proxy_through_orbot", false));
+            preferencesContentValues.put(TOR_HOMEPAGE, sharedPreferences.getString("tor_homepage", "http://ulrn6sryqaifefld.onion/"));
+            preferencesContentValues.put(TOR_SEARCH, sharedPreferences.getString("tor_search", "http://ulrn6sryqaifefld.onion/?q="));
+            preferencesContentValues.put(TOR_SEARCH_CUSTOM_URL, sharedPreferences.getString("tor_search_custom_url", ""));
+            preferencesContentValues.put(SEARCH, sharedPreferences.getString("search", "https://searx.me/?q="));
+            preferencesContentValues.put(SEARCH_CUSTOM_URL, sharedPreferences.getString("search_custom_url", ""));
+            preferencesContentValues.put(FULL_SCREEN_BROWSING_MODE, sharedPreferences.getBoolean("full_screen_browsing_mode", false));
+            preferencesContentValues.put(HIDE_SYSTEM_BARS, sharedPreferences.getBoolean("hide_system_bars", false));
+            preferencesContentValues.put(TRANSLUCENT_NAVIGATION_BAR, sharedPreferences.getBoolean("translucent_navigation_bar", true));
+            preferencesContentValues.put(CLEAR_EVERYTHING, sharedPreferences.getBoolean("clear_everything", true));
+            preferencesContentValues.put(CLEAR_COOKIES, sharedPreferences.getBoolean("clear_cookies", true));
+            preferencesContentValues.put(CLEAR_DOM_STORAGE, sharedPreferences.getBoolean("clear_dom_storage", true));
+            preferencesContentValues.put(CLEAR_FORM_DATA, sharedPreferences.getBoolean("clear_form_data", true));  // Clear form data can be removed once the minimum API >= 26.
+            preferencesContentValues.put(CLEAR_CACHE, sharedPreferences.getBoolean("clear_cache", true));
+            preferencesContentValues.put(HOMEPAGE, sharedPreferences.getString("homepage", "https://searx.me"));
+            preferencesContentValues.put(DEFAULT_FONT_SIZE, sharedPreferences.getString("default_font_size", "100"));
+            preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean("swipe_to_refresh", true));
+            preferencesContentValues.put(DISPLAY_ADDITIONAL_APP_BAR_ICONS, sharedPreferences.getBoolean("display_additional_app_bar_icons", false));
+            preferencesContentValues.put(DARK_THEME, sharedPreferences.getBoolean("dark_theme", false));
+            preferencesContentValues.put(NIGHT_MODE, sharedPreferences.getBoolean("night_mode", false));
+            preferencesContentValues.put(DISPLAY_WEBPAGE_IMAGES, sharedPreferences.getBoolean("display_webpage_images", true));
+
+            // Insert the preferences into the export database.
+            exportDatabase.insert(PREFERENCES_TABLE, null, preferencesContentValues);
+
+            // Close the export database.
+            exportDatabase.close();
+
+            // Convert the database file to a string.
+            String databaseString = databaseFile.toString();
+
+            // Create strings for the temporary database files.
+            String journalFileString = databaseString + "-journal";
+
+            // Get `Files` for the temporary database files.
+            File journalFile = new File(journalFileString);
+
+            // Delete the Journal file if it exists.
+            if (journalFile.exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                journalFile.delete();
+            }
+
+            // Export successful.
+            return EXPORT_SUCCESSFUL;
+        } catch (Exception exception) {
+            // Return the export error.
+            return exception.toString();
+        }
+    }
+
+    public String importUnencrypted(File databaseFile, Context context){
+        try {
+            // Convert the database file to a string.  Once API >= 27 the file can be opened directly.
+            String databaseString = databaseFile.toString();
+
+            // Open the import database.
+            SQLiteDatabase importDatabase = SQLiteDatabase.openDatabase(databaseString, null, SQLiteDatabase.OPEN_READONLY);
+
+            // Get a cursor for the domains table.
+            Cursor importDomainsCursor = importDatabase.rawQuery("SELECT * FROM " + DomainsDatabaseHelper.DOMAINS_TABLE + " ORDER BY " + DomainsDatabaseHelper.DOMAIN_NAME + " ASC", null);
+
+            // Delete the current domains database.
+            context.deleteDatabase(DomainsDatabaseHelper.DOMAINS_DATABASE);
+
+            // Create a new domains database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+            DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(context, null, null, 0);
+
+            // Move to the first domain.
+            importDomainsCursor.moveToFirst();
+
+            // Copy the data from the import domains cursor into the domains database.
+            for (int i = 0; i < importDomainsCursor.getCount(); i++) {
+                // Extract the record from the cursor and store the data in a ContentValues.
+                ContentValues domainsContentValues = new ContentValues();
+                domainsContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST,
+                        importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST,
+                        importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)));
+                domainsContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)));
+                domainsContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS,
+                        importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)));
+                domainsContentValues.put(DomainsDatabaseHelper.USER_AGENT, importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)));
+                domainsContentValues.put(DomainsDatabaseHelper.FONT_SIZE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH)));
+                domainsContentValues.put(DomainsDatabaseHelper.NIGHT_MODE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)));
+                domainsContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)));
+                domainsContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION, importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT,
+                        importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION, importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT,
+                        importDomainsCursor.getString(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
+                domainsContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
+
+                // Insert the record into the export database.
+                domainsDatabaseHelper.addDomain(domainsContentValues);
+
+                // Advance to the next record.
+                importDomainsCursor.moveToNext();
+            }
+
+            // Close the domains cursor.
+            importDomainsCursor.close();
+
+            // Close the domains database.
+            domainsDatabaseHelper.close();
+
+
+            // Get a cursor for the bookmarks table.
+            Cursor importBookmarksCursor = importDatabase.rawQuery("SELECT * FROM " + BookmarksDatabaseHelper.BOOKMARKS_TABLE, null);
+
+            // Delete the current bookmarks database.
+            context.deleteDatabase(BookmarksDatabaseHelper.BOOKMARKS_DATABASE);
+
+            // Create a new bookmarks database.  The `0` specifies the database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
+            BookmarksDatabaseHelper bookmarksDatabaseHelper = new BookmarksDatabaseHelper(context, null, null, 0);
+
+            // Move to the first bookmark.
+            importBookmarksCursor.moveToFirst();
+
+            // Copy the data from the import bookmarks cursor into the bookmarks database.
+            for (int i = 0; i < importBookmarksCursor.getCount(); i++) {
+                // Extract the record from the cursor and store the data in a ContentValues.
+                ContentValues bookmarksContentValues = new ContentValues();
+                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)));
+                bookmarksContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, importBookmarksCursor.getBlob(importBookmarksCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON)));
+
+                // Insert the record into the export database.
+                bookmarksDatabaseHelper.createBookmark(bookmarksContentValues);
+
+                // Advance to the next record.
+                importBookmarksCursor.moveToNext();
+            }
+
+            // Close the bookmarks cursor.
+            importBookmarksCursor.close();
+
+            // Close the bookmarks database.
+            bookmarksDatabaseHelper.close();
+
+
+            // Get a handle for the shared preference.
+            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+            // Get a cursor for the bookmarks table.
+            Cursor importPreferencesCursor = importDatabase.rawQuery("SELECT * FROM " + PREFERENCES_TABLE, null);
+
+            // Move to the first preference.
+            importPreferencesCursor.moveToFirst();
+
+            // Import the preference data.
+            sharedPreferences.edit()
+                    .putBoolean("javascript_enabled", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(JAVASCRIPT)) == 1)
+                    .putBoolean("first_party_cookies_enabled", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(FIRST_PARTY_COOKIES)) == 1)
+                    .putBoolean("third_party_cookies_enabled", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(THIRD_PARTY_COOKIES)) == 1)
+                    .putBoolean("dom_storage_enabled", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DOM_STORAGE)) == 1)
+                    // Save form data can be removed once the minimum API >= 26.
+                    .putBoolean("save_form_data_enabled", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(SAVE_FORM_DATA)) == 1)
+                    .putString("user_agent", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(USER_AGENT)))
+                    .putString("custom_user_agent", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(CUSTOM_USER_AGENT)))
+                    .putBoolean("incognito_mode", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(INCOGNITO_MODE)) == 1)
+                    .putBoolean("do_not_track", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DO_NOT_TRACK)) == 1)
+                    .putBoolean("allow_screenshots", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(ALLOW_SCREENSHOTS)) == 1)
+                    .putBoolean("easylist", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(EASYLIST)) == 1)
+                    .putBoolean("easyprivacy", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(EASYPRIVACY)) == 1)
+                    .putBoolean("fanboy_annoyance_list", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(FANBOYS_ANNOYANCE_LIST)) == 1)
+                    .putBoolean("fanboy_social_blocking_list", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(FANBOYS_SOCIAL_BLOCKING_LIST)) == 1)
+                    .putBoolean("ultraprivacy", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(ULTRAPRIVACY)) == 1)
+                    .putBoolean("block_all_third_party_requests", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1)
+                    .putBoolean("proxy_through_orbot", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(PROXY_THROUGH_ORBOT)) == 1)
+                    .putString("tor_homepage", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(TOR_HOMEPAGE)))
+                    .putString("tor_search", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(TOR_SEARCH)))
+                    .putString("tor_search_custom_url", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(TOR_SEARCH_CUSTOM_URL)))
+                    .putString("search", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(SEARCH)))
+                    .putString("search_custom_url", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(SEARCH_CUSTOM_URL)))
+                    .putBoolean("full_screen_browsing_mode", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(FULL_SCREEN_BROWSING_MODE)) == 1)
+                    .putBoolean("hide_system_bars", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(HIDE_SYSTEM_BARS)) == 1)
+                    .putBoolean("translucent_navigation_bar", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(TRANSLUCENT_NAVIGATION_BAR)) == 1)
+                    .putBoolean("clear_everything", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_EVERYTHING)) == 1)
+                    .putBoolean("clear_cookies", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_COOKIES)) == 1)
+                    .putBoolean("clear_dom_storage", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_DOM_STORAGE)) == 1)
+                    // Clear form data can be removed once the minimum API >= 26.
+                    .putBoolean("clear_form_data", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_FORM_DATA)) == 1)
+                    .putBoolean("clear_cache", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_CACHE)) == 1)
+                    .putString("homepage", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(HOMEPAGE)))
+                    .putString("default_font_size", importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(DEFAULT_FONT_SIZE)))
+                    .putBoolean("swipe_to_refresh", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(SWIPE_TO_REFRESH)) == 1)
+                    .putBoolean("display_additional_app_bar_icons", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DISPLAY_ADDITIONAL_APP_BAR_ICONS)) == 1)
+                    .putBoolean("dark_theme", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DARK_THEME)) == 1)
+                    .putBoolean("night_mode", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(NIGHT_MODE)) == 1)
+                    .putBoolean("display_webpage_images", importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(DISPLAY_WEBPAGE_IMAGES)) == 1).apply();
+
+            // Close the preferences cursor.
+            importPreferencesCursor.close();
+
+
+            // Close the import database.
+            importDatabase.close();
+
+            // Create strings for the temporary database files.
+            String shmFileString = databaseString + "-shm";
+            String walFileString = databaseString + "-wal";
+            String journalFileString = databaseString + "-journal";
+
+            // Get `Files` for the temporary database files.
+            File shmFile = new File(shmFileString);
+            File walFile = new File(walFileString);
+            File journalFile = new File(journalFileString);
+
+            // Delete the Shared Memory file if it exists.
+            if (shmFile.exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                shmFile.delete();
+            }
+
+            // Delete the Write Ahead Log file if it exists.
+            if (walFile.exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                walFile.delete();
+            }
+
+            // Delete the Journal file if it exists.
+            if (journalFile.exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                journalFile.delete();
+            }
+
+            // Import successful.
+            return IMPORT_SUCCESSFUL;
+        } catch (Exception exception) {
+            // Return the import error.
+            return exception.toString();
+        }
+    }
+}
index f22f38a..d7947b1 100644 (file)
@@ -4,15 +4,15 @@
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:autoMirrored="true"
     android:height="24dp"
     android:width="24dp"
     android:viewportHeight="24.0"
     android:viewportWidth="24.0"
+    android:autoMirrored="true"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF9E9E9E"
-        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
+        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
 </vector>
index e1baa4c..95f5b1a 100644 (file)
@@ -4,15 +4,15 @@
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:autoMirrored="true"
     android:height="24dp"
     android:width="24dp"
     android:viewportHeight="24.0"
     android:viewportWidth="24.0"
+    android:autoMirrored="true"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF757575"
-        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
+        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
 </vector>
index 9167e1e..cb45f47 100644 (file)
@@ -4,15 +4,15 @@
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:autoMirrored="true"
     android:height="24dp"
     android:width="24dp"
     android:viewportHeight="24.0"
     android:viewportWidth="24.0"
+    android:autoMirrored="true"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF1E88E5"
-        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
+        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
 </vector>
index 94f0b28..6241eb9 100644 (file)
@@ -4,15 +4,15 @@
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:autoMirrored="true"
     android:height="24dp"
     android:width="24dp"
     android:viewportHeight="24.0"
     android:viewportWidth="24.0"
+    android:autoMirrored="true"
     tools:ignore="VectorRaster" >
 
-    <!-- We have to use a hard coded color until API >= 21.  Then we can use `@color`. -->
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
     <path
         android:fillColor="#FF1565C0"
-        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
+        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
 </vector>
diff --git a/app/src/main/res/drawable/import_export_dark.xml b/app/src/main/res/drawable/import_export_dark.xml
new file mode 100644 (file)
index 0000000..21cdea8
--- /dev/null
@@ -0,0 +1,18 @@
+<!-- `import_export_dark.xml` comes from the Android Material icon set, where it is called `import_export`.  It is released under the Apache License 2.0. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:autoMirrored="true"
+    tools:ignore="VectorRaster" >
+
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FFE0E0E0"
+        android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z" />
+</vector>
diff --git a/app/src/main/res/drawable/import_export_light.xml b/app/src/main/res/drawable/import_export_light.xml
new file mode 100644 (file)
index 0000000..602cf37
--- /dev/null
@@ -0,0 +1,18 @@
+<!-- `import_export_light.xml` comes from the Android Material icon set, where it is called `import_export`.  It is released under the Apache License 2.0. -->
+
+<!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:autoMirrored="true"
+    tools:ignore="VectorRaster" >
+
+    <!-- A hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z" />
+</vector>
diff --git a/app/src/main/res/layout/import_export_coordinatorlayout.xml b/app/src/main/res/layout/import_export_coordinatorlayout.xml
new file mode 100644 (file)
index 0000000..76464fe
--- /dev/null
@@ -0,0 +1,210 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright © 2018 Soren Stoutner <soren@stoutner.com>.
+
+  This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+
+  Privacy Browser is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Privacy Browser is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>. -->
+
+<!-- `android:fitsSystemWindows="true"` moves the AppBar below the status bar.
+    When it is specified the theme should include `<item name="android:windowTranslucentStatus">true</item>` to make the status bar a transparent, darkened overlay. -->
+<android.support.design.widget.CoordinatorLayout
+    android:id="@+id/import_export_coordinatorlayout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:fitsSystemWindows="true" >
+
+    <!-- the `LinearLayout` with `orientation="vertical"` moves the content below the `AppBarLayout`. -->
+    <LinearLayout
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical" >
+
+        <android.support.design.widget.AppBarLayout
+            android:id="@+id/import_export_appbarlayout"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent" >
+
+            <android.support.v7.widget.Toolbar
+                android:id="@+id/import_export_toolbar"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:background="?attr/colorPrimaryDark"
+                android:theme="?attr/appBarTextTheme"
+                app:popupTheme="?attr/popupsTheme" />
+        </android.support.design.widget.AppBarLayout>
+
+        <ScrollView
+            android:layout_height="match_parent"
+            android:layout_width="match_parent" >
+
+            <!-- Align the cards vertically. -->
+            <LinearLayout
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:orientation="vertical" >
+
+                <!-- The export card. -->
+                <android.support.v7.widget.CardView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:layout_marginTop="10dp"
+                    android:layout_marginBottom="5dp"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp" >
+
+                    <!-- Align the contents of the card vertically. -->
+                    <LinearLayout
+                        android:layout_height="match_parent"
+                        android:layout_width="match_parent"
+                        android:orientation="vertical"
+                        android:layout_margin="10dp" >
+
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_marginBottom="6dp"
+                            android:text="@string/export_settings"
+                            android:textSize="25sp"
+                            android:textStyle="bold"
+                            android:textColor="?android:textColorPrimary" />
+
+                        <!-- Align the export EditText and the select file button horizontally. -->
+                        <LinearLayout
+                            android:layout_height="wrap_content"
+                            android:layout_width="match_parent"
+                            android:orientation="horizontal" >
+
+                            <!-- `android.support.design.widget.TextInputLayout` makes the `android:hint` float above the `EditText`. -->
+                            <android.support.design.widget.TextInputLayout
+                                android:layout_height="wrap_content"
+                                android:layout_width="0dp"
+                                android:layout_weight="1" >
+
+                                <!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
+                                <android.support.design.widget.TextInputEditText
+                                    android:id="@+id/export_file_edittext"
+                                    android:layout_height="wrap_content"
+                                    android:layout_width="match_parent"
+                                    android:hint="@string/export_file_name"
+                                    android:inputType="textMultiLine|textUri" />
+                            </android.support.design.widget.TextInputLayout>
+
+                            <Button
+                                android:id="@+id/select_export_file"
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:layout_gravity="center_vertical"
+                                android:text="@string/browse"
+                                android:onClick="exportBrowse" />
+                        </LinearLayout>
+
+                        <Button
+                            android:id="@+id/export_button"
+                            android:layout_height="wrap_content"
+                            android:layout_width="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:text="@string/export"
+                            android:textSize="18sp"
+                            android:onClick="onClickExport" />
+                    </LinearLayout>
+                </android.support.v7.widget.CardView>
+
+                <!-- The import card. -->
+                <android.support.v7.widget.CardView
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:layout_marginTop="5dp"
+                    android:layout_marginBottom="10dp"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp" >
+
+                    <!-- Align the contents of the card vertically. -->
+                    <LinearLayout
+                        android:layout_height="match_parent"
+                        android:layout_width="match_parent"
+                        android:orientation="vertical"
+                        android:layout_margin="10dp" >
+
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_marginBottom="6dp"
+                            android:text="@string/import_settings"
+                            android:textSize="25sp"
+                            android:textStyle="bold"
+                            android:textColor="?android:textColorPrimary" />
+
+                        <!-- Align the import EditText and the select file button horizontally. -->
+                        <LinearLayout
+                            android:layout_height="wrap_content"
+                            android:layout_width="match_parent"
+                            android:orientation="horizontal" >
+
+                            <!-- `android.support.design.widget.TextInputLayout` makes the `android:hint` float above the `EditText`. -->
+                            <android.support.design.widget.TextInputLayout
+                                android:layout_height="wrap_content"
+                                android:layout_width="0dp"
+                                android:layout_weight="1" >
+
+                                <!-- `android:inputType="textUri" disables spell check and places an `/` on the main keyboard. -->
+                                <android.support.design.widget.TextInputEditText
+                                    android:id="@+id/import_file_edittext"
+                                    android:layout_height="wrap_content"
+                                    android:layout_width="match_parent"
+                                    android:hint="@string/import_file_name"
+                                    android:inputType="textMultiLine|textUri" />
+                            </android.support.design.widget.TextInputLayout>
+
+                            <Button
+                                android:id="@+id/select_import_file"
+                                android:layout_height="wrap_content"
+                                android:layout_width="wrap_content"
+                                android:layout_gravity="center_vertical"
+                                android:text="@string/browse"
+                                android:onClick="importBrowse" />
+                        </LinearLayout>
+
+                        <!-- `import` is a reserved word and cannot be used for the `onClick` name. -->
+                        <Button
+                            android:id="@+id/import_button"
+                            android:layout_height="wrap_content"
+                            android:layout_width="wrap_content"
+                            android:layout_gravity="center_horizontal"
+                            android:text="@string/import_button"
+                            android:textSize="18sp"
+                            android:onClick="onClickImport" />
+                    </LinearLayout>
+                </android.support.v7.widget.CardView>
+
+                <TextView
+                    android:id="@+id/import_export_storage_permission_textview"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"
+                    android:layout_marginBottom="10dp"
+                    android:layout_marginStart="15dp"
+                    android:layout_marginEnd="15dp"
+                    android:text="@string/file_permissions_explanation"
+                    android:textColor="?android:textColorPrimary"
+                    android:textAlignment="center" />
+            </LinearLayout>
+        </ScrollView>
+    </LinearLayout>
+</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
index 7f11c6c..126fd5f 100644 (file)
@@ -21,7 +21,7 @@
 <!-- `android:fitsSystemWindows="true"` moves the AppBar below the status bar.
     When it is specified the theme should include `<item name="android:windowTranslucentStatus">true</item>` to make the status bar a transparent, darkened overlay. -->
 <android.support.design.widget.CoordinatorLayout
-    android:id="@+id/blocklists_coordinatorlayout"
+    android:id="@+id/requests_coordinatorlayout"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_height="match_parent"
         android:orientation="vertical" >
 
         <android.support.design.widget.AppBarLayout
-            android:id="@+id/blocklists_appbarlayout"
+            android:id="@+id/requests_appbarlayout"
             android:layout_height="wrap_content"
             android:layout_width="match_parent" >
 
             <android.support.v7.widget.Toolbar
-                android:id="@+id/blocklists_toolbar"
+                android:id="@+id/requests_toolbar"
                 android:layout_height="wrap_content"
                 android:layout_width="match_parent"
                 android:background="?attr/colorPrimaryDark"
@@ -50,7 +50,7 @@
 
         <!-- `android:dividerHeight` must be at least `1dp` or the `ListView` is inconsistent in calculating how many bookmarks are displayed. -->
         <ListView
-            android:id="@+id/resource_requests_listview"
+            android:id="@+id/requests_listview"
             android:layout_height="match_parent"
             android:layout_width="match_parent"
             android:divider="@color/transparent"
index 9032af7..d17d658 100644 (file)
@@ -46,8 +46,7 @@
 
     <!-- If a group has an id, a line is drawn above it in the navigation view. -->
     <group
-        android:id="@+id/navigationGroup1" >
-
+        android:id="@+id/features_group" >
         <item
             android:id="@+id/requests"
             android:title="@string/requests"
             android:title="@string/downloads"
             android:icon="@drawable/downloads_light"
             android:orderInCategory="60" />
+    </group>
 
+    <!-- If a group has an id, a line is drawn above it in the navigation view. -->
+    <group
+        android:id="@+id/settings_group" >
         <item
             android:id="@+id/domains"
             android:title="@string/domains"
             android:title="@string/settings"
             android:icon="@drawable/settings"
             android:orderInCategory="80" />
+
+        <item
+            android:id="@+id/import_export"
+            android:title="@string/import_export"
+            android:icon="@drawable/import_export_light"
+            android:orderInCategory="90" />
     </group>
 
     <!-- If a group has an id, a line is drawn above it in the navigation view. -->
     <group
-        android:id="@+id/navigationGroup2" >
+        android:id="@+id/info_group" >
         <item
             android:id="@+id/guide"
             android:title="@string/guide"
             android:icon="@drawable/guide"
-            android:orderInCategory="90" />
+            android:orderInCategory="100" />
 
         <item
             android:id="@+id/about"
             android:title="@string/about"
             android:icon="@drawable/about_light"
-            android:orderInCategory="100" />
+            android:orderInCategory="110" />
     </group>
 
     <!-- If a group has an id, a line is drawn above it in the navigation view. -->
     <group
-        android:id="@+id/navigationGroup3" >
+        android:id="@+id/exit_group" >
         <item
             android:id="@+id/clearAndExit"
             android:title="@string/clear_and_exit"
             android:icon="@drawable/clear_and_exit"
-            android:orderInCategory="110" />
+            android:orderInCategory="120" />
     </group>
 </menu>
\ No newline at end of file
index c4fcfb9..1b011f4 100644 (file)
     <string name="bookmarks">Bookmarks</string>
     <string name="downloads">Downloads</string>
     <string name="settings">Settings</string>
+    <string name="import_export">Import/Export</string>
     <string name="guide">Guide</string>
     <string name="about">About</string>
     <string name="clear_and_exit">Clear and Exit</string>
     <string name="current_website_ssl_certificate">Current website SSL certificate</string>
     <string name="load_an_encrypted_website">Load an encrypted website before opening Domain Settings to populate the current website SSL certificate.</string>
 
+    <!-- Import/Export. -->
+    <string name="browse">Browse</string>
+    <string name="file_permissions_explanation">Accessing files in public directories requires the storage permission. Otherwise only private directories will work.</string>
+    <string name="privacy_browser_backup">Privacy Browser Backup</string>
+    <string name="export_settings">Export Settings</string>
+    <string name="import_settings">Import Settings</string>
+    <string name="export_file_name">Export file name</string>
+    <string name="import_file_name">Import file name</string>
+    <string name="export">Export</string>
+    <string name="import_button">Import</string>  <!-- `import` is a reserved word and cannot be used as the name -->
+    <string name="export_successful">Export successful.</string>
+    <string name="export_failed">Export failed:</string>
+    <string name="import_failed">Import failed:</string>
+    <string name="cannot_export">The settings cannot be exported to this location because the storage permission has not been granted.</string>
+    <string name="cannot_import">The settings cannot be imported to this location because the storage permission has not been granted.</string>
+    <string name="invalid_location">is not a valid location.</string>
+    <string name="storage_permission">Storage Permission</string>
+    <string name="storage_permission_message">Privacy Browser needs the storage permission to access public directories. If it is denied, the app’s private directory can still be used.</string>
+
     <!-- Guide. -->
     <string name="privacy_browser_guide">Privacy Browser Guide</string>
     <string name="overview">Overview</string>
     <string name="links">Links</string>
 
     <!-- Ad Control. There are no ads in the standard flavor, but these strings must exist because they are referenced in the code. -->
-    <string name="ad_id" translatable="false">Null</string>
+    <string name="google_app_id" translatable="false">Null</string>
+    <string name="ad_unit_id" translatable="false">Null</string>
     <string name="ad_consent">Ad Consent</string>
 </resources>
index 068d272..4da818e 100644 (file)
@@ -25,11 +25,11 @@ import android.view.View;
 
 @SuppressWarnings("unused")
 public class AdHelper {
-    public static void initializeAds(View view, Context applicationContext, FragmentManager fragmentManager, String ad_id) {
+    public static void initializeAds(View view, Context applicationContext, FragmentManager fragmentManager, String googleAppId, String adUnitId) {
         // Do nothing because this is the standard flavor.
     }
 
-    public static void loadAd(View view, Context applicationContext, String ad_id) {
+    public static void loadAd(View view, Context applicationContext, String adUnitId) {
         // Do nothing because this is the standard flavor.
     }
 
index b9bfa2c..1ba5b9d 100644 (file)
@@ -25,7 +25,7 @@ buildscript {
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.4'
+        classpath 'com.android.tools.build:gradle:3.2.1'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
index 7e874e1..bc26198 100644 (file)
@@ -1,6 +1,6 @@
-#Mon Mar 26 23:47:31 MST 2018
+#Tue Sep 25 14:33:14 MST 2018
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip