diff --git a/app/src/main/java/acr/browser/lightning/activity/BookmarkUiModel.java b/app/src/main/java/acr/browser/lightning/activity/BookmarkUiModel.java new file mode 100644 index 0000000..457a984 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/activity/BookmarkUiModel.java @@ -0,0 +1,48 @@ +package acr.browser.lightning.activity; + +import android.support.annotation.Nullable; + +import acr.browser.lightning.browser.BookmarksView; + +/** + * The UI model representing the current folder shown + * by the {@link BookmarksView}. + *

+ * Created by anthonycr on 5/7/17. + */ +public class BookmarkUiModel { + + @Nullable private String mCurrentFolder; + + /** + * Sets the current folder that is being shown. + * Use null as the root folder. + * + * @param folder the current folder, null for root. + */ + public void setCurrentFolder(@Nullable String folder) { + mCurrentFolder = folder; + } + + /** + * Determines if the current folder is + * the root folder. + * + * @return true if the current folder is + * the root, false otherwise. + */ + public boolean isRootFolder() { + return mCurrentFolder == null; + } + + /** + * Gets the current folder that is being shown. + * + * @return the current folder, null for root. + */ + @Nullable + public String getCurrentFolder() { + return mCurrentFolder; + } + +} diff --git a/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java b/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java index 15373f0..a542889 100644 --- a/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java @@ -99,9 +99,9 @@ import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; -import acr.browser.lightning.database.HistoryModel; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.fragment.BookmarksFragment; @@ -181,7 +181,7 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements private String mCameraPhotoPath; // The singleton BookmarkManager - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; @Inject LightningDialogBuilder mBookmarksDialogBuilder; @@ -827,23 +827,37 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements // By using a manager, adds a bookmark and notifies third parties about that private void addBookmark(final String title, final String url) { - final HistoryItem item = !mBookmarkManager.isBookmark(url) - ? new HistoryItem(url, title) - : null; - if (item != null && mBookmarkManager.addBookmark(item)) { - mSuggestionsAdapter.refreshBookmarks(); - mBookmarksView.handleUpdatedUrl(url); - } + + final HistoryItem item = new HistoryItem(url, title); + mBookmarkManager.addBookmarkIfNotExists(item) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (Boolean.TRUE.equals(item)) { + mSuggestionsAdapter.refreshBookmarks(); + mBookmarksView.handleUpdatedUrl(url); + } + } + }); } private void deleteBookmark(final String title, final String url) { - final HistoryItem item = mBookmarkManager.isBookmark(url) - ? new HistoryItem(url, title) - : null; - if (item != null && mBookmarkManager.deleteBookmark(item)) { - mSuggestionsAdapter.refreshBookmarks(); - mBookmarksView.handleUpdatedUrl(url); - } + final HistoryItem item = new HistoryItem(url, title); + + mBookmarkManager.deleteBookmark(item) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (Boolean.TRUE.equals(item)) { + mSuggestionsAdapter.refreshBookmarks(); + mBookmarksView.handleUpdatedUrl(url); + } + } + }); } private void putToolbarInRoot() { @@ -1098,11 +1112,19 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements } if (!UrlUtils.isSpecialUrl(url)) { - if (!mBookmarkManager.isBookmark(url)) { - addBookmark(title, url); - } else { - deleteBookmark(title, url); - } + mBookmarkManager.isBookmark(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (Boolean.TRUE.equals(item)) { + addBookmark(title, url); + } else { + deleteBookmark(title, url); + } + } + }); } } diff --git a/app/src/main/java/acr/browser/lightning/activity/TabsManager.java b/app/src/main/java/acr/browser/lightning/activity/TabsManager.java index 02f825d..e2bf713 100644 --- a/app/src/main/java/acr/browser/lightning/activity/TabsManager.java +++ b/app/src/main/java/acr/browser/lightning/activity/TabsManager.java @@ -35,7 +35,6 @@ import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.utils.FileUtils; @@ -51,7 +50,7 @@ import acr.browser.lightning.view.LightningView; public class TabsManager { private static final String TAG = "TabsManager"; - + private static final String BUNDLE_KEY = "WEBVIEW_"; private static final String URL_KEY = "URL_KEY"; private static final String BUNDLE_STORAGE = "SAVED_TABS.parcel"; @@ -64,7 +63,6 @@ public class TabsManager { private final List mPostInitializationWorkList = new ArrayList<>(); @Inject PreferenceManager mPreferenceManager; - @Inject BookmarkManager mBookmarkManager; @Inject Application mApp; public TabsManager() { diff --git a/app/src/main/java/acr/browser/lightning/app/AppComponent.java b/app/src/main/java/acr/browser/lightning/app/AppComponent.java index 12ed5ef..6605da1 100644 --- a/app/src/main/java/acr/browser/lightning/app/AppComponent.java +++ b/app/src/main/java/acr/browser/lightning/app/AppComponent.java @@ -11,7 +11,7 @@ import acr.browser.lightning.browser.BrowserPresenter; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; -import acr.browser.lightning.database.HistoryDatabase; +import acr.browser.lightning.database.history.HistoryDatabase; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.download.LightningDownloadListener; import acr.browser.lightning.fragment.BookmarkSettingsFragment; diff --git a/app/src/main/java/acr/browser/lightning/app/AppModule.java b/app/src/main/java/acr/browser/lightning/app/AppModule.java index 605baf2..d4c68f8 100644 --- a/app/src/main/java/acr/browser/lightning/app/AppModule.java +++ b/app/src/main/java/acr/browser/lightning/app/AppModule.java @@ -8,6 +8,8 @@ import net.i2p.android.ui.I2PAndroidHelper; import javax.inject.Singleton; +import acr.browser.lightning.database.bookmark.BookmarkDatabase; +import acr.browser.lightning.database.bookmark.BookmarkModel; import dagger.Module; import dagger.Provides; @@ -29,6 +31,13 @@ public class AppModule { return mApp.getApplicationContext(); } + @NonNull + @Provides + @Singleton + public BookmarkModel provideBookmarkMode() { + return new BookmarkDatabase(mApp); + } + @NonNull @Provides @Singleton diff --git a/app/src/main/java/acr/browser/lightning/app/BrowserApp.java b/app/src/main/java/acr/browser/lightning/app/BrowserApp.java index 1fd4962..e51ee5f 100644 --- a/app/src/main/java/acr/browser/lightning/app/BrowserApp.java +++ b/app/src/main/java/acr/browser/lightning/app/BrowserApp.java @@ -12,14 +12,20 @@ import android.support.annotation.Nullable; import android.util.Log; import android.webkit.WebView; +import com.anthonycr.bonsai.Schedulers; import com.squareup.leakcanary.LeakCanary; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import javax.inject.Inject; import acr.browser.lightning.BuildConfig; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkExporter; +import acr.browser.lightning.database.bookmark.legacy.LegacyBookmarkManager; +import acr.browser.lightning.database.bookmark.BookmarkModel; import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.utils.FileUtils; import acr.browser.lightning.utils.MemoryLeakUtils; @@ -33,6 +39,7 @@ public class BrowserApp extends Application { private static final Executor mIOThread = Executors.newSingleThreadExecutor(); @Inject PreferenceManager mPreferenceManager; + @Inject BookmarkModel mBookmarkModel; @Override protected void attachBaseContext(Context base) { @@ -74,6 +81,22 @@ public class BrowserApp extends Application { sAppComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); sAppComponent.inject(this); + Schedulers.worker().execute(new Runnable() { + @Override + public void run() { + List oldBookmarks = LegacyBookmarkManager.destructiveGetBookmarks(BrowserApp.this); + + if (!oldBookmarks.isEmpty()) { + // If there are old bookmarks, import them + mBookmarkModel.addBookmarkList(oldBookmarks).subscribeOn(Schedulers.io()).subscribe(); + } else if (mBookmarkModel.count() == 0) { + // If the database is empty, fill it from the assets list + List assetsBookmarks = BookmarkExporter.importBookmarksFromAssets(BrowserApp.this); + mBookmarkModel.addBookmarkList(assetsBookmarks).subscribeOn(Schedulers.io()).subscribe(); + } + } + }); + if (mPreferenceManager.getUseLeakCanary() && !isRelease()) { LeakCanary.install(this); } diff --git a/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java b/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java index bdf878d..208dc81 100644 --- a/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java +++ b/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java @@ -11,6 +11,7 @@ import android.support.annotation.Nullable; import com.anthonycr.bonsai.Single; import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; import com.anthonycr.bonsai.SingleSubscriber; import java.io.File; @@ -24,8 +25,9 @@ import javax.inject.Inject; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.Utils; @@ -37,25 +39,25 @@ public final class BookmarkPage { public static final String FILENAME = "bookmarks.html"; private static final String HEADING_1 = "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - ""; + "<head>\n" + + "<meta content=en-us http-equiv=Content-Language />\n" + + "<meta content='text/html; charset=utf-8' http-equiv=Content-Type />\n" + + "<meta name=viewport content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>\n" + + "<title>"; private static final String HEADING_2 = "\n" + - "\n" + - "\n" + - "

