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