Skip to content
This repository was archived by the owner on Apr 2, 2024. It is now read-only.

Commit 2eb3f5f

Browse files
committed
Add support for importing bookmarks from backups in the netscape bookmark format
Support importing from backup files in the new HTML format as well as the old custom JSON format.
1 parent fb977a8 commit 2eb3f5f

File tree

5 files changed

+160
-43
lines changed

5 files changed

+160
-43
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package acr.browser.lightning.bookmark
2+
3+
import acr.browser.lightning.database.Bookmark
4+
import java.io.InputStream
5+
6+
/**
7+
* An importer that imports [Bookmark.Entry] from an [InputStream]. Supported formats are details of
8+
* the implementation.
9+
*/
10+
interface BookmarkImporter {
11+
12+
/**
13+
* Synchronously converts an [InputStream] to a [List] of [Bookmark.Entry].
14+
*/
15+
fun importBookmarks(inputStream: InputStream): List<Bookmark.Entry>
16+
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package acr.browser.lightning.bookmark
2+
3+
import acr.browser.lightning.database.Bookmark
4+
import acr.browser.lightning.database.bookmark.BookmarkExporter
5+
import java.io.InputStream
6+
import javax.inject.Inject
7+
8+
/**
9+
* A [BookmarkImporter] that imports bookmark files that were produced by [BookmarkExporter].
10+
*/
11+
class LegacyBookmarkImporter @Inject constructor() : BookmarkImporter {
12+
13+
override fun importBookmarks(inputStream: InputStream): List<Bookmark.Entry> {
14+
return BookmarkExporter.importBookmarksFromFileStream(inputStream)
15+
}
16+
17+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package acr.browser.lightning.bookmark
2+
3+
import acr.browser.lightning.constant.UTF8
4+
import acr.browser.lightning.database.Bookmark
5+
import acr.browser.lightning.database.asFolder
6+
import org.jsoup.Jsoup
7+
import org.jsoup.nodes.Element
8+
import java.io.InputStream
9+
import javax.inject.Inject
10+
11+
/**
12+
* An importer that supports the Netscape Bookmark File Format.
13+
*
14+
* See https://msdn.microsoft.com/en-us/ie/aa753582(v=vs.94)
15+
*/
16+
class NetscapeBookmarkFormatImporter @Inject constructor() : BookmarkImporter {
17+
18+
override fun importBookmarks(inputStream: InputStream): List<Bookmark.Entry> {
19+
val document = Jsoup.parse(inputStream, UTF8, "")
20+
21+
val rootList = document.body().children().first { it.isTag(LIST_TAG) }
22+
23+
return rootList.processFolder(ROOT_FOLDER_NAME)
24+
}
25+
26+
/**
27+
* @return The [List] of [Bookmark.Entry] held by [Element] with the provided [folderName].
28+
*/
29+
private fun Element.processFolder(folderName: String): List<Bookmark.Entry> {
30+
return children()
31+
.filter { it.isTag(ITEM_TAG) }
32+
.flatMap {
33+
val immediateChild = it.child(0)
34+
when {
35+
immediateChild.isTag(FOLDER_TAG) ->
36+
immediateChild.nextElementSibling()
37+
.processFolder(computeFolderName(folderName, immediateChild.text()))
38+
immediateChild.isTag(BOOKMARK_TAG) ->
39+
listOf(Bookmark.Entry(
40+
url = immediateChild.attr(HREF),
41+
title = immediateChild.text(),
42+
position = 0,
43+
folder = folderName.asFolder()
44+
))
45+
else -> emptyList()
46+
}
47+
}
48+
}
49+
50+
/**
51+
* @return True if the element's tag name matches the [tagName], case insentitive, false
52+
* otherwise.
53+
*/
54+
private fun Element.isTag(tagName: String): Boolean {
55+
return tagName().equals(tagName, ignoreCase = true)
56+
}
57+
58+
/**
59+
* @return The [currentFolder] if the [parentFolder] is empty, otherwise prepend the
60+
* [parentFolder] to the [currentFolder] and return that.
61+
*/
62+
private fun computeFolderName(parentFolder: String, currentFolder: String): String =
63+
if (parentFolder.isEmpty()) {
64+
currentFolder
65+
} else {
66+
"$parentFolder/${currentFolder}"
67+
}
68+
69+
companion object {
70+
const val ITEM_TAG = "DT"
71+
const val LIST_TAG = "DL"
72+
const val BOOKMARK_TAG = "A"
73+
const val FOLDER_TAG = "H3"
74+
const val HREF = "HREF"
75+
const val ROOT_FOLDER_NAME = ""
76+
}
77+
78+
}

app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.io.BufferedReader;
1111
import java.io.BufferedWriter;
1212
import java.io.File;
13-
import java.io.FileReader;
1413
import java.io.FileWriter;
1514
import java.io.IOException;
1615
import java.io.InputStream;
@@ -26,7 +25,6 @@
2625
import androidx.annotation.NonNull;
2726
import androidx.annotation.WorkerThread;
2827
import io.reactivex.Completable;
29-
import io.reactivex.Single;
3028

3129
/**
3230
* The class responsible for importing and exporting
@@ -127,38 +125,34 @@ public static Completable exportBookmarksToFile(@NonNull final List<Bookmark.Ent
127125
* given file. If the file is not in a
128126
* supported format, it will fail.
129127
*
130-
* @param file the file to import from.
131-
* @return an observable that emits the
132-
* imported bookmarks, or an error if the
133-
* file cannot be imported.
128+
* @param inputStream The stream to import from.
129+
* @return A list of bookmarks, or throws an exception if the bookmarks cannot be imported.
134130
*/
135131
@NonNull
136-
public static Single<List<Bookmark.Entry>> importBookmarksFromFile(@NonNull final File file) {
137-
return Single.fromCallable(() -> {
138-
BufferedReader bookmarksReader = null;
139-
try {
140-
//noinspection IOResourceOpenedButNotSafelyClosed
141-
bookmarksReader = new BufferedReader(new FileReader(file));
142-
String line;
143-
144-
List<Bookmark.Entry> bookmarks = new ArrayList<>();
145-
while ((line = bookmarksReader.readLine()) != null) {
146-
JSONObject object = new JSONObject(line);
147-
final String folderName = object.getString(KEY_FOLDER);
148-
final Bookmark.Entry entry = new Bookmark.Entry(
149-
object.getString(KEY_TITLE),
150-
object.getString(KEY_URL),
151-
object.getInt(KEY_ORDER),
152-
WebPageKt.asFolder(folderName)
153-
);
154-
bookmarks.add(entry);
155-
}
132+
public static List<Bookmark.Entry> importBookmarksFromFileStream(@NonNull InputStream inputStream) throws Exception {
133+
BufferedReader bookmarksReader = null;
134+
try {
135+
//noinspection IOResourceOpenedButNotSafelyClosed
136+
bookmarksReader = new BufferedReader(new InputStreamReader(inputStream));
137+
String line;
156138

157-
return bookmarks;
158-
} finally {
159-
Utils.close(bookmarksReader);
139+
List<Bookmark.Entry> bookmarks = new ArrayList<>();
140+
while ((line = bookmarksReader.readLine()) != null) {
141+
JSONObject object = new JSONObject(line);
142+
final String folderName = object.getString(KEY_FOLDER);
143+
final Bookmark.Entry entry = new Bookmark.Entry(
144+
object.getString(KEY_TITLE),
145+
object.getString(KEY_URL),
146+
object.getInt(KEY_ORDER),
147+
WebPageKt.asFolder(folderName)
148+
);
149+
bookmarks.add(entry);
160150
}
161-
});
151+
152+
return bookmarks;
153+
} finally {
154+
Utils.close(bookmarksReader);
155+
}
162156
}
163157

164158
/**

app/src/main/java/acr/browser/lightning/settings/fragment/BookmarkSettingsFragment.kt

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package acr.browser.lightning.settings.fragment
55

66
import acr.browser.lightning.R
7+
import acr.browser.lightning.bookmark.LegacyBookmarkImporter
8+
import acr.browser.lightning.bookmark.NetscapeBookmarkFormatImporter
79
import acr.browser.lightning.database.bookmark.BookmarkExporter
810
import acr.browser.lightning.database.bookmark.BookmarkRepository
911
import acr.browser.lightning.di.DatabaseScheduler
@@ -24,6 +26,7 @@ import androidx.appcompat.app.AlertDialog
2426
import com.anthonycr.grant.PermissionsManager
2527
import com.anthonycr.grant.PermissionsResultAction
2628
import io.reactivex.Scheduler
29+
import io.reactivex.Single
2730
import io.reactivex.disposables.Disposable
2831
import io.reactivex.rxkotlin.subscribeBy
2932
import java.io.File
@@ -34,6 +37,8 @@ class BookmarkSettingsFragment : AbstractSettingsFragment() {
3437

3538
@Inject internal lateinit var bookmarkRepository: BookmarkRepository
3639
@Inject internal lateinit var application: Application
40+
@Inject internal lateinit var netscapeBookmarkFormatImporter: NetscapeBookmarkFormatImporter
41+
@Inject internal lateinit var legacyBookmarkImporter: LegacyBookmarkImporter
3742
@Inject @field:DatabaseScheduler internal lateinit var databaseScheduler: Scheduler
3843
@Inject @field:MainScheduler internal lateinit var mainScheduler: Scheduler
3944
@Inject internal lateinit var logger: Logger
@@ -197,23 +202,27 @@ class BookmarkSettingsFragment : AbstractSettingsFragment() {
197202
if (fileList[which].isDirectory) {
198203
showImportBookmarkDialog(fileList[which])
199204
} else {
200-
importSubscription = BookmarkExporter
201-
.importBookmarksFromFile(fileList[which])
205+
Single.fromCallable(fileList[which]::inputStream)
206+
.map {
207+
if (fileList[which].extension == EXTENSION_HTML) {
208+
netscapeBookmarkFormatImporter.importBookmarks(it)
209+
} else {
210+
legacyBookmarkImporter.importBookmarks(it)
211+
}
212+
}
213+
.flatMap {
214+
bookmarkRepository.addBookmarkList(it).andThen(Single.just(it.size))
215+
}
202216
.subscribeOn(databaseScheduler)
203217
.observeOn(mainScheduler)
204218
.subscribeBy(
205-
onSuccess = { importList ->
206-
bookmarkRepository.addBookmarkList(importList)
207-
.subscribeOn(databaseScheduler)
208-
.observeOn(mainScheduler)
209-
.subscribe {
210-
activity?.apply {
211-
snackbar("${importList.size} ${getString(R.string.message_import)}")
212-
}
213-
}
219+
onSuccess = { count ->
220+
activity?.apply {
221+
snackbar("$count ${getString(R.string.message_import)}")
222+
}
214223
},
215-
onError = { throwable ->
216-
logger.log(TAG, "onError: importing bookmarks", throwable)
224+
onError = {
225+
logger.log(TAG, "onError: importing bookmarks", it)
217226
val activity = activity
218227
if (activity != null && !activity.isFinishing && isAdded) {
219228
Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error)
@@ -231,6 +240,8 @@ class BookmarkSettingsFragment : AbstractSettingsFragment() {
231240

232241
private const val TAG = "BookmarkSettingsFrag"
233242

243+
private const val EXTENSION_HTML = "html"
244+
234245
private const val SETTINGS_EXPORT = "export_bookmark"
235246
private const val SETTINGS_IMPORT = "import_bookmark"
236247
private const val SETTINGS_DELETE_BOOKMARKS = "delete_bookmarks"

0 commit comments

Comments
 (0)