"; + "\n" + + "\n" + + "
"; private static final String PART1 = "
\n" + - "
\n" + - "
\n" + - "

\n" + - " list = mManager.getBookmarksCopyFromFolder(folder, true); - final File bookmarkWebPage; - if (folder == null || folder.isEmpty()) { - bookmarkWebPage = new File(mFilesDir, FILENAME); - } else { - bookmarkWebPage = new File(mFilesDir, folder + '-' + FILENAME); - } - final StringBuilder bookmarkBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2); - - final String folderIconPath = Constants.FILE + mCacheDir + '/' + FOLDER_ICON; - for (int n = 0, size = list.size(); n < size; n++) { - final HistoryItem item = list.get(n); - bookmarkBuilder.append(PART1); - if (item.isFolder()) { - final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME); - bookmarkBuilder.append(Constants.FILE).append(folderPage); - bookmarkBuilder.append(PART2); - bookmarkBuilder.append(folderIconPath); - buildBookmarkPage(item.getTitle()); - } else { - bookmarkBuilder.append(item.getUrl()); - bookmarkBuilder.append(PART2).append(PART3); - bookmarkBuilder.append(item.getUrl()); - } - bookmarkBuilder.append(PART4); - bookmarkBuilder.append(item.getTitle()); - bookmarkBuilder.append(PART5); - } - bookmarkBuilder.append(END); - FileWriter bookWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookWriter = new FileWriter(bookmarkWebPage, false); - bookWriter.write(bookmarkBuilder.toString()); - } catch (IOException e) { - e.printStackTrace(); - } finally { - Utils.close(bookWriter); - } + mManager.getBookmarksFromFolder(folder) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List list) { + Preconditions.checkNonNull(list); + + final File bookmarkWebPage; + if (folder == null || folder.isEmpty()) { + bookmarkWebPage = new File(mFilesDir, FILENAME); + } else { + bookmarkWebPage = new File(mFilesDir, folder + '-' + FILENAME); + } + final StringBuilder bookmarkBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2); + + final String folderIconPath = Constants.FILE + mCacheDir + '/' + FOLDER_ICON; + for (int n = 0, size = list.size(); n < size; n++) { + final HistoryItem item = list.get(n); + bookmarkBuilder.append(PART1); + if (item.isFolder()) { + final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME); + bookmarkBuilder.append(Constants.FILE).append(folderPage); + bookmarkBuilder.append(PART2); + bookmarkBuilder.append(folderIconPath); + buildBookmarkPage(item.getTitle()); + } else { + bookmarkBuilder.append(item.getUrl()); + bookmarkBuilder.append(PART2).append(PART3); + bookmarkBuilder.append(item.getUrl()); + } + bookmarkBuilder.append(PART4); + bookmarkBuilder.append(item.getTitle()); + bookmarkBuilder.append(PART5); + } + bookmarkBuilder.append(END); + FileWriter bookWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookWriter = new FileWriter(bookmarkWebPage, false); + bookWriter.write(bookmarkBuilder.toString()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.close(bookWriter); + } + } + }); } } diff --git a/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java b/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java index 323c190..b175082 100644 --- a/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java +++ b/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java @@ -27,7 +27,7 @@ import javax.inject.Inject; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.database.HistoryItem; -import acr.browser.lightning.database.HistoryModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.Utils; diff --git a/app/src/main/java/acr/browser/lightning/database/BookmarkManager.java b/app/src/main/java/acr/browser/lightning/database/BookmarkManager.java deleted file mode 100644 index cf6fac4..0000000 --- a/app/src/main/java/acr/browser/lightning/database/BookmarkManager.java +++ /dev/null @@ -1,552 +0,0 @@ -package acr.browser.lightning.database; - -import android.app.Activity; -import android.content.Context; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Executor; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import acr.browser.lightning.R; -import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.constant.Constants; -import acr.browser.lightning.utils.Utils; - -@Singleton -public class BookmarkManager { - - private static final String TAG = "BookmarkManager"; - - private static final String TITLE = "title"; - private static final String URL = "url"; - private static final String FOLDER = "folder"; - private static final String ORDER = "order"; - private static final String FILE_BOOKMARKS = "bookmarks.dat"; - - @NonNull private final String DEFAULT_BOOKMARK_TITLE; - - private Map mBookmarksMap; - @NonNull private String mCurrentFolder = ""; - @NonNull private final Executor mExecutor; - private File mFilesDir; - - @Inject - public BookmarkManager(@NonNull Context context) { - mExecutor = BrowserApp.getIOThread(); - DEFAULT_BOOKMARK_TITLE = context.getString(R.string.untitled); - mExecutor.execute(new BookmarkInitializer(context)); - } - - /** - * Initialize the BookmarkManager, it's a one-time operation and will be executed asynchronously. - * When done, mReady flag will been set to true. - */ - private class BookmarkInitializer implements Runnable { - private final Context mContext; - - BookmarkInitializer(Context context) { - mContext = context; - } - - @Override - public void run() { - synchronized (BookmarkManager.this) { - mFilesDir = mContext.getFilesDir(); - final Map bookmarks = new HashMap<>(); - final File bookmarksFile = new File(mFilesDir, FILE_BOOKMARKS); - - BufferedReader bookmarksReader = null; - InputStream inputStream = null; - try { - if (bookmarksFile.exists() && bookmarksFile.isFile()) { - //noinspection IOResourceOpenedButNotSafelyClosed - inputStream = new FileInputStream(bookmarksFile); - } else { - inputStream = mContext.getResources().openRawResource(R.raw.default_bookmarks); - } - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarksReader = new BufferedReader(new InputStreamReader(inputStream)); - String line; - while ((line = bookmarksReader.readLine()) != null) { - try { - JSONObject object = new JSONObject(line); - HistoryItem item = new HistoryItem(); - item.setTitle(object.getString(TITLE)); - final String url = object.getString(URL); - item.setUrl(url); - item.setFolder(object.getString(FOLDER)); - item.setOrder(object.getInt(ORDER)); - item.setImageId(R.drawable.ic_bookmark); - bookmarks.put(url, item); - } catch (JSONException e) { - Log.e(TAG, "Can't parse line " + line, e); - } - } - } catch (IOException e) { - Log.e(TAG, "Error reading the bookmarks file", e); - } finally { - Utils.close(bookmarksReader); - Utils.close(inputStream); - } - mBookmarksMap = bookmarks; - } - } - - } - - /** - * Dump all the given bookmarks to the bookmark file using a temporary file - */ - private class BookmarksWriter implements Runnable { - - private final List mBookmarks; - - BookmarksWriter(List bookmarks) { - mBookmarks = bookmarks; - } - - @Override - public void run() { - final File tempFile = new File(mFilesDir, - String.format(Locale.US, "bm_%d.dat", System.currentTimeMillis())); - final File bookmarksFile = new File(mFilesDir, FILE_BOOKMARKS); - boolean success = false; - BufferedWriter bookmarkWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarkWriter = new BufferedWriter(new FileWriter(tempFile, false)); - JSONObject object = new JSONObject(); - for (HistoryItem item : mBookmarks) { - object.put(TITLE, item.getTitle()); - object.put(URL, item.getUrl()); - object.put(FOLDER, item.getFolder()); - object.put(ORDER, item.getOrder()); - bookmarkWriter.write(object.toString()); - bookmarkWriter.newLine(); - } - success = true; - } catch (@NonNull IOException | JSONException e) { - e.printStackTrace(); - } finally { - Utils.close(bookmarkWriter); - } - - if (success) { - // Overwrite the bookmarks file by renaming the temp file - //noinspection ResultOfMethodCallIgnored - tempFile.renameTo(bookmarksFile); - } - } - } - - /** - * Look for bookmark using the url - * - * @param url the lookup url - * @return the bookmark as an {@link HistoryItem} or null - */ - @Nullable - public HistoryItem findBookmarkForUrl(final String url) { - return mBookmarksMap.get(url); - } - - public boolean isBookmark(String url) { - return mBookmarksMap.containsKey(url); - } - - /** - * This method adds the the HistoryItem item to permanent bookmark storage.
- * This operation is blocking if the manager is still not ready. - * - * @param item the item to add - * @return It returns true if the operation was successful. - */ - public synchronized boolean addBookmark(@NonNull HistoryItem item) { - final String url = item.getUrl(); - if (mBookmarksMap.containsKey(url)) { - return false; - } - mBookmarksMap.put(url, item); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - return true; - } - - /** - * This method adds the list of HistoryItems to permanent bookmark storage - * - * @param list the list of HistoryItems to add to bookmarks - */ - public synchronized void addBookmarkList(@Nullable List list) { - if (list == null || list.isEmpty()) { - return; - } - for (HistoryItem item : list) { - final String url = item.getUrl(); - if (!mBookmarksMap.containsKey(url)) { - mBookmarksMap.put(url, item); - } - } - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method deletes the bookmark with the given url. It returns - * true if the deletion was successful. - * - * @param deleteItem the bookmark item to delete - */ - public synchronized boolean deleteBookmark(@Nullable HistoryItem deleteItem) { - if (deleteItem == null || deleteItem.isFolder()) { - return false; - } - mBookmarksMap.remove(deleteItem.getUrl()); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - return true; - } - - /** - * renames a folder and moves all it's contents to that folder - * - * @param oldName the folder to be renamed - * @param newName the new name of the folder - */ - public synchronized void renameFolder(@NonNull String oldName, @NonNull String newName) { - if (newName.isEmpty()) { - return; - } - for (HistoryItem item : mBookmarksMap.values()) { - if (item.getFolder().equals(oldName)) { - item.setFolder(newName); - } else if (item.isFolder() && item.getTitle().equals(oldName)) { - item.setTitle(newName); - item.setUrl(Constants.FOLDER + newName); - } - } - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * Delete the folder and move all bookmarks to the top level - * - * @param name the name of the folder to be deleted - */ - public synchronized void deleteFolder(@NonNull String name) { - final Map bookmarks = new HashMap<>(); - for (HistoryItem item : mBookmarksMap.values()) { - final String url = item.getUrl(); - if (item.isFolder()) { - if (!item.getTitle().equals(name)) { - bookmarks.put(url, item); - } - } else { - if (item.getFolder().equals(name)) { - item.setFolder(""); - } - bookmarks.put(url, item); - } - } - mBookmarksMap = bookmarks; - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method deletes ALL bookmarks created - * by the user. Use this method carefully and - * do not use it without explicit user consent. - */ - public synchronized void deleteAllBookmarks() { - mBookmarksMap = new HashMap<>(); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method edits a particular bookmark in the bookmark database - * - * @param oldItem This is the old item that you wish to edit - * @param newItem This is the new item that will overwrite the old item - */ - public synchronized void editBookmark(@Nullable HistoryItem oldItem, @Nullable HistoryItem newItem) { - if (oldItem == null || newItem == null || oldItem.isFolder()) { - return; - } - if (newItem.getUrl().isEmpty()) { - deleteBookmark(oldItem); - return; - } - if (newItem.getTitle().isEmpty()) { - newItem.setTitle(DEFAULT_BOOKMARK_TITLE); - } - final String oldUrl = oldItem.getUrl(); - final String newUrl = newItem.getUrl(); - if (!oldUrl.equals(newUrl)) { - // The url has been changed, remove the old one - mBookmarksMap.remove(oldUrl); - } - mBookmarksMap.put(newUrl, newItem); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method exports the stored bookmarks to a text file in the device's - * external download directory - */ - public synchronized void exportBookmarks(@NonNull Activity activity) { - List bookmarkList = getAllBookmarks(true); - File bookmarksExport = new File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "BookmarksExport.txt"); - int counter = 0; - while (bookmarksExport.exists()) { - counter++; - bookmarksExport = new File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "BookmarksExport-" + counter + ".txt"); - } - BufferedWriter bookmarkWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksExport, - false)); - JSONObject object = new JSONObject(); - for (HistoryItem item : bookmarkList) { - object.put(TITLE, item.getTitle()); - object.put(URL, item.getUrl()); - object.put(FOLDER, item.getFolder()); - object.put(ORDER, item.getOrder()); - bookmarkWriter.write(object.toString()); - bookmarkWriter.newLine(); - } - Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path) - + ' ' + bookmarksExport.getPath()); - } catch (@NonNull IOException | JSONException e) { - e.printStackTrace(); - } finally { - Utils.close(bookmarkWriter); - } - - } - - /** - * This method returns a list of ALL stored bookmarks. - * This is a disk-bound operation and should not be - * done very frequently. - * - * @param sort force to sort the returned bookmarkList - * @return returns a list of bookmarks that can be sorted - */ - @NonNull - public synchronized List getAllBookmarks(boolean sort) { - final List bookmarks = new ArrayList<>(mBookmarksMap.values()); - if (sort) { - Collections.sort(bookmarks, new SortIgnoreCase()); - } - return bookmarks; - } - - /** - * This method returns a list of bookmarks and folders located in the specified folder. - * This method should generally be used by the UI when it needs a list to display to the - * user as it returns a subset of all bookmarks and includes folders as well which are - * really 'fake' bookmarks. - * - * @param folder the name of the folder to retrieve bookmarks from - * @return a list of bookmarks found in that folder - */ - @NonNull - public synchronized List getBookmarksFromFolder(@Nullable String folder, boolean sort) { - List bookmarks = new ArrayList<>(1); - if (folder == null || folder.isEmpty()) { - bookmarks.addAll(getFolders(sort)); - folder = ""; - } - mCurrentFolder = folder; - for (HistoryItem item : mBookmarksMap.values()) { - if (item.getFolder().equals(folder)) - bookmarks.add(item); - } - if (sort) { - Collections.sort(bookmarks, new SortIgnoreCase()); - } - return bookmarks; - } - - /** - * Different from {@link #getBookmarksFromFolder(String, boolean)} only in - * that it doesn't affect the internal state of the bookmark manager which - * tracks the current folder used by the bookmark drawer. - *

- * This method returns a list of bookmarks and folders located in the specified folder. - * This method should generally be used by the UI when it needs a list to display to the - * user as it returns a subset of all bookmarks and includes folders as well which are - * really 'fake' bookmarks. - * - * @param folder the name of the folder to retrieve bookmarks from - * @return a list of bookmarks found in that folder - */ - @NonNull - public synchronized List getBookmarksCopyFromFolder(@Nullable String folder, boolean sort) { - List bookmarks = new ArrayList<>(1); - if (folder == null || folder.isEmpty()) { - bookmarks.addAll(getFolders(sort)); - folder = ""; - } - for (HistoryItem item : mBookmarksMap.values()) { - if (item.getFolder().equals(folder)) - bookmarks.add(item); - } - if (sort) { - Collections.sort(bookmarks, new SortIgnoreCase()); - } - return bookmarks; - } - - /** - * Tells you if you are at the root folder or in a subfolder - * - * @return returns true if you are in the root folder - */ - public boolean isRootFolder() { - return mCurrentFolder.isEmpty(); - } - - /** - * Returns the current folder - * - * @return the current folder - */ - @NonNull - public String getCurrentFolder() { - return mCurrentFolder; - } - - /** - * This method returns a list of all folders. - * Folders cannot be empty as they are generated from - * the list of bookmarks that have non-empty folder fields. - * - * @return a list of all folders - */ - @NonNull - private synchronized List getFolders(boolean sort) { - final HashMap folders = new HashMap<>(); - for (HistoryItem item : mBookmarksMap.values()) { - final String folderName = item.getFolder(); - if (!folderName.isEmpty() && !folders.containsKey(folderName)) { - final HistoryItem folder = new HistoryItem(); - folder.setIsFolder(true); - folder.setTitle(folderName); - folder.setImageId(R.drawable.ic_folder); - folder.setUrl(Constants.FOLDER + folderName); - folders.put(folderName, folder); - } - } - final List result = new ArrayList<>(folders.values()); - if (sort) { - Collections.sort(result, new SortIgnoreCase()); - } - return result; - } - - /** - * returns a list of folder titles that can be used for suggestions in a - * simple list adapter - * - * @return a list of folder title strings - */ - @NonNull - public synchronized List getFolderTitles() { - final Set folders = new HashSet<>(); - for (HistoryItem item : mBookmarksMap.values()) { - final String folderName = item.getFolder(); - if (!folderName.isEmpty()) { - folders.add(folderName); - } - } - return new ArrayList<>(folders); - } - - /** - * This method imports the bookmarks from a backup file that is located on - * external storage - * - * @param file the file to attempt to import bookmarks from - */ - public synchronized void importBookmarksFromFile(@Nullable File file, @NonNull Activity activity) { - if (file == null) { - return; - } - List list = new ArrayList<>(); - BufferedReader bookmarksReader = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarksReader = new BufferedReader(new FileReader(file)); - String line; - int number = 0; - while ((line = bookmarksReader.readLine()) != null) { - JSONObject object = new JSONObject(line); - HistoryItem item = new HistoryItem(); - item.setTitle(object.getString(TITLE)); - item.setUrl(object.getString(URL)); - item.setFolder(object.getString(FOLDER)); - item.setOrder(object.getInt(ORDER)); - list.add(item); - number++; - } - addBookmarkList(list); - String message = activity.getResources().getString(R.string.message_import); - Utils.showSnackbar(activity, number + " " + message); - } catch (@NonNull IOException | JSONException e) { - e.printStackTrace(); - Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error); - } finally { - Utils.close(bookmarksReader); - } - } - - /** - * This class sorts bookmarks alphabetically, with folders coming after bookmarks - */ - private static class SortIgnoreCase implements Comparator { - - public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) { - if (o1 == null || o2 == null) { - return 0; - } - if (o1.isFolder() == o2.isFolder()) { - return o1.getTitle().toLowerCase(Locale.getDefault()) - .compareTo(o2.getTitle().toLowerCase(Locale.getDefault())); - - } else { - return o1.isFolder() ? 1 : -1; - } - } - - } -} diff --git a/app/src/main/java/acr/browser/lightning/database/HistoryItem.java b/app/src/main/java/acr/browser/lightning/database/HistoryItem.java index b439b1b..9021e73 100644 --- a/app/src/main/java/acr/browser/lightning/database/HistoryItem.java +++ b/app/src/main/java/acr/browser/lightning/database/HistoryItem.java @@ -25,7 +25,7 @@ public class HistoryItem implements Comparable { private Bitmap mBitmap = null; private int mImageId = 0; - private int mOrder = 0; + private int mPosition = 0; private boolean mIsFolder = false; public HistoryItem() {} @@ -63,12 +63,12 @@ public class HistoryItem implements Comparable { mFolder = (folder == null) ? "" : folder; } - public void setOrder(int order) { - mOrder = order; + public void setPosition(int order) { + mPosition = order; } - public int getOrder() { - return mOrder; + public int getPosition() { + return mPosition; } @NonNull @@ -132,8 +132,8 @@ public class HistoryItem implements Comparable { HistoryItem that = (HistoryItem) object; return mImageId == that.mImageId && - this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) && - this.mFolder.equals(that.mFolder); + this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) && + this.mFolder.equals(that.mFolder); } @Override diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkDatabase.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkDatabase.java new file mode 100644 index 0000000..e82904b --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkDatabase.java @@ -0,0 +1,390 @@ +package acr.browser.lightning.database.bookmark; + +import android.app.Application; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import acr.browser.lightning.R; +import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.database.HistoryItem; + +/** + * The disk backed bookmark database. + * See {@link BookmarkModel} for method + * documentation. + *

+ * Created by anthonycr on 5/6/17. + */ +@Singleton +public class BookmarkDatabase extends SQLiteOpenHelper implements BookmarkModel { + + private static final String TAG = "BookmarkDatabase"; + + // Database Version + private static final int DATABASE_VERSION = 1; + + // Database Name + private static final String DATABASE_NAME = "bookmarkManager"; + + // HistoryItems table name + private static final String TABLE_BOOKMARK = "bookmark"; + + // HistoryItems Table Columns names + private static final String KEY_ID = "id"; + private static final String KEY_URL = "url"; + private static final String KEY_TITLE = "title"; + private static final String KEY_FOLDER = "folder"; + private static final String KEY_POSITION = "position"; + + + @NonNull private final String DEFAULT_BOOKMARK_TITLE; + + @Nullable private SQLiteDatabase mDatabase; + + @Inject + public BookmarkDatabase(@NonNull Application application) { + super(application, DATABASE_NAME, null, DATABASE_VERSION); + DEFAULT_BOOKMARK_TITLE = application.getString(R.string.untitled); + } + + /** + * Lazily initializes the database + * field when called. + * + * @return a non null writable database. + */ + @NonNull + private SQLiteDatabase lazyDatabase() { + if (mDatabase == null) { + mDatabase = getWritableDatabase(); + } + + return mDatabase; + } + + // Creating Tables + @Override + public void onCreate(@NonNull SQLiteDatabase db) { + String CREATE_BOOKMARK_TABLE = "CREATE TABLE " + + DatabaseUtils.sqlEscapeString(TABLE_BOOKMARK) + '(' + + DatabaseUtils.sqlEscapeString(KEY_ID) + " INTEGER PRIMARY KEY," + + DatabaseUtils.sqlEscapeString(KEY_URL) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_TITLE) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_FOLDER) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_POSITION) + " INTEGER" + ')'; + db.execSQL(CREATE_BOOKMARK_TABLE); + } + + // Upgrading database + @Override + public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + // Drop older table if it exists + db.execSQL("DROP TABLE IF EXISTS " + DatabaseUtils.sqlEscapeString(TABLE_BOOKMARK)); + // Create tables again + onCreate(db); + } + + @NonNull + private static ContentValues bindBookmarkToContentValues(@NonNull HistoryItem bookmarkItem) { + ContentValues contentValues = new ContentValues(4); + contentValues.put(KEY_TITLE, bookmarkItem.getTitle()); + contentValues.put(KEY_URL, bookmarkItem.getUrl()); + contentValues.put(KEY_FOLDER, bookmarkItem.getFolder()); + contentValues.put(KEY_POSITION, bookmarkItem.getPosition()); + + return contentValues; + } + + @NonNull + private static HistoryItem bindCursorToHistoryItem(@NonNull Cursor cursor) { + HistoryItem bookmark = new HistoryItem(); + + bookmark.setImageId(R.drawable.ic_bookmark); + bookmark.setUrl(cursor.getString(cursor.getColumnIndex(KEY_URL))); + bookmark.setTitle(cursor.getString(cursor.getColumnIndex(KEY_TITLE))); + bookmark.setFolder(cursor.getString(cursor.getColumnIndex(KEY_FOLDER))); + bookmark.setPosition(cursor.getInt(cursor.getColumnIndex(KEY_POSITION))); + + return bookmark; + } + + @NonNull + private static List bindCursorToHistoryItemList(@NonNull Cursor cursor) { + List bookmarks = new ArrayList<>(); + + while (cursor.moveToNext()) { + bookmarks.add(bindCursorToHistoryItem(cursor)); + } + + cursor.close(); + + return bookmarks; + } + + @NonNull + @Override + public Single findBookmarkForUrl(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_URL + "=?", new String[]{url}, null, null, null, "1"); + + if (cursor.moveToFirst()) { + subscriber.onItem(bindCursorToHistoryItem(cursor)); + } else { + subscriber.onItem(null); + } + + cursor.close(); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single isBookmark(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_URL + "=?", new String[]{url}, null, null, null, "1"); + + subscriber.onItem(cursor.moveToFirst()); + + cursor.close(); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single addBookmarkIfNotExists(@NonNull final HistoryItem item) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_URL + "=?", new String[]{item.getUrl()}, null, null, null, "1"); + + if (cursor.moveToFirst()) { + cursor.close(); + subscriber.onItem(false); + subscriber.onComplete(); + return; + } + + cursor.close(); + + long id = lazyDatabase().insert(TABLE_BOOKMARK, null, bindBookmarkToContentValues(item)); + + subscriber.onItem(id != -1); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable addBookmarkList(@NonNull final List bookmarkItems) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + lazyDatabase().beginTransaction(); + + for (HistoryItem item : bookmarkItems) { + addBookmarkIfNotExists(item).subscribe(); + } + + lazyDatabase().setTransactionSuccessful(); + lazyDatabase().endTransaction(); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single deleteBookmark(@NonNull final HistoryItem bookmark) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + int rows = lazyDatabase().delete(TABLE_BOOKMARK, KEY_URL + "=?", new String[]{bookmark.getUrl()}); + + subscriber.onItem(rows > 0); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable renameFolder(@NonNull final String oldName, @NonNull final String newName) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(KEY_FOLDER, newName); + + lazyDatabase().update(TABLE_BOOKMARK, contentValues, KEY_FOLDER + "=?", new String[]{oldName}); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable deleteFolder(@NonNull final String folderToDelete) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + renameFolder(folderToDelete, "").subscribe(); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable deleteAllBookmarks() { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + lazyDatabase().delete(TABLE_BOOKMARK, null, null); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable editBookmark(@NonNull final HistoryItem oldBookmark, @NonNull final HistoryItem newBookmark) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + if (newBookmark.getTitle().isEmpty()) { + newBookmark.setTitle(DEFAULT_BOOKMARK_TITLE); + } + ContentValues contentValues = bindBookmarkToContentValues(newBookmark); + + lazyDatabase().update(TABLE_BOOKMARK, contentValues, KEY_URL + "=?", new String[]{oldBookmark.getUrl()}); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getAllBookmarks() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, null, null, null, null, null); + + subscriber.onItem(bindCursorToHistoryItemList(cursor)); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getBookmarksFromFolder(@Nullable final String folder) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + String finalFolder = folder != null ? folder : ""; + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_FOLDER + "=?", new String[]{finalFolder}, null, null, null); + + subscriber.onItem(bindCursorToHistoryItemList(cursor)); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getFolders() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(true, TABLE_BOOKMARK, new String[]{KEY_FOLDER}, null, null, null, null, null, null); + + List folders = new ArrayList<>(); + while (cursor.moveToNext()) { + String folderName = cursor.getString(cursor.getColumnIndex(KEY_FOLDER)); + if (TextUtils.isEmpty(folderName)) { + continue; + } + + final HistoryItem folder = new HistoryItem(); + folder.setIsFolder(true); + folder.setTitle(folderName); + folder.setImageId(R.drawable.ic_folder); + folder.setUrl(Constants.FOLDER + folderName); + + folders.add(folder); + } + + cursor.close(); + + subscriber.onItem(folders); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getFolderNames() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(true, TABLE_BOOKMARK, new String[]{KEY_FOLDER}, null, null, null, null, null, null); + + List folders = new ArrayList<>(); + while (cursor.moveToNext()) { + String folderName = cursor.getString(cursor.getColumnIndex(KEY_FOLDER)); + if (TextUtils.isEmpty(folderName)) { + continue; + } + + folders.add(folderName); + } + + cursor.close(); + + subscriber.onItem(folders); + subscriber.onComplete(); + } + }); + } + + @Override + public long count() { + return DatabaseUtils.queryNumEntries(lazyDatabase(), TABLE_BOOKMARK); + } + +} diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java new file mode 100644 index 0000000..005f395 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java @@ -0,0 +1,201 @@ +package acr.browser.lightning.database.bookmark; + +import android.content.Context; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.utils.Preconditions; +import acr.browser.lightning.utils.Utils; + +/** + * The class responsible for importing and exporting + * bookmarks in the JSON format. + *

+ * Created by anthonycr on 5/7/17. + */ +public class BookmarkExporter { + + private static final String TAG = "BookmarkExporter"; + + private static final String KEY_URL = "url"; + private static final String KEY_TITLE = "title"; + private static final String KEY_FOLDER = "folder"; + private static final String KEY_ORDER = "order"; + + /** + * Retrieves all the default bookmarks stored + * in the raw file within assets. + * + * @param context the context necessary to open assets. + * @return a non null list of the bookmarks stored in assets. + */ + @NonNull + public static List importBookmarksFromAssets(@NonNull Context context) { + List bookmarks = new ArrayList<>(); + BufferedReader bookmarksReader = null; + InputStream inputStream = null; + try { + inputStream = context.getResources().openRawResource(R.raw.default_bookmarks); + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarksReader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = bookmarksReader.readLine()) != null) { + try { + JSONObject object = new JSONObject(line); + HistoryItem item = new HistoryItem(); + item.setTitle(object.getString(KEY_TITLE)); + final String url = object.getString(KEY_URL); + item.setUrl(url); + item.setFolder(object.getString(KEY_FOLDER)); + item.setPosition(object.getInt(KEY_ORDER)); + item.setImageId(R.drawable.ic_bookmark); + bookmarks.add(item); + } catch (JSONException e) { + Log.e(TAG, "Can't parse line " + line, e); + } + } + } catch (IOException e) { + Log.e(TAG, "Error reading the bookmarks file", e); + } finally { + Utils.close(bookmarksReader); + Utils.close(inputStream); + } + + return bookmarks; + } + + /** + * Exports the list of bookmarks to a file. + * + * @param bookmarkList the bookmarks to export. + * @param file the file to export to. + * @return an observable that emits a completion + * event when the export is complete, or an error + * event if there is a problem. + */ + @NonNull + public static Completable exportBookmarksToFile(@NonNull final List bookmarkList, + @NonNull final File file) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull final CompletableSubscriber subscriber) { + Preconditions.checkNonNull(bookmarkList); + BufferedWriter bookmarkWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarkWriter = new BufferedWriter(new FileWriter(file, false)); + + JSONObject object = new JSONObject(); + for (HistoryItem item : bookmarkList) { + object.put(KEY_TITLE, item.getTitle()); + object.put(KEY_URL, item.getUrl()); + object.put(KEY_FOLDER, item.getFolder()); + object.put(KEY_ORDER, item.getPosition()); + bookmarkWriter.write(object.toString()); + bookmarkWriter.newLine(); + } + subscriber.onComplete(); + } catch (@NonNull IOException | JSONException e) { + subscriber.onError(e); + } finally { + Utils.close(bookmarkWriter); + } + } + }); + } + + /** + * Attempts to import bookmarks from the + * given file. If the file is not in a + * supported format, it will fail. + * + * @param file the file to import from. + * @return an observable that emits the + * imported bookmarks, or an error if the + * file cannot be imported. + */ + @NonNull + public static Single> importBookmarksFromFile(@NonNull final File file) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + BufferedReader bookmarksReader = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarksReader = new BufferedReader(new FileReader(file)); + String line; + + List bookmarks = new ArrayList<>(); + while ((line = bookmarksReader.readLine()) != null) { + JSONObject object = new JSONObject(line); + HistoryItem item = new HistoryItem(); + item.setTitle(object.getString(KEY_TITLE)); + item.setUrl(object.getString(KEY_URL)); + item.setFolder(object.getString(KEY_FOLDER)); + item.setPosition(object.getInt(KEY_ORDER)); + bookmarks.add(item); + } + + subscriber.onItem(bookmarks); + subscriber.onComplete(); + } catch (IOException | JSONException e) { + subscriber.onError(e); + } finally { + Utils.close(bookmarksReader); + } + } + }); + } + + /** + * A blocking call that creates a new export file with + * the name "BookmarkExport.txt" and an appropriate + * numerical appendage if a file already exists with + * that name. + * + * @return a non null empty file that can be used + * to export bookmarks to. + */ + @WorkerThread + @NonNull + public static File createNewExportFile() { + File bookmarksExport = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "BookmarksExport.txt"); + int counter = 0; + while (bookmarksExport.exists()) { + counter++; + bookmarksExport = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "BookmarksExport-" + counter + ".txt"); + } + + return bookmarksExport; + } + +} diff --git a/app/src/main/java/acr/browser/lightning/database/BookmarkLocalSync.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkLocalSync.java similarity index 98% rename from app/src/main/java/acr/browser/lightning/database/BookmarkLocalSync.java rename to app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkLocalSync.java index ce329a9..d7bd6d7 100644 --- a/app/src/main/java/acr/browser/lightning/database/BookmarkLocalSync.java +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkLocalSync.java @@ -1,4 +1,4 @@ -package acr.browser.lightning.database; +package acr.browser.lightning.database.bookmark; import android.content.Context; import android.database.Cursor; @@ -15,6 +15,7 @@ import com.anthonycr.bonsai.SingleSubscriber; import java.util.ArrayList; import java.util.List; +import acr.browser.lightning.database.HistoryItem; import acr.browser.lightning.utils.Utils; public class BookmarkLocalSync { diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkModel.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkModel.java new file mode 100644 index 0000000..04d8f00 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkModel.java @@ -0,0 +1,164 @@ +package acr.browser.lightning.database.bookmark; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.Single; + +import java.util.List; + +import acr.browser.lightning.database.HistoryItem; + +/** + * The interface that should be used to + * communicate with the bookmark database. + *

+ * Created by anthonycr on 5/6/17. + */ +public interface BookmarkModel { + + /** + * Gets the bookmark associated with the URL. + * + * @param url the URL to look for. + * @return an observable that will emit either + * the bookmark associated with the URL or null. + */ + @NonNull + Single findBookmarkForUrl(@NonNull String url); + + /** + * Determines if a URL is associated with a bookmark. + * + * @param url the URL to check. + * @return an observable that will emit true if + * the URL is a bookmark, false otherwise. + */ + @NonNull + Single isBookmark(@NonNull String url); + + /** + * Adds a bookmark if one does not already exist with + * the same URL. + * + * @param item the bookmark to add. + * @return an observable that emits true if the bookmark + * was added, false otherwise. + */ + @NonNull + Single addBookmarkIfNotExists(@NonNull HistoryItem item); + + /** + * Adds a list of bookmarks to the database. + * + * @param bookmarkItems the bookmarks to add. + * @return an observable that emits a complete event + * when all the bookmarks have been added. + */ + @NonNull + Completable addBookmarkList(@NonNull List bookmarkItems); + + /** + * Deletes a bookmark from the database. + * + * @param bookmark the bookmark to delete. + * @return an observable that emits true when + * the bookmark is deleted, false otherwise. + */ + @NonNull + Single deleteBookmark(@NonNull HistoryItem bookmark); + + /** + * Moves all bookmarks in the old folder to the new folder. + * + * @param oldName the name of the old folder. + * @param newName the name of the new folder. + * @return an observable that emits a completion + * event when the folder is renamed. + */ + @NonNull + Completable renameFolder(@NonNull String oldName, @NonNull String newName); + + /** + * Deletes a folder from the database, all bookmarks + * in that folder will be moved to the root level. + * + * @param folderToDelete the folder to delete. + * @return an observable that emits a completion + * event when the folder has been deleted. + */ + @NonNull + Completable deleteFolder(@NonNull String folderToDelete); + + /** + * Deletes all bookmarks in the database. + * + * @return an observable that emits a completion + * event when all bookmarks have been deleted. + */ + @NonNull + Completable deleteAllBookmarks(); + + /** + * Changes the bookmark with the original URL + * with all the data from the new bookmark. + * + * @param oldBookmark the old bookmark to replace. + * @param newBookmark the new bookmark. + * @return an observable that emits a completion event + * when the bookmark edit is done. + */ + @NonNull + Completable editBookmark(@NonNull HistoryItem oldBookmark, @NonNull HistoryItem newBookmark); + + /** + * Emits a list of all bookmarks + * + * @return an observable that emits a list + * of all bookmarks. + */ + @NonNull + Single> getAllBookmarks(); + + /** + * Emits all bookmarks in a certain folder. + * If the folder chosen is null, then all bookmarks + * without a specified folder will be returned. + * + * @param folder gets the bookmarks from this folder, may be null. + * @return an observable that emits a list of bookmarks + * in the given folder. + */ + @NonNull + Single> getBookmarksFromFolder(@Nullable String folder); + + /** + * Returns all folders as {@link HistoryItem}. + * The root folder is omitted. + * + * @return an observable that emits a list of folders. + */ + @NonNull + Single> getFolders(); + + /** + * Returns the names of all folders. + * The root folder is omitted. + * + * @return an observable that emits a list of folder names. + */ + @NonNull + Single> getFolderNames(); + + /** + * A synchronous call to the model + * that returns the number of bookmarks. + * Should be called from a background thread. + * + * @return the number of bookmarks in the database. + */ + @WorkerThread + long count(); +} diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/legacy/LegacyBookmarkManager.java b/app/src/main/java/acr/browser/lightning/database/bookmark/legacy/LegacyBookmarkManager.java new file mode 100644 index 0000000..50de3e6 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/legacy/LegacyBookmarkManager.java @@ -0,0 +1,112 @@ +package acr.browser.lightning.database.bookmark.legacy; + +import android.app.Application; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.utils.Utils; + +@Deprecated +public class LegacyBookmarkManager { + + private static final String TAG = "LegacyBookmarkManager"; + + private static final String TITLE = "title"; + private static final String URL = "url"; + private static final String FOLDER = "folder"; + private static final String ORDER = "order"; + private static final String FILE_BOOKMARKS = "bookmarks.dat"; + + /** + * Gets all bookmarks from the old bookmark file + * and then deletes the file. + * + * @param application the context needed to open the file. + * @return a list of bookmarks from the old bookmark file. + */ + @WorkerThread + @NonNull + public static List destructiveGetBookmarks(@NonNull Application application) { + File filesDir = application.getFilesDir(); + List bookmarks = new ArrayList<>(); + final File bookmarksFile = new File(filesDir, FILE_BOOKMARKS); + + BufferedReader bookmarksReader = null; + InputStream inputStream = null; + try { + if (bookmarksFile.exists() && bookmarksFile.isFile()) { + //noinspection IOResourceOpenedButNotSafelyClosed + inputStream = new FileInputStream(bookmarksFile); + } else { + return bookmarks; + } + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarksReader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = bookmarksReader.readLine()) != null) { + try { + JSONObject object = new JSONObject(line); + + HistoryItem item = new HistoryItem(); + + item.setTitle(object.getString(TITLE)); + item.setUrl(object.getString(URL)); + item.setFolder(object.getString(FOLDER)); + item.setPosition(object.getInt(ORDER)); + item.setImageId(R.drawable.ic_bookmark); + + bookmarks.add(item); + } catch (JSONException e) { + Log.e(TAG, "Can't parse line " + line, e); + } + } + } catch (IOException e) { + Log.e(TAG, "Error reading the bookmarks file", e); + } finally { + Utils.close(bookmarksReader); + Utils.close(inputStream); + } + + bookmarksFile.delete(); + + return bookmarks; + } + + /** + * This class sorts bookmarks alphabetically, with folders coming after bookmarks + */ + private static class SortIgnoreCase implements Comparator { + + public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) { + if (o1 == null || o2 == null) { + return 0; + } + if (o1.isFolder() == o2.isFolder()) { + return o1.getTitle().toLowerCase(Locale.getDefault()) + .compareTo(o2.getTitle().toLowerCase(Locale.getDefault())); + + } else { + return o1.isFolder() ? 1 : -1; + } + } + + } +} diff --git a/app/src/main/java/acr/browser/lightning/database/HistoryDatabase.java b/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.java similarity index 98% rename from app/src/main/java/acr/browser/lightning/database/HistoryDatabase.java rename to app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.java index 07ae7cc..7bbc07e 100644 --- a/app/src/main/java/acr/browser/lightning/database/HistoryDatabase.java +++ b/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.java @@ -1,7 +1,7 @@ /* * Copyright 2014 A.C.R. Development */ -package acr.browser.lightning.database; +package acr.browser.lightning.database.history; import android.app.Application; import android.content.ContentValues; @@ -20,6 +20,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; @Singleton @WorkerThread diff --git a/app/src/main/java/acr/browser/lightning/database/HistoryModel.java b/app/src/main/java/acr/browser/lightning/database/history/HistoryModel.java similarity index 96% rename from app/src/main/java/acr/browser/lightning/database/HistoryModel.java rename to app/src/main/java/acr/browser/lightning/database/history/HistoryModel.java index a577c09..3660584 100644 --- a/app/src/main/java/acr/browser/lightning/database/HistoryModel.java +++ b/app/src/main/java/acr/browser/lightning/database/history/HistoryModel.java @@ -1,4 +1,4 @@ -package acr.browser.lightning.database; +package acr.browser.lightning.database.history; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -13,6 +13,7 @@ import com.anthonycr.bonsai.SingleSubscriber; import java.util.List; import acr.browser.lightning.app.BrowserApp; +import acr.browser.lightning.database.HistoryItem; /** * A model class providing reactive bindings diff --git a/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java b/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java index b07cb88..1202299 100644 --- a/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java +++ b/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java @@ -5,6 +5,7 @@ import android.app.Dialog; import android.content.DialogInterface; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.view.View; @@ -14,6 +15,7 @@ import android.widget.EditText; import com.anthonycr.bonsai.CompletableOnSubscribe; import com.anthonycr.bonsai.Schedulers; +import com.anthonycr.bonsai.SingleOnSubscribe; import java.util.List; @@ -25,10 +27,11 @@ import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; -import acr.browser.lightning.database.HistoryModel; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.preference.PreferenceManager; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.Utils; /** @@ -44,7 +47,7 @@ public class LightningDialogBuilder { INCOGNITO } - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; @Inject PreferenceManager mPreferenceManager; @Inject @@ -60,7 +63,7 @@ public class LightningDialogBuilder { * @param url the long pressed url */ public void showLongPressedDialogForBookmarkUrl(@NonNull final Activity activity, - @NonNull UIController uiController, + @NonNull final UIController uiController, @NonNull final String url) { final HistoryItem item; if (url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME)) { @@ -73,15 +76,19 @@ public class LightningDialogBuilder { item.setTitle(folderTitle); item.setImageId(R.drawable.ic_folder); item.setUrl(Constants.FOLDER + folderTitle); + showBookmarkFolderLongPressedDialog(activity, uiController, item); } else { - item = mBookmarkManager.findBookmarkForUrl(url); - } - if (item != null) { - if (item.isFolder()) { - showBookmarkFolderLongPressedDialog(activity, uiController, item); - } else { - showLongPressedDialogForBookmarkUrl(activity, uiController, item); - } + mBookmarkManager.findBookmarkForUrl(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable HistoryItem historyItem) { + if (historyItem != null) { + showLongPressedDialogForBookmarkUrl(activity, uiController, historyItem); + } + } + }); } } @@ -116,9 +123,18 @@ public class LightningDialogBuilder { new BrowserDialog.Item(R.string.dialog_remove_bookmark) { @Override public void onClick() { - if (mBookmarkManager.deleteBookmark(item)) { - uiController.handleBookmarkDeleted(item); - } + mBookmarkManager.deleteBookmark(item) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean success) { + Preconditions.checkNonNull(success); + if (success) { + uiController.handleBookmarkDeleted(item); + } + } + }); } }, new BrowserDialog.Item(R.string.dialog_edit_bookmark) { @@ -143,28 +159,44 @@ public class LightningDialogBuilder { (AutoCompleteTextView) dialogLayout.findViewById(R.id.bookmark_folder); getFolder.setHint(R.string.folder); getFolder.setText(item.getFolder()); - final List folders = mBookmarkManager.getFolderTitles(); - final ArrayAdapter suggestionsAdapter = new ArrayAdapter<>(activity, - android.R.layout.simple_dropdown_item_1line, folders); - getFolder.setThreshold(1); - getFolder.setAdapter(suggestionsAdapter); - editBookmarkDialog.setView(dialogLayout); - editBookmarkDialog.setPositiveButton(activity.getString(R.string.action_ok), - new DialogInterface.OnClickListener() { + mBookmarkManager.getFolderNames() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { @Override - public void onClick(DialogInterface dialog, int which) { - HistoryItem editedItem = new HistoryItem(); - editedItem.setTitle(getTitle.getText().toString()); - editedItem.setUrl(getUrl.getText().toString()); - editedItem.setUrl(getUrl.getText().toString()); - editedItem.setFolder(getFolder.getText().toString()); - mBookmarkManager.editBookmark(item, editedItem); - uiController.handleBookmarksChange(); + public void onItem(@Nullable List folders) { + Preconditions.checkNonNull(folders); + final ArrayAdapter suggestionsAdapter = new ArrayAdapter<>(activity, + android.R.layout.simple_dropdown_item_1line, folders); + getFolder.setThreshold(1); + getFolder.setAdapter(suggestionsAdapter); + editBookmarkDialog.setView(dialogLayout); + editBookmarkDialog.setPositiveButton(activity.getString(R.string.action_ok), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + HistoryItem editedItem = new HistoryItem(); + editedItem.setTitle(getTitle.getText().toString()); + editedItem.setUrl(getUrl.getText().toString()); + editedItem.setUrl(getUrl.getText().toString()); + editedItem.setFolder(getFolder.getText().toString()); + mBookmarkManager.editBookmark(item, editedItem) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleBookmarksChange(); + } + }); + } + }); + Dialog dialog = editBookmarkDialog.show(); + BrowserDialog.setDialogSize(activity, dialog); } }); - Dialog dialog = editBookmarkDialog.show(); - BrowserDialog.setDialogSize(activity, dialog); } public void showBookmarkFolderLongPressedDialog(@NonNull final Activity activity, @@ -181,8 +213,15 @@ public class LightningDialogBuilder { new BrowserDialog.Item(R.string.dialog_remove_folder) { @Override public void onClick() { - mBookmarkManager.deleteFolder(item.getTitle()); - uiController.handleBookmarkDeleted(item); + mBookmarkManager.deleteFolder(item.getTitle()) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleBookmarkDeleted(item); + } + }); } }); } @@ -202,8 +241,15 @@ public class LightningDialogBuilder { editedItem.setUrl(Constants.FOLDER + text); editedItem.setFolder(item.getFolder()); editedItem.setIsFolder(true); - mBookmarkManager.renameFolder(oldTitle, text); - uiController.handleBookmarksChange(); + mBookmarkManager.renameFolder(oldTitle, text) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleBookmarksChange(); + } + }); } } }); diff --git a/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java index 3bf2590..5a7e1af 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java @@ -21,6 +21,7 @@ import android.support.v7.app.AlertDialog; import android.util.Log; import android.widget.ArrayAdapter; +import com.anthonycr.bonsai.CompletableOnSubscribe; import com.anthonycr.bonsai.SingleOnSubscribe; import com.anthonycr.grant.PermissionsManager; import com.anthonycr.grant.PermissionsResultAction; @@ -36,13 +37,14 @@ import javax.inject.Inject; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.BookmarkLocalSync; -import acr.browser.lightning.database.BookmarkLocalSync.Source; -import acr.browser.lightning.database.BookmarkManager; +import acr.browser.lightning.database.bookmark.BookmarkExporter; +import acr.browser.lightning.database.bookmark.BookmarkLocalSync; +import acr.browser.lightning.database.bookmark.BookmarkLocalSync.Source; import acr.browser.lightning.database.HistoryItem; import com.anthonycr.bonsai.Schedulers; +import acr.browser.lightning.database.bookmark.BookmarkModel; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.Utils; @@ -58,7 +60,7 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref @Nullable private Activity mActivity; - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; private File[] mFileList; private String[] mFileNameList; @Nullable private BookmarkLocalSync mSync; @@ -187,7 +189,28 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref new PermissionsResultAction() { @Override public void onGranted() { - mBookmarkManager.exportBookmarks(getActivity()); + mBookmarkManager.getAllBookmarks() + .subscribeOn(Schedulers.io()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List item) { + Preconditions.checkNonNull(item); + final File exportFile = BookmarkExporter.createNewExportFile(); + BookmarkExporter.exportBookmarksToFile(item, exportFile) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + Activity activity = getActivity(); + if (activity != null) { + Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path) + + ' ' + exportFile.getPath()); + } + } + }); + } + }); } @Override @@ -245,7 +268,7 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - mBookmarkManager.deleteAllBookmarks(); + mBookmarkManager.deleteAllBookmarks().subscribeOn(Schedulers.io()).subscribe(); } }); Dialog dialog = builder.show(); @@ -408,7 +431,32 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref Dialog dialog1 = builder.show(); BrowserDialog.setDialogSize(mActivity, dialog1); } else { - mBookmarkManager.importBookmarksFromFile(mFileList[which], getActivity()); + BookmarkExporter.importBookmarksFromFile(mFileList[which]) + .subscribeOn(Schedulers.io()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable final List importList) { + Preconditions.checkNonNull(importList); + mBookmarkManager.addBookmarkList(importList) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + Activity activity = getActivity(); + if (activity != null) { + String message = activity.getResources().getString(R.string.message_import); + Utils.showSnackbar(activity, importList.size() + " " + message); + } + } + }); + } + + @Override + public void onError(@NonNull Throwable throwable) { + Log.e(TAG, "onError: importing bookmarks", throwable); + Utils.createInformativeDialog(getActivity(), R.string.title_error, R.string.import_bookmark_error); + } + }); } } diff --git a/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java b/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java index c48cffe..8c4964f 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java @@ -22,10 +22,7 @@ import android.widget.ImageView; import android.widget.TextView; import com.anthonycr.bonsai.Schedulers; -import com.anthonycr.bonsai.Single; -import com.anthonycr.bonsai.SingleAction; import com.anthonycr.bonsai.SingleOnSubscribe; -import com.anthonycr.bonsai.SingleSubscriber; import com.anthonycr.bonsai.Subscription; import java.lang.ref.WeakReference; @@ -35,6 +32,7 @@ import java.util.List; import javax.inject.Inject; import acr.browser.lightning.R; +import acr.browser.lightning.activity.BookmarkUiModel; import acr.browser.lightning.activity.ReadingActivity; import acr.browser.lightning.activity.TabsManager; import acr.browser.lightning.animation.AnimationUtils; @@ -42,8 +40,8 @@ import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.browser.BookmarksView; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.favicon.FaviconModel; import acr.browser.lightning.preference.PreferenceManager; @@ -71,7 +69,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, public final static String INCOGNITO_MODE = TAG + ".INCOGNITO_MODE"; // Managers - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; // Dialog builder @Inject LightningDialogBuilder mBookmarksDialogBuilder; @@ -109,16 +107,8 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @Nullable private Subscription mBookmarksSubscription; - private static Single> initBookmarks(@NonNull final BookmarkManager bookmarkManager) { - return Single.create(new SingleAction>() { - @Override - public void onSubscribe(@NonNull SingleSubscriber> subscriber) { - subscriber.onItem(bookmarkManager.getBookmarksFromFolder(null, true)); - subscriber.onComplete(); - - } - }); - } + @NonNull + private final BookmarkUiModel mUiModel = new BookmarkUiModel(); @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -150,7 +140,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, final HistoryItem item = mBookmarks.get(position); if (item.isFolder()) { mScrollIndex = ((LinearLayoutManager) mBookmarksListView.getLayoutManager()).findFirstVisibleItemPosition(); - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), true); + setBookmarksShown(item.getTitle(), true); } else { mUiController.bookmarkItemClicked(item); } @@ -170,7 +160,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, public void onResume() { super.onResume(); if (mBookmarkAdapter != null) { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false); + setBookmarksShown(null, false); } } @@ -184,9 +174,8 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, backView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mBookmarkManager == null) return; - if (!mBookmarkManager.isRootFolder()) { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true); + if (!mUiModel.isRootFolder()) { + setBookmarksShown(null, true); mBookmarksListView.getLayoutManager().scrollToPosition(mScrollIndex); } } @@ -201,17 +190,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, mBookmarksListView.setLayoutManager(new LinearLayoutManager(getContext())); mBookmarksListView.setAdapter(mBookmarkAdapter); - mBookmarksSubscription = initBookmarks(mBookmarkManager) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.main()) - .subscribe(new SingleOnSubscribe>() { - @Override - public void onItem(@Nullable List item) { - mBookmarksSubscription = null; - Preconditions.checkNonNull(item); - setBookmarkDataSet(item, true); - } - }); + setBookmarksShown(null, true); return view; } @@ -251,31 +230,70 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, } private void updateBookmarkIndicator(final String url) { - if (!mBookmarkManager.isBookmark(url)) { - mBookmarkImage.setImageResource(R.drawable.ic_action_star); - mBookmarkImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); - } else { - mBookmarkImage.setImageResource(R.drawable.ic_bookmark); - mBookmarkImage.setColorFilter(ThemeUtils.getAccentColor(getContext()), PorterDuff.Mode.SRC_IN); - } + mBookmarkManager.isBookmark(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + Preconditions.checkNonNull(item); + if (!item) { + mBookmarkImage.setImageResource(R.drawable.ic_action_star); + mBookmarkImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); + } else { + mBookmarkImage.setImageResource(R.drawable.ic_bookmark); + mBookmarkImage.setColorFilter(ThemeUtils.getAccentColor(getContext()), PorterDuff.Mode.SRC_IN); + } + } + }); } @Override public void handleBookmarkDeleted(@NonNull HistoryItem item) { mBookmarks.remove(item); if (item.isFolder()) { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false); + setBookmarksShown(null, false); } else { mBookmarkAdapter.notifyDataSetChanged(); } } + private void setBookmarksShown(@Nullable final String folder, final boolean animate) { + mBookmarksSubscription = mBookmarkManager.getBookmarksFromFolder(folder) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable final List item) { + mBookmarksSubscription = null; + Preconditions.checkNonNull(item); + + mUiModel.setCurrentFolder(folder); + if (folder == null) { + mBookmarkManager.getFolders() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List folders) { + Preconditions.checkNonNull(folders); + item.addAll(folders); + setBookmarkDataSet(item, animate); + } + }); + } else { + setBookmarkDataSet(item, animate); + } + } + }); + } + private void setBookmarkDataSet(@NonNull List items, boolean animate) { mBookmarks.clear(); mBookmarks.addAll(items); mBookmarkAdapter.notifyDataSetChanged(); final int resource; - if (mBookmarkManager.isRootFolder()) { + if (mUiModel.isRootFolder()) { resource = R.drawable.ic_action_star; } else { resource = R.drawable.ic_action_back; @@ -339,10 +357,10 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @Override public void navigateBack() { - if (mBookmarkManager.isRootFolder()) { + if (mUiModel.isRootFolder()) { mUiController.closeBookmarksDrawer(); } else { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true); + setBookmarksShown(null, true); mBookmarksListView.getLayoutManager().scrollToPosition(mScrollIndex); } } @@ -350,8 +368,8 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @Override public void handleUpdatedUrl(@NonNull String url) { updateBookmarkIndicator(url); - String folder = mBookmarkManager.getCurrentFolder(); - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false); + String folder = mUiModel.getCurrentFolder(); + setBookmarksShown(folder, false); } static class BookmarkViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { diff --git a/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java b/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java index a29f83e..2937f70 100644 --- a/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java +++ b/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java @@ -38,9 +38,9 @@ import javax.inject.Inject; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; -import acr.browser.lightning.database.HistoryModel; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ThemeUtils; @@ -65,7 +65,7 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable { private final Comparator mFilterComparator = new SuggestionsComparator(); - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; @Inject PreferenceManager mPreferenceManager; @Inject Application mApplication; @@ -102,15 +102,16 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable { } public void refreshBookmarks() { - Completable.create(new CompletableAction() { - @Override - public void onSubscribe(@NonNull CompletableSubscriber subscriber) { - mAllBookmarks.clear(); - mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true)); - - subscriber.onComplete(); - } - }).subscribeOn(Schedulers.io()).subscribe(); + mBookmarkManager.getAllBookmarks() + .subscribeOn(Schedulers.io()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List item) { + Preconditions.checkNonNull(item); + mAllBookmarks.clear(); + mAllBookmarks.addAll(item); + } + }); } @Override diff --git a/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java b/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java index 165f8d8..0660f39 100644 --- a/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java @@ -9,7 +9,6 @@ import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.AttrRes; @@ -19,7 +18,6 @@ import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.util.TypedValue; -import android.widget.ImageView; import acr.browser.lightning.R; diff --git a/app/src/main/java/acr/browser/lightning/utils/WebUtils.java b/app/src/main/java/acr/browser/lightning/utils/WebUtils.java index 7905664..2ae62c6 100644 --- a/app/src/main/java/acr/browser/lightning/utils/WebUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/WebUtils.java @@ -13,7 +13,7 @@ import android.webkit.WebViewDatabase; import com.anthonycr.bonsai.Schedulers; -import acr.browser.lightning.database.HistoryModel; +import acr.browser.lightning.database.history.HistoryModel; /** * Copyright 8/4/2015 Anthony Restaino diff --git a/app/src/main/java/acr/browser/lightning/view/LightningView.java b/app/src/main/java/acr/browser/lightning/view/LightningView.java index 4bf514e..4148fe5 100644 --- a/app/src/main/java/acr/browser/lightning/view/LightningView.java +++ b/app/src/main/java/acr/browser/lightning/view/LightningView.java @@ -32,6 +32,7 @@ import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebSettings.PluginState; import android.webkit.WebView; +import com.anthonycr.bonsai.Schedulers; import com.anthonycr.bonsai.Single; import com.anthonycr.bonsai.SingleAction; import com.anthonycr.bonsai.SingleOnSubscribe; @@ -49,13 +50,9 @@ import acr.browser.lightning.constant.Constants; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.download.LightningDownloadListener; import acr.browser.lightning.preference.PreferenceManager; - -import com.anthonycr.bonsai.Schedulers; - import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ProxyUtils; import acr.browser.lightning.utils.UrlUtils; @@ -110,7 +107,6 @@ public class LightningView { @Inject PreferenceManager mPreferences; @Inject LightningDialogBuilder mBookmarksDialogBuilder; @Inject ProxyUtils mProxyUtils; - @Inject BookmarkManager mBookmarkManager; public LightningView(@NonNull Activity activity, @Nullable String url, boolean isIncognito) { BrowserApp.getAppComponent().inject(this);