]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.kt
Add WebView DevTools to the navigation menu. https://redmine.stoutner.com/issues/893
[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 = 16
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 INCOGNITO_MODE = "incognito_mode"
50 private const val ALLOW_SCREENSHOTS = "allow_screenshots"
51 private const val EASYLIST = "easylist"
52 private const val EASYPRIVACY = "easyprivacy"
53 private const val FANBOYS_ANNOYANCE_LIST = "fanboys_annoyance_list"
54 private const val FANBOYS_SOCIAL_BLOCKING_LIST = "fanboys_social_blocking_list"
55 private const val ULTRALIST = "ultralist"
56 private const val ULTRAPRIVACY = "ultraprivacy"
57 private const val BLOCK_ALL_THIRD_PARTY_REQUESTS = "block_all_third_party_requests"
58 private const val TRACKING_QUERIES = "tracking_queries"
59 private const val AMP_REDIRECTS = "amp_redirects"
60 private const val SEARCH = "search"
61 private const val SEARCH_CUSTOM_URL = "search_custom_url"
62 private const val PROXY = "proxy"
63 private const val PROXY_CUSTOM_URL = "proxy_custom_url"
64 private const val FULL_SCREEN_BROWSING_MODE = "full_screen_browsing_mode"
65 private const val HIDE_APP_BAR = "hide_app_bar"
66 private const val CLEAR_EVERYTHING = "clear_everything"
67 private const val CLEAR_COOKIES = "clear_cookies"
68 private const val CLEAR_DOM_STORAGE = "clear_dom_storage"
69 private const val CLEAR_FORM_DATA = "clear_form_data"
70 private const val CLEAR_LOGCAT = "clear_logcat"
71 private const val CLEAR_CACHE = "clear_cache"
72 private const val HOMEPAGE = "homepage"
73 private const val FONT_SIZE = "font_size"
74 private const val OPEN_INTENTS_IN_NEW_TAB = "open_intents_in_new_tab"
75 private const val SWIPE_TO_REFRESH = "swipe_to_refresh"
76 private const val DOWNLOAD_WITH_EXTERNAL_APP = "download_with_external_app"
77 private const val SCROLL_APP_BAR = "scroll_app_bar"
78 private const val BOTTOM_APP_BAR = "bottom_app_bar"
79 private const val DISPLAY_ADDITIONAL_APP_BAR_ICONS = "display_additional_app_bar_icons"
80 private const val APP_THEME = "app_theme"
81 private const val WEBVIEW_THEME = "webview_theme"
82 private const val WIDE_VIEWPORT = "wide_viewport"
83 private const val DISPLAY_WEBPAGE_IMAGES = "display_webpage_images"
84
85 class ImportExportDatabaseHelper {
86     // Define the public companion object constants.  These can be moved to public class constants once the entire project has migrated to Kotlin.
87     companion object {
88         // Define the public class constants.
89         const val EXPORT_SUCCESSFUL = "Export Successful"
90         const val IMPORT_SUCCESSFUL = "Import Successful"
91     }
92
93     fun importUnencrypted(importFileInputStream: InputStream, context: Context): String {
94         return try {
95             // Create a temporary import file.
96             val temporaryImportFile = File.createTempFile("temporary_import_file", null, context.cacheDir)
97
98             // 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>
99             // It can be copied in Android using `Files.copy` once the minimum API >= 26.
100             // <https://developer.android.com/reference/java/nio/file/Files#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...)>
101             // 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>
102
103             // Create a temporary file output stream.
104             val temporaryImportFileOutputStream = FileOutputStream(temporaryImportFile)
105
106             // Create a transfer byte array.
107             val transferByteArray = ByteArray(1024)
108
109             // Create an integer to track the number of bytes read.
110             var bytesRead: Int
111
112             // Copy the import file to the temporary import file.
113             while (importFileInputStream.read(transferByteArray).also { bytesRead = it } > 0) {
114                 temporaryImportFileOutputStream.write(transferByteArray, 0, bytesRead)
115             }
116
117             // Flush the temporary import file output stream.
118             temporaryImportFileOutputStream.flush()
119
120             // Close the file streams.
121             importFileInputStream.close()
122             temporaryImportFileOutputStream.close()
123
124
125             // Get a handle for the shared preference.
126             val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
127
128             // Open the import database.  Once the minimum API >= 27 the file can be opened directly without using the string.
129             val importDatabase = SQLiteDatabase.openDatabase(temporaryImportFile.toString(), null, SQLiteDatabase.OPEN_READWRITE)
130
131             // Get the database version.
132             val importDatabaseVersion = importDatabase.version
133
134             // Upgrade from schema version 1, first used in Privacy Browser 2.13, to schema version 2, first used in Privacy Browser 2.14.
135             // Previously this upgrade added `download_with_external_app` to the Preferences table.  But that is now removed in schema version 10.
136
137             // Upgrade from schema version 2, first used in Privacy Browser 2.14, to schema version 3, first used in Privacy Browser 2.15.
138             if (importDatabaseVersion < 3) {
139                 // `default_font_size` was renamed `font_size`.
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 minimum 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                 // There is no need to delete the columns as they will simply be ignored by the import.
351
352                 // `enablefirstpartycookies` was renamed `cookies`.
353                 // 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>
354                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
355                 // 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.
356
357                 // Create the new cookies columns.
358                 importDatabase.execSQL("ALTER TABLE ${DomainsDatabaseHelper.DOMAINS_TABLE} ADD COLUMN ${DomainsDatabaseHelper.COOKIES} BOOLEAN")
359                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $COOKIES BOOLEAN")
360
361                 // Copy the data from the old cookies columns to the new ones.
362                 importDatabase.execSQL("UPDATE ${DomainsDatabaseHelper.DOMAINS_TABLE} SET ${DomainsDatabaseHelper.COOKIES} = enablefirstpartycookies")
363                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $COOKIES = first_party_cookies")
364
365                 // Create the new download with external app and bottom app bar columns.
366                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $DOWNLOAD_WITH_EXTERNAL_APP BOOLEAN")
367                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $BOTTOM_APP_BAR BOOLEAN")
368
369                 // Get the current values for the new columns.
370                 val downloadWithExternalApp = sharedPreferences.getBoolean(DOWNLOAD_WITH_EXTERNAL_APP, false)
371                 val bottomAppBar = sharedPreferences.getBoolean(BOTTOM_APP_BAR, false)
372
373                 // Populate the preferences table with the current download with external app value.
374                 // This can switch to using the variables directly once the API >= 30.  <https://www.sqlite.org/datatype3.html#boolean_datatype>
375                 // <https://developer.android.com/reference/android/database/sqlite/package-summary>
376                 if (downloadWithExternalApp)
377                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $DOWNLOAD_WITH_EXTERNAL_APP = 1")
378                 else
379                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $DOWNLOAD_WITH_EXTERNAL_APP = 0")
380
381                 // Populate the preferences table with the current bottom app bar value.
382                 if (bottomAppBar)
383                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $BOTTOM_APP_BAR = 1")
384                 else
385                     importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $BOTTOM_APP_BAR = 0")
386             }
387
388             // Upgrade from schema version 14, first used in Privacy Browser 3.8, to schema version 15, first used in Privacy Browser 3.11.
389             if (importDatabaseVersion < 15) {
390                 // `facebook_click_ids` was removed from the preferences table.
391                 // There is no need to delete the columns as they will simply be ignored by the import.
392
393                 // `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.
394
395                 // Create the new URL modification columns.
396                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $TRACKING_QUERIES BOOLEAN")
397                 importDatabase.execSQL("ALTER TABLE $PREFERENCES_TABLE ADD COLUMN $AMP_REDIRECTS BOOLEAN")
398
399                 // Copy the data from the old columns to the new ones.
400                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $TRACKING_QUERIES = google_analytics")
401                 importDatabase.execSQL("UPDATE $PREFERENCES_TABLE SET $AMP_REDIRECTS = twitter_amp_redirects")
402             }
403
404             // Upgrade from schema version 15, first used in Privacy Browser 3.11, to schema version 16, first used in Privacy Browser 3.12.
405             // This upgrade removed the `x_requested_with_header` from the domains and preferences tables.
406             // There is no need to delete the columns as they will simply be ignored by the import.
407
408             // Get a cursor for the bookmarks table.
409             val importBookmarksCursor = importDatabase.rawQuery("SELECT * FROM ${BookmarksDatabaseHelper.BOOKMARKS_TABLE}", null)
410
411             // Delete the current bookmarks database.
412             context.deleteDatabase(BookmarksDatabaseHelper.BOOKMARKS_DATABASE)
413
414             // Create a new bookmarks database.
415             val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
416
417             // Move to the first record.
418             importBookmarksCursor.moveToFirst()
419
420             // Copy the data from the import bookmarks cursor into the bookmarks database.
421             for (i in 0 until importBookmarksCursor.count) {
422                 // Create a bookmark content values.
423                 val bookmarkContentValues = ContentValues()
424
425                 // Add the information for this bookmark to the content values.
426                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)))
427                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
428                 bookmarkContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, importBookmarksCursor.getString(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
429                 bookmarkContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)))
430                 bookmarkContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, importBookmarksCursor.getInt(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)))
431                 bookmarkContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, importBookmarksCursor.getBlob(importBookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON)))
432
433                 // Insert the content values into the bookmarks database.
434                 bookmarksDatabaseHelper.createBookmark(bookmarkContentValues)
435
436                 // Advance to the next record.
437                 importBookmarksCursor.moveToNext()
438             }
439
440             // Close the bookmarks cursor and database.
441             importBookmarksCursor.close()
442             bookmarksDatabaseHelper.close()
443
444
445             // Get a cursor for the domains table.
446             val importDomainsCursor = importDatabase.rawQuery("SELECT * FROM ${DomainsDatabaseHelper.DOMAINS_TABLE} ORDER BY ${DomainsDatabaseHelper.DOMAIN_NAME} ASC", null)
447
448             // Delete the current domains database.
449             context.deleteDatabase(DomainsDatabaseHelper.DOMAINS_DATABASE)
450
451             // Create a new domains database.
452             val domainsDatabaseHelper = DomainsDatabaseHelper(context)
453
454             // Move to the first record.
455             importDomainsCursor.moveToFirst()
456
457             // Copy the data from the import domains cursor into the domains database.
458             for (i in 0 until importDomainsCursor.count) {
459                 // Create a domain content values.
460                 val domainContentValues = ContentValues()
461
462                 // Populate the domain content values.
463                 domainContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME)))
464                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)))
465                 domainContentValues.put(DomainsDatabaseHelper.COOKIES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)))
466                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)))
467                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)))
468                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)))
469                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)))
470                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST,
471                     importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)))
472                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST,
473                     importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)))
474                 domainContentValues.put(DomainsDatabaseHelper.ULTRALIST, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)))
475                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)))
476                 domainContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS,
477                     importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)))
478                 domainContentValues.put(DomainsDatabaseHelper.USER_AGENT, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT)))
479                 domainContentValues.put(DomainsDatabaseHelper.FONT_SIZE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE)))
480                 domainContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH)))
481                 domainContentValues.put(DomainsDatabaseHelper.WEBVIEW_THEME, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME)))
482                 domainContentValues.put(DomainsDatabaseHelper.WIDE_VIEWPORT, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT)))
483                 domainContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES)))
484                 domainContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)))
485                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME,
486                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)))
487                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION,
488                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)))
489                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT,
490                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)))
491                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME,
492                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)))
493                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION,
494                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)))
495                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT,
496                     importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)))
497                 domainContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)))
498                 domainContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, importDomainsCursor.getLong(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)))
499                 domainContentValues.put(DomainsDatabaseHelper.PINNED_IP_ADDRESSES, importDomainsCursor.getInt(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)))
500                 domainContentValues.put(DomainsDatabaseHelper.IP_ADDRESSES, importDomainsCursor.getString(importDomainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES)))
501
502                 // Insert the content values into the domains database.
503                 domainsDatabaseHelper.addDomain(domainContentValues)
504
505                 // Advance to the next record.
506                 importDomainsCursor.moveToNext()
507             }
508
509             // Close the domains cursor and database.
510             importDomainsCursor.close()
511             domainsDatabaseHelper.close()
512
513
514             // Get a cursor for the preferences table.
515             val importPreferencesCursor = importDatabase.rawQuery("SELECT * FROM $PREFERENCES_TABLE", null)
516
517             // Move to the first record.
518             importPreferencesCursor.moveToFirst()
519
520             // Import the preference data.
521             sharedPreferences.edit()
522                 .putBoolean(JAVASCRIPT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(JAVASCRIPT)) == 1)
523                 .putBoolean(COOKIES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(COOKIES)) == 1)
524                 .putBoolean(DOM_STORAGE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DOM_STORAGE)) == 1)
525                 .putBoolean(SAVE_FORM_DATA, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(SAVE_FORM_DATA)) == 1)  // Save form data can be removed once the minimum API >= 26.
526                 .putString(USER_AGENT, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(USER_AGENT)))
527                 .putString(CUSTOM_USER_AGENT, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(CUSTOM_USER_AGENT)))
528                 .putBoolean(INCOGNITO_MODE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(INCOGNITO_MODE)) == 1)
529                 .putBoolean(ALLOW_SCREENSHOTS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(ALLOW_SCREENSHOTS)) == 1)
530                 .putBoolean(EASYLIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(EASYLIST)) == 1)
531                 .putBoolean(EASYPRIVACY, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(EASYPRIVACY)) == 1)
532                 .putBoolean(FANBOYS_ANNOYANCE_LIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(FANBOYS_ANNOYANCE_LIST)) == 1)
533                 .putBoolean(FANBOYS_SOCIAL_BLOCKING_LIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(FANBOYS_SOCIAL_BLOCKING_LIST)) == 1)
534                 .putBoolean(ULTRALIST, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(ULTRALIST)) == 1)
535                 .putBoolean(ULTRAPRIVACY, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(ULTRAPRIVACY)) == 1)
536                 .putBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1)
537                 .putBoolean(TRACKING_QUERIES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(TRACKING_QUERIES)) == 1)
538                 .putBoolean(AMP_REDIRECTS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(AMP_REDIRECTS)) == 1)
539                 .putString(SEARCH, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(SEARCH)))
540                 .putString(SEARCH_CUSTOM_URL, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(SEARCH_CUSTOM_URL)))
541                 .putString(PROXY, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(PROXY)))
542                 .putString(PROXY_CUSTOM_URL, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(PROXY_CUSTOM_URL)))
543                 .putBoolean(FULL_SCREEN_BROWSING_MODE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(FULL_SCREEN_BROWSING_MODE)) == 1)
544                 .putBoolean(HIDE_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(HIDE_APP_BAR)) == 1)
545                 .putBoolean(CLEAR_EVERYTHING, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_EVERYTHING)) == 1)
546                 .putBoolean(CLEAR_COOKIES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_COOKIES)) == 1)
547                 .putBoolean(CLEAR_DOM_STORAGE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_DOM_STORAGE)) == 1)
548                 .putBoolean(CLEAR_FORM_DATA, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_FORM_DATA)) == 1)  // Clear form data can be removed once the minimum API >= 26.
549                 .putBoolean(CLEAR_LOGCAT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_LOGCAT)) == 1)
550                 .putBoolean(CLEAR_CACHE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(CLEAR_CACHE)) == 1)
551                 .putString(HOMEPAGE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(HOMEPAGE)))
552                 .putString(FONT_SIZE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(FONT_SIZE)))
553                 .putBoolean(OPEN_INTENTS_IN_NEW_TAB, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(OPEN_INTENTS_IN_NEW_TAB)) == 1)
554                 .putBoolean(SWIPE_TO_REFRESH, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH)) == 1)
555                 .putBoolean(DOWNLOAD_WITH_EXTERNAL_APP, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DOWNLOAD_WITH_EXTERNAL_APP)) == 1)
556                 .putBoolean(SCROLL_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(SCROLL_APP_BAR)) == 1)
557                 .putBoolean(BOTTOM_APP_BAR, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(BOTTOM_APP_BAR)) == 1)
558                 .putBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DISPLAY_ADDITIONAL_APP_BAR_ICONS)) == 1)
559                 .putString(APP_THEME, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(APP_THEME)))
560                 .putString(WEBVIEW_THEME, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndexOrThrow(WEBVIEW_THEME)))
561                 .putBoolean(WIDE_VIEWPORT, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(WIDE_VIEWPORT)) == 1)
562                 .putBoolean(DISPLAY_WEBPAGE_IMAGES, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndexOrThrow(DISPLAY_WEBPAGE_IMAGES)) == 1)
563                 .apply()
564
565             // Close the preferences cursor and database.
566             importPreferencesCursor.close()
567             importDatabase.close()
568
569             // Delete the temporary import file database, journal, and other related auxiliary files.
570             SQLiteDatabase.deleteDatabase(temporaryImportFile)
571
572             // Return the import successful string.
573             IMPORT_SUCCESSFUL
574         } catch (exception: Exception) {
575             // Return the import error.
576             exception.toString()
577         }
578     }
579
580     fun exportUnencrypted(exportFileOutputStream: OutputStream, context: Context): String {
581         return try {
582             // Create a temporary export file.
583             val temporaryExportFile = File.createTempFile("temporary_export_file", null, context.cacheDir)
584
585             // Create the temporary export database.
586             val temporaryExportDatabase = SQLiteDatabase.openOrCreateDatabase(temporaryExportFile, null)
587
588             // Set the temporary export database version number.
589             temporaryExportDatabase.version = SCHEMA_VERSION
590
591
592             // Create the temporary export database bookmarks table.
593             temporaryExportDatabase.execSQL(BookmarksDatabaseHelper.CREATE_BOOKMARKS_TABLE)
594
595             // Open the bookmarks database.
596             val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
597
598             // Get a full bookmarks cursor.
599             val bookmarksCursor = bookmarksDatabaseHelper.allBookmarks
600
601             // Move to the first record.
602             bookmarksCursor.moveToFirst()
603
604             // Copy the data from the bookmarks cursor into the export database.
605             for (i in 0 until bookmarksCursor.count) {
606                 // Create a bookmark content values.
607                 val bookmarkContentValues = ContentValues()
608
609                 // Populate the bookmark content values.
610                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_NAME, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME)))
611                 bookmarkContentValues.put(BookmarksDatabaseHelper.BOOKMARK_URL, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
612                 bookmarkContentValues.put(BookmarksDatabaseHelper.PARENT_FOLDER, bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER)))
613                 bookmarkContentValues.put(BookmarksDatabaseHelper.DISPLAY_ORDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)))
614                 bookmarkContentValues.put(BookmarksDatabaseHelper.IS_FOLDER, bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)))
615                 bookmarkContentValues.put(BookmarksDatabaseHelper.FAVORITE_ICON, bookmarksCursor.getBlob(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON)))
616
617                 // Insert the content values into the temporary export database.
618                 temporaryExportDatabase.insert(BookmarksDatabaseHelper.BOOKMARKS_TABLE, null, bookmarkContentValues)
619
620                 // Advance to the next record.
621                 bookmarksCursor.moveToNext()
622             }
623
624             // Close the bookmarks cursor and database.
625             bookmarksCursor.close()
626             bookmarksDatabaseHelper.close()
627
628
629             // Create the temporary export database domains table.
630             temporaryExportDatabase.execSQL(DomainsDatabaseHelper.CREATE_DOMAINS_TABLE)
631
632             // Open the domains database.
633             val domainsDatabaseHelper = DomainsDatabaseHelper(context)
634
635             // Get a full domains database cursor.
636             val domainsCursor = domainsDatabaseHelper.completeCursorOrderedByDomain
637
638             // Move to the first record.
639             domainsCursor.moveToFirst()
640
641             // Copy the data from the domains cursor into the export database.
642             for (i in 0 until domainsCursor.count) {
643                 // Create a domain content values.
644                 val domainContentValues = ContentValues()
645
646                 // Populate the domain content values.
647                 domainContentValues.put(DomainsDatabaseHelper.DOMAIN_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME)))
648                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_JAVASCRIPT, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)))
649                 domainContentValues.put(DomainsDatabaseHelper.COOKIES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)))
650                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_DOM_STORAGE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)))
651                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FORM_DATA, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)))
652                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYLIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)))
653                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_EASYPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)))
654                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)))
655                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST,
656                     domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)))
657                 domainContentValues.put(DomainsDatabaseHelper.ULTRALIST, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)))
658                 domainContentValues.put(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)))
659                 domainContentValues.put(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)))
660                 domainContentValues.put(DomainsDatabaseHelper.USER_AGENT, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT)))
661                 domainContentValues.put(DomainsDatabaseHelper.FONT_SIZE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE)))
662                 domainContentValues.put(DomainsDatabaseHelper.SWIPE_TO_REFRESH, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH)))
663                 domainContentValues.put(DomainsDatabaseHelper.WEBVIEW_THEME, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME)))
664                 domainContentValues.put(DomainsDatabaseHelper.WIDE_VIEWPORT, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT)))
665                 domainContentValues.put(DomainsDatabaseHelper.DISPLAY_IMAGES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES)))
666                 domainContentValues.put(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)))
667                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)))
668                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)))
669                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT,
670                     domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)))
671                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)))
672                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)))
673                 domainContentValues.put(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT,
674                     domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)))
675                 domainContentValues.put(DomainsDatabaseHelper.SSL_START_DATE, domainsCursor.getLong(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)))
676                 domainContentValues.put(DomainsDatabaseHelper.SSL_END_DATE, domainsCursor.getLong(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)))
677                 domainContentValues.put(DomainsDatabaseHelper.PINNED_IP_ADDRESSES, domainsCursor.getInt(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)))
678                 domainContentValues.put(DomainsDatabaseHelper.IP_ADDRESSES, domainsCursor.getString(domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES)))
679
680                 // Insert the content values into the temporary export database.
681                 temporaryExportDatabase.insert(DomainsDatabaseHelper.DOMAINS_TABLE, null, domainContentValues)
682
683                 // Advance to the next record.
684                 domainsCursor.moveToNext()
685             }
686
687             // Close the domains cursor and database.
688             domainsCursor.close()
689             domainsDatabaseHelper.close()
690
691
692             // Prepare the preferences table SQL creation string.
693             val createPreferencesTable = "CREATE TABLE $PREFERENCES_TABLE (" +
694                     "$ID INTEGER PRIMARY KEY, " +
695                     "$JAVASCRIPT BOOLEAN, " +
696                     "$COOKIES BOOLEAN, " +
697                     "$DOM_STORAGE BOOLEAN, " +
698                     "$SAVE_FORM_DATA BOOLEAN, " +
699                     "$USER_AGENT TEXT, " +
700                     "$CUSTOM_USER_AGENT TEXT, " +
701                     "$INCOGNITO_MODE BOOLEAN, " +
702                     "$ALLOW_SCREENSHOTS BOOLEAN, " +
703                     "$EASYLIST BOOLEAN, " +
704                     "$EASYPRIVACY BOOLEAN, " +
705                     "$FANBOYS_ANNOYANCE_LIST BOOLEAN, " +
706                     "$FANBOYS_SOCIAL_BLOCKING_LIST BOOLEAN, " +
707                     "$ULTRALIST BOOLEAN, " +
708                     "$ULTRAPRIVACY BOOLEAN, " +
709                     "$BLOCK_ALL_THIRD_PARTY_REQUESTS BOOLEAN, " +
710                     "$TRACKING_QUERIES BOOLEAN, " +
711                     "$AMP_REDIRECTS BOOLEAN, " +
712                     "$SEARCH TEXT, " +
713                     "$SEARCH_CUSTOM_URL TEXT, " +
714                     "$PROXY TEXT, " +
715                     "$PROXY_CUSTOM_URL TEXT, " +
716                     "$FULL_SCREEN_BROWSING_MODE BOOLEAN, " +
717                     "$HIDE_APP_BAR BOOLEAN, " +
718                     "$CLEAR_EVERYTHING BOOLEAN, " +
719                     "$CLEAR_COOKIES BOOLEAN, " +
720                     "$CLEAR_DOM_STORAGE BOOLEAN, " +
721                     "$CLEAR_FORM_DATA BOOLEAN, " +
722                     "$CLEAR_LOGCAT BOOLEAN, " +
723                     "$CLEAR_CACHE BOOLEAN, " +
724                     "$HOMEPAGE TEXT, " +
725                     "$FONT_SIZE TEXT, " +
726                     "$OPEN_INTENTS_IN_NEW_TAB BOOLEAN, " +
727                     "$SWIPE_TO_REFRESH BOOLEAN, " +
728                     "$DOWNLOAD_WITH_EXTERNAL_APP BOOLEAN, " +
729                     "$SCROLL_APP_BAR BOOLEAN, " +
730                     "$BOTTOM_APP_BAR BOOLEAN, " +
731                     "$DISPLAY_ADDITIONAL_APP_BAR_ICONS BOOLEAN, " +
732                     "$APP_THEME TEXT, " +
733                     "$WEBVIEW_THEME TEXT, " +
734                     "$WIDE_VIEWPORT BOOLEAN, " +
735                     "$DISPLAY_WEBPAGE_IMAGES BOOLEAN)"
736
737             // Create the temporary export database preferences table.
738             temporaryExportDatabase.execSQL(createPreferencesTable)
739
740             // Get a handle for the shared preference.
741             val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
742
743             // Create a preferences content values.
744             val preferencesContentValues = ContentValues()
745
746             // Populate the preferences content values.
747             preferencesContentValues.put(JAVASCRIPT, sharedPreferences.getBoolean(JAVASCRIPT, false))
748             preferencesContentValues.put(COOKIES, sharedPreferences.getBoolean(COOKIES, false))
749             preferencesContentValues.put(DOM_STORAGE, sharedPreferences.getBoolean(DOM_STORAGE, false))
750             preferencesContentValues.put(SAVE_FORM_DATA, sharedPreferences.getBoolean(SAVE_FORM_DATA, false))  // Save form data can be removed once the minimum API >= 26.
751             preferencesContentValues.put(USER_AGENT, sharedPreferences.getString(USER_AGENT, context.getString(R.string.user_agent_default_value)))
752             preferencesContentValues.put(CUSTOM_USER_AGENT, sharedPreferences.getString(CUSTOM_USER_AGENT, context.getString(R.string.custom_user_agent_default_value)))
753             preferencesContentValues.put(INCOGNITO_MODE, sharedPreferences.getBoolean(INCOGNITO_MODE, false))
754             preferencesContentValues.put(ALLOW_SCREENSHOTS, sharedPreferences.getBoolean(ALLOW_SCREENSHOTS, false))
755             preferencesContentValues.put(EASYLIST, sharedPreferences.getBoolean(EASYLIST, true))
756             preferencesContentValues.put(EASYPRIVACY, sharedPreferences.getBoolean(EASYPRIVACY, true))
757             preferencesContentValues.put(FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean(FANBOYS_ANNOYANCE_LIST, true))
758             preferencesContentValues.put(FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean(FANBOYS_SOCIAL_BLOCKING_LIST, true))
759             preferencesContentValues.put(ULTRALIST, sharedPreferences.getBoolean(ULTRALIST, true))
760             preferencesContentValues.put(ULTRAPRIVACY, sharedPreferences.getBoolean(ULTRAPRIVACY, true))
761             preferencesContentValues.put(BLOCK_ALL_THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, false))
762             preferencesContentValues.put(TRACKING_QUERIES, sharedPreferences.getBoolean(TRACKING_QUERIES, true))
763             preferencesContentValues.put(AMP_REDIRECTS, sharedPreferences.getBoolean(AMP_REDIRECTS, true))
764             preferencesContentValues.put(SEARCH, sharedPreferences.getString(SEARCH, context.getString(R.string.search_default_value)))
765             preferencesContentValues.put(SEARCH_CUSTOM_URL, sharedPreferences.getString(SEARCH_CUSTOM_URL, context.getString(R.string.search_custom_url_default_value)))
766             preferencesContentValues.put(PROXY, sharedPreferences.getString(PROXY, context.getString(R.string.proxy_default_value)))
767             preferencesContentValues.put(PROXY_CUSTOM_URL, sharedPreferences.getString(PROXY_CUSTOM_URL, context.getString(R.string.proxy_custom_url_default_value)))
768             preferencesContentValues.put(FULL_SCREEN_BROWSING_MODE, sharedPreferences.getBoolean(FULL_SCREEN_BROWSING_MODE, false))
769             preferencesContentValues.put(HIDE_APP_BAR, sharedPreferences.getBoolean(HIDE_APP_BAR, true))
770             preferencesContentValues.put(CLEAR_EVERYTHING, sharedPreferences.getBoolean(CLEAR_EVERYTHING, true))
771             preferencesContentValues.put(CLEAR_COOKIES, sharedPreferences.getBoolean(CLEAR_COOKIES, true))
772             preferencesContentValues.put(CLEAR_DOM_STORAGE, sharedPreferences.getBoolean(CLEAR_DOM_STORAGE, true))
773             preferencesContentValues.put(CLEAR_FORM_DATA, sharedPreferences.getBoolean(CLEAR_FORM_DATA, true))  // Clear form data can be removed once the minimum API >= 26.
774             preferencesContentValues.put(CLEAR_LOGCAT, sharedPreferences.getBoolean(CLEAR_LOGCAT, true))
775             preferencesContentValues.put(CLEAR_CACHE, sharedPreferences.getBoolean(CLEAR_CACHE, true))
776             preferencesContentValues.put(HOMEPAGE, sharedPreferences.getString(HOMEPAGE, context.getString(R.string.homepage_default_value)))
777             preferencesContentValues.put(FONT_SIZE, sharedPreferences.getString(FONT_SIZE, context.getString(R.string.font_size_default_value)))
778             preferencesContentValues.put(OPEN_INTENTS_IN_NEW_TAB, sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true))
779             preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean(SWIPE_TO_REFRESH, true))
780             preferencesContentValues.put(DOWNLOAD_WITH_EXTERNAL_APP, sharedPreferences.getBoolean(DOWNLOAD_WITH_EXTERNAL_APP, false))
781             preferencesContentValues.put(SCROLL_APP_BAR, sharedPreferences.getBoolean(SCROLL_APP_BAR, true))
782             preferencesContentValues.put(BOTTOM_APP_BAR, sharedPreferences.getBoolean(BOTTOM_APP_BAR, false))
783             preferencesContentValues.put(DISPLAY_ADDITIONAL_APP_BAR_ICONS, sharedPreferences.getBoolean(DISPLAY_ADDITIONAL_APP_BAR_ICONS, false))
784             preferencesContentValues.put(APP_THEME, sharedPreferences.getString(APP_THEME, context.getString(R.string.app_theme_default_value)))
785             preferencesContentValues.put(WEBVIEW_THEME, sharedPreferences.getString(WEBVIEW_THEME, context.getString(R.string.webview_theme_default_value)))
786             preferencesContentValues.put(WIDE_VIEWPORT, sharedPreferences.getBoolean(WIDE_VIEWPORT, true))
787             preferencesContentValues.put(DISPLAY_WEBPAGE_IMAGES, sharedPreferences.getBoolean(DISPLAY_WEBPAGE_IMAGES, true))
788
789             // Insert the preferences content values into the temporary export database.
790             temporaryExportDatabase.insert(PREFERENCES_TABLE, null, preferencesContentValues)
791
792             // Close the temporary export database.
793             temporaryExportDatabase.close()
794
795
796             // 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>
797             // It can be copied in Android using `Files.copy` once the minimum API >= 26.
798             // <https://developer.android.com/reference/java/nio/file/Files#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...)>
799             // 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>
800
801             // Create the temporary export file input stream.
802             val temporaryExportFileInputStream = FileInputStream(temporaryExportFile)
803
804             // Create a byte array.
805             val transferByteArray = ByteArray(1024)
806
807             // Create an integer to track the number of bytes read.
808             var bytesRead: Int
809
810             // Copy the temporary export file to the export file output stream.
811             while (temporaryExportFileInputStream.read(transferByteArray).also { bytesRead = it } > 0) {
812                 exportFileOutputStream.write(transferByteArray, 0, bytesRead)
813             }
814
815             // Flush the export file output stream.
816             exportFileOutputStream.flush()
817
818             // Close the file streams.
819             temporaryExportFileInputStream.close()
820             exportFileOutputStream.close()
821
822             // Delete the temporary export file database, journal, and other related auxiliary files.
823             SQLiteDatabase.deleteDatabase(temporaryExportFile)
824
825             // Return the export successful string.
826             EXPORT_SUCCESSFUL
827         } catch (exception: Exception) {
828             // Return the export error.
829             exception.toString()
830         }
831     }
832 }