2 * Copyright 2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.helpers
22 import android.content.Context
23 import android.graphics.Bitmap
24 import android.graphics.drawable.BitmapDrawable
25 import android.net.Uri
26 import android.util.Base64
27 import android.widget.ScrollView
29 import androidx.appcompat.content.res.AppCompatResources
31 import com.google.android.material.snackbar.Snackbar
32 import com.stoutner.privacybrowser.BuildConfig
34 import com.stoutner.privacybrowser.R
36 import java.io.BufferedReader
37 import java.io.ByteArrayOutputStream
38 import java.io.InputStreamReader
39 import java.lang.Exception
40 import java.nio.charset.StandardCharsets
42 class ImportExportBookmarksHelper {
43 // Define the class variables.
44 private var bookmarksAndFolderExported = 0
46 fun importBookmarks(fileNameString: String, context: Context, scrollView: ScrollView) {
48 // Get an input stream for the file name.
49 val inputStream = context.contentResolver.openInputStream(Uri.parse(fileNameString))!!
51 // Load the bookmarks input stream into a buffered reader.
52 val bufferedReader = BufferedReader(InputStreamReader(inputStream))
54 // Instantiate the bookmarks database helper.
55 val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
57 // Get the default icon drawables.
58 val defaultFavoriteIconDrawable = AppCompatResources.getDrawable(context, R.drawable.world)
59 val defaultFolderIconDrawable = AppCompatResources.getDrawable(context, R.drawable.folder_blue_bitmap)
61 // Cast the default icon drawables to bitmap drawables.
62 val defaultFavoriteIconBitmapDrawable = (defaultFavoriteIconDrawable as BitmapDrawable?)!!
63 val defaultFolderIconBitmapDrawable = (defaultFolderIconDrawable as BitmapDrawable)
65 // Get the default icon bitmaps.
66 val defaultFavoriteIconBitmap = defaultFavoriteIconBitmapDrawable.bitmap
67 val defaultFolderIconBitmap = defaultFolderIconBitmapDrawable.bitmap
69 // Create the default icon byte array output streams.
70 val defaultFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
71 val defaultFolderIconByteArrayOutputStream = ByteArrayOutputStream()
73 // Convert the default icon bitmaps to byte array streams. `0` is for lossless compression (the only option for a PNG).
74 defaultFavoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFavoriteIconByteArrayOutputStream)
75 defaultFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, defaultFolderIconByteArrayOutputStream)
77 // Convert the icon byte array streams to byte array stream to byte arrays.
78 val defaultFavoriteIconByteArray = defaultFavoriteIconByteArrayOutputStream.toByteArray()
79 val defaultFolderIconByteArray = defaultFolderIconByteArrayOutputStream.toByteArray()
81 // Get a cursor with all the bookmarks.
82 val initialNumberOfBookmarksAndFoldersCursor = bookmarksDatabaseHelper.allBookmarkAndFolderIds
84 // Get an initial count of the folders and bookmarks.
85 val initialNumberOfFoldersAndBookmarks = initialNumberOfBookmarksAndFoldersCursor.count
88 initialNumberOfBookmarksAndFoldersCursor.close()
90 // Get a cursor with the contents of the home folder.
91 val homeFolderContentCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(0L)
93 // Initialize the variables.
94 var parentFolderId = 0L
95 var displayOrder = homeFolderContentCursor.count
98 homeFolderContentCursor.close()
100 // Parse the bookmarks.
101 bufferedReader.forEachLine {
103 var line = it.trimStart()
105 // Only process interesting lines.
106 if (line.startsWith("<DT>")) { // This is a bookmark or a folder.
107 // Remove the initial `<DT>`
108 line = line.substring(4)
110 // Check to see if this is a bookmark or a folder.
111 if (line.contains("HREF=\"")) { // This is a bookmark.
112 // Remove the text before the bookmark name.
113 var bookmarkName = line.substring(line.indexOf(">") + 1)
115 // Remove the text after the bookmark name.
116 bookmarkName = bookmarkName.substring(0, bookmarkName.indexOf("<"))
118 // Remove the text before the bookmark URL.
119 var bookmarkUrl = line.substring(line.indexOf("HREF=\"") + 6)
121 // Remove the text after the bookmark URL.
122 bookmarkUrl = bookmarkUrl.substring(0, bookmarkUrl.indexOf("\""))
124 // Initialize the favorite icon string.
125 var favoriteIconString = ""
127 // Populate the favorite icon string.
128 if (line.contains("ICON=\"data:image/png;base64,")) { // The `ICON` attribute contains a Base64 encoded icon.
129 // Remove the text before the icon string.
130 favoriteIconString = line.substring(line.indexOf("ICON=\"data:image/png;base64,") + 28)
132 // Remove the text after the icon string.
133 favoriteIconString = favoriteIconString.substring(0, favoriteIconString.indexOf("\""))
134 } else if (line.contains("ICON_URI=\"data:image/png;base64,")) { // The `ICON_URI` attribute contains a Base64 encoded icon.
135 // Remove the text before the icon string.
136 favoriteIconString = line.substring(line.indexOf("ICON_URI=\"data:image/png;base64,") + 32)
138 // Remove the text after the icon string.
139 favoriteIconString = favoriteIconString.substring(0, favoriteIconString.indexOf("\""))
142 // Populate the favorite icon byte array.
143 val favoriteIconByteArray = if (favoriteIconString.isEmpty()) // The favorite icon string is empty. Use the default favorite icon.
144 defaultFavoriteIconByteArray
145 else // The favorite icon string is populated. Decode it to a byte array.
146 Base64.decode(favoriteIconString, Base64.DEFAULT)
149 bookmarksDatabaseHelper.createBookmark(bookmarkName, bookmarkUrl, parentFolderId, displayOrder, favoriteIconByteArray)
151 // Increment the display order.
153 } else { // This is a folder. The following lines will be contain in this folder until a `</DL>` is encountered.
154 // Remove the text before the folder name.
155 var folderName = line.substring(line.indexOf(">") + 1)
157 // Remove the text after the folder name.
158 folderName = folderName.substring(0, folderName.indexOf("<"))
160 // Add the folder and set it as the new parent folder ID.
161 parentFolderId = bookmarksDatabaseHelper.createFolder(folderName, parentFolderId, displayOrder, defaultFolderIconByteArray)
163 // Reset the display order.
166 } else if (line.startsWith("</DL>")) { // This is the end of a folder's contents.
167 // Reset the parent folder id if it isn't 0.
168 if (parentFolderId != 0L)
169 parentFolderId = bookmarksDatabaseHelper.getParentFolderId(parentFolderId)
171 // Get a cursor with the contents of the new parent folder.
172 val folderContentCursor = bookmarksDatabaseHelper.getBookmarkAndFolderIds(parentFolderId)
174 // Reset the display order.
175 displayOrder = folderContentCursor.count
178 folderContentCursor.close()
182 // Get a cursor with all the bookmarks.
183 val finalNumberOfBookmarksAndFoldersCursor = bookmarksDatabaseHelper.allBookmarkAndFolderIds
185 // Get the final count of the folders and bookmarks.
186 val finalNumberOfFoldersAndBookmarks = finalNumberOfBookmarksAndFoldersCursor.count
189 finalNumberOfBookmarksAndFoldersCursor.close()
191 // Close the bookmarks database helper.
192 bookmarksDatabaseHelper.close()
194 // Close the input stream.
197 // Display a snackbar with the number of folders and bookmarks imported.
198 Snackbar.make(scrollView, context.getString(R.string.bookmarks_imported, finalNumberOfFoldersAndBookmarks - initialNumberOfFoldersAndBookmarks), Snackbar.LENGTH_LONG).show()
199 } catch (exception: Exception) {
200 // Display a snackbar with the error message.
201 Snackbar.make(scrollView, context.getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
205 fun exportBookmarks(fileNameString: String, context: Context, scrollView: ScrollView) {
206 // Create a bookmarks string builder
207 val bookmarksStringBuilder = StringBuilder()
209 // Populate the headers.
210 bookmarksStringBuilder.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>")
211 bookmarksStringBuilder.append("\n\n")
212 bookmarksStringBuilder.append("<!-- These bookmarks were exported from Privacy Browser Android ${BuildConfig.VERSION_NAME}. -->")
213 bookmarksStringBuilder.append("\n\n")
214 bookmarksStringBuilder.append("<meta HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">")
215 bookmarksStringBuilder.append("\n\n")
217 // Begin the bookmarks.
218 bookmarksStringBuilder.append("<DL><p>")
219 bookmarksStringBuilder.append("\n")
221 // Instantiate the bookmarks database helper.
222 val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context)
224 // Initialize the indent string.
225 val indentString = " "
227 // Populate the bookmarks, starting with the home folder.
228 bookmarksStringBuilder.append(populateBookmarks(bookmarksDatabaseHelper, 0L, indentString))
230 // End the bookmarks.
231 bookmarksStringBuilder.append("</DL>")
232 bookmarksStringBuilder.append("\n")
235 // Get an output stream for the file name, truncating any existing content.
236 val outputStream = context.contentResolver.openOutputStream(Uri.parse(fileNameString), "wt")!!
238 // Write the bookmarks string to the output stream.
239 outputStream.write(bookmarksStringBuilder.toString().toByteArray(StandardCharsets.UTF_8))
241 // Close the output stream.
244 // Display a snackbar with the number of folders and bookmarks exported.
245 Snackbar.make(scrollView, context.getString(R.string.bookmarks_exported, bookmarksAndFolderExported), Snackbar.LENGTH_LONG).show()
246 } catch (exception: Exception) {
247 // Display a snackbar with the error message.
248 Snackbar.make(scrollView, context.getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
251 // Close the bookmarks database helper.
252 bookmarksDatabaseHelper.close()
255 private fun populateBookmarks(bookmarksDatabaseHelper: BookmarksDatabaseHelper, folderId: Long, indentString: String): String {
256 // Create a bookmarks string builder.
257 val bookmarksStringBuilder = StringBuilder()
259 // Get all the bookmarks in the current folder.
260 val bookmarksCursor = bookmarksDatabaseHelper.getBookmarks(folderId)
262 // Process each bookmark.
263 while (bookmarksCursor.moveToNext()) {
264 if (bookmarksCursor.getInt(bookmarksCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) { // This is a folder.
265 // Export the folder.
266 bookmarksStringBuilder.append(indentString)
267 bookmarksStringBuilder.append("<DT><H3>")
268 bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME)))
269 bookmarksStringBuilder.append("</H3>")
270 bookmarksStringBuilder.append("\n")
272 // Begin the folder contents.
273 bookmarksStringBuilder.append(indentString)
274 bookmarksStringBuilder.append("<DL><p>")
275 bookmarksStringBuilder.append("\n")
277 // Populate the folder contents.
278 bookmarksStringBuilder.append(populateBookmarks(bookmarksDatabaseHelper, bookmarksCursor.getLong(bookmarksCursor.getColumnIndexOrThrow(FOLDER_ID)), " $indentString"))
280 // End the folder contents.
281 bookmarksStringBuilder.append(indentString)
282 bookmarksStringBuilder.append("</DL><p>")
283 bookmarksStringBuilder.append("\n")
285 // Increment the bookmarks and folders exported counter.
286 ++bookmarksAndFolderExported
287 } else { // This is a bookmark.
288 // Export the bookmark.
289 bookmarksStringBuilder.append(indentString)
290 bookmarksStringBuilder.append("<DT><A HREF=\"")
291 bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_URL)))
292 bookmarksStringBuilder.append("\" ICON=\"data:image/png;base64,")
293 bookmarksStringBuilder.append(Base64.encodeToString(bookmarksCursor.getBlob(bookmarksCursor.getColumnIndexOrThrow(FAVORITE_ICON)), Base64.NO_WRAP))
294 bookmarksStringBuilder.append("\">")
295 bookmarksStringBuilder.append(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_NAME)))
296 bookmarksStringBuilder.append("</A>")
297 bookmarksStringBuilder.append("\n")
299 // Increment the bookmarks and folders exported counter.
300 ++bookmarksAndFolderExported
304 // Return the bookmarks string.
305 return bookmarksStringBuilder.toString()