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