diff --git a/Clover/app/proguard.cfg b/Clover/app/proguard.cfg index fe0b56e0..11489a3d 100644 --- a/Clover/app/proguard.cfg +++ b/Clover/app/proguard.cfg @@ -147,3 +147,5 @@ -keepclassmembers class ** { public void onEvent*(**); } + +-keep public class * extends android.support.design.** diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml index 86bb2077..f1efbee5 100644 --- a/Clover/app/src/main/AndroidManifest.xml +++ b/Clover/app/src/main/AndroidManifest.xml @@ -76,8 +76,6 @@ along with this program. If not, see . - - diff --git a/Clover/app/src/main/java/org/floens/chan/Chan.java b/Clover/app/src/main/java/org/floens/chan/Chan.java index 979e0c6b..2a9aee84 100644 --- a/Clover/app/src/main/java/org/floens/chan/Chan.java +++ b/Clover/app/src/main/java/org/floens/chan/Chan.java @@ -30,13 +30,13 @@ import com.squareup.leakcanary.RefWatcher; import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.cache.FileCache; +import org.floens.chan.core.database.DatabaseManager; +import org.floens.chan.core.http.ReplyManager; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.net.BitmapLruImageCache; -import org.floens.chan.core.http.ReplyManager; import org.floens.chan.core.net.ProxiedHurlStack; import org.floens.chan.core.settings.ChanSettings; -import org.floens.chan.database.DatabaseManager; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; @@ -51,7 +51,6 @@ public class Chan extends Application { private static final long FILE_CACHE_DISK_SIZE = 50 * 1024 * 1024; private static final String FILE_CACHE_NAME = "filecache"; - private static final int VOLLEY_LRU_CACHE_SIZE = 8 * 1024 * 1024; private static final int VOLLEY_CACHE_SIZE = 10 * 1024 * 1024; public static Context con; @@ -152,9 +151,13 @@ public class Chan extends Application { replyManager = new ReplyManager(this); - String userAgent = getUserAgent(); + String userAgent = getUserAgent(); volleyRequestQueue = Volley.newRequestQueue(this, userAgent, new ProxiedHurlStack(userAgent), new File(cacheDir, Volley.DEFAULT_CACHE_DIR), VOLLEY_CACHE_SIZE); - imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(VOLLEY_LRU_CACHE_SIZE)); + + final int runtimeMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + final int lruImageCacheSize = runtimeMemory / 8; + + imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(lruImageCacheSize)); fileCache = new FileCache(new File(cacheDir, FILE_CACHE_NAME), FILE_CACHE_DISK_SIZE, getUserAgent()); 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 5bb70a13..127dca1f 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 @@ -19,6 +19,7 @@ package org.floens.chan.chan; import android.text.TextUtils; +import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.VolleyError; @@ -39,7 +40,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class ChanLoader { +public class ChanLoader implements Response.ErrorListener, Response.Listener { private static final String TAG = "ChanLoader"; private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); @@ -47,6 +48,7 @@ public class ChanLoader { private final List listeners = new ArrayList<>(); private final Loadable loadable; + private final RequestQueue volleyRequestQueue; private ChanThread thread; private boolean destroyed = false; @@ -64,6 +66,8 @@ public class ChanLoader { if (loadable.mode == Loadable.Mode.BOARD) { loadable.mode = Loadable.Mode.CATALOG; } + + volleyRequestQueue = Chan.getVolleyRequestQueue(); } /** @@ -149,11 +153,7 @@ public class ChanLoader { public void requestMoreData() { clearTimer(); - if (loadable.isThreadMode()) { - if (request != null) { - return; - } - + if (loadable.isThreadMode() && request == null) { request = getData(); } } @@ -166,9 +166,6 @@ public class ChanLoader { requestMoreData(); } - /** - * @return Returns if this loader is currently loading - */ public boolean isLoading() { return request != null; } @@ -193,10 +190,102 @@ public class ChanLoader { return thread; } + @Override + public void onResponse(ChanReaderRequest.ChanReaderResponse response) { + request = null; + if (destroyed) + return; + + if (response.posts.size() == 0) { + onErrorResponse(new VolleyError("Post size is 0")); + return; + } + + if (thread == null) { + thread = new ChanThread(loadable, new ArrayList()); + } + + thread.posts.clear(); + thread.posts.addAll(response.posts); + + processResponse(response); + + if (TextUtils.isEmpty(loadable.title)) { + loadable.title = PostHelper.getTitle(thread.op, loadable); + } + + for (Post post : thread.posts) { + post.title = loadable.title; + } + + lastLoadTime = Time.get(); + + if (loadable.isThreadMode()) { + setTimer(response.posts.size()); + } + + for (ChanLoaderCallback l : listeners) { + l.onChanLoaderData(thread); + } + } + + @Override + public void onErrorResponse(VolleyError error) { + request = null; + if (destroyed) + return; + + Logger.i(TAG, "Loading error", error); + + clearTimer(); + + for (ChanLoaderCallback l : listeners) { + l.onChanLoaderError(error); + } + } + + /** + * Final processing af a response that needs to happen on the main thread. + * + * @param response Response to process + */ + private void processResponse(ChanReaderRequest.ChanReaderResponse response) { + if (loadable.isThreadMode() && thread.posts.size() > 0) { + // Replace some op parameters to the real op (index 0). + // This is done on the main thread to avoid race conditions. + Post realOp = thread.posts.get(0); + thread.op = realOp; + Post fakeOp = response.op; + if (fakeOp != null) { + thread.closed = realOp.closed = fakeOp.closed; + thread.archived = realOp.archived = fakeOp.archived; + realOp.sticky = fakeOp.sticky; + realOp.replies = fakeOp.replies; + realOp.images = fakeOp.images; + realOp.uniqueIps = fakeOp.uniqueIps; + } else { + Logger.e(TAG, "Thread has no op!"); + } + } + + for (Post sourcePost : thread.posts) { + sourcePost.repliesFrom.clear(); + + for (Post replyToSource : thread.posts) { + if (replyToSource != sourcePost) { + if (replyToSource.repliesTo.contains(sourcePost.no)) { + sourcePost.repliesFrom.add(replyToSource.no); + } + } + } + } + } + private void setTimer(int postCount) { clearTimer(); if (postCount > lastPostCount) { + lastPostCount = postCount; currentTimeout = 0; } else { currentTimeout++; @@ -209,8 +298,6 @@ public class ChanLoader { currentTimeout = 4; // At least 60 seconds in the background } - lastPostCount = postCount; - if (autoReload) { Runnable pendingRunnable = new Runnable() { @Override @@ -243,76 +330,13 @@ public class ChanLoader { Logger.d(TAG, "Requested " + loadable.board + ", " + loadable.no); List cached = thread == null ? new ArrayList() : thread.posts; - ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached, - new Response.Listener>() { - @Override - public void onResponse(List list) { - ChanLoader.this.request = null; - onData(list); - } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - ChanLoader.this.request = null; - onError(error); - } - } - ); + ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached, this, this); - Chan.getVolleyRequestQueue().add(request); + volleyRequestQueue.add(request); return request; } - private void onData(List result) { - if (destroyed) - return; - - if (thread == null) { - thread = new ChanThread(loadable, new ArrayList()); - } - - thread.posts.clear(); - thread.posts.addAll(result); - - if (loadable.isThreadMode() && thread.posts.size() > 0) { - thread.op = thread.posts.get(0); - thread.closed = thread.op.closed; - thread.archived = thread.op.archived; - } - - if (TextUtils.isEmpty(loadable.title)) { - loadable.title = PostHelper.getTitle(thread.op, loadable); - } - - for (Post post : thread.posts) { - post.title = loadable.title; - } - - lastLoadTime = Time.get(); - - if (loadable.isThreadMode()) { - setTimer(result.size()); - } - - for (ChanLoaderCallback l : listeners) { - l.onChanLoaderData(thread); - } - } - - private void onError(VolleyError error) { - if (destroyed) - return; - - Logger.e(TAG, "Loading error"); - - clearTimer(); - - for (ChanLoaderCallback l : listeners) { - l.onChanLoaderError(error); - } - } - public interface ChanLoaderCallback { void onChanLoaderData(ChanThread result); diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java index 397acb6d..4f13f147 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java @@ -33,6 +33,7 @@ import org.floens.chan.Chan; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.utils.Logger; @@ -60,6 +61,11 @@ public class ChanParser { private static final Pattern colorPattern = Pattern.compile("color:#([0-9a-fA-F]*)"); private static ChanParser instance = new ChanParser(); + private final DatabaseManager databaseManager; + + public ChanParser() { + databaseManager = Chan.getDatabaseManager(); + } public static ChanParser getInstance() { return instance; @@ -404,8 +410,7 @@ public class ChanParser { } // Append You when it's a reply to an saved reply - // todo synchronized - if (Chan.getDatabaseManager().isSavedReply(post.board, id)) { + if (databaseManager.isSavedReply(post.board, id)) { key += " (You)"; } } diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java index 3d72c3e0..e448c9b5 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java @@ -62,10 +62,6 @@ public class ChanUrls { return scheme + "://s.4cdn.org/image/country/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif"; } - public static String getTrollCountryFlagUrl(String countryCode) { - return scheme + "://s.4cdn.org/image/country/troll/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif"; - } - public static String getBoardsUrl() { return scheme + "://a.4cdn.org/boards.json"; } diff --git a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java similarity index 86% rename from Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java rename to Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java index 5e7d32be..6e57372d 100644 --- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.floens.chan.database; +package org.floens.chan.core.database; import android.content.Context; import android.database.sqlite.SQLiteDatabase; @@ -26,6 +26,7 @@ import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; import org.floens.chan.core.model.Board; +import org.floens.chan.core.model.History; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.SavedReply; @@ -41,13 +42,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { private static final String TAG = "DatabaseHelper"; private static final String DATABASE_NAME = "ChanDB"; - private static final int DATABASE_VERSION = 16; + private static final int DATABASE_VERSION = 18; public Dao pinDao; public Dao loadableDao; public Dao savedDao; public Dao boardsDao; public Dao threadHideDao; + public Dao historyDao; private final Context context; @@ -62,6 +64,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { savedDao = getDao(SavedReply.class); boardsDao = getDao(Board.class); threadHideDao = getDao(ThreadHide.class); + historyDao = getDao(History.class); } catch (SQLException e) { Logger.e(TAG, "Error creating Daos", e); } @@ -75,6 +78,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { TableUtils.createTable(connectionSource, SavedReply.class); TableUtils.createTable(connectionSource, Board.class); TableUtils.createTable(connectionSource, ThreadHide.class); + TableUtils.createTable(connectionSource, History.class); } catch (SQLException e) { Logger.e(TAG, "Error creating db", e); throw new RuntimeException(e); @@ -150,11 +154,27 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { if (oldVersion < 16) { try { - TableUtils.createTable(connectionSource, ThreadHide.class); + threadHideDao.executeRawNoArgs("CREATE TABLE `threadhide` (`board` VARCHAR , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `no` INTEGER );"); } catch (SQLException e) { Logger.e(TAG, "Error upgrading to version 16", e); } } + + if (oldVersion < 17) { + try { + boardsDao.executeRawNoArgs("ALTER TABLE board ADD COLUMN description TEXT;"); + } catch (SQLException e) { + Logger.e(TAG, "Error upgrading to version 17", e); + } + } + + if (oldVersion < 18) { + try { + historyDao.executeRawNoArgs("CREATE TABLE `history` (`date` BIGINT , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `loadable_id` INTEGER NOT NULL , `thumbnailUrl` VARCHAR );"); + } catch (SQLException e) { + Logger.e(TAG, "Error upgrading to version 18", e); + } + } } public void reset() { diff --git a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java similarity index 66% rename from Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java rename to Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java index f3ffc508..60149818 100644 --- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java @@ -15,14 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.floens.chan.database; +package org.floens.chan.core.database; import android.content.Context; 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.core.model.Board; +import org.floens.chan.core.model.History; +import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.SavedReply; @@ -31,9 +35,12 @@ 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.ExecutorService; +import java.util.concurrent.Executors; import static com.j256.ormlite.misc.TransactionManager.callInTransaction; @@ -44,14 +51,22 @@ public class DatabaseManager { private static final long SAVED_REPLY_TRIM_COUNT = 50; private static final long THREAD_HIDE_TRIM_TRIGGER = 250; private static final long THREAD_HIDE_TRIM_COUNT = 50; + private static final long HISTORY_TRIM_TRIGGER = 500; + private static final long HISTORY_TRIM_COUNT = 50; + + private final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); private final DatabaseHelper helper; - private List savedReplies = new ArrayList<>(); - private HashSet savedRepliesIds = new HashSet<>(); + private final Object savedRepliesLock = new Object(); + private final List savedReplies = new ArrayList<>(); + private final HashSet savedRepliesIds = new HashSet<>(); + + private final List threadHides = new ArrayList<>(); + private final HashSet threadHidesIds = new HashSet<>(); - private List threadHides = new ArrayList<>(); - private HashSet threadHidesIds = new HashSet<>(); + private final Object historyLock = new Object(); + private final HashMap historyByLoadable = new HashMap<>(); public DatabaseManager(Context context) { helper = new DatabaseHelper(context); @@ -60,6 +75,7 @@ public class DatabaseManager { /** * Save a reply to the savedreply table. + * Threadsafe. * * @param saved the {@link SavedReply} to save */ @@ -70,22 +86,27 @@ public class DatabaseManager { Logger.e(TAG, "Error saving reply", e); } - savedReplies.add(saved); - savedRepliesIds.add(saved.no); + synchronized (savedRepliesLock) { + savedReplies.add(saved); + savedRepliesIds.add(saved.no); + } } /** * Searches a saved reply. This is done through caching members, no database lookups. + * Threadsafe. * * @param board board for the reply to search * @param no no for the reply to search * @return A {@link SavedReply} that matches {@code board} and {@code no}, or {@code null} */ public SavedReply getSavedReply(String board, int no) { - if (savedRepliesIds.contains(no)) { - for (SavedReply r : savedReplies) { - if (r.no == no && r.board.equals(board)) { - return r; + synchronized (savedRepliesLock) { + if (savedRepliesIds.contains(no)) { + for (SavedReply r : savedReplies) { + if (r.no == no && r.board.equals(board)) { + return r; + } } } } @@ -95,6 +116,7 @@ public class DatabaseManager { /** * Searches if a saved reply exists. This is done through caching members, no database lookups. + * Threadsafe. * * @param board board for the reply to search * @param no no for the reply to search @@ -191,6 +213,74 @@ public class DatabaseManager { return list; } + /** + * 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; + } + /** * Create or updates these boards in the boards table. * @@ -322,17 +412,23 @@ public class DatabaseManager { private void initialize() { loadSavedReplies(); loadThreadHides(); + loadHistory(); } + /** + * Threadsafe. + */ private void loadSavedReplies() { try { trimTable(helper.savedDao, "savedreply", SAVED_REPLY_TRIM_TRIGGER, SAVED_REPLY_TRIM_COUNT); - savedReplies.clear(); - savedReplies.addAll(helper.savedDao.queryForAll()); - savedRepliesIds.clear(); - for (SavedReply reply : savedReplies) { - savedRepliesIds.add(reply.no); + synchronized (savedRepliesLock) { + savedReplies.clear(); + savedReplies.addAll(helper.savedDao.queryForAll()); + savedRepliesIds.clear(); + for (SavedReply reply : savedReplies) { + savedRepliesIds.add(reply.no); + } } } catch (SQLException e) { Logger.e(TAG, "Error loading saved replies", e); @@ -354,6 +450,48 @@ 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. * diff --git a/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java index 8d1a9e47..7ea1427f 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java @@ -18,7 +18,6 @@ package org.floens.chan.core.http; import android.content.Context; -import android.content.Intent; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; @@ -26,7 +25,6 @@ import com.squareup.okhttp.Request; import org.floens.chan.Chan; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Reply; -import org.floens.chan.ui.activity.ImagePickActivity; import java.io.File; import java.util.HashMap; @@ -40,7 +38,6 @@ public class ReplyManager { private static final int TIMEOUT = 30000; private final Context context; - private FileListener fileListener; private OkHttpClient client; private Map drafts = new HashMap<>(); @@ -77,51 +74,10 @@ public class ReplyManager { drafts.put(loadable, reply); } - /** - * Pick an file. Starts up the ImagePickActivity. - * - * @param listener FileListener to listen on. - */ - public void pickFile(FileListener listener) { - fileListener = listener; - - Intent intent = new Intent(context, ImagePickActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - public File getPickFile() { return new File(context.getCacheDir(), "picked_file"); } - public void _onFilePickLoading() { - if (fileListener != null) { - fileListener.onFilePickLoading(); - } - } - - public void _onFilePicked(String name, File file) { - if (fileListener != null) { - fileListener.onFilePicked(name, file); - fileListener = null; - } - } - - public void _onFilePickError(boolean cancelled) { - if (fileListener != null) { - fileListener.onFilePickError(cancelled); - fileListener = null; - } - } - - public interface FileListener { - void onFilePickLoading(); - - void onFilePicked(String name, File file); - - void onFilePickError(boolean cancelled); - } - public void makeHttpCall(HttpCall httpCall, HttpCallback callback) { httpCall.setCallback(callback); diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java index 4dde2ed7..3d581388 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java @@ -52,6 +52,7 @@ public class BoardManager { loadFromServer(); } + // TODO: synchronize public Board getBoardByValue(String value) { return allBoardsByValue.get(value); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Board.java b/Clover/app/src/main/java/org/floens/chan/core/model/Board.java index 2f9baa10..0e3dd652 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Board.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Board.java @@ -46,53 +46,85 @@ public class Board { */ @DatabaseField public String value; + + /** + * True if this board appears in the dropdown, false otherwise. + */ @DatabaseField public boolean saved = false; + @DatabaseField public int order; + @DatabaseField public boolean workSafe = false; + @DatabaseField public int perPage = -1; + @DatabaseField public int pages = -1; + @DatabaseField public int maxFileSize = -1; + @DatabaseField public int maxWebmSize = -1; + @DatabaseField public int maxCommentChars = -1; + @DatabaseField public int bumpLimit = -1; + @DatabaseField public int imageLimit = -1; + @DatabaseField public int cooldownThreads = -1; + @DatabaseField public int cooldownReplies = -1; + @DatabaseField public int cooldownImages = -1; + @DatabaseField public int cooldownRepliesIntra = -1; + @DatabaseField public int cooldownImagesIntra = -1; + @DatabaseField public boolean spoilers = false; + @DatabaseField public int customSpoilers = -1; + @DatabaseField public boolean userIds = false; + @DatabaseField public boolean codeTags = false; + @DatabaseField public boolean preuploadCaptcha = false; + @DatabaseField public boolean countryFlags = false; + + /** + * Not used anymore. + */ @DatabaseField public boolean trollFlags = false; + @DatabaseField public boolean mathTags = false; + @DatabaseField + public String description; + public boolean finish() { if (key == null || value == null || perPage < 0 || pages < 0) return false; 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 new file mode 100644 index 00000000..43c14710 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/model/History.java @@ -0,0 +1,36 @@ +/* + * 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.model; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +@DatabaseTable +public class History { + @DatabaseField(generatedId = true) + public int id; + + @DatabaseField(canBeNull = false, foreign = true, foreignAutoRefresh = true) + public Loadable loadable; + + @DatabaseField + public String thumbnailUrl; + + @DatabaseField + public long date; +} 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 6c2ac3e2..60331747 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 @@ -28,7 +28,7 @@ import com.j256.ormlite.table.DatabaseTable; @DatabaseTable public class Loadable { @DatabaseField(generatedId = true) - private int id; + public int id; @DatabaseField public int mode = Mode.INVALID; @@ -93,7 +93,7 @@ public class Loadable { Loadable other = (Loadable) object; - return mode == other.mode && board.equals(other.board) && no == other.no; + return no == other.no && mode == other.mode && board.equals(other.board); } @Override diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Post.java b/Clover/app/src/main/java/org/floens/chan/core/model/Post.java index b6d59a8b..5f95b9e8 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Post.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Post.java @@ -21,20 +21,25 @@ import android.text.SpannableString; import android.text.TextUtils; import org.floens.chan.Chan; -import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanParser; +import org.floens.chan.chan.ChanUrls; import org.jsoup.parser.Parser; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; /** - * Contains all data needed to represent a single post. + * Contains all data needed to represent a single post.
+ * Call {@link #finish()} to parse the comment etc. The post data is invalid if finish returns false.
+ * This class has members that are threadsafe and some that are not, see the source for more info. */ public class Post { private static final Random random = new Random(); + // *** These next members don't get changed after finish() is called. Effectively final. *** public String board; public int no = -1; public int resto = -1; @@ -46,42 +51,25 @@ public class Post { public long tim = -1; public String ext; public String filename; - public int replies = -1; public int imageWidth; public int imageHeight; public boolean hasImage = false; public String thumbnailUrl; public String imageUrl; - public boolean sticky = false; - public boolean closed = false; - public boolean archived = false; public String tripcode = ""; public String id = ""; public String capcode = ""; public String country = ""; public String countryName = ""; public long time = -1; - public boolean isSavedReply = false; - public String title = ""; public int fileSize; - public int images = -1; public String rawComment; public String countryUrl; public boolean spoiler = false; - public int uniqueIps = 1; - - public boolean deleted = false; - /** - * This post replies to the these ids + * This post replies to the these ids. Is an unmodifiable list after finish(). */ public List repliesTo = new ArrayList<>(); - - /** - * These ids replied to this post - */ - public List repliesFrom = new ArrayList<>(); - public final ArrayList linkables = new ArrayList<>(); public boolean parsedSpans = false; public SpannableString subjectSpan; @@ -91,11 +79,25 @@ public class Post { public SpannableString capcodeSpan; public CharSequence nameTripcodeIdCapcodeSpan; - public Post() { - } + // *** These next members may only change on the main thread after finish(). *** + public boolean sticky = false; + public boolean closed = false; + public boolean archived = false; + public int replies = -1; + public int images = -1; + public int uniqueIps = 1; + public String title = ""; + /** + * These ids replied to this post. Only modify this on the main thread. + */ + public List repliesFrom = new ArrayList<>(); + + // *** Threadsafe members, may be read and modified on any thread. *** + public AtomicBoolean deleted = new AtomicBoolean(false); + public AtomicBoolean isSavedReply = new AtomicBoolean(false); /** - * Finish up the data + * Finish up the data: parse the comment, check if the data is valid etc. * * @return false if this data is invalid */ @@ -129,12 +131,13 @@ public class Post { } if (!TextUtils.isEmpty(country)) { - Board b = Chan.getBoardManager().getBoardByValue(board); - countryUrl = b.trollFlags ? ChanUrls.getTrollCountryFlagUrl(country) : ChanUrls.getCountryFlagUrl(country); + countryUrl = ChanUrls.getCountryFlagUrl(country); } ChanParser.getInstance().parse(this); + repliesTo = Collections.unmodifiableList(repliesTo); + return true; } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java b/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java index 5aa060c5..665c9850 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java +++ b/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java @@ -29,7 +29,7 @@ public class BitmapLruImageCache extends LruCache implements Ima @Override protected int sizeOf(String key, Bitmap value) { - return value.getRowBytes() * value.getHeight(); + return value.getRowBytes() * value.getHeight() / 1024; } @Override diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java b/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java index 3da18890..b9c6891e 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java @@ -150,12 +150,12 @@ public class BoardsRequest extends JsonReaderRequest> { case "country_flags": board.countryFlags = reader.nextInt() == 1; break; - case "troll_flags": - board.trollFlags = reader.nextInt() == 1; - break; case "math_tags": board.mathTags = reader.nextInt() == 1; break; + case "meta_description": + board.description = reader.nextString(); + break; default: reader.skipValue(); break; diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java b/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java index f634dac5..779e0657 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java @@ -26,22 +26,23 @@ import org.floens.chan.Chan; import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; +import org.floens.chan.utils.Logger; -import java.io.IOException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; -public class ChanReaderRequest extends JsonReaderRequest> { +public class ChanReaderRequest extends JsonReaderRequest { + private static final String TAG = "ChanReaderRequest"; + private Loadable loadable; private List cached; + private Post op; - private ChanReaderRequest(String url, Listener> listener, ErrorListener errorListener) { + private ChanReaderRequest(String url, Listener listener, ErrorListener errorListener) { super(url, listener, errorListener); } - public static ChanReaderRequest newInstance(Loadable loadable, List cached, Listener> listener, ErrorListener errorListener) { + public static ChanReaderRequest newInstance(Loadable loadable, List cached, Listener listener, ErrorListener errorListener) { String url; if (loadable.isThreadMode()) { @@ -67,7 +68,7 @@ public class ChanReaderRequest extends JsonReaderRequest> { } @Override - public List readJson(JsonReader reader) throws Exception { + public ChanReaderResponse readJson(JsonReader reader) throws Exception { List list; if (loadable.isThreadMode()) { @@ -81,11 +82,14 @@ public class ChanReaderRequest extends JsonReaderRequest> { return processPosts(list); } - private List processPosts(List serverList) throws Exception { - List totalList = new ArrayList<>(serverList.size()); + private ChanReaderResponse processPosts(List serverList) throws Exception { + ChanReaderResponse response = new ChanReaderResponse(); + response.posts = new ArrayList<>(serverList.size()); + response.op = op; if (cached.size() > 0) { - totalList.addAll(cached); + // Add all posts that were parsed before + response.posts.addAll(cached); // If there's a cached post but it's not in the list received from the server, mark it as deleted if (loadable.isThreadMode()) { @@ -99,7 +103,7 @@ public class ChanReaderRequest extends JsonReaderRequest> { } } - cache.deleted = !serverHas; + cache.deleted.set(!serverHas); } } @@ -115,68 +119,19 @@ public class ChanReaderRequest extends JsonReaderRequest> { } } - // serverPost is not in finalList if (!known) { - totalList.add(post); + response.posts.add(post); } } - - // Replace OPs - if (totalList.get(0).isOP && serverList.size() > 0 && serverList.get(0).isOP) { - totalList.set(0, serverList.get(0)); - } - - // Sort if it got out of order due to posts disappearing/reappearing - /*if (loadable.isThreadMode()) { - Collections.sort(totalList, new Comparator() { - @Override - public int compare(Post lhs, Post rhs) { - return lhs.time == rhs.time ? 0 : (lhs.time < rhs.time ? -1 : 1); - } - }); - }*/ - } else { - totalList.addAll(serverList); - } - - Set postsReplyingToDeleted = new HashSet<>(); - for (Post post : totalList) { - if (!post.deleted) { - post.repliesFrom.clear(); - - for (Post other : totalList) { - if (other.repliesTo.contains(post.no) && !other.deleted) { - post.repliesFrom.add(other.no); - } - } - } else { - post.repliesTo.clear(); - - for (int no : post.repliesFrom) { - postsReplyingToDeleted.add(no); - } - - post.repliesFrom.clear(); - } + response.posts.addAll(serverList); } - for (int no : postsReplyingToDeleted) { - for (Post post : totalList) { - if (post.no == no) { - if (!post.finish()) { - throw new IOException("Incorrect data about post received."); - } - break; - } - } + for (Post post : response.posts) { + post.isSavedReply.set(Chan.getDatabaseManager().isSavedReply(post.board, post.no)); } - for (Post post : totalList) { - post.isSavedReply = Chan.getDatabaseManager().isSavedReply(post.board, post.no); - } - - return totalList; + return response; } private List loadThread(JsonReader reader) throws Exception { @@ -191,7 +146,10 @@ public class ChanReaderRequest extends JsonReaderRequest> { // Thread array while (reader.hasNext()) { // Thread object - list.add(readPostObject(reader)); + Post post = readPostObject(reader); + if (post != null) { + list.add(post); + } } reader.endArray(); } else { @@ -216,7 +174,10 @@ public class ChanReaderRequest extends JsonReaderRequest> { reader.beginArray(); // Threads array while (reader.hasNext()) { - list.add(readPostObject(reader)); + Post post = readPostObject(reader); + if (post != null) { + list.add(post); + } } reader.endArray(); @@ -324,28 +285,28 @@ public class ChanReaderRequest extends JsonReaderRequest> { break; default: // Unknown/ignored key - // log("Unknown/ignored key: " + key + "."); reader.skipValue(); break; } } reader.endObject(); + if (post.resto == 0) { + // Update OP fields later on the main thread + op = new Post(); + op.closed = post.closed; + op.archived = post.archived; + op.sticky = post.sticky; + op.replies = post.replies; + op.images = post.images; + op.uniqueIps = post.uniqueIps; + } + Post cached = null; for (Post item : this.cached) { if (item.no == post.no) { cached = item; - if (post.resto == 0) { - // Update OP fields - cached.sticky = post.sticky; - cached.closed = post.closed; - cached.archived = post.archived; - cached.replies = post.replies; - cached.images = post.images; - cached.uniqueIps = post.uniqueIps; - } - break; } } @@ -354,10 +315,18 @@ public class ChanReaderRequest extends JsonReaderRequest> { return cached; } else { if (!post.finish()) { - throw new IOException("Incorrect data about post received."); + Logger.e(TAG, "Incorrect data about post received for post " + post.no); + return null; + } else { + return post; } - - return post; } } + + public static class ChanReaderResponse { + // Op Post that is created new each time.
+ // Used to later copy members like image count to the real op on the main thread. + public Post op; + public List posts; + } } 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 6e0005a3..8b564a4f 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 @@ -32,7 +32,8 @@ 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.settings.ChanSettings; -import org.floens.chan.database.DatabaseManager; +import org.floens.chan.core.database.DatabaseManager; +import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.layout.CaptchaLayout; import java.io.File; @@ -43,7 +44,7 @@ import static org.floens.chan.utils.AndroidUtils.getReadableFileSize; import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.getString; -public class ReplyPresenter implements ReplyManager.FileListener, ReplyManager.HttpCallback, CaptchaLayout.CaptchaCallback { +public class ReplyPresenter implements ReplyManager.HttpCallback, CaptchaLayout.CaptchaCallback, ImagePickDelegate.ImagePickCallback { public enum Page { INPUT, CAPTCHA, @@ -160,7 +161,7 @@ public class ReplyPresenter implements ReplyManager.FileListener, ReplyManager.H } previewOpen = false; } else { - Chan.getReplyManager().pickFile(this); + callback.getImagePickDelegate().pick(this); pickingFile = true; } } @@ -424,5 +425,7 @@ public class ReplyPresenter implements ReplyManager.FileListener, ReplyManager.H void highlightPostNo(int no); void showThread(Loadable loadable); + + ImagePickDelegate getImagePickDelegate(); } } 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 dc07e68e..ed0e7bd5 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 @@ -25,10 +25,12 @@ import org.floens.chan.Chan; import org.floens.chan.R; import org.floens.chan.chan.ChanLoader; import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.http.DeleteHttpCall; import org.floens.chan.core.http.ReplyManager; import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.model.ChanThread; +import org.floens.chan.core.model.History; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; @@ -37,7 +39,6 @@ import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.net.LoaderPool; import org.floens.chan.core.settings.ChanSettings; -import org.floens.chan.database.DatabaseManager; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostFilter; import org.floens.chan.ui.cell.PostCellInterface; @@ -79,6 +80,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt private boolean searchOpen = false; private String searchQuery; private PostFilter.Order order = PostFilter.Order.BUMP; + private boolean historyAdded = false; + private int notificationPostCount = -1; public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { this.threadPresenterCallback = threadPresenterCallback; @@ -112,7 +115,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt LoaderPool.getInstance().release(chanLoader, this); chanLoader = null; loadable = null; - order = PostFilter.Order.BUMP; + historyAdded = false; + notificationPostCount = -1; threadPresenterCallback.showLoading(); } @@ -214,6 +218,20 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } } + if (loadable.isThreadMode()) { + int postsSize = result.posts.size(); + if (notificationPostCount < 0) { + notificationPostCount = postsSize; + } else { + if (postsSize > notificationPostCount) { + int more = postsSize - notificationPostCount; + notificationPostCount = postsSize; + + threadPresenterCallback.showNewPostsNotification(true, more); + } + } + } + chanLoader.setAutoLoadMore(isWatching()); showPosts(); @@ -225,6 +243,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } loadable.markedNo = -1; } + + addHistory(); } @Override @@ -247,6 +267,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt pin.onBottomPostViewed(); watchManager.updatePin(pin); } + + threadPresenterCallback.showNewPostsNotification(false, -1); } public void scrollTo(int position, boolean smooth) { @@ -563,6 +585,19 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt threadPresenterCallback.showPosts(chanLoader.getThread(), new PostFilter(order, searchQuery)); } + private void addHistory() { + 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.id = 0; + history.thumbnailUrl = chanLoader.getThread().op.thumbnailUrl; + databaseManager.addHistory(history); + } + } + public interface ThreadPresenterCallback { void showPosts(ChanThread thread, PostFilter filter); @@ -609,5 +644,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt void hideDeleting(String message); void hideThread(Post post); + + void showNewPostsNotification(boolean show, int more); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 0fe4b500..affd4b91 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -94,6 +94,8 @@ public class ChanSettings { public static final StringSetting passPin; public static final StringSetting passId; + public static final BooleanSetting historyEnabled; + public static final BooleanSetting proxyEnabled; public static final StringSetting proxyAddress; public static final IntegerSetting proxyPort; @@ -159,6 +161,8 @@ public class ChanSettings { passPin = new StringSetting(p, "preference_pass_pin", ""); passId = new StringSetting(p, "preference_pass_id", ""); + historyEnabled = new BooleanSetting(p, "preference_history_enabled", true); + proxyEnabled = new BooleanSetting(p, "preference_proxy_enabled", false); proxyAddress = new StringSetting(p, "preference_proxy_address", ""); proxyPort = new IntegerSetting(p, "preference_proxy_port", 80); diff --git a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java index 6df7e77c..ae354aa2 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java +++ b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java @@ -147,7 +147,7 @@ public class PinWatcher implements ChanLoader.ChanLoaderCallback { for (Post item : thread.posts) { // saved.title = pin.loadable.title; - if (item.isSavedReply) { + if (item.isSavedReply.get()) { savedReplies.add(item); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java b/Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java deleted file mode 100644 index 3cf8dfc7..00000000 --- a/Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java +++ /dev/null @@ -1,82 +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.ui; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; - -public class BadgeDrawable { - public static Drawable get(Resources resources, int id, int count, boolean red) { - BitmapFactory.Options opt = new BitmapFactory.Options(); - opt.inMutable = true; - Bitmap bitmap = BitmapFactory.decodeResource(resources, id, opt); - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - - Paint paint = new Paint(); - paint.setAntiAlias(true); - - Canvas canvas = new Canvas(bitmap); - - float badgeX = w * 0.3f; - float badgeY = h * 0.3f; - float badgeW = w * 0.6f; - float badgeH = h * 0.6f; - - RectF rect = new RectF(badgeX, badgeY, badgeX + badgeW, badgeY + badgeH); - if (red) { - paint.setColor(0xddff4444); - } else { - paint.setColor(0xaa000000); - } - canvas.drawRoundRect(rect, w * 0.1f, h * 0.1f, paint); - - String text = Integer.toString(count); - if (count > 999) { - text = "1k+"; - } - - paint.setColor(0xffffffff); - - float textHeight; - float bottomOffset; - if (text.length() <= 2) { - textHeight = badgeH * 0.8f; - bottomOffset = badgeH * 0.2f; - } else { - textHeight = badgeH * 0.5f; - bottomOffset = badgeH * 0.3f; - } - - paint.setTextSize(textHeight); - - Rect bounds = new Rect(); - paint.getTextBounds(text, 0, text.length(), bounds); - - canvas.drawText(text, badgeX + badgeW / 2f - bounds.right / 2f, badgeY + badgeH - bottomOffset, paint); - - return new BitmapDrawable(resources, bitmap); - } -} 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 592af0ce..e8834129 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,6 +37,7 @@ import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.controller.BrowseController; import org.floens.chan.ui.controller.RootNavigationController; import org.floens.chan.ui.controller.ViewThreadController; +import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.state.ChanState; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.utils.Logger; @@ -56,6 +57,8 @@ public class StartActivity extends AppCompatActivity { private RootNavigationController rootNavigationController; private BrowseController browseController; + private ImagePickDelegate imagePickDelegate; + public StartActivity() { boardManager = Chan.getBoardManager(); } @@ -66,6 +69,8 @@ public class StartActivity extends AppCompatActivity { ThemeHelper.getInstance().setupContext(this); + imagePickDelegate = new ImagePickDelegate(this); + contentView = (ViewGroup) findViewById(android.R.id.content); rootNavigationController = new RootNavigationController(this); @@ -180,6 +185,10 @@ public class StartActivity extends AppCompatActivity { return contentView; } + public ImagePickDelegate getImagePickDelegate() { + return imagePickDelegate; + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -233,6 +242,13 @@ public class StartActivity extends AppCompatActivity { Chan.getInstance().activityEnteredBackground(); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + imagePickDelegate.onActivityResult(requestCode, resultCode, data); + } + private Controller stackTop() { return stack.get(stack.size() - 1); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java index 9ebc3b17..19de204f 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import org.floens.chan.Chan; import org.floens.chan.core.model.Post; -import org.floens.chan.database.DatabaseManager; +import org.floens.chan.core.database.DatabaseManager; import java.util.ArrayList; import java.util.Collections; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java index e8c9b1d3..18c48f57 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java @@ -273,7 +273,7 @@ public class PostCell extends RelativeLayout implements PostCellInterface, PostL if (highlighted) { setBackgroundColor(theme.highlightedColor); - } else if (post.isSavedReply) { + } else if (post.isSavedReply.get()) { setBackgroundColor(theme.savedReplyColor); } else if (threadMode) { setBackgroundResource(0); @@ -320,7 +320,7 @@ public class PostCell extends RelativeLayout implements PostCellInterface, PostL iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.closedIcon, iconsTextSize); } - if (post.deleted) { + if (post.deleted.get()) { iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.trashIcon, iconsTextSize); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java index 14f03cfc..84ccbca4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java @@ -18,14 +18,20 @@ package org.floens.chan.ui.cell; import android.content.Context; +import android.graphics.Typeface; import android.os.Handler; import android.os.Message; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.StyleSpan; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; +import org.floens.chan.Chan; import org.floens.chan.R; +import org.floens.chan.core.model.Board; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Post; @@ -108,9 +114,20 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen } Post op = chanThread.op; - statusText += getContext().getString(R.string.thread_stats, op.replies, op.images, op.uniqueIps); - text.setText(statusText); + Board board = Chan.getBoardManager().getBoardByValue(chanThread.loadable.board); + if (board != null) { + SpannableString replies = new SpannableString(op.replies + "R"); + if (op.replies >= board.bumpLimit) { + replies.setSpan(new StyleSpan(Typeface.ITALIC), 0, replies.length(), 0); + } + SpannableString images = new SpannableString(op.images + "I"); + if (op.images >= board.imageLimit) { + images.setSpan(new StyleSpan(Typeface.ITALIC), 0, images.length(), 0); + } + + text.setText(TextUtils.concat(statusText, replies, " / ", images, " / ", String.valueOf(op.uniqueIps))); + } } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java index ed256894..f814ab44 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java @@ -17,6 +17,7 @@ */ package org.floens.chan.ui.controller; +import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -49,6 +50,7 @@ import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.utils.AndroidUtils; +import org.jsoup.parser.Parser; import java.util.ArrayList; import java.util.Collections; @@ -109,7 +111,7 @@ public class BoardEditController extends Controller implements SwipeListener.Cal Collections.sort(boards, new Comparator() { @Override public int compare(Board lhs, Board rhs) { - return lhs.key.compareTo(rhs.key); + return lhs.value.compareTo(rhs.value); } }); adapter.notifyDataSetChanged(); @@ -303,9 +305,10 @@ public class BoardEditController extends Controller implements SwipeListener.Cal public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + @SuppressLint("ViewHolder") TextView view = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, parent, false); Board b = filtered.get(position); - view.setText("/" + b.value + "/ " + b.key); + view.setText("/" + b.value + "/ - " + b.key); view.setOnTouchListener(new View.OnTouchListener() { @Override @@ -394,6 +397,8 @@ public class BoardEditController extends Controller implements SwipeListener.Cal BoardEditItem item = (BoardEditItem) holder; Board board = boards.get(position - 1); item.text.setText("/" + board.value + "/ " + board.key); + + item.description.setText(board.description == null ? null : Parser.unescapeEntities(board.description, false)); } } @@ -420,11 +425,13 @@ public class BoardEditController extends Controller implements SwipeListener.Cal private class BoardEditItem extends RecyclerView.ViewHolder { private ImageView image; private TextView text; + private TextView description; public BoardEditItem(View itemView) { super(itemView); image = (ImageView) itemView.findViewById(R.id.thumb); text = (TextView) itemView.findViewById(R.id.text); + description = (TextView) itemView.findViewById(R.id.description); image.setImageDrawable(new ThumbDrawable()); } } 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 new file mode 100644 index 00000000..030b9565 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java @@ -0,0 +1,244 @@ +/* + * 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.ui.controller; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SwitchCompat; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.ImageView; +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.DatabaseManager; +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.toolbar.ToolbarMenu; +import org.floens.chan.ui.toolbar.ToolbarMenuItem; +import org.floens.chan.ui.view.FloatingMenuItem; +import org.floens.chan.ui.view.ThumbnailView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.floens.chan.ui.theme.ThemeHelper.theme; +import static org.floens.chan.utils.AndroidUtils.dp; + +public class HistoryController extends Controller implements CompoundButton.OnCheckedChangeListener, ToolbarMenuItem.ToolbarMenuItemCallback, RootNavigationController.ToolbarSearchCallback { + private static final int SEARCH_ID = 1; + private static final int CLEAR_ID = 101; + + private DatabaseManager databaseManager; + private RecyclerView recyclerView; + private HistoryAdapter adapter; + + public HistoryController(Context context) { + super(context); + } + + @Override + public void onCreate() { + super.onCreate(); + + databaseManager = Chan.getDatabaseManager(); + + navigationItem.title = string(R.string.history_screen); + List items = new ArrayList<>(); + items.add(new FloatingMenuItem(CLEAR_ID, R.string.history_clear)); + navigationItem.menu = new ToolbarMenu(context); + 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 globalSwitch = new SwitchCompat(context); + globalSwitch.setChecked(ChanSettings.historyEnabled.get()); + globalSwitch.setOnCheckedChangeListener(this); + navigationItem.rightView = globalSwitch; + + recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + + adapter = new HistoryAdapter(); + recyclerView.setAdapter(adapter); + adapter.load(); + } + + @Override + public void onMenuItemClicked(ToolbarMenuItem item) { + if ((Integer) item.getId() == SEARCH_ID) { + navigationController.showSearch(); + } + } + + @Override + public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { + if ((Integer) item.getId() == CLEAR_ID) { + new AlertDialog.Builder(context) + .setTitle(R.string.history_clear_confirm) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.history_clear_confirm_button, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + databaseManager.clearHistory(); + adapter.load(); + } + }) + .show(); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ChanSettings.historyEnabled.set(isChecked); + } + + private void openThread(History history) { + ViewThreadController viewThreadController = new ViewThreadController(context); + viewThreadController.setLoadable(history.loadable); + navigationController.pushController(viewThreadController); + } + + private void deleteHistory(History history) { + databaseManager.removeHistory(history); + adapter.load(); + } + + @Override + public void onSearchVisibilityChanged(boolean visible) { + if (!visible) { + adapter.search(null); + } + } + + @Override + public void onSearchEntered(String entered) { + adapter.search(entered); + } + + private class HistoryAdapter extends RecyclerView.Adapter { + private List sourceList = new ArrayList<>(); + private List displayList = new ArrayList<>(); + private String searchQuery; + + public HistoryAdapter() { + setHasStableIds(true); + } + + @Override + public HistoryCell onCreateViewHolder(ViewGroup parent, int viewType) { + return new HistoryCell(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_history, parent, false)); + } + + @Override + public void onBindViewHolder(HistoryCell holder, int position) { + History history = displayList.get(position); + holder.thumbnail.setUrl(history.thumbnailUrl, dp(48), dp(48)); + holder.text.setText(history.loadable.title); + Board board = Chan.getBoardManager().getBoardByValue(history.loadable.board); + holder.subtext.setText(board == null ? null : ("/" + board.value + "/ - " + board.key)); + } + + @Override + public int getItemCount() { + return displayList.size(); + } + + @Override + public long getItemId(int position) { + return displayList.get(position).id; + } + + public void search(String query) { + this.searchQuery = query; + filter(); + } + + private void load() { + sourceList.clear(); + sourceList.addAll(databaseManager.getHistory()); + + filter(); + } + + private void filter() { + displayList.clear(); + if (!TextUtils.isEmpty(searchQuery)) { + String query = searchQuery.toLowerCase(Locale.ENGLISH); + for (History history : sourceList) { + if (history.loadable.title.toLowerCase(Locale.ENGLISH).contains(query)) { + displayList.add(history); + } + } + } else { + displayList.addAll(sourceList); + } + + notifyDataSetChanged(); + } + } + + private class HistoryCell extends RecyclerView.ViewHolder implements View.OnClickListener { + private ThumbnailView thumbnail; + private TextView text; + private TextView subtext; + private ImageView delete; + + public HistoryCell(View itemView) { + super(itemView); + + thumbnail = (ThumbnailView) itemView.findViewById(R.id.thumbnail); + thumbnail.setCircular(true); + text = (TextView) itemView.findViewById(R.id.text); + subtext = (TextView) itemView.findViewById(R.id.subtext); + delete = (ImageView) itemView.findViewById(R.id.delete); + + theme().clearDrawable.apply(delete); + + delete.setOnClickListener(this); + + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + int position = getAdapterPosition(); + if (position >= 0 && position < adapter.getItemCount()) { + History history = adapter.displayList.get(position); + if (v == itemView) { + openThread(history); + } else if (v == delete) { + deleteHistory(history); + } + } + + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java index d5dbea5e..8256fa14 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java @@ -178,8 +178,8 @@ public class RootNavigationController extends NavigationController implements Pi @Override public void onPinClicked(Pin pin) { Controller top = getTop(); - if (top instanceof DrawerCallbacks) { - ((DrawerCallbacks) top).onPinClicked(pin); + if (top instanceof DrawerCallback) { + ((DrawerCallback) top).onPinClicked(pin); drawerLayout.closeDrawer(Gravity.LEFT); pinAdapter.updateHighlighted(recyclerView); } @@ -187,8 +187,8 @@ public class RootNavigationController extends NavigationController implements Pi public boolean isHighlighted(Pin pin) { Controller top = getTop(); - if (top instanceof DrawerCallbacks) { - return ((DrawerCallbacks) top).isPinCurrent(pin); + if (top instanceof DrawerCallback) { + return ((DrawerCallback) top).isPinCurrent(pin); } return false; } @@ -256,6 +256,7 @@ public class RootNavigationController extends NavigationController implements Pi @Override public void openHistory() { + pushController(new HistoryController(context)); } public void onEvent(WatchManager.PinAddedMessage message) { @@ -313,24 +314,26 @@ public class RootNavigationController extends NavigationController implements Pi @Override public void onSearchVisibilityChanged(boolean visible) { Controller top = getTop(); - if (top instanceof DrawerCallbacks) { - ((DrawerCallbacks) top).onSearchVisibilityChanged(visible); + if (top instanceof ToolbarSearchCallback) { + ((ToolbarSearchCallback) top).onSearchVisibilityChanged(visible); } } @Override public void onSearchEntered(String entered) { Controller top = getTop(); - if (top instanceof DrawerCallbacks) { - ((DrawerCallbacks) top).onSearchEntered(entered); + if (top instanceof ToolbarSearchCallback) { + ((ToolbarSearchCallback) top).onSearchEntered(entered); } } - public interface DrawerCallbacks { + public interface DrawerCallback { void onPinClicked(Pin pin); boolean isPinCurrent(Pin pin); + } + public interface ToolbarSearchCallback { void onSearchVisibilityChanged(boolean visible); void onSearchEntered(String entered); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java index 60ce48eb..02869bea 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java @@ -33,7 +33,7 @@ import java.util.List; import de.greenrobot.event.EventBus; -public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback, RootNavigationController.DrawerCallbacks, SwipeRefreshLayout.OnRefreshListener { +public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback, RootNavigationController.DrawerCallback, SwipeRefreshLayout.OnRefreshListener, RootNavigationController.ToolbarSearchCallback { protected ThreadLayout threadLayout; private SwipeRefreshLayout swipeRefreshLayout; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java similarity index 57% rename from Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java rename to Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java index bdabaabe..0c994005 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java @@ -15,13 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.floens.chan.ui.activity; +package org.floens.chan.ui.helper; import android.app.Activity; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; @@ -37,45 +36,60 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -public class ImagePickActivity extends Activity implements Runnable { +import static org.floens.chan.utils.AndroidUtils.runOnUiThread; + +public class ImagePickDelegate implements Runnable { private static final String TAG = "ImagePickActivity"; - private static final int IMAGE_RESULT = 1; + private static final int IMAGE_PICK_RESULT = 2; private static final long MAX_FILE_SIZE = 15 * 1024 * 1024; + private static final String DEFAULT_FILE_NAME = "file"; private ReplyManager replyManager; + private Activity activity; + + private ImagePickCallback callback; private Uri uri; - private String fileName = "file"; + private String fileName; private boolean success = false; private File cacheFile; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public ImagePickDelegate(Activity activity) { + this.activity = activity; replyManager = Chan.getReplyManager(); + } - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - - if (intent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(intent, IMAGE_RESULT); + public boolean pick(ImagePickCallback callback) { + if (this.callback != null) { + return false; } else { - Logger.e(TAG, "No activity found to get file with"); - replyManager._onFilePickError(false); + this.callback = callback; + + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + + if (intent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(intent, IMAGE_PICK_RESULT); + return true; + } else { + Logger.e(TAG, "No activity found to get file with"); + callback.onFilePickError(false); + reset(); + return false; + } } } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { boolean ok = false; boolean cancelled = false; - if (requestCode == IMAGE_RESULT) { - if (resultCode == RESULT_OK && data != null) { + if (requestCode == IMAGE_PICK_RESULT) { + if (resultCode == Activity.RESULT_OK && data != null) { uri = data.getData(); - Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); + Cursor returnCursor = activity.getContentResolver().query(uri, null, null, null, null); if (returnCursor != null) { int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); returnCursor.moveToFirst(); @@ -86,20 +100,29 @@ public class ImagePickActivity extends Activity implements Runnable { returnCursor.close(); } - replyManager._onFilePickLoading(); + if (fileName == null) { + // As per the comment on OpenableColumns.DISPLAY_NAME: + // If this is not provided then the name should default to the last segment of the file's URI. + fileName = uri.getLastPathSegment(); + } + + if (fileName == null) { + fileName = DEFAULT_FILE_NAME; + } + + callback.onFilePickLoading(); new Thread(this).start(); ok = true; - } else if (resultCode == RESULT_CANCELED) { + } else if (resultCode == Activity.RESULT_CANCELED) { cancelled = true; } } if (!ok) { - replyManager._onFilePickError(cancelled); + callback.onFilePickError(cancelled); + reset(); } - - finish(); } @Override @@ -110,7 +133,7 @@ public class ImagePickActivity extends Activity implements Runnable { InputStream is = null; OutputStream os = null; try { - fileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); + fileDescriptor = activity.getContentResolver().openFileDescriptor(uri, "r"); is = new FileInputStream(fileDescriptor.getFileDescriptor()); os = new FileOutputStream(cacheFile); boolean fullyCopied = IOUtils.copy(is, os, MAX_FILE_SIZE); @@ -135,11 +158,28 @@ public class ImagePickActivity extends Activity implements Runnable { @Override public void run() { if (success) { - replyManager._onFilePicked(fileName, cacheFile); + callback.onFilePicked(fileName, cacheFile); } else { - replyManager._onFilePickError(false); + callback.onFilePickError(false); } + reset(); } }); } + + private void reset() { + callback = null; + cacheFile = null; + success = false; + fileName = null; + uri = null; + } + + public interface ImagePickCallback { + void onFilePickLoading(); + + void onFilePicked(String fileName, File file); + + void onFilePickError(boolean cancelled); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index ae7d61d0..211e5f14 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -38,6 +38,8 @@ import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Reply; import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.ui.helper.ImagePickDelegate; +import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.drawable.DropdownArrowDrawable; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.ui.view.LoadView; @@ -396,6 +398,11 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima callback.showThread(loadable); } + @Override + public ImagePickDelegate getImagePickDelegate() { + return ((StartActivity) getContext()).getImagePickDelegate(); + } + public interface ReplyLayoutCallback { void highlightPostNo(int no); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index b3bc6e5e..59daf19a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -46,6 +46,7 @@ import com.android.volley.VolleyError; import org.floens.chan.Chan; import org.floens.chan.R; import org.floens.chan.controller.Controller; +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; @@ -54,7 +55,6 @@ import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.ThreadHide; import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.settings.ChanSettings; -import org.floens.chan.database.DatabaseManager; import org.floens.chan.ui.adapter.PostFilter; import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.helper.PostPopupHelper; @@ -97,6 +97,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T private ProgressDialog deletingDialog; private boolean refreshedFromSwipe; private boolean showingReplyButton = false; + private Snackbar newPostsNotification; public ThreadLayout(Context context) { super(context); @@ -379,6 +380,30 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T fixSnackbarText(getContext(), snackbar); } + @Override + public void showNewPostsNotification(boolean show, int more) { + if (show) { + if (!threadListLayout.scrolledToBottom()) { + String text = getContext().getString(R.string.thread_new_posts, + more, getContext().getResources().getQuantityString(R.plurals.posts, more, more)); + + newPostsNotification = Snackbar.make(this, text, Snackbar.LENGTH_LONG); + newPostsNotification.setAction(R.string.thread_new_posts_goto, new OnClickListener() { + @Override + public void onClick(View v) { + presenter.scrollTo(-1, false); + } + }).show(); + fixSnackbarText(getContext(), newPostsNotification); + } + } else { + if (newPostsNotification != null) { + newPostsNotification.dismiss(); + newPostsNotification = null; + } + } + } + public ThumbnailView getThumbnail(PostImage postImage) { if (postPopupHelper.isOpen()) { return postPopupHelper.getThumbnail(postImage); @@ -414,6 +439,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T postPopupHelper.popAll(); showSearch(false); showReplyButton(false); + newPostsNotification = null; break; } } 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 e4c102f8..d9774254 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 @@ -248,7 +248,7 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL if (query != null) { int size = postAdapter.getDisplaySize(); searchStatus.setText(getContext().getString(R.string.search_results, - getContext().getResources().getQuantityString(R.plurals.posts, size, size), query)); + size, getContext().getResources().getQuantityString(R.plurals.posts, size, size), query)); } } @@ -274,6 +274,22 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL return true; } + public boolean scrolledToBottom() { + switch (postViewMode) { + case LIST: + if (((LinearLayoutManager) layoutManager).findLastVisibleItemPosition() == postAdapter.getItemCount() - 1) { + return true; + } + break; + case CARD: + if (((GridLayoutManager) layoutManager).findLastVisibleItemPosition() == postAdapter.getItemCount() - 1) { + return true; + } + break; + } + return false; + } + public void cleanup() { /*if (ChanBuild.DEVELOPER_MODE) { Pin pin = ChanApplication.getWatchManager().findPinByLoadable(showingThread.loadable); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java index 1b2ce8d8..2d23dc86 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java @@ -74,7 +74,7 @@ public class SettingsController extends Controller implements AndroidUtils.OnMea } private void setMargins() { - boolean tablet = view.getWidth() > dp(500); // TODO is tablet + boolean tablet = context.getResources().getBoolean(R.bool.is_tablet); int margin = 0; if (tablet) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java index 9c9d1fc6..9bf27ab1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java @@ -394,15 +394,15 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV titleContainer.removeView(subtitleView); } - if (item.menu != null) { - menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); - } - if (item.rightView != null) { item.rightView.setPadding(0, 0, dp(16), 0); menu.addView(item.rightView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); } + if (item.menu != null) { + menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + } + AndroidUtils.waitForMeasure(titleView, new AndroidUtils.OnMeasuredCallback() { @Override public boolean onMeasured(View view) { diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png new file mode 100644 index 00000000..bbfbc96c Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png new file mode 100644 index 00000000..faefc59c Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png new file mode 100644 index 00000000..bfc3e393 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png new file mode 100644 index 00000000..abbb9895 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png new file mode 100644 index 00000000..dd5adfc7 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png differ diff --git a/Clover/app/src/main/res/layout/cell_board_edit.xml b/Clover/app/src/main/res/layout/cell_board_edit.xml index 31d5b5e0..52a498c5 100644 --- a/Clover/app/src/main/res/layout/cell_board_edit.xml +++ b/Clover/app/src/main/res/layout/cell_board_edit.xml @@ -18,7 +18,7 @@ along with this program. If not, see . @@ -32,17 +32,35 @@ along with this program. If not, see . android:scaleType="center" tools:ignore="ContentDescription" /> - + android:orientation="vertical"> + + + + + + diff --git a/Clover/app/src/main/res/layout/cell_history.xml b/Clover/app/src/main/res/layout/cell_history.xml new file mode 100644 index 00000000..08c29766 --- /dev/null +++ b/Clover/app/src/main/res/layout/cell_history.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/layout/cell_pin.xml b/Clover/app/src/main/res/layout/cell_pin.xml index 257cb680..304d71cb 100644 --- a/Clover/app/src/main/res/layout/cell_pin.xml +++ b/Clover/app/src/main/res/layout/cell_pin.xml @@ -18,7 +18,7 @@ along with this program. If not, see . + diff --git a/Clover/app/src/main/res/layout/setting_description.xml b/Clover/app/src/main/res/layout/setting_description.xml index 0d1d0ebf..820bc445 100644 --- a/Clover/app/src/main/res/layout/setting_description.xml +++ b/Clover/app/src/main/res/layout/setting_description.xml @@ -21,11 +21,8 @@ along with this program. If not, see . android:id="@+id/top" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="end" - android:maxLines="1" android:paddingLeft="16dp" android:paddingRight="16dp" - android:singleLine="true" android:textColor="?setting_description_top" android:textSize="16sp" /> @@ -33,11 +30,8 @@ along with this program. If not, see . android:id="@+id/bottom" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="end" - android:maxLines="1" android:paddingLeft="16dp" android:paddingRight="16dp" - android:singleLine="true" android:textColor="?setting_description_bottom" android:textSize="14sp" /> diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 517910ea..cedfdbd7 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -33,8 +33,8 @@ along with this program. If not, see . - %d post - %d posts + post + posts @@ -71,7 +71,7 @@ along with this program. If not, see . Oldest Search - Found %1$s for "%2$s" + Found %1$d %2$s for "%3$s" Search subjects, comments, names and filenames Open link? @@ -103,7 +103,8 @@ along with this program. If not, see . Retry Archived Closed - %1$sR / %2$sI / %3$sP + %1$d new %2$s + View Board editor Add, remove and reorder your boards here.\nThe topmost board will be loaded automatically. @@ -115,6 +116,10 @@ along with this program. If not, see . The board with code %1$s is not known. Sort A-Z + Clear history + Clear history? + Clear + Board Catalog Watching threads diff --git a/docs/database.txt b/docs/database.txt index f65791f4..cf5d2edc 100644 --- a/docs/database.txt +++ b/docs/database.txt @@ -41,5 +41,14 @@ ALTER TABLE pin ADD COLUMN order INTEGER; Changes is version 15: ALTER TABLE pin ADD COLUMN archived INTEGER; + Changes in version 16: -Table ThreadHide added +CREATE TABLE `threadhide` (`board` VARCHAR , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `no` INTEGER ) + + +Changes is version 17: +ALTER TABLE board ADD COLUMN description TEXT; + + +Changes in version 18: +CREATE TABLE `history` (`date` BIGINT , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `loadable_id` INTEGER NOT NULL , `thumbnailUrl` VARCHAR )