Store all thread loadables in the db, keep them in sync everywhere

multisite
Floens 9 years ago
parent 98333e2588
commit 9db1044fbd
  1. 11
      Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java
  2. 8
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java
  3. 126
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHistoryManager.java
  4. 172
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java
  5. 200
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
  6. 18
      Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java
  7. 2
      Clover/app/src/main/java/org/floens/chan/core/model/History.java
  8. 133
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  9. 50
      Clover/app/src/main/java/org/floens/chan/core/pool/LoadablePool.java
  10. 3
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  11. 15
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  12. 3
      Clover/app/src/main/java/org/floens/chan/test/TestActivity.java
  13. 9
      Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java
  14. 16
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  15. 49
      Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java
  16. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java
  17. 4
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  18. 6
      Clover/app/src/main/java/org/floens/chan/ui/state/ChanState.java
  19. 36
      Clover/app/src/main/res/layout/controller_history.xml
  20. 1
      Clover/app/src/main/res/values/strings.xml

@ -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;
}

@ -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<Cha
private final List<ChanLoaderCallback> 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<Cha
}
volleyRequestQueue = Chan.getVolleyRequestQueue();
databaseManager = Chan.getDatabaseManager();
}
/**
@ -110,7 +113,8 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
if (loadable.isCatalogMode()) {
loadable.no = 0;
loadable.listViewIndex = 0;
loadable.
listViewIndex = 0;
loadable.listViewTop = 0;
}
@ -208,7 +212,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
processResponse(response);
if (TextUtils.isEmpty(loadable.title)) {
loadable.title = PostHelper.getTitle(thread.op, loadable);
loadable.setTitle(PostHelper.getTitle(thread.op, loadable));
}
for (Post post : thread.posts) {

@ -0,0 +1,126 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Void> load() {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
databaseManager.trimTable(helper.historyDao, "history", HISTORY_TRIM_TRIGGER, HISTORY_TRIM_COUNT);
return null;
}
};
}
public Callable<List<History>> getHistory() {
return new Callable<List<History>>() {
@Override
public List<History> call() throws Exception {
QueryBuilder<History, Integer> historyQuery = helper.historyDao.queryBuilder();
List<History> 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<History> 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<History>() {
@Override
public History call() throws Exception {
QueryBuilder<History, Integer> builder = helper.historyDao.queryBuilder();
List<History> 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<Void> removeHistory(final History history) {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
helper.historyDao.delete(history);
return null;
}
};
}
public Callable<Void> clearHistory() {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
long start = Time.startTiming();
TableUtils.clearTable(helper.getConnectionSource(), History.class);
Time.endTiming("Clear history table", start);
return null;
}
};
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Loadable, Loadable> 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<Void> flush() {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
List<Loadable> 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.
* <p>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
* <p>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<Loadable> getLoadable(final Loadable loadable) {
if (!loadable.isThreadMode()) {
throw new IllegalArgumentException("getLoadable can only be used for thread loadables");
}
return new Callable<Loadable>() {
@Override
public Loadable call() throws Exception {
Loadable cachedLoadable = cachedLoadables.get(loadable);
if (cachedLoadable != null) {
Logger.v(TAG, "Cached loadable found");
return cachedLoadable;
} else {
QueryBuilder<Loadable, Integer> builder = helper.loadableDao.queryBuilder();
List<Loadable> 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;
}
}
};
}
}

@ -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<ThreadHide> threadHides = new ArrayList<>();
private final HashSet<Integer> threadHidesIds = new HashSet<>();
private final Object historyLock = new Object();
private final HashMap<Loadable, History> 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<Void>() {
@Override
public Void call() throws Exception {
List<History> 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<History> getHistory() {
List<History> list = null;
try {
QueryBuilder<History, Integer> 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<History> 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<Void>() {
@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 <T> void runTask(final Callable<T> taskCallable, final TaskResult<T> 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> T runTaskSync(final Callable<T> 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 <T> void completeTask(final TaskResult<T> task, final T result) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
private <T> Future<T> executeTask(final Callable<T> taskCallable, final TaskResult<T> taskResult) {
return backgroundExecutor.submit(new Callable<T>() {
@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);
}
}
});
}

@ -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<Pin> createPin(final Pin pin) {
if (pin.loadable.id == 0) {
throw new IllegalArgumentException("Pin loadable is not yet in the db");
}
return new Callable<Pin>() {
@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<Pin> 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;
}

@ -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

@ -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;
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Loadable, Loadable> 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;
}
}
}

@ -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<ReplyHttpCall>,
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) {

@ -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<Post> 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);
}
}

@ -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

@ -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));

@ -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<FloatingMenuItem> 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) {

@ -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<FloatingMenuItem> 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<HistoryCell> {
private class HistoryAdapter extends RecyclerView.Adapter<HistoryCell> implements DatabaseManager.TaskResult<List<History>> {
private List<History> sourceList = new ArrayList<>();
private List<History> 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<History> result) {
resultPending = false;
sourceList.clear();
sourceList.addAll(result);
crossfade.toggle(!sourceList.isEmpty(), true);
filter();
}

@ -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() {

@ -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) {

@ -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

@ -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 <http://www.gnu.org/licenses/>.
-->
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
<org.floens.chan.ui.view.CrossfadeView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/crossfade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?backcolor"
android:clipToPadding="false"
android:padding="16dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
android:background="?backcolor">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="16dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:visibility="gone">
<TextView
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/history_empty_info" />
</FrameLayout>
</org.floens.chan.ui.view.CrossfadeView>

@ -228,6 +228,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="history_clear_confirm">Clear history?</string>
<string name="history_clear_confirm_button">Clear</string>
<string name="history_toggle_hint">Enable or disable history</string>
<string name="history_empty_info">No history</string>
<string name="drawer_board">Board</string>
<string name="drawer_catalog">Catalog</string>

Loading…
Cancel
Save