From 2afc6a66d6ec58113f3a6dde0c0f3ce7d6faa9d5 Mon Sep 17 00:00:00 2001 From: Floens Date: Sat, 27 Jun 2015 15:18:06 +0200 Subject: [PATCH 01/16] Add board descriptions --- .../java/org/floens/chan/chan/ChanUrls.java | 4 -- .../org/floens/chan/core/model/Board.java | 32 ++++++++++++++++ .../java/org/floens/chan/core/model/Post.java | 3 +- .../floens/chan/core/net/BoardsRequest.java | 6 +-- .../floens/chan/database/DatabaseHelper.java | 10 ++++- .../ui/controller/BoardEditController.java | 11 +++++- .../src/main/res/layout/cell_board_edit.xml | 38 ++++++++++++++----- docs/database.txt | 3 ++ 8 files changed, 85 insertions(+), 22 deletions(-) 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/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/Post.java b/Clover/app/src/main/java/org/floens/chan/core/model/Post.java index b6d59a8b..5c976980 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 @@ -129,8 +129,7 @@ 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); 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/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java index 5e7d32be..b8461d50 100644 --- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java @@ -41,7 +41,7 @@ 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 = 17; public Dao pinDao; public Dao loadableDao; @@ -155,6 +155,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { 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); + } + } } public void reset() { 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..83b05990 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(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/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/docs/database.txt b/docs/database.txt index f65791f4..cc76422b 100644 --- a/docs/database.txt +++ b/docs/database.txt @@ -43,3 +43,6 @@ ALTER TABLE pin ADD COLUMN archived INTEGER; Changes in version 16: Table ThreadHide added + +Changes is version 17: +ALTER TABLE board ADD COLUMN description TEXT; From 624c5bcc01603757bb5044d0a4ad9291cda4f491 Mon Sep 17 00:00:00 2001 From: Floens Date: Sat, 27 Jun 2015 15:19:45 +0200 Subject: [PATCH 02/16] Fix possible crash --- .../java/org/floens/chan/ui/controller/BoardEditController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 83b05990..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 @@ -398,7 +398,7 @@ public class BoardEditController extends Controller implements SwipeListener.Cal Board board = boards.get(position - 1); item.text.setText("/" + board.value + "/ " + board.key); - item.description.setText(Parser.unescapeEntities(board.description, false)); + item.description.setText(board.description == null ? null : Parser.unescapeEntities(board.description, false)); } } From d2f924763aaf9df7173cb93014d8ea884a047626 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 00:50:05 +0200 Subject: [PATCH 03/16] Temp fix for coordinatorlayout not working when proguard is turned on --- Clover/app/proguard.cfg | 2 ++ 1 file changed, 2 insertions(+) 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.** From 78efeafb6425956bfb98a7a54134b0e5d9b4e5d7 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 11:46:35 +0200 Subject: [PATCH 04/16] Improvements for concurrency Marked some Post members for when they may be modified. Closed, archived, sticky etc. is now only modified on the main thread. BoardManager.getBoardByValue still gets called from multiple threads, next thing to improve. --- .../java/org/floens/chan/chan/ChanLoader.java | 176 ++++++++++-------- .../java/org/floens/chan/chan/ChanParser.java | 9 +- .../chan/core/manager/BoardManager.java | 1 + .../java/org/floens/chan/core/model/Post.java | 50 ++--- .../chan/core/net/ChanReaderRequest.java | 131 +++++-------- .../floens/chan/core/watch/PinWatcher.java | 2 +- .../floens/chan/database/DatabaseManager.java | 43 +++-- .../org/floens/chan/ui/cell/PostCell.java | 4 +- 8 files changed, 216 insertions(+), 200 deletions(-) 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..c1d81de4 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.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/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/Post.java b/Clover/app/src/main/java/org/floens/chan/core/model/Post.java index 5c976980..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 */ @@ -134,6 +136,8 @@ public class Post { ChanParser.getInstance().parse(this); + repliesTo = Collections.unmodifiableList(repliesTo); + return true; } } 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/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/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java index f3ffc508..3475ddf8 100644 --- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java @@ -47,11 +47,12 @@ public class DatabaseManager { 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 List threadHides = new ArrayList<>(); - private HashSet threadHidesIds = new HashSet<>(); + private final List threadHides = new ArrayList<>(); + private final HashSet threadHidesIds = new HashSet<>(); public DatabaseManager(Context context) { helper = new DatabaseHelper(context); @@ -60,6 +61,7 @@ public class DatabaseManager { /** * Save a reply to the savedreply table. + * Threadsafe. * * @param saved the {@link SavedReply} to save */ @@ -70,22 +72,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 +102,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 @@ -324,15 +332,20 @@ public class DatabaseManager { loadThreadHides(); } + /** + * 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); 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); } From 681cf9a8d3b33d0b24dc84ad2ceb5f696e9af953 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 11:48:26 +0200 Subject: [PATCH 05/16] Slant the replies/image count if it is over the limit. --- .../floens/chan/ui/cell/ThreadStatusCell.java | 21 +++++++++++++++++-- Clover/app/src/main/res/values/strings.xml | 1 - 2 files changed, 19 insertions(+), 3 deletions(-) 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/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 6d8fcc99..a738d8f8 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -103,7 +103,6 @@ along with this program. If not, see . Retry Archived Closed - %1$sR / %2$sI / %3$sP Board editor Add, remove and reorder your boards here.\nThe topmost board will be loaded automatically. From 72b016764a54220bff4134d8dcdf3f66a870b9e3 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 11:50:19 +0200 Subject: [PATCH 06/16] Don't reset order on unbind, keep the order between switching boards --- .../java/org/floens/chan/core/presenter/ThreadPresenter.java | 1 - 1 file changed, 1 deletion(-) 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..dc979d80 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 @@ -112,7 +112,6 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt LoaderPool.getInstance().release(chanLoader, this); chanLoader = null; loadable = null; - order = PostFilter.Order.BUMP; threadPresenterCallback.showLoading(); } From 097425a0bacd363a614b75e7da5b6a1210f5577a Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 12:40:25 +0200 Subject: [PATCH 07/16] Pick files from StartActivity Also add another way to find the filename. --- Clover/app/src/main/AndroidManifest.xml | 2 - .../floens/chan/core/http/ReplyManager.java | 44 --------- .../chan/core/presenter/ReplyPresenter.java | 7 +- .../chan/ui/activity/StartActivity.java | 16 ++++ .../ImagePickDelegate.java} | 96 +++++++++++++------ .../floens/chan/ui/layout/ReplyLayout.java | 7 ++ 6 files changed, 96 insertions(+), 76 deletions(-) rename Clover/app/src/main/java/org/floens/chan/ui/{activity/ImagePickActivity.java => helper/ImagePickDelegate.java} (57%) 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/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/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 6e0005a3..8bd5106d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -33,6 +33,7 @@ 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.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/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/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); From 419f42a596f5bc59b3cfb4c39dc8eaf5ef81f9f7 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 12:43:11 +0200 Subject: [PATCH 08/16] Check is_tablet bool for tablet/mobile layout in SettingsController --- .../java/org/floens/chan/ui/settings/SettingsController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3b525b2f..f8a55926 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 @@ -71,7 +71,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) { From 3ee49f7c2e904a19fd7f48f4a67a1a8085320587 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 12:45:38 +0200 Subject: [PATCH 09/16] Do not singleline title and description for setting views --- Clover/app/src/main/res/layout/setting_description.xml | 6 ------ 1 file changed, 6 deletions(-) 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" /> From dd22474ca8f8ca346282ba3a20afc5c2a7246380 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 13:02:56 +0200 Subject: [PATCH 10/16] Delete BadgeDrawable, unused --- .../org/floens/chan/ui/BadgeDrawable.java | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java 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); - } -} From 39567f1ba799aa1707a09824b6ecbbfdb97002fe Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 13:11:41 +0200 Subject: [PATCH 11/16] Add reminder for altering the hide table --- .../src/main/java/org/floens/chan/database/DatabaseHelper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java index b8461d50..f152436c 100644 --- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java @@ -150,6 +150,8 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { if (oldVersion < 16) { try { + // WARNING: change this to a sql query when the columns need to be altered later on! + // Otherwise it could create a table with the columns already added, and an add column query could error out. TableUtils.createTable(connectionSource, ThreadHide.class); } catch (SQLException e) { Logger.e(TAG, "Error upgrading to version 16", e); From 19e7066f82f84d2d5a1050313ab5e75c3c52afd5 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 28 Jun 2015 13:12:32 +0200 Subject: [PATCH 12/16] Move the database package to the core package --- Clover/app/src/main/java/org/floens/chan/Chan.java | 2 +- Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java | 2 +- .../org/floens/chan/{ => core}/database/DatabaseHelper.java | 2 +- .../org/floens/chan/{ => core}/database/DatabaseManager.java | 2 +- .../java/org/floens/chan/core/presenter/ReplyPresenter.java | 2 +- .../java/org/floens/chan/core/presenter/ThreadPresenter.java | 2 +- .../src/main/java/org/floens/chan/ui/adapter/PostFilter.java | 2 +- .../src/main/java/org/floens/chan/ui/layout/ThreadLayout.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename Clover/app/src/main/java/org/floens/chan/{ => core}/database/DatabaseHelper.java (99%) rename Clover/app/src/main/java/org/floens/chan/{ => core}/database/DatabaseManager.java (99%) 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 40a43f4b..b7146dc4 100644 --- a/Clover/app/src/main/java/org/floens/chan/Chan.java +++ b/Clover/app/src/main/java/org/floens/chan/Chan.java @@ -35,7 +35,7 @@ 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.settings.ChanSettings; -import org.floens.chan.database.DatabaseManager; +import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; 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 c1d81de4..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,7 +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.database.DatabaseManager; +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; 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 99% 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 f152436c..7a916fa0 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; 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 99% 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 3475ddf8..7a8b9f7b 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,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; 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 8bd5106d..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,7 @@ 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; 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 dc979d80..3b4cd8d7 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 @@ -37,7 +37,7 @@ 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.core.database.DatabaseManager; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostFilter; import org.floens.chan.ui.cell.PostCellInterface; 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/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index b3bc6e5e..f8e1b2b7 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 @@ -54,7 +54,7 @@ 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.core.database.DatabaseManager; import org.floens.chan.ui.adapter.PostFilter; import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.helper.PostPopupHelper; From 3b250b82b6aa47571f847f784f014e853604e560 Mon Sep 17 00:00:00 2001 From: Floens Date: Mon, 29 Jun 2015 19:41:24 +0200 Subject: [PATCH 13/16] Add history --- .../chan/core/database/DatabaseHelper.java | 18 +- .../chan/core/database/DatabaseManager.java | 125 +++++++++ .../org/floens/chan/core/model/History.java | 36 +++ .../org/floens/chan/core/model/Loadable.java | 2 +- .../chan/core/presenter/ThreadPresenter.java | 17 +- .../chan/core/settings/ChanSettings.java | 4 + .../chan/ui/controller/HistoryController.java | 244 ++++++++++++++++++ .../controller/RootNavigationController.java | 21 +- .../chan/ui/controller/ThreadController.java | 2 +- .../org/floens/chan/ui/toolbar/Toolbar.java | 8 +- .../drawable-hdpi/ic_search_white_24dp.png | Bin 0 -> 396 bytes .../drawable-mdpi/ic_search_white_24dp.png | Bin 0 -> 247 bytes .../drawable-xhdpi/ic_search_white_24dp.png | Bin 0 -> 465 bytes .../drawable-xxhdpi/ic_search_white_24dp.png | Bin 0 -> 728 bytes .../drawable-xxxhdpi/ic_search_white_24dp.png | Bin 0 -> 915 bytes .../app/src/main/res/layout/cell_history.xml | 92 +++++++ Clover/app/src/main/res/layout/cell_pin.xml | 2 +- .../main/res/layout/controller_history.xml | 26 ++ Clover/app/src/main/res/values/strings.xml | 4 + docs/database.txt | 8 +- 20 files changed, 587 insertions(+), 22 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/model/History.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java create mode 100644 Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png create mode 100644 Clover/app/src/main/res/layout/cell_history.xml create mode 100644 Clover/app/src/main/res/layout/controller_history.xml diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java index 7a916fa0..6e57372d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java @@ -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 = 17; + 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,9 +154,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { if (oldVersion < 16) { try { - // WARNING: change this to a sql query when the columns need to be altered later on! - // Otherwise it could create a table with the columns already added, and an add column query could error out. - 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); } @@ -165,6 +167,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { 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/core/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java index 7a8b9f7b..60149818 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java @@ -20,9 +20,13 @@ 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,6 +51,10 @@ 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; @@ -54,6 +65,9 @@ public class DatabaseManager { private final List threadHides = new ArrayList<>(); private final HashSet threadHidesIds = new HashSet<>(); + private final Object historyLock = new Object(); + private final HashMap historyByLoadable = new HashMap<>(); + public DatabaseManager(Context context) { helper = new DatabaseHelper(context); initialize(); @@ -199,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. * @@ -330,6 +412,7 @@ public class DatabaseManager { private void initialize() { loadSavedReplies(); loadThreadHides(); + loadHistory(); } /** @@ -367,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/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..ca2ccdb1 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 @@ -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/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java index 3b4cd8d7..5ccf6aa5 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.core.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,7 @@ 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; public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { this.threadPresenterCallback = threadPresenterCallback; @@ -112,6 +114,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt LoaderPool.getInstance().release(chanLoader, this); chanLoader = null; loadable = null; + historyAdded = false; threadPresenterCallback.showLoading(); } @@ -224,6 +227,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } loadable.markedNo = -1; } + + addHistory(); } @Override @@ -562,6 +567,16 @@ 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(); + history.loadable = loadable; + history.thumbnailUrl = chanLoader.getThread().op.thumbnailUrl; + databaseManager.addHistory(history); + } + } + public interface ThreadPresenterCallback { void showPosts(ChanThread thread, PostFilter filter); 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 4f6ae8b4..a85ab6d1 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; + static { SharedPreferences p = AndroidUtils.getPreferences(); @@ -155,6 +157,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); + // Old (but possibly still in some users phone) // preference_board_view_mode default "list" // preference_board_editor_filler default false 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/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 0000000000000000000000000000000000000000..bbfbc96cbce0dc0bf2a0b5ac4fa924dab5e34919 GIT binary patch literal 396 zcmV;70dxL|P)MlVO6$ z;_nvIa)$r8oBh5veEc|b;H00d8XJzB8#}e9C&kTuJTUVGfHxC^|AqQ$xx5-GhxGjg zEIhh0QF{S=I4p;1z^yImyK9a$ptVy59RMztM>fsDtPGlg+w5@HC)kgn4PRK;>X_bu z>liu$k6lAU@DxMm;HncEfHsE4VA2Woz#@iDLEQ;$gC>Udz>Bqx=?R#|P|q7U?)b51 zFp8mM0vel{;U#$YTMYT-70m4}o#qm7R{GY4KES6NLjyhAPCNtV)=Qz}P%ED~n5xR4 zWXH^M-kn(oJmjgYZr`;ht%b(a==+MNfO}N{1?^|3m7t{Z0Ju|yP(a=1sbqEPk1B)$ q>JBiDp_nQ^Q9zA@J6g3rP~3lFwlD09o&!k$0000O-uP3I_MSD8CL_;(V?AVEarj0ldk$~bl6y3&Eqek6=6UA+aBGp8+4bCOt0yJyz3VGY| z^>9EE59hXLP{{L0Lqkciz%eblTs;71sDpKM$pE;wf;`qWNA#EmA z3}Zav^tfQ1X+{`jh7B(9+LToU=QvL+>I0VO;G8NRYdGH+S07Zk#aU4-eqf#$*8o)L zVD1%%1x$}ceS&42s$y`AxljOCm_x;&g;`eso0uELpo^JS0JE4E#URIwD1Z{?O)C5yhj~>DnwSj*u!U(V21l4n1#pdNCjP>$mkQ{TM zA}DjpU_DNl!r50btZ+|1chvL?-kyr0${{zr@Wwl@G-*)PA2jglN`V8sx>8^tZ&xX> zhqt2?sN-!b1?qU2QeYP^Qwr?hWJ-f=e#?{&8JUKL{zpFn4bJ?-H56sJ00000NkvXX Hu0mjfDd@)s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..abbb9895108b56aedde011bdde5eb8ddaff78838 GIT binary patch literal 728 zcmV;}0w?{6P)18zk+dXf)UDAjLvsN|p{t6$05L%?Ktw?Z^gaZCQV^>Zx>oT5B!UZD>LPSm z|L~VCszm}4VSKu%ZO@sc>73z9GKG1U!+E|uoS*N^M5!=g!h{LqKXj94ff8p_aHw!f ziFtB#wL|6=o7_=9cWf}!N|_gY;@|neKr3V(v542)QzX`0ojyKemomN0(|N`@_Bkcf ztl6p6`NkqSGQ^1xCqtekuIebI<#is{EY>;WMC*)uR$b$Z z(g)oNPHrpSfn4QL-e6kR;0(D@Nb>GoKwwK4j`B+2oi$`B^GkO6(b* zLPzpSF^b0NX%vSUqI4L~4#ky}qMuz4Iy=7(y6gF2Q~ohxkTt4*QK_=7cV&)SqNEg^ z3^T_TC*0t0%L&`eGUC2MdD=21&+2^~xs(oqsFZHE(qX9Np|QG^_S>&?P@~inD!G)> z|AW$(_ABiNO30vw6u_#jj0000< KMNUMnLSTZewp6SD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd5adfc7f990333743a5f3b34f09098c1377a4a4 GIT binary patch literal 915 zcmV;E18n?>P)jEh5hl3oKJ*oh`Ol zr^+oB7$T?k#zD^UoR4|N90j!>Iyk`!J9UraYA5vbfM(sNPwjv%=Bd+)Iv42D?jpzU zgz<>1b`M8+O;{V0v^)5kO`=faE5(naY|_YI%ri)VZaV3tn*xJeVwFZ}e5rVm<25b; zS2&~wILuW7TsFumK77U>xZGf`K7kxJad}8aabceA-f~(y#CO!#?!4kbKXq&`=v5Oa z@)BE}gYu6K9$9%}f0n;T#fJID;@`WKxs|t+{9%CxY z7joDRD=+fxQFdqwTLs= z?#MTeW2;eA3+U$^wqx>-``BKRQl7eyEDoP5*aH}XO7{#Tc + + + + + + + + + + + + + + + + + + + + + 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/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index a738d8f8..aeba43b1 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -114,6 +114,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 cc76422b..cf5d2edc 100644 --- a/docs/database.txt +++ b/docs/database.txt @@ -41,8 +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 ) From fec551bc555b6eaa5d98d22e3ec552b47479bb97 Mon Sep 17 00:00:00 2001 From: Floens Date: Mon, 29 Jun 2015 19:49:58 +0200 Subject: [PATCH 14/16] Set the volley lru cache size to 1/8 of the available memory --- Clover/app/src/main/java/org/floens/chan/Chan.java | 11 +++++++---- .../org/floens/chan/core/net/BitmapLruImageCache.java | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) 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 b7146dc4..5d0c3c95 100644 --- a/Clover/app/src/main/java/org/floens/chan/Chan.java +++ b/Clover/app/src/main/java/org/floens/chan/Chan.java @@ -30,12 +30,12 @@ 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.settings.ChanSettings; -import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; @@ -50,7 +50,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,7 +151,11 @@ public class Chan extends Application { replyManager = new ReplyManager(this); volleyRequestQueue = Volley.newRequestQueue(this, getUserAgent(), null, 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/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 From 17e747b2fc3fbcce793f379df48ffc5d6989df84 Mon Sep 17 00:00:00 2001 From: Floens Date: Mon, 29 Jun 2015 19:55:53 +0200 Subject: [PATCH 15/16] 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. --- .../src/main/java/org/floens/chan/core/model/Loadable.java | 2 +- .../java/org/floens/chan/core/presenter/ThreadPresenter.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 ca2ccdb1..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; 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 5ccf6aa5..208afae3 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 @@ -571,7 +571,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt if (!historyAdded && ChanSettings.historyEnabled.get() && loadable.isThreadMode()) { historyAdded = true; History history = new History(); - history.loadable = loadable; + // 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); } From 17705278ab213a13b55256b810e1e6a06497afa8 Mon Sep 17 00:00:00 2001 From: Floens Date: Mon, 29 Jun 2015 20:50:49 +0200 Subject: [PATCH 16/16] Add "n new posts" snackbar --- .../chan/core/presenter/ThreadPresenter.java | 20 +++++++++++++ .../floens/chan/ui/layout/ThreadLayout.java | 28 ++++++++++++++++++- .../chan/ui/layout/ThreadListLayout.java | 18 +++++++++++- Clover/app/src/main/res/values/strings.xml | 8 ++++-- 4 files changed, 69 insertions(+), 5 deletions(-) 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 208afae3..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 @@ -81,6 +81,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt 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; @@ -115,6 +116,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt chanLoader = null; loadable = null; historyAdded = false; + notificationPostCount = -1; threadPresenterCallback.showLoading(); } @@ -216,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(); @@ -251,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) { @@ -626,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/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index f8e1b2b7..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.core.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/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index aeba43b1..59870d99 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,6 +103,8 @@ along with this program. If not, see . Retry Archived Closed + %1$d new %2$s + View Board editor Add, remove and reorder your boards here.\nThe topmost board will be loaded automatically.