diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java index 8cc4cc0f..2ed7a9ec 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java @@ -20,8 +20,9 @@ package org.floens.chan.chan; import android.net.Uri; import org.floens.chan.Chan; +import org.floens.chan.core.database.DatabaseLoadableManager; +import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.Loadable; -import org.floens.chan.core.pool.LoadablePool; import java.util.List; @@ -33,10 +34,12 @@ public class ChanHelper { if (parts.size() > 0) { String rawBoard = parts.get(0); - if (Chan.getBoardManager().getBoardExists(rawBoard)) { + BoardManager boardManager = Chan.getBoardManager(); + DatabaseLoadableManager loadableManager = Chan.getDatabaseManager().getDatabaseLoadableManager(); + if (boardManager.getBoardExists(rawBoard)) { if (parts.size() == 1 || (parts.size() == 2 && "catalog".equals(parts.get(1)))) { // Board mode - loadable = LoadablePool.getInstance().obtain(new Loadable(rawBoard)); + loadable = loadableManager.get(Loadable.forCatalog(rawBoard)); } else if (parts.size() >= 3) { // Thread mode int no = -1; @@ -59,7 +62,7 @@ public class ChanHelper { } if (no >= 0) { - loadable = LoadablePool.getInstance().obtain(new Loadable(rawBoard, no)); + loadable = loadableManager.get(Loadable.forThread(rawBoard, no)); if (post >= 0) { loadable.markedNo = post; } diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java index a98b8df3..a580807e 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java @@ -24,6 +24,7 @@ import com.android.volley.Response; import com.android.volley.VolleyError; import org.floens.chan.Chan; +import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; @@ -49,6 +50,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener listeners = new ArrayList<>(); private final Loadable loadable; private final RequestQueue volleyRequestQueue; + private final DatabaseManager databaseManager; private ChanThread thread; private ChanReaderRequest request; @@ -66,6 +68,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener. + */ +package org.floens.chan.core.database; + +import com.j256.ormlite.stmt.QueryBuilder; +import com.j256.ormlite.table.TableUtils; + +import org.floens.chan.core.model.History; +import org.floens.chan.utils.Time; + +import java.util.List; +import java.util.concurrent.Callable; + +public class DatabaseHistoryManager { + private static final String TAG = "DatabaseHistoryManager"; + + private static final long HISTORY_TRIM_TRIGGER = 100; + private static final long HISTORY_TRIM_COUNT = 50; + + private DatabaseManager databaseManager; + private DatabaseHelper helper; + private DatabaseLoadableManager databaseLoadableManager; + + public DatabaseHistoryManager(DatabaseManager databaseManager, DatabaseHelper helper, DatabaseLoadableManager databaseLoadableManager) { + this.databaseManager = databaseManager; + this.helper = helper; + this.databaseLoadableManager = databaseLoadableManager; + } + + public void add(History history) { + databaseManager.runTaskSync(addHistory(history)); + } + + public Callable load() { + return new Callable() { + @Override + public Void call() throws Exception { + databaseManager.trimTable(helper.historyDao, "history", HISTORY_TRIM_TRIGGER, HISTORY_TRIM_COUNT); + + return null; + } + }; + } + + public Callable> getHistory() { + return new Callable>() { + @Override + public List call() throws Exception { + QueryBuilder historyQuery = helper.historyDao.queryBuilder(); + List date = historyQuery.orderBy("date", false).query(); + for (int i = 0; i < date.size(); i++) { + History history = date.get(i); + history.loadable = databaseLoadableManager.refreshForeign(history.loadable); + } + return date; + } + }; + } + + public Callable addHistory(final History history) { + if (!history.loadable.isThreadMode()) { + throw new IllegalArgumentException("History loadables must be in thread mode"); + } + + if (history.loadable.id == 0) { + throw new IllegalArgumentException("History loadable is not yet in the db"); + } + + return new Callable() { + @Override + public History call() throws Exception { + QueryBuilder builder = helper.historyDao.queryBuilder(); + List existingHistories = builder.where().eq("loadable_id", history.loadable.id).query(); + History existingHistoryForLoadable = existingHistories.isEmpty() ? null : existingHistories.get(0); + + if (existingHistoryForLoadable != null) { + existingHistoryForLoadable.date = Time.get(); + helper.historyDao.update(existingHistoryForLoadable); + } else { + history.date = Time.get(); + helper.historyDao.create(history); + } + + return history; + } + }; + } + + public Callable removeHistory(final History history) { + return new Callable() { + @Override + public Void call() throws Exception { + helper.historyDao.delete(history); + return null; + } + }; + } + + public Callable clearHistory() { + return new Callable() { + @Override + public Void call() throws Exception { + long start = Time.startTiming(); + TableUtils.clearTable(helper.getConnectionSource(), History.class); + Time.endTiming("Clear history table", start); + + return null; + } + }; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java new file mode 100644 index 00000000..7624c546 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java @@ -0,0 +1,172 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.core.database; + +import android.util.Log; + +import com.j256.ormlite.stmt.QueryBuilder; + +import org.floens.chan.core.model.Loadable; +import org.floens.chan.utils.Logger; +import org.floens.chan.utils.Time; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +public class DatabaseLoadableManager { + private static final String TAG = "DatabaseLoadableManager"; + + private DatabaseManager databaseManager; + private DatabaseHelper helper; + + private Map cachedLoadables = new HashMap<>(); + + public DatabaseLoadableManager(DatabaseManager databaseManager, DatabaseHelper helper) { + this.databaseManager = databaseManager; + this.helper = helper; + } + + /** + * Called when the application goes into the background, to do intensive update calls for loadables + * whose list indexes or titles have changed. + */ + public Callable flush() { + return new Callable() { + @Override + public Void call() throws Exception { + List toFlush = new ArrayList<>(); + for (Loadable loadable : cachedLoadables.values()) { + if (loadable.dirty) { + loadable.dirty = false; + toFlush.add(loadable); + } + } + + if (!toFlush.isEmpty()) { + Logger.d(TAG, "Flushing " + toFlush.size() + " loadable(s) list positions"); + long start = Time.startTiming(); + for (int i = 0; i < toFlush.size(); i++) { + Loadable loadable = toFlush.get(i); + helper.loadableDao.update(loadable); + } + Time.endTiming("Loadable flushing", start); + } + + return null; + } + }; + } + + /** + * All loadables that are not gotten from a database (like from any of the Loadable.for...() factory methods) + * need to go through this method to correctly get a loadable if it already existed in the db. + *

It will search the database for existing loadables of the mode is THREAD, and return one of those if there is + * else it will create the loadable in the database and return the given loadable. + * + * @param loadable Loadable to search from that was not yet gotten from the db. + * @return a loadable ready to use. + */ + public Loadable get(final Loadable loadable) { + if (loadable.id != 0) { + throw new IllegalArgumentException("get() only works for transient loadables"); + } + + // We only cache THREAD loadables in the db + if (loadable.isThreadMode()) { + long start = Time.startTiming(); + Loadable result = databaseManager.runTaskSync(getLoadable(loadable)); + Time.endTiming("get loadable from db " + loadable.board, start); + return result; + } else { + return loadable; + } + } + + /** + * Call this when you use a thread loadable as a foreign object on your table + *

It will correctly update the loadable cache + * + * @param loadable Loadable that only has its id loaded + * @return a loadable ready to use. + * @throws SQLException + */ + public Loadable refreshForeign(final Loadable loadable) throws SQLException { + if (loadable.id == 0) { + throw new IllegalArgumentException("This only works loadables that have their id loaded"); + } + + // If the loadable was already loaded in the cache, return that entry + for (Loadable key : cachedLoadables.keySet()) { + if (key.id == loadable.id) { + return key; + } + } + + // Add it to the cache, refresh contents + helper.loadableDao.refresh(loadable); + cachedLoadables.put(loadable, loadable); + return loadable; + } + + private Callable getLoadable(final Loadable loadable) { + if (!loadable.isThreadMode()) { + throw new IllegalArgumentException("getLoadable can only be used for thread loadables"); + } + + return new Callable() { + @Override + public Loadable call() throws Exception { + Loadable cachedLoadable = cachedLoadables.get(loadable); + if (cachedLoadable != null) { + Logger.v(TAG, "Cached loadable found"); + return cachedLoadable; + } else { + QueryBuilder builder = helper.loadableDao.queryBuilder(); + List results = builder.where() + .eq("mode", loadable.mode) + .and().eq("board", loadable.board) + .and().eq("no", loadable.no) + .query(); + + if (results.size() > 1) { + Log.w(TAG, "Multiple loadables found for where Loadable.equals() would return true"); + for (Loadable result : results) { + Log.w(TAG, result.toString()); + } + } + + Loadable result = results.isEmpty() ? null : results.get(0); + if (result == null) { + Log.d(TAG, "Creating loadable"); + helper.loadableDao.create(loadable); + result = loadable; + } else { + Log.d(TAG, "Loadable found in db"); + } + + cachedLoadables.put(result, result); + return result; + } + } + }; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java index a430bd74..9b979edf 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java @@ -23,13 +23,11 @@ import android.os.Looper; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.misc.TransactionManager; -import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.table.TableUtils; +import org.floens.chan.Chan; import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Filter; -import org.floens.chan.core.model.History; -import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.model.ThreadHide; @@ -37,12 +35,15 @@ import org.floens.chan.utils.Logger; import java.sql.SQLException; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import de.greenrobot.event.EventBus; import static com.j256.ormlite.misc.TransactionManager.callInTransaction; @@ -56,8 +57,7 @@ public class DatabaseManager { private static final long HISTORY_TRIM_TRIGGER = 500; private static final long HISTORY_TRIM_COUNT = 50; - private final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); - + private final ExecutorService backgroundExecutor; private final DatabaseHelper helper; private final Object savedRepliesLock = new Object(); @@ -67,21 +67,44 @@ public class DatabaseManager { private final List threadHides = new ArrayList<>(); private final HashSet threadHidesIds = new HashSet<>(); - private final Object historyLock = new Object(); - private final HashMap historyByLoadable = new HashMap<>(); - private final DatabasePinManager databasePinManager; + private final DatabaseLoadableManager databaseLoadableManager; + private final DatabaseHistoryManager databaseHistoryManager; public DatabaseManager(Context context) { + backgroundExecutor = Executors.newSingleThreadExecutor(); + helper = new DatabaseHelper(context); - databasePinManager = new DatabasePinManager(this, helper); + databaseLoadableManager = new DatabaseLoadableManager(this, helper); + databasePinManager = new DatabasePinManager(this, helper, databaseLoadableManager); + databaseHistoryManager = new DatabaseHistoryManager(this, helper, databaseLoadableManager); initialize(); + EventBus.getDefault().register(this); + } + + public DatabasePinManager getDatabasePinManager() { + return databasePinManager; + } + + public DatabaseLoadableManager getDatabaseLoadableManager() { + return databaseLoadableManager; + } + + public DatabaseHistoryManager getDatabaseHistoryManager() { + return databaseHistoryManager; + } + + // Called when the app changes foreground state + public void onEvent(Chan.ForegroundChangedMessage message) { + if (!message.inForeground) { + runTask(databaseLoadableManager.flush()); + } } private void initialize() { loadSavedReplies(); loadThreadHides(); - loadHistory(); + databaseHistoryManager.load(); } /** @@ -145,78 +168,6 @@ public class DatabaseManager { return getSavedReply(board, no) != null; } - public DatabasePinManager getDatabasePinManager() { - return databasePinManager; - } - - /** - * Adds or updates a {@link History} to the history table. - * Only updates the date if the history is already in the table. - * - * @param history History to save - */ - public void addHistory(final History history) { - backgroundExecutor.submit(new Runnable() { - @Override - public void run() { - addHistoryInternal(history); - } - }); - } - - /** - * Deletes a {@link History} from the history table. - * - * @param history History to delete - */ - public void removeHistory(History history) { - try { - helper.historyDao.delete(history); - helper.loadableDao.delete(history.loadable); - historyByLoadable.remove(history.loadable); - } catch (SQLException e) { - Logger.e(TAG, "Error removing history from db", e); - } - } - - /** - * Clears all history and the referenced loadables from the database. - */ - public void clearHistory() { - try { - TransactionManager.callInTransaction(helper.getConnectionSource(), new Callable() { - @Override - public Void call() throws Exception { - List historyList = getHistory(); - for (History history : historyList) { - removeHistory(history); - } - - return null; - } - }); - } catch (SQLException e) { - Logger.e(TAG, "Error clearing history", e); - } - } - - /** - * Get a list of {@link History} entries from the history table. - * - * @return List of History - */ - public List getHistory() { - List list = null; - try { - QueryBuilder historyQuery = helper.historyDao.queryBuilder(); - list = historyQuery.orderBy("date", false).query(); - } catch (SQLException e) { - Logger.e(TAG, "Error getting history from db", e); - } - - return list; - } - public void addOrUpdateFilter(Filter filter) { try { helper.filterDao.createOrUpdate(filter); @@ -401,48 +352,6 @@ public class DatabaseManager { } } - private void loadHistory() { - synchronized (historyLock) { - try { - trimTable(helper.historyDao, "history", HISTORY_TRIM_TRIGGER, HISTORY_TRIM_COUNT); - - historyByLoadable.clear(); - List historyList = helper.historyDao.queryForAll(); - for (History history : historyList) { - historyByLoadable.put(history.loadable, history); - } - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - - private void addHistoryInternal(final History history) { - try { - TransactionManager.callInTransaction(helper.getConnectionSource(), new Callable() { - @Override - public Void call() throws Exception { - synchronized (historyLock) { - History existingHistory = historyByLoadable.get(history.loadable); - if (existingHistory != null) { - existingHistory.date = System.currentTimeMillis(); - helper.historyDao.update(existingHistory); - } else { - history.date = System.currentTimeMillis(); - helper.loadableDao.create(history.loadable); - helper.historyDao.create(history); - historyByLoadable.put(history.loadable, history); - } - } - - return null; - } - }); - } catch (SQLException e) { - Logger.e(TAG, "Error adding history", e); - } - } - /** * Trim a table with the specified trigger and trim count. * @@ -451,7 +360,7 @@ public class DatabaseManager { * @param trigger Trim if there are more rows than {@code trigger}. * @param trim Count of rows to trim. */ - private void trimTable(Dao dao, String table, long trigger, long trim) { + /*package*/ void trimTable(Dao dao, String table, long trigger, long trim) { try { long count = dao.countOf(); if (count > trigger) { @@ -468,34 +377,35 @@ public class DatabaseManager { } public void runTask(final Callable taskCallable, final TaskResult taskResult) { - backgroundExecutor.submit(new Runnable() { - @Override - public void run() { - try { - T result = TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); - if (taskResult != null) { - completeTask(taskResult, result); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }); + executeTask(taskCallable, taskResult); } public T runTaskSync(final Callable taskCallable) { try { - return TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); - } catch (SQLException e) { + return executeTask(taskCallable, null).get(); + } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } - public void completeTask(final TaskResult task, final T result) { - new Handler(Looper.getMainLooper()).post(new Runnable() { + private Future executeTask(final Callable taskCallable, final TaskResult taskResult) { + return backgroundExecutor.submit(new Callable() { @Override - public void run() { - task.onComplete(result); + public T call() { + try { + final T result = TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); + if (taskResult != null) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + taskResult.onComplete(result); + } + }); + } + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } } }); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java index a208c2c6..4eb288ec 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java @@ -27,17 +27,22 @@ public class DatabasePinManager { private DatabaseManager databaseManager; private DatabaseHelper helper; + private DatabaseLoadableManager databaseLoadableManager; - public DatabasePinManager(DatabaseManager databaseManager, DatabaseHelper helper) { + public DatabasePinManager(DatabaseManager databaseManager, DatabaseHelper helper, DatabaseLoadableManager databaseLoadableManager) { this.databaseManager = databaseManager; this.helper = helper; + this.databaseLoadableManager = databaseLoadableManager; } public Callable createPin(final Pin pin) { + if (pin.loadable.id == 0) { + throw new IllegalArgumentException("Pin loadable is not yet in the db"); + } + return new Callable() { @Override public Pin call() throws Exception { - helper.loadableDao.create(pin.loadable); helper.pinDao.create(pin); return pin; } @@ -49,7 +54,6 @@ public class DatabasePinManager { @Override public Void call() throws Exception { helper.pinDao.delete(pin); - helper.loadableDao.delete(pin.loadable); return null; } @@ -61,7 +65,6 @@ public class DatabasePinManager { @Override public Pin call() throws Exception { helper.pinDao.update(pin); - helper.loadableDao.update(pin.loadable); return pin; } }; @@ -76,11 +79,6 @@ public class DatabasePinManager { helper.pinDao.update(pin); } - for (int i = 0; i < pins.size(); i++) { - Pin pin = pins.get(i); - helper.loadableDao.update(pin.loadable); - } - return null; } }; @@ -93,7 +91,7 @@ public class DatabasePinManager { List list = helper.pinDao.queryForAll(); for (int i = 0; i < list.size(); i++) { Pin p = list.get(i); - helper.loadableDao.refresh(p.loadable); + p.loadable = databaseLoadableManager.refreshForeign(p.loadable); } return list; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/History.java b/Clover/app/src/main/java/org/floens/chan/core/model/History.java index 43c14710..54998f4c 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/History.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/History.java @@ -25,7 +25,7 @@ public class History { @DatabaseField(generatedId = true) public int id; - @DatabaseField(canBeNull = false, foreign = true, foreignAutoRefresh = true) + @DatabaseField(canBeNull = false, foreign = true) public Loadable loadable; @DatabaseField diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java b/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java index a077e688..368c6638 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java @@ -33,13 +33,13 @@ public class Loadable { @DatabaseField public int mode = Mode.INVALID; - @DatabaseField - public String board = ""; + @DatabaseField(canBeNull = false, index = true) + public String board; - @DatabaseField + @DatabaseField(index = true) public int no = -1; - @DatabaseField + @DatabaseField(canBeNull = false) public String title = ""; @DatabaseField @@ -53,35 +53,57 @@ public class Loadable { public int markedNo = -1; + // when the title, listViewTop, listViewIndex or lastViewed were changed + public boolean dirty = false; + /** * Constructs an empty loadable. The mode is INVALID. */ - public Loadable() { + private Loadable() { } - public Loadable(String board) { - mode = Mode.CATALOG; - this.board = board; - this.no = 0; + public static Loadable emptyLoadable() { + return new Loadable(); } - /** - * Quick constructor for a thread loadable. - */ - public Loadable(String board, int no) { - mode = Mode.THREAD; - this.board = board; - this.no = no; + public static Loadable forCatalog(String board) { + Loadable loadable = new Loadable(); + loadable.mode = Mode.CATALOG; + loadable.board = board; + return loadable; } - /** - * Quick constructor for a thread loadable with an title. - */ - public Loadable(String board, int no, String title) { - mode = Mode.THREAD; - this.board = board; - this.no = no; + public static Loadable forThread(String board, int no) { + return Loadable.forThread(board, no, ""); + } + + public static Loadable forThread(String board, int no, String title) { + Loadable loadable = new Loadable(); + loadable.mode = Mode.THREAD; + loadable.board = board; + loadable.no = no; + loadable.title = title; + return loadable; + } + + public void setTitle(String title) { this.title = title; + dirty = true; + } + + public void setLastViewed(int lastViewed) { + this.lastViewed = lastViewed; + dirty = true; + } + + public void setListViewTop(int listViewTop) { + this.listViewTop = listViewTop; + dirty = true; + } + + public void setListViewIndex(int listViewIndex) { + this.listViewIndex = listViewIndex; + dirty = true; } /** @@ -94,17 +116,52 @@ public class Loadable { Loadable other = (Loadable) object; - return no == other.no && mode == other.mode && board.equals(other.board); + if (mode == other.mode) { + switch (mode) { + case Mode.INVALID: + return true; + case Mode.CATALOG: + case Mode.BOARD: + return board.equals(other.board); + case Mode.THREAD: + return board.equals(other.board) && no == other.no; + default: + throw new IllegalArgumentException(); + } + } else { + return false; + } } @Override public int hashCode() { int result = mode; - result = 31 * result + (board != null ? board.hashCode() : 0); - result = 31 * result + no; + + if (mode == Mode.THREAD || mode == Mode.CATALOG || mode == Mode.BOARD) { + result = 31 * result + (board != null ? board.hashCode() : 0); + } + if (mode == Mode.THREAD) { + result = 31 * result + no; + } return result; } + @Override + public String toString() { + return "Loadable{" + + "id=" + id + + ", mode=" + mode + + ", board='" + board + '\'' + + ", no=" + no + + ", title='" + title + '\'' + + ", listViewIndex=" + listViewIndex + + ", listViewTop=" + listViewTop + + ", lastViewed=" + lastViewed + + ", markedNo=" + markedNo + + ", dirty=" + dirty + + '}'; + } + public boolean isThreadMode() { return mode == Mode.THREAD; } @@ -113,6 +170,19 @@ public class Loadable { return mode == Mode.CATALOG; } + public static Loadable readFromParcel(Parcel parcel) { + Loadable loadable = new Loadable(); + /*loadable.id = */ + parcel.readInt(); + loadable.mode = parcel.readInt(); + loadable.board = parcel.readString(); + loadable.no = parcel.readInt(); + loadable.title = parcel.readString(); + loadable.listViewIndex = parcel.readInt(); + loadable.listViewTop = parcel.readInt(); + return loadable; + } + public void writeToParcel(Parcel parcel) { parcel.writeInt(id); parcel.writeInt(mode); @@ -123,16 +193,6 @@ public class Loadable { parcel.writeInt(listViewTop); } - public void readFromParcel(Parcel parcel) { - id = parcel.readInt(); - mode = parcel.readInt(); - board = parcel.readString(); - no = parcel.readInt(); - title = parcel.readString(); - listViewIndex = parcel.readInt(); - listViewTop = parcel.readInt(); - } - public Loadable copy() { Loadable copy = new Loadable(); copy.mode = mode; @@ -149,6 +209,7 @@ public class Loadable { public static class Mode { public static final int INVALID = -1; public static final int THREAD = 0; + @Deprecated public static final int BOARD = 1; public static final int CATALOG = 2; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/pool/LoadablePool.java b/Clover/app/src/main/java/org/floens/chan/core/pool/LoadablePool.java deleted file mode 100644 index 2d823aab..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/pool/LoadablePool.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.core.pool; - -import org.floens.chan.core.model.Loadable; - -import java.util.HashMap; -import java.util.Map; - -public class LoadablePool { - private static final LoadablePool instance = new LoadablePool(); - - private Map pool = new HashMap<>(); - - private LoadablePool() { - } - - public static LoadablePool getInstance() { - return instance; - } - - public Loadable obtain(Loadable searchLoadable) { - if (searchLoadable.isThreadMode()) { - Loadable poolLoadable = pool.get(searchLoadable); - if (poolLoadable == null) { - poolLoadable = searchLoadable; - pool.put(poolLoadable, poolLoadable); - } - - return poolLoadable; - } else { - return searchLoadable; - } - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 209b4084..9f13743c 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -33,7 +33,6 @@ import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Reply; import org.floens.chan.core.model.SavedReply; -import org.floens.chan.core.pool.LoadablePool; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.layout.CaptchaCallback; @@ -228,7 +227,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, callback.onPosted(); if (bound && !loadable.isThreadMode()) { - callback.showThread(LoadablePool.getInstance().obtain(new Loadable(loadable.board, replyCall.postNo))); + callback.showThread(databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.board, replyCall.postNo))); } } else { if (replyCall.errorMessage == null) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java index ead7ccb7..bbf446b3 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java @@ -38,7 +38,6 @@ import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.SavedReply; -import org.floens.chan.core.pool.LoadablePool; import org.floens.chan.core.pool.LoaderPool; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.adapter.PostAdapter; @@ -270,7 +269,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt public void onListScrolledToBottom() { if (loadable.isThreadMode()) { List posts = chanLoader.getThread().posts; - loadable.lastViewed = posts.get(posts.size() - 1).no; + loadable.setLastViewed(posts.get(posts.size() - 1).no); } Pin pin = watchManager.findPinByLoadable(loadable); @@ -334,7 +333,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt @Override public void onPostClicked(Post post) { if (loadable.isCatalogMode()) { - Loadable threadLoadable = LoadablePool.getInstance().obtain(new Loadable(post.board, post.no)); + Loadable threadLoadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(post.board, post.no)); threadLoadable.title = PostHelper.getTitle(post, loadable); threadPresenterCallback.showThread(threadLoadable); } else { @@ -448,7 +447,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt databaseManager.saveReply(new SavedReply(post.board, post.no, "foo")); break; case POST_OPTION_PIN: - Loadable pinLoadable = LoadablePool.getInstance().obtain(new Loadable(post.board, post.no)); + Loadable pinLoadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(post.board, post.no)); watchManager.createPin(pinLoadable, post); break; case POST_OPTION_OPEN_BROWSER: @@ -484,7 +483,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value; if (boardManager.getBoardExists(link.board)) { - Loadable thread = LoadablePool.getInstance().obtain(new Loadable(link.board, link.threadId)); + Loadable thread = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(link.board, link.threadId)); thread.markedNo = link.postId; threadPresenterCallback.showThread(thread); @@ -630,11 +629,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt if (!historyAdded && ChanSettings.historyEnabled.get() && loadable.isThreadMode()) { historyAdded = true; History history = new History(); - // Copy the loadable when adding to history - // Otherwise the database will possible use the loadable from a pin, and when clearing the history also deleting the loadable from the pin. - history.loadable = loadable.copy(); + history.loadable = loadable; history.thumbnailUrl = chanLoader.getThread().op.thumbnailUrl; - databaseManager.addHistory(history); + databaseManager.getDatabaseHistoryManager().add(history); } } diff --git a/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java b/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java index ecd84569..ff89a947 100644 --- a/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java @@ -201,8 +201,7 @@ public class TestActivity extends Activity implements View.OnClickListener { } private void testCache() { - Loadable loadable = new Loadable("g"); - loadable.mode = Loadable.Mode.CATALOG; + Loadable loadable = Loadable.forCatalog("g"); ChanLoader loader = new ChanLoader(loadable); loader.addListener(new ChanLoader.ChanLoaderCallback() { @Override diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java index f367a9d8..fec63447 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java @@ -37,11 +37,11 @@ import org.floens.chan.R; import org.floens.chan.chan.ChanHelper; import org.floens.chan.controller.Controller; import org.floens.chan.controller.NavigationController; +import org.floens.chan.core.database.DatabaseLoadableManager; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; -import org.floens.chan.core.pool.LoadablePool; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.controller.BrowseController; import org.floens.chan.ui.controller.DrawerController; @@ -132,8 +132,9 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat if (chanState == null) { Logger.w(TAG, "savedInstanceState was not null, but no ChanState was found!"); } else { - chanState.board = LoadablePool.getInstance().obtain(chanState.board); - chanState.thread = LoadablePool.getInstance().obtain(chanState.thread); + DatabaseLoadableManager loadableManager = Chan.getDatabaseManager().getDatabaseLoadableManager(); + chanState.board = loadableManager.get(chanState.board); + chanState.thread = loadableManager.get(chanState.thread); loadDefault = false; Board board = boardManager.getBoardByValue(chanState.board.board); @@ -247,7 +248,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat if (thread == null) { // Make the parcel happy - thread = new Loadable(); + thread = Loadable.emptyLoadable(); } outState.putParcelable(STATE_KEY, new ChanState(board, thread)); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java index 9cd32a16..ec3eaa71 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java @@ -30,11 +30,12 @@ import android.widget.TextView; import org.floens.chan.Chan; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; -import org.floens.chan.core.pool.LoadablePool; +import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.cell.PostCellInterface; @@ -59,6 +60,8 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte private static final int ORDER_ID = 105; private static final int OPEN_BROWSER_ID = 106; + private final DatabaseManager databaseManager; + private PostCellInterface.PostViewMode postViewMode; private PostsFilter.Order order; private List boardItems; @@ -70,6 +73,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte public BrowseController(Context context) { super(context); + databaseManager = Chan.getDatabaseManager(); } @Override @@ -264,14 +268,14 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte } public void loadBoard(Board board) { - Loadable loadable = LoadablePool.getInstance().obtain(new Loadable(board.value)); - loadable.mode = Loadable.Mode.CATALOG; + Loadable loadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forCatalog(board.value)); loadable.title = board.key; navigationItem.title = board.key; - threadLayout.getPresenter().unbindLoadable(); - threadLayout.getPresenter().bindLoadable(loadable); - threadLayout.getPresenter().requestData(); + ThreadPresenter presenter = threadLayout.getPresenter(); + presenter.unbindLoadable(); + presenter.bindLoadable(loadable); + presenter.requestData(); for (FloatingMenuItem item : boardItems) { if (((FloatingMenuItemBoard) item).board == board) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java index 2b06f1e7..460d5c20 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java @@ -34,15 +34,19 @@ import android.widget.TextView; import org.floens.chan.Chan; import org.floens.chan.R; import org.floens.chan.controller.Controller; +import org.floens.chan.core.database.DatabaseHistoryManager; import org.floens.chan.core.database.DatabaseManager; +import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.Board; import org.floens.chan.core.model.History; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenuItem; +import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.ui.view.ThumbnailView; +import org.floens.chan.utils.Logger; import java.util.ArrayList; import java.util.List; @@ -56,6 +60,10 @@ public class HistoryController extends Controller implements CompoundButton.OnCh private static final int CLEAR_ID = 101; private DatabaseManager databaseManager; + private DatabaseHistoryManager databaseHistoryManager; + private BoardManager boardManager; + + private CrossfadeView crossfade; private RecyclerView recyclerView; private HistoryAdapter adapter; @@ -68,6 +76,8 @@ public class HistoryController extends Controller implements CompoundButton.OnCh super.onCreate(); databaseManager = Chan.getDatabaseManager(); + databaseHistoryManager = databaseManager.getDatabaseHistoryManager(); + boardManager = Chan.getBoardManager(); navigationItem.setTitle(R.string.history_screen); List items = new ArrayList<>(); @@ -76,26 +86,31 @@ public class HistoryController extends Controller implements CompoundButton.OnCh navigationItem.menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp)); navigationItem.createOverflow(context, this, items); - view = inflateRes(R.layout.controller_history); - SwitchCompat historyEnabledSwitch = new SwitchCompat(context); historyEnabledSwitch.setChecked(ChanSettings.historyEnabled.get()); historyEnabledSwitch.setOnCheckedChangeListener(this); navigationItem.rightView = historyEnabledSwitch; + view = inflateRes(R.layout.controller_history); + crossfade = (CrossfadeView) view.findViewById(R.id.crossfade); recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(context)); adapter = new HistoryAdapter(); recyclerView.setAdapter(adapter); - adapter.load(); if (ChanSettings.historyOpenCounter.increase() == 1) { HintPopup.show(context, historyEnabledSwitch, R.string.history_toggle_hint); } } + @Override + public void onShow() { + super.onShow(); + adapter.load(); + } + @Override public void onMenuItemClicked(ToolbarMenuItem item) { if ((Integer) item.getId() == SEARCH_ID) { @@ -112,7 +127,7 @@ public class HistoryController extends Controller implements CompoundButton.OnCh .setPositiveButton(R.string.history_clear_confirm_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - databaseManager.clearHistory(); + databaseManager.runTask(databaseHistoryManager.clearHistory()); adapter.load(); } }) @@ -132,7 +147,7 @@ public class HistoryController extends Controller implements CompoundButton.OnCh } private void deleteHistory(History history) { - databaseManager.removeHistory(history); + databaseManager.runTaskSync(databaseHistoryManager.removeHistory(history)); adapter.load(); } @@ -148,11 +163,13 @@ public class HistoryController extends Controller implements CompoundButton.OnCh adapter.search(entered); } - private class HistoryAdapter extends RecyclerView.Adapter { + private class HistoryAdapter extends RecyclerView.Adapter implements DatabaseManager.TaskResult> { private List sourceList = new ArrayList<>(); private List displayList = new ArrayList<>(); private String searchQuery; + private boolean resultPending = false; + public HistoryAdapter() { setHasStableIds(true); } @@ -166,8 +183,13 @@ public class HistoryController extends Controller implements CompoundButton.OnCh public void onBindViewHolder(HistoryCell holder, int position) { History history = displayList.get(position); holder.thumbnail.setUrl(history.thumbnailUrl, dp(48), dp(48)); + + if (history.loadable == null) { + Logger.test("null!"); + } + holder.text.setText(history.loadable.title); - Board board = Chan.getBoardManager().getBoardByValue(history.loadable.board); + Board board = boardManager.getBoardByValue(history.loadable.board); holder.subtext.setText(board == null ? null : ("/" + board.value + "/ - " + board.key)); } @@ -187,9 +209,18 @@ public class HistoryController extends Controller implements CompoundButton.OnCh } private void load() { - sourceList.clear(); - sourceList.addAll(databaseManager.getHistory()); + if (!resultPending) { + resultPending = true; + databaseManager.runTask(databaseHistoryManager.getHistory(), this); + } + } + @Override + public void onComplete(List result) { + resultPending = false; + sourceList.clear(); + sourceList.addAll(result); + crossfade.toggle(!sourceList.isEmpty(), true); filter(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java index a39b4ac1..e0d6e279 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java @@ -66,7 +66,7 @@ import static org.floens.chan.utils.AndroidUtils.getString; public class ThemeSettingsController extends Controller implements View.OnClickListener { private PostCell.PostCellCallback DUMMY_POST_CALLBACK = new PostCell.PostCellCallback() { - private Loadable loadable = new Loadable("g", 1234); + private Loadable loadable = Loadable.forThread("g", 1234); @Override public Loadable getLoadable() { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java index 66ed8ceb..07b380c4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -118,8 +118,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa top = layoutManager.getDecoratedTop(topChild) - params.topMargin - recyclerView.getPaddingTop(); } - showingThread.loadable.listViewIndex = index; - showingThread.loadable.listViewTop = top; + showingThread.loadable.setListViewIndex(index); + showingThread.loadable.setListViewTop(top); int last = getCompleteBottomAdapterPosition(); if (last == postAdapter.getUnfilteredDisplaySize() - 1 && last > lastPostCount) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/state/ChanState.java b/Clover/app/src/main/java/org/floens/chan/ui/state/ChanState.java index bb8b90e4..35dcdbae 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/state/ChanState.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/state/ChanState.java @@ -32,10 +32,8 @@ public class ChanState implements Parcelable { } public ChanState(Parcel parcel) { - board = new Loadable(); - board.readFromParcel(parcel); - thread = new Loadable(); - thread.readFromParcel(parcel); + board = Loadable.readFromParcel(parcel); + thread = Loadable.readFromParcel(parcel); } @Override diff --git a/Clover/app/src/main/res/layout/controller_history.xml b/Clover/app/src/main/res/layout/controller_history.xml index 00fac075..64c5fc09 100644 --- a/Clover/app/src/main/res/layout/controller_history.xml +++ b/Clover/app/src/main/res/layout/controller_history.xml @@ -15,12 +15,34 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> - + android:background="?backcolor"> + + + + + + + + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 82598b0d..c41c98ec 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -228,6 +228,7 @@ along with this program. If not, see . Clear history? Clear Enable or disable history + No history Board Catalog