]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.kt
5dfd845e09c556bf4f10a44ef57d04c3c7005240
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / helpers / ImportExportDatabaseHelper.kt
1 /*
2  * Copyright © 2018-2022 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser Android is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.helpers
21
22 import android.content.ContentValues
23 import android.content.Context
24 import android.database.DatabaseUtils
25 import android.database.sqlite.SQLiteDatabase
26
27 import androidx.preference.PreferenceManager
28
29 import com.stoutner.privacybrowser.R
30
31 import java.io.File
32 import java.io.FileInputStream
33 import java.io.FileOutputStream
34 import java.io.InputStream
35 import java.io.OutputStream
36
37 // Define the private class constants.
38 private const val SCHEMA_VERSION = 15
39 private const val PREFERENCES_TABLE = "preferences"
40
41 // Define the private preferences constants.
42 private const val ID = "_id"
43 private const val JAVASCRIPT = "javascript"
44 private const val COOKIES = "cookies"
45 private const val DOM_STORAGE = "dom_storage"
46 private const val SAVE_FORM_DATA = "save_form_data"
47 private const val USER_AGENT = "user_agent"
48 private const val CUSTOM_USER_AGENT = "custom_user_agent"
49 private const val X_REQUESTED_WITH_HEADER = "x_requested_with_header"
50 private const val INCOGNITO_MODE = "incognito_mode"
51 private const val ALLOW_SCREENSHOTS = "allow_screenshots"
52 private const val EASYLIST = "easylist"
53 private const val EASYPRIVACY = "easyprivacy"
54 private const val FANBOYS_ANNOYANCE_LIST = "fanboys_annoyance_list"
55 private const val FANBOYS_SOCIAL_BLOCKING_LIST = "fanboys_social_blocking_list"
56 private const val ULTRALIST = "ultralist"
57 private const val ULTRAPRIVACY = "ultraprivacy"
58 private const val BLOCK_ALL_THIRD_PARTY_REQUESTS = "block_all_third_party_requests"
59 private const val TRACKING_QUERIES = "tracking_queries"
60 private const val AMP_REDIRECTS = "amp_redirects"
61 private const val SEARCH = "search"
62 private const val SEARCH_CUSTOM_URL = "search_custom_url"
63 private const val PROXY = "proxy"
64 private const val PROXY_CUSTOM_URL = "proxy_custom_url"
65 private const val FULL_SCREEN_BROWSING_MODE = "full_screen_browsing_mode"
66 private const val HIDE_APP_BAR = "hide_app_bar"
67 private const val CLEAR_EVERYTHING = "clear_everything"
68 private const val CLEAR_COOKIES = "clear_cookies"
69 private const val CLEAR_DOM_STORAGE = "clear_dom_storage"
70 private const val CLEAR_FORM_DATA = "clear_form_data"
71 private const val CLEAR_LOGCAT = "clear_logcat"
72 private const val CLEAR_CACHE = "clear_cache"
73 private const val HOMEPAGE = "homepage"
74 private const val FONT_SIZE = "font_size"
75 private const val OPEN_INTENTS_IN_NEW_TAB = "open_intents_in_new_tab"
76 private const val SWIPE_TO_REFRESH = "swipe_to_refresh"
77 private const val DOWNLOAD_WITH_EXTERNAL_APP = "download_with_external_app"
78 private const val SCROLL_APP_BAR = "scroll_app_bar"
79 private const val BOTTOM_APP_BAR = "bottom_app_bar"
80 private const val DISPLAY_ADDITIONAL_APP_BAR_ICONS = "display_additional_app_bar_icons"
81 private const val APP_THEME = "app_theme"
82 private const val WEBVIEW_THEME = "webview_theme"
83 private const val WIDE_VIEWPORT = "wide_viewport"
84 private const val DISPLAY_WEBPAGE_IMAGES = "display_webpage_images"
85
86 class ImportExportDatabaseHelper {
87     // Define the public companion object constants.  These can be moved to public class constants once the entire project has migrated to Kotlin.
88     companion object {
89         // Define the public class constants.
90         const val EXPORT_SUCCESSFUL = "Export Successful"
91         const val IMPORT_SUCCESSFUL = "Import Successful"
92     }
93
94     fun importUnencrypted(importFileInputStream: InputStream, context: Context): String {
95         return try {
96             // Create a temporary import file.
97             val temporaryImportFile = File.createTempFile("temporary_import_file", null, context.cacheDir)
98
99             // The file may be copied directly in Kotlin using `File.copyTo`.  <https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/copy-to.html>
100             // It can be copied in Android using `Files.copy` once the minimum API >= 26.
101             // <https://developer.android.com/reference/java/nio/file/Files#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...)>
102             // However, the file cannot be acquired from the content URI until the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
103
104             // Create a temporary file output stream.
105             val temporaryImportFileOutputStream = FileOutputStream(temporaryImportFile)
106
107             // Create a transfer byte array.
108             val transferByteArray = ByteArray(1024)
109
110             // Create an integer to track the number of bytes read.
111             var bytesRead: Int
112
113             // Copy the import file to the temporary import file.
114             while (importFileInputStream.read(transferByteArray).also { bytesRead = it } > 0) {
115                 temporaryImportFileOutputStream.write(transferByteArray, 0, bytesRead)
116             }
117
118             // Flush the temporary import file output stream.
119             temporaryImportFileOutputStream.flush()
120
121             // Close the file streams.
122             importFileInputStream.close()
123             temporaryImportFileOutputStream.close()
124
125
126             // Get a handle for the shared preference.
127             val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
128
129             // Open the import database.  Once the minimum API >= 27 the file can be opened directly without using the string.
130             val importDatabase = SQLiteDatabase.openDatabase(temporaryImportFile.toString(), null, SQLiteDatabase.OPEN_READWRITE)
131
132             // Get the database version.
133             val importDatabaseVersion = importDatabase.version
134
135             // Upgrade from schema version 1, first used in Privacy Browser 2.13, to schema version 2, first used in Privacy Browser 2.14.
136             // Previously this upgrade added `download_with_external_app` to the Preferences table.  But that is now removed in schema version 10.
137
138             // Upgrade from schema version 2, first used in Privacy Browser 2.14, to schema version 3, first used in Privacy Browser 2.15.
139             if (importDatabaseVersion < 3) {
140                 // Once the SQLite version is >= 3.25.0 (Android API >= 30) `ALTER TABLE RENAME COLUMN` can be used.  <https://www.sqlite.org/lang_altertable.html> <https://www.sqlite.org/changes.html>
141                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
142                 // In the meantime, a new column must be created with the new name.  There is no need to delete the old column on the temporary import database.
143
144                 // Create the new font size column.
145                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $FONT_SIZE TEXT")
146
147                 // Populate the preferences table with the current font size value.
148                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $FONT_SIZE = default_font_size")
149             }
150
151             // Upgrade from schema version 3, first used in Privacy Browser 2.15, to schema version 4, first used in Privacy Browser 2.16.
152             if (importDatabaseVersion < 4) {
153                 // Add the Pinned IP Addresses columns to the domains table.
154                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.PINNED_IP_ADDRESSES}  BOOLEAN")
155                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.IP_ADDRESSES} TEXT")
156             }
157
158             // Upgrade from schema version 4, first used in Privacy Browser 2.16, to schema version 5, first used in Privacy Browser 2.17.
159             if (importDatabaseVersion < 5) {
160                 // Add the hide and scroll app bar columns to the preferences table.
161                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $HIDE_APP_BAR BOOLEAN")
162                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $SCROLL_APP_BAR BOOLEAN")
163
164                 // Get the current hide and scroll app bar settings.
165                 val hideAppBar = sharedPreferences.getBoolean(HIDE_APP_BAR, true)
166                 val scrollAppBar = sharedPreferences.getBoolean(SCROLL_APP_BAR, true)
167
168                 // Populate the preferences table with the current app bar values.
169                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
170                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
171                 if (hideAppBar)
172                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $HIDE_APP_BAR = 1")
173                 else
174                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $HIDE_APP_BAR = 0")
175
176                 if (scrollAppBar)
177                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $SCROLL_APP_BAR = 1")
178                 else
179                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $SCROLL_APP_BAR = 0")
180             }
181
182             // Upgrade from schema version 5, first used in Privacy Browser 2.17, to schema version 6, first used in Privacy Browser 3.0.
183             if (importDatabaseVersion < 6) {
184                 // Add the open intents in new tab column to the preferences table.
185                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $OPEN_INTENTS_IN_NEW_TAB BOOLEAN")
186
187                 // Get the current open intents in new tab preference.
188                 val openIntentsInNewTab = sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true)
189
190                 // Populate the preferences table with the current open intents value.
191                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
192                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
193                 if (openIntentsInNewTab)
194                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $OPEN_INTENTS_IN_NEW_TAB = 1")
195                 else
196                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $OPEN_INTENTS_IN_NEW_TAB = 0")
197             }
198
199             // Upgrade from schema version 6, first used in Privacy Browser 3.0, to schema version 7, first used in Privacy Browser 3.1.
200             if (importDatabaseVersion < 7) {
201                 // Previously this upgrade added `facebook_click_ids` to the Preferences table.  But that is now removed in schema version 15.
202
203                 // Add the wide viewport column to the domains table.
204                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.WIDE_VIEWPORT} INTEGER")
205
206                 // Add the Google Analytics, Twitter AMP redirects, and wide viewport columns to the preferences table.
207                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN google_analytics BOOLEAN")
208                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN twitter_amp_redirects BOOLEAN")
209                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $WIDE_VIEWPORT BOOLEAN")
210
211                 // Get the current preference values.
212                 val trackingQueries = sharedPreferences.getBoolean(TRACKING_QUERIES, true)
213                 val ampRedirects = sharedPreferences.getBoolean(AMP_REDIRECTS, true)
214                 val wideViewport = sharedPreferences.getBoolean(WIDE_VIEWPORT, true)
215
216                 // Populate the preferences with the current Tracking Queries value.  Google Analytics was renamed Tracking Queries in schema version 15.
217                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
218                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
219                 if (trackingQueries)
220                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET google_analytics = 1")
221                 else
222                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET google_analytics = 0")
223
224                 // Populate the preferences table with the current AMP Redirects value.  Twitter AMP Redirects was renamed AMP Redirects in schema version 15.
225                 if (ampRedirects)
226                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET twitter_amp_redirects = 1")
227                 else
228                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET twitter_amp_redirects = 0")
229
230                 // Populate the preferences table with the current wide viewport value.
231                 if (wideViewport)
232                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $WIDE_VIEWPORT = 1")
233                 else
234                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $WIDE_VIEWPORT = 0")
235             }
236
237             // Upgrade from schema version 7, first used in Privacy Browser 3.1, to schema version 8, first used in Privacy Browser 3.2.
238             if (importDatabaseVersion < 8) {
239                 // Add the UltraList column to the tables.
240                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.ULTRALIST} BOOLEAN")
241                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $ULTRALIST BOOLEAN")
242
243                 // Get the current preference values.
244                 val ultraList = sharedPreferences.getBoolean(ULTRALIST, true)
245
246                 // Populate the tables with the current UltraList value.
247                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
248                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
249                 if (ultraList) {
250                     importDatabase.execSQL("UPDATE ${DomainsDatabaseHelper.DOMAINS_TABLE} SET ${DomainsDatabaseHelper.ULTRALIST}  =  1")
251                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $ULTRALIST = 1")
252                 } else {
253                     importDatabase.execSQL("UPDATE ${DomainsDatabaseHelper.DOMAINS_TABLE} SET ${DomainsDatabaseHelper.ULTRALIST} = 0")
254                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $ULTRALIST = 0")
255                 }
256             }
257
258             // Upgrade from schema version 8, first used in Privacy Browser 3.2, to schema version 9, first used in Privacy Browser 3.3.
259             if (importDatabaseVersion < 9) {
260                 // Add the new proxy columns to the preferences table.
261                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $PROXY TEXT")
262                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $PROXY_CUSTOM_URL TEXT")
263
264                 // Get the current proxy values.
265                 val proxy = sharedPreferences.getString(PROXY, context.getString(R.string.proxy_default_value))
266                 var proxyCustomUrl = sharedPreferences.getString(PROXY_CUSTOM_URL, context.getString(R.string.proxy_custom_url_default_value))
267
268                 // SQL escape the proxy custom URL string.
269                 proxyCustomUrl = DatabaseUtils.sqlEscapeString(proxyCustomUrl)
270
271                 // Populate the preferences table with the current proxy values. The proxy custom URL does not need to be surrounded by `'` because it was SLQ escaped above.
272                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $PROXY = '$proxy'")
273                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $PROXY_CUSTOM_URL = $proxyCustomUrl")
274             }
275
276             // Upgrade from schema version 9, first used in Privacy Browser 3.3, to schema version 10, first used in Privacy Browser 3.4.
277             // Previously this upgrade added `download_location` and `download_custom_location` to the Preferences table.  But they were removed in schema version 13.
278
279             // Upgrade from schema version 10, first used in Privacy Browser 3.4, to schema version 11, first used in Privacy Browser 3.5.
280             if (importDatabaseVersion < 11) {
281                 // Add the app theme column to the preferences table.
282                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $APP_THEME TEXT")
283
284                 // Get a cursor for the dark theme preference.
285                 val darkThemePreferencesCursor = importDatabase.rawQuery("SELECT dark_theme FROM $PREFERENCES_TABLE", null)
286
287                 // Move to the first entry.
288                 darkThemePreferencesCursor.moveToFirst()
289
290                 // Get the old dark theme value, which is in column 0.
291                 val darkTheme = darkThemePreferencesCursor.getInt(0)
292
293                 // Close the dark theme preference cursor.
294                 darkThemePreferencesCursor.close()
295
296                 // Get the system default string.
297                 val systemDefault = context.getString(R.string.app_theme_default_value)
298
299                 // Get the theme entry values string array.
300                 val appThemeEntryValuesStringArray: Array<String> = context.resources.getStringArray(R.array.app_theme_entry_values)
301
302                 // Get the dark string.
303                 val dark = appThemeEntryValuesStringArray[2]
304
305                 // Populate the app theme according to the old dark theme preference.
306                 if (darkTheme == 0) {  // A light theme was selected.
307                     // Set the app theme to be the system default.
308                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $APP_THEME = '$systemDefault'")
309                 } else {  // A dark theme was selected.
310                     // Set the app theme to be dark.
311                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $APP_THEME = '$dark'")
312                 }
313
314                 // Add the WebView theme to the domains table.  This defaults to 0, which is `System default`, so a separate step isn't needed to populate the database.
315                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.WEBVIEW_THEME} INTEGER")
316
317                 // Add the WebView theme to the preferences table.
318                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $WEBVIEW_THEME TEXT")
319
320                 // Get the WebView theme default value string.
321                 val webViewThemeDefaultValue = context.getString(R.string.webview_theme_default_value)
322
323                 // Set the WebView theme in the preferences table to be the default.
324                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $WEBVIEW_THEME = '$webViewThemeDefaultValue'")
325             }
326
327             // Upgrade from schema version 11, first used in Privacy Browser 3.5, to schema version 12, first used in Privacy Browser 3.6.
328             if (importDatabaseVersion < 12) {
329                 // Add the clear logcat column to the preferences table.
330                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $CLEAR_LOGCAT BOOLEAN")
331
332                 // Get the current clear logcat value.
333                 val clearLogcat = sharedPreferences.getBoolean(CLEAR_LOGCAT, true)
334
335                 // Populate the preferences table with the current clear logcat value.
336                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
337                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
338                 if (clearLogcat)
339                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $CLEAR_LOGCAT = 1")
340                 else
341                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $CLEAR_LOGCAT = 0")
342             }
343
344             // Upgrade from schema version 12, first used in Privacy Browser 3.6, to schema version 13, first used in Privacy Browser 3.7.
345             // Do nothing.  `download_location` and `download_custom_location` were removed from the preferences table, but they can be left in the temporary import database without issue.
346
347             // Upgrade from schema version 13, first used in Privacy Browser 3.7, to schema version 14, first used in Privacy Browser 3.8.
348             if (importDatabaseVersion < 14) {
349                 // `enabledthirdpartycookies` was removed from the domains table.  `do_not_track` and `third_party_cookies` were removed from the preferences table.
350
351                 // Once the SQLite version is >= 3.25.0 (Android API >= 30) `ALTER TABLE RENAME COLUMN` can be used.  <https://www.sqlite.org/lang_altertable.html> <https://www.sqlite.org/changes.html>
352                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
353                 // In the meantime, a new column must be created with the new name.  There is no need to delete the old column on the temporary import database.
354
355                 // Create the new cookies columns.
356                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.COOKIES} BOOLEAN")
357                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $COOKIES BOOLEAN")
358
359                 // Copy the data from the old cookies columns to the new ones.
360                 importDatabase.execSQL("UPDATE ${DomainsDatabaseHelper.DOMAINS_TABLE} SET ${DomainsDatabaseHelper.COOKIES} = enablefirstpartycookies")
361                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $COOKIES = first_party_cookies")
362
363                 // Create the new download with external app and bottom app bar columns.
364                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $DOWNLOAD_WITH_EXTERNAL_APP BOOLEAN")
365                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $BOTTOM_APP_BAR BOOLEAN")
366
367                 // Get the current values for the new columns.
368                 val downloadWithExternalApp = sharedPreferences.getBoolean(DOWNLOAD_WITH_EXTERNAL_APP, false)
369                 val bottomAppBar = sharedPreferences.getBoolean(BOTTOM_APP_BAR, false)
370
371                 // Populate the preferences table with the current download with external app value.
372                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
373                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
374                 if (downloadWithExternalApp)
375                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $DOWNLOAD_WITH_EXTERNAL_APP = 1")
376                 else
377                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $DOWNLOAD_WITH_EXTERNAL_APP = 0")
378
379                 // Populate the preferences table with the current bottom app bar value.
380                 if (bottomAppBar)
381                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $BOTTOM_APP_BAR = 1")
382                 else
383                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $BOTTOM_APP_BAR = 0")
384             }
385
386             // Upgrade from schema version 14, first used in Privacy Browser 3.8, to schema version 15, first used in Privacy Browser 3.11.
387             if (importDatabaseVersion < 15) {
388                 // `facebook_click_ids` was removed from the preferences table.
389
390                 // Once the SQLite version is >= 3.25.0 (Android API >= 30) `ALTER TABLE RENAME COLUMN` can be used.  <https://www.sqlite.org/lang_altertable.html> <https://www.sqlite.org/changes.html>
391                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
392                 // In the meantime, a new column must be created with the new name.  There is no need to delete the old column on the temporary import database.
393
394                 // Create the new URL modification columns.
395                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $TRACKING_QUERIES BOOLEAN")
396                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $AMP_REDIRECTS BOOLEAN")
397
398                 // Copy the data from the old columns to the new ones.
399                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $TRACKING_QUERIES = google_analytics")
400                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $AMP_REDIRECTS = twitter_amp_redirects")
401
402                 // Create the new X-Requested-with header columns.
403                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $X_REQUESTED_WITH_HEADER BOOLEAN")
404                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.X_REQUESTED_WITH_HEADER} INTEGER")
405
406                 // Get the current X-Requested-With header preferences value.
407                 val xRequestedWithHeader = sharedPreferences.getBoolean(X_REQUESTED_WITH_HEADER, true)
408
409                 // Populate the Preferences X-Requested-With header with the current value.  The domains X-Requested-With header will default to 0, which is `System default`.
410                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
411                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
412                 if (xRequestedWithHeader)
413                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $X_REQUESTED_WITH_HEADER = 1")
414                 else
415                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $X_REQUESTED_WITH_HEADER = 0")
416             }
417
418             // Get a cursor for the bookmarks table.
419             val importBookmarksCursor = importDatabase.rawQuery("SELECT * FROM ${BookmarksDatabaseHelper.BOOKMARKS_TABLE}", null)
420
421             // Delete the current bookmarks database.
422             context.deleteDatabase(BookmarksDatabaseHelper.BOOKMARKS_DATABASE)
423
424             // Create a new bookmarks database.
425             val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
426
427             // Move to the first record.
428             importBookmarksCursor.moveToFirst()
429
430             // Copy the data from the import bookmarks cursor into the bookmarks database.
431             for (i in 0 until importBookmarksCursor.count) {
432                 // Create a bookmark content values.
433                 val bookmarkContentValues = ContentValues()
434
435                 // Add the information for this bookmark to the content values.
436                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)))
437                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
438                 bookmarkContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
439                 bookmarkContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)))
440                 bookmarkContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)))
441                 bookmarkContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, importBookmarksCursor.getBlob(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON)))
442
443                 // Insert the content values into the bookmarks database.
444                 bookmarksDatabaseHelper.createBookmark(bookmarkContentValues)
445
446                 // Advance to the next record.
447                 importBookmarksCursor.moveToNext()
448             }
449
450             // Close the bookmarks cursor and database.
451             importBookmarksCursor.close()
452             bookmarksDatabaseHelper.close()
453
454
455             // Get a cursor for the domains table.
456             val importDomainsCursor = importDatabase.rawQuery("SELECT * FROM ${DomainsDatabaseHelper.DOMAINS_TABLE} ORDER BY ${DomainsDatabaseHelper.DOMAIN_NAME} ASC", null)
457
458             // Delete the current domains database.
459             context.deleteDatabase(DomainsDatabaseHelper.DOMAINS_DATABASE)
460
461             // Create a new domains database.
462             val domainsDatabaseHelper = DomainsDatabaseHelper(context)
463
464             // Move to the first record.
465             importDomainsCursor.moveToFirst()
466
467             // Copy the data from the import domains cursor into the domains database.
468             for (i in 0 until importDomainsCursor.count) {
469                 // Create a domain content values.
470                 val domainContentValues = ContentValues()
471
472                 // Populate the domain content values.
473                 domainContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME)))
474                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)))
475                 domainContentValues.put(DomainsDatabaseHelper.COOKIES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)))
476                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)))
477                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)))
478                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)))
479                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)))
480                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST,
481                     importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)))
482                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST,
483                     importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)))
484                 domainContentValues.put(DomainsDatabaseHelper.ULTRALIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)))
485                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)))
486                 domainContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS,
487                     importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)))
488                 domainContentValues.put(DomainsDatabaseHelper.USER_AGENT, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT)))
489                 domainContentValues.put(DomainsDatabaseHelper.X_REQUESTED_WITH_HEADER, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.X_REQUESTED_WITH_HEADER)))
490                 domainContentValues.put(DomainsDatabaseHelper.FONT_SIZE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE)))
491                 domainContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH)))
492                 domainContentValues.put(DomainsDatabaseHelper.WEBVIEW_THEME, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME)))
493                 domainContentValues.put(DomainsDatabaseHelper.WIDE_VIEWPORT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT)))
494                 domainContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES)))
495                 domainContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)))
496                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME,
497                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)))
498                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION,
499                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)))
500                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT,
501                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)))
502                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME,
503                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)))
504                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION,
505                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)))
506                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT,
507                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)))
508                 domainContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)))
509                 domainContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)))
510                 domainContentValues.put(DomainsDatabaseHelper.PINNED_IP_ADDRESSES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)))
511                 domainContentValues.put(DomainsDatabaseHelper.IP_ADDRESSES, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES)))
512
513                 // Insert the content values into the domains database.
514                 domainsDatabaseHelper.addDomain(domainContentValues)
515
516                 // Advance to the next record.
517                 importDomainsCursor.moveToNext()
518             }
519
520             // Close the domains cursor and database.
521             importDomainsCursor.close()
522             domainsDatabaseHelper.close()
523
524
525             // Get a cursor for the preferences table.
526             val importPreferencesCursor = importDatabase.rawQuery("SELECT * FROM $PREFERENCES_TABLE", null)
527
528             // Move to the first record.
529             importPreferencesCursor.moveToFirst()
530
531             // Import the preference data.
532             sharedPreferences.edit()
533                 .putBoolean(JAVASCRIPT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(JAVASCRIPT)) == 1)
534                 .putBoolean(COOKIES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(COOKIES)) == 1)
535                 .putBoolean(DOM_STORAGE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DOM_STORAGE)) == 1)
536                 .putBoolean(SAVE_FORM_DATA, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(SAVE_FORM_DATA)) == 1)  // Save form data can be removed once the minimum API >= 26.
537                 .putString(USER_AGENT, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(USER_AGENT)))
538                 .putString(CUSTOM_USER_AGENT, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(CUSTOM_USER_AGENT)))
539                 .putBoolean(X_REQUESTED_WITH_HEADER, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(X_REQUESTED_WITH_HEADER)) == 1)
540                 .putBoolean(INCOGNITO_MODE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(INCOGNITO_MODE)) == 1)
541                 .putBoolean(ALLOW_SCREENSHOTS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(ALLOW_SCREENSHOTS)) == 1)
542                 .putBoolean(EASYLIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(EASYLIST)) == 1)
543                 .putBoolean(EASYPRIVACY, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(EASYPRIVACY)) == 1)
544                 .putBoolean(FANBOYS_ANNOYANCE_LIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(FANBOYS_ANNOYANCE_LIST)) == 1)
545                 .putBoolean(FANBOYS_SOCIAL_BLOCKING_LIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(FANBOYS_SOCIAL_BLOCKING_LIST)) == 1)
546                 .putBoolean(ULTRALIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(ULTRALIST)) == 1)
547                 .putBoolean(ULTRAPRIVACY, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(ULTRAPRIVACY)) == 1)
548                 .putBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1)
549                 .putBoolean(TRACKING_QUERIES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(TRACKING_QUERIES)) == 1)
550                 .putBoolean(AMP_REDIRECTS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(AMP_REDIRECTS)) == 1)
551                 .putString(SEARCH, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(SEARCH)))
552                 .putString(SEARCH_CUSTOM_URL, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(SEARCH_CUSTOM_URL)))
553                 .putString(PROXY, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(PROXY)))
554                 .putString(PROXY_CUSTOM_URL, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(PROXY_CUSTOM_URL)))
555                 .putBoolean(FULL_SCREEN_BROWSING_MODE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(FULL_SCREEN_BROWSING_MODE)) == 1)
556                 .putBoolean(HIDE_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(HIDE_APP_BAR)) == 1)
557                 .putBoolean(CLEAR_EVERYTHING, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_EVERYTHING)) == 1)
558                 .putBoolean(CLEAR_COOKIES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_COOKIES)) == 1)
559                 .putBoolean(CLEAR_DOM_STORAGE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_DOM_STORAGE)) == 1)
560                 .putBoolean(CLEAR_FORM_DATA, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_FORM_DATA)) == 1)  // Clear form data can be removed once the minimum API >= 26.
561                 .putBoolean(CLEAR_LOGCAT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_LOGCAT)) == 1)
562                 .putBoolean(CLEAR_CACHE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_CACHE)) == 1)
563                 .putString(HOMEPAGE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(HOMEPAGE)))
564                 .putString(FONT_SIZE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(FONT_SIZE)))
565                 .putBoolean(OPEN_INTENTS_IN_NEW_TAB, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(OPEN_INTENTS_IN_NEW_TAB)) == 1)
566                 .putBoolean(SWIPE_TO_REFRESH, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH)) == 1)
567                 .putBoolean(DOWNLOAD_WITH_EXTERNAL_APP, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DOWNLOAD_WITH_EXTERNAL_APP)) == 1)
568                 .putBoolean(SCROLL_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(SCROLL_APP_BAR)) == 1)
569                 .putBoolean(BOTTOM_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(BOTTOM_APP_BAR)) == 1)
570                 .putBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DISPLAY_ADDITIONAL_APP_BAR_ICONS)) == 1)
571                 .putString(APP_THEME, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(APP_THEME)))
572                 .putString(WEBVIEW_THEME, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(WEBVIEW_THEME)))
573                 .putBoolean(WIDE_VIEWPORT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(WIDE_VIEWPORT)) == 1)
574                 .putBoolean(DISPLAY_WEBPAGE_IMAGES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DISPLAY_WEBPAGE_IMAGES)) == 1)
575                 .apply()
576
577             // Close the preferences cursor and database.
578             importPreferencesCursor.close()
579             importDatabase.close()
580
581             // Delete the temporary import file database, journal, and other related auxiliary files.
582             SQLiteDatabase.deleteDatabase(temporaryImportFile)
583
584             // Return the import successful string.
585             IMPORT_SUCCESSFUL
586         } catch (exception: Exception) {
587             // Return the import error.
588             exception.toString()
589         }
590     }
591
592     fun exportUnencrypted(exportFileOutputStream: OutputStream, context: Context): String {
593         return try {
594             // Create a temporary export file.
595             val temporaryExportFile = File.createTempFile("temporary_export_file", null, context.cacheDir)
596
597             // Create the temporary export database.
598             val temporaryExportDatabase = SQLiteDatabase.openOrCreateDatabase(temporaryExportFile, null)
599
600             // Set the temporary export database version number.
601             temporaryExportDatabase.version = SCHEMA_VERSION
602
603
604             // Create the temporary export database bookmarks table.
605             temporaryExportDatabase.execSQL(BookmarksDatabaseHelper.CREATE_BOOKMARKS_TABLE)
606
607             // Open the bookmarks database.
608             val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
609
610             // Get a full bookmarks cursor.
611             val bookmarksCursor = bookmarksDatabaseHelper.allBookmarks
612
613             // Move to the first record.
614             bookmarksCursor.moveToFirst()
615
616             // Copy the data from the bookmarks cursor into the export database.
617             for (i in 0 until bookmarksCursor.count) {
618                 // Create a bookmark content values.
619                 val bookmarkContentValues = ContentValues()
620
621                 // Populate the bookmark content values.
622                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)))
623                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
624                 bookmarkContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
625                 bookmarkContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)))
626                 bookmarkContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)))
627                 bookmarkContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, bookmarksCursor.getBlob(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON)))
628
629                 // Insert the content values into the temporary export database.
630                 temporaryExportDatabase.insert(BookmarksDatabaseHelper.BOOKMARKS_TABLE, null, bookmarkContentValues)
631
632                 // Advance to the next record.
633                 bookmarksCursor.moveToNext()
634             }
635
636             // Close the bookmarks cursor and database.
637             bookmarksCursor.close()
638             bookmarksDatabaseHelper.close()
639
640
641             // Create the temporary export database domains table.
642             temporaryExportDatabase.execSQL(DomainsDatabaseHelper.CREATE_DOMAINS_TABLE)
643
644             // Open the domains database.
645             val domainsDatabaseHelper = DomainsDatabaseHelper(context)
646
647             // Get a full domains database cursor.
648             val domainsCursor = domainsDatabaseHelper.completeCursorOrderedByDomain
649
650             // Move to the first record.
651             domainsCursor.moveToFirst()
652
653             // Copy the data from the domains cursor into the export database.
654             for (i in 0 until domainsCursor.count) {
655                 // Create a domain content values.
656                 val domainContentValues = ContentValues()
657
658                 // Populate the domain content values.
659                 domainContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME)))
660                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)))
661                 domainContentValues.put(DomainsDatabaseHelper.COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)))
662                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)))
663                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)))
664                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)))
665                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)))
666                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)))
667                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST,
668                     domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)))
669                 domainContentValues.put(DomainsDatabaseHelper.ULTRALIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)))
670                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)))
671                 domainContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)))
672                 domainContentValues.put(DomainsDatabaseHelper.USER_AGENT, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT)))
673                 domainContentValues.put(DomainsDatabaseHelper.X_REQUESTED_WITH_HEADER, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.X_REQUESTED_WITH_HEADER)))
674                 domainContentValues.put(DomainsDatabaseHelper.FONT_SIZE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE)))
675                 domainContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH)))
676                 domainContentValues.put(DomainsDatabaseHelper.WEBVIEW_THEME, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME)))
677                 domainContentValues.put(DomainsDatabaseHelper.WIDE_VIEWPORT, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT)))
678                 domainContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES)))
679                 domainContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)))
680                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)))
681                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)))
682                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT,
683                     domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)))
684                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)))
685                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)))
686                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT,
687                     domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)))
688                 domainContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, domainsCursor.getLong(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)))
689                 domainContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, domainsCursor.getLong(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)))
690                 domainContentValues.put(DomainsDatabaseHelper.PINNED_IP_ADDRESSES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)))
691                 domainContentValues.put(DomainsDatabaseHelper.IP_ADDRESSES, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES)))
692
693                 // Insert the content values into the temporary export database.
694                 temporaryExportDatabase.insert(DomainsDatabaseHelper.DOMAINS_TABLE, null, domainContentValues)
695
696                 // Advance to the next record.
697                 domainsCursor.moveToNext()
698             }
699
700             // Close the domains cursor and database.
701             domainsCursor.close()
702             domainsDatabaseHelper.close()
703
704
705             // Prepare the preferences table SQL creation string.
706             val createPreferencesTable = "CREATE TABLE $PREFERENCES_TABLE (" +
707                     "$ID INTEGER PRIMARY KEY, " +
708                     "$JAVASCRIPT BOOLEAN, " +
709                     "$COOKIES BOOLEAN, " +
710                     "$DOM_STORAGE BOOLEAN, " +
711                     "$SAVE_FORM_DATA BOOLEAN, " +
712                     "$USER_AGENT TEXT, " +
713                     "$CUSTOM_USER_AGENT TEXT, " +
714                     "$X_REQUESTED_WITH_HEADER BOOLEAN, " +
715                     "$INCOGNITO_MODE BOOLEAN, " +
716                     "$ALLOW_SCREENSHOTS BOOLEAN, " +
717                     "$EASYLIST BOOLEAN, " +
718                     "$EASYPRIVACY BOOLEAN, " +
719                     "$FANBOYS_ANNOYANCE_LIST BOOLEAN, " +
720                     "$FANBOYS_SOCIAL_BLOCKING_LIST BOOLEAN, " +
721                     "$ULTRALIST BOOLEAN, " +
722                     "$ULTRAPRIVACY BOOLEAN, " +
723                     "$BLOCK_ALL_THIRD_PARTY_REQUESTS BOOLEAN, " +
724                     "$TRACKING_QUERIES BOOLEAN, " +
725                     "$AMP_REDIRECTS BOOLEAN, " +
726                     "$SEARCH TEXT, " +
727                     "$SEARCH_CUSTOM_URL TEXT, " +
728                     "$PROXY TEXT, " +
729                     "$PROXY_CUSTOM_URL TEXT, " +
730                     "$FULL_SCREEN_BROWSING_MODE BOOLEAN, " +
731                     "$HIDE_APP_BAR BOOLEAN, " +
732                     "$CLEAR_EVERYTHING BOOLEAN, " +
733                     "$CLEAR_COOKIES BOOLEAN, " +
734                     "$CLEAR_DOM_STORAGE BOOLEAN, " +
735                     "$CLEAR_FORM_DATA BOOLEAN, " +
736                     "$CLEAR_LOGCAT BOOLEAN, " +
737                     "$CLEAR_CACHE BOOLEAN, " +
738                     "$HOMEPAGE TEXT, " +
739                     "$FONT_SIZE TEXT, " +
740                     "$OPEN_INTENTS_IN_NEW_TAB BOOLEAN, " +
741                     "$SWIPE_TO_REFRESH BOOLEAN, " +
742                     "$DOWNLOAD_WITH_EXTERNAL_APP BOOLEAN, " +
743                     "$SCROLL_APP_BAR BOOLEAN, " +
744                     "$BOTTOM_APP_BAR BOOLEAN, " +
745                     "$DISPLAY_ADDITIONAL_APP_BAR_ICONS BOOLEAN, " +
746                     "$APP_THEME TEXT, " +
747                     "$WEBVIEW_THEME TEXT, " +
748                     "$WIDE_VIEWPORT BOOLEAN, " +
749                     "$DISPLAY_WEBPAGE_IMAGES BOOLEAN)"
750
751             // Create the temporary export database preferences table.
752             temporaryExportDatabase.execSQL(createPreferencesTable)
753
754             // Get a handle for the shared preference.
755             val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
756
757             // Create a preferences content values.
758             val preferencesContentValues = ContentValues()
759
760             // Populate the preferences content values.
761             preferencesContentValues.put(JAVASCRIPT, sharedPreferences.getBoolean(JAVASCRIPT, false))
762             preferencesContentValues.put(COOKIES, sharedPreferences.getBoolean(COOKIES, false))
763             preferencesContentValues.put(DOM_STORAGE, sharedPreferences.getBoolean(DOM_STORAGE, false))
764             preferencesContentValues.put(SAVE_FORM_DATA, sharedPreferences.getBoolean(SAVE_FORM_DATA, false))  // Save form data can be removed once the minimum API >= 26.
765             preferencesContentValues.put(USER_AGENT, sharedPreferences.getString(USER_AGENT, context.getString(R.string.user_agent_default_value)))
766             preferencesContentValues.put(CUSTOM_USER_AGENT, sharedPreferences.getString(CUSTOM_USER_AGENT, context.getString(R.string.custom_user_agent_default_value)))
767             preferencesContentValues.put(X_REQUESTED_WITH_HEADER, sharedPreferences.getBoolean(X_REQUESTED_WITH_HEADER, true))
768             preferencesContentValues.put(INCOGNITO_MODE, sharedPreferences.getBoolean(INCOGNITO_MODE, false))
769             preferencesContentValues.put(ALLOW_SCREENSHOTS, sharedPreferences.getBoolean(ALLOW_SCREENSHOTS, false))
770             preferencesContentValues.put(EASYLIST, sharedPreferences.getBoolean(EASYLIST, true))
771             preferencesContentValues.put(EASYPRIVACY, sharedPreferences.getBoolean(EASYPRIVACY, true))
772             preferencesContentValues.put(FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean(FANBOYS_ANNOYANCE_LIST, true))
773             preferencesContentValues.put(FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean(FANBOYS_SOCIAL_BLOCKING_LIST, true))
774             preferencesContentValues.put(ULTRALIST, sharedPreferences.getBoolean(ULTRALIST, true))
775             preferencesContentValues.put(ULTRAPRIVACY, sharedPreferences.getBoolean(ULTRAPRIVACY, true))
776             preferencesContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, false))
777             preferencesContentValues.put(TRACKING_QUERIES, sharedPreferences.getBoolean(TRACKING_QUERIES, true))
778             preferencesContentValues.put(AMP_REDIRECTS, sharedPreferences.getBoolean(AMP_REDIRECTS, true))
779             preferencesContentValues.put(SEARCH, sharedPreferences.getString(SEARCH, context.getString(R.string.search_default_value)))
780             preferencesContentValues.put(SEARCH_CUSTOM_URL, sharedPreferences.getString(SEARCH_CUSTOM_URL, context.getString(R.string.search_custom_url_default_value)))
781             preferencesContentValues.put(PROXY, sharedPreferences.getString(PROXY, context.getString(R.string.proxy_default_value)))
782             preferencesContentValues.put(PROXY_CUSTOM_URL, sharedPreferences.getString(PROXY_CUSTOM_URL, context.getString(R.string.proxy_custom_url_default_value)))
783             preferencesContentValues.put(FULL_SCREEN_BROWSING_MODE, sharedPreferences.getBoolean(FULL_SCREEN_BROWSING_MODE, false))
784             preferencesContentValues.put(HIDE_APP_BAR, sharedPreferences.getBoolean(HIDE_APP_BAR, true))
785             preferencesContentValues.put(CLEAR_EVERYTHING, sharedPreferences.getBoolean(CLEAR_EVERYTHING, true))
786             preferencesContentValues.put(CLEAR_COOKIES, sharedPreferences.getBoolean(CLEAR_COOKIES, true))
787             preferencesContentValues.put(CLEAR_DOM_STORAGE, sharedPreferences.getBoolean(CLEAR_DOM_STORAGE, true))
788             preferencesContentValues.put(CLEAR_FORM_DATA, sharedPreferences.getBoolean(CLEAR_FORM_DATA, true))  // Clear form data can be removed once the minimum API >= 26.
789             preferencesContentValues.put(CLEAR_LOGCAT, sharedPreferences.getBoolean(CLEAR_LOGCAT, true))
790             preferencesContentValues.put(CLEAR_CACHE, sharedPreferences.getBoolean(CLEAR_CACHE, true))
791             preferencesContentValues.put(HOMEPAGE, sharedPreferences.getString(HOMEPAGE, context.getString(R.string.homepage_default_value)))
792             preferencesContentValues.put(FONT_SIZE, sharedPreferences.getString(FONT_SIZE, context.getString(R.string.font_size_default_value)))
793             preferencesContentValues.put(OPEN_INTENTS_IN_NEW_TAB, sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true))
794             preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean(SWIPE_TO_REFRESH, true))
795             preferencesContentValues.put(DOWNLOAD_WITH_EXTERNAL_APP, sharedPreferences.getBoolean(DOWNLOAD_WITH_EXTERNAL_APP, false))
796             preferencesContentValues.put(SCROLL_APP_BAR, sharedPreferences.getBoolean(SCROLL_APP_BAR, true))
797             preferencesContentValues.put(BOTTOM_APP_BAR, sharedPreferences.getBoolean(BOTTOM_APP_BAR, false))
798             preferencesContentValues.put(DISPLAY_ADDITIONAL_APP_BAR_ICONS, sharedPreferences.getBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, false))
799             preferencesContentValues.put(APP_THEME, sharedPreferences.getString(APP_THEME, context.getString(R.string.app_theme_default_value)))
800             preferencesContentValues.put(WEBVIEW_THEME, sharedPreferences.getString(WEBVIEW_THEME, context.getString(R.string.webview_theme_default_value)))
801             preferencesContentValues.put(WIDE_VIEWPORT, sharedPreferences.getBoolean(WIDE_VIEWPORT, true))
802             preferencesContentValues.put(DISPLAY_WEBPAGE_IMAGES, sharedPreferences.getBoolean(DISPLAY_WEBPAGE_IMAGES, true))
803
804             // Insert the preferences content values into the temporary export database.
805             temporaryExportDatabase.insert(PREFERENCES_TABLE, null, preferencesContentValues)
806
807             // Close the temporary export database.
808             temporaryExportDatabase.close()
809
810
811             // The file may be copied directly in Kotlin using `File.copyTo`.  <https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/copy-to.html>
812             // It can be copied in Android using `Files.copy` once the minimum API >= 26.
813             // <https://developer.android.com/reference/java/nio/file/Files#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...)>
814             // However, the file cannot be acquired from the content URI until the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
815
816             // Create the temporary export file input stream.
817             val temporaryExportFileInputStream = FileInputStream(temporaryExportFile)
818
819             // Create a byte array.
820             val transferByteArray = ByteArray(1024)
821
822             // Create an integer to track the number of bytes read.
823             var bytesRead: Int
824
825             // Copy the temporary export file to the export file output stream.
826             while (temporaryExportFileInputStream.read(transferByteArray).also { bytesRead = it } > 0) {
827                 exportFileOutputStream.write(transferByteArray, 0, bytesRead)
828             }
829
830             // Flush the export file output stream.
831             exportFileOutputStream.flush()
832
833             // Close the file streams.
834             temporaryExportFileInputStream.close()
835             exportFileOutputStream.close()
836
837             // Delete the temporary export file database, journal, and other related auxiliary files.
838             SQLiteDatabase.deleteDatabase(temporaryExportFile)
839
840             // Return the export successful string.
841             EXPORT_SUCCESSFUL
842         } catch (exception: Exception) {
843             // Return the export error.
844             exception.toString()
845         }
846     }
847 }