Add generic login/out support.

Use getters and setters for the mutable fields of Post.
Use HttpUrl for all endpoints.
multisite
Floens 9 years ago
parent f8c182bba5
commit 9f462aa416
  1. 16
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java
  2. 12
      Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java
  3. 4
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  4. 1
      Clover/app/src/main/java/org/floens/chan/core/model/Board.java
  5. 118
      Clover/app/src/main/java/org/floens/chan/core/model/Post.java
  6. 14
      Clover/app/src/main/java/org/floens/chan/core/model/PostImage.java
  7. 2
      Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java
  8. 10
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  9. 5
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  10. 2
      Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
  11. 28
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  12. 18
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  13. 22
      Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java
  14. 31
      Clover/app/src/main/java/org/floens/chan/core/site/SiteRequestModifier.java
  15. 7
      Clover/app/src/main/java/org/floens/chan/core/site/Sites.java
  16. 5
      Clover/app/src/main/java/org/floens/chan/core/site/http/DeleteRequest.java
  17. 7
      Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java
  18. 6
      Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCallManager.java
  19. 6
      Clover/app/src/main/java/org/floens/chan/core/site/http/LoginRequest.java
  20. 4
      Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java
  21. 209
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  22. 2
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4BoardsRequest.java
  23. 6
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4DeleteHttpCall.java
  24. 6
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4PassHttpCall.java
  25. 10
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4ReaderRequest.java
  26. 10
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4ReplyHttpCall.java
  27. 2
      Clover/app/src/main/java/org/floens/chan/test/TestActivity.java
  28. 4
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostsFilter.java
  29. 2
      Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java
  30. 17
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
  31. 11
      Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java
  32. 10
      Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
  33. 5
      Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
  34. 28
      Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java
  35. 24
      Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java
  36. 6
      Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java
  37. 8
      Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
  38. 2
      Clover/app/src/main/java/org/floens/chan/ui/view/PostImageThumbnailView.java
  39. 4
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java

@ -265,7 +265,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
} }
for (Post post : thread.posts) { for (Post post : thread.posts) {
post.title = loadable.title; post.setTitle(loadable.title);
} }
lastLoadTime = Time.get(); lastLoadTime = Time.get();
@ -296,12 +296,14 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
thread.op = realOp; thread.op = realOp;
Post.Builder fakeOp = response.op; Post.Builder fakeOp = response.op;
if (fakeOp != null) { if (fakeOp != null) {
thread.closed = realOp.closed = fakeOp.closed; realOp.setClosed(fakeOp.closed);
thread.archived = realOp.archived = fakeOp.archived; thread.closed = realOp.isClosed();
realOp.sticky = fakeOp.sticky; realOp.setArchived(fakeOp.archived);
realOp.replies = fakeOp.replies; thread.archived = realOp.isArchived();
realOp.images = fakeOp.images; realOp.setSticky(fakeOp.sticky);
realOp.uniqueIps = fakeOp.uniqueIps; realOp.setReplies(fakeOp.replies);
realOp.setImages(fakeOp.images);
realOp.setUniqueIps(fakeOp.uniqueIps);
} else { } else {
Logger.e(TAG, "Thread has no op!"); Logger.e(TAG, "Thread has no op!");
} }

@ -40,18 +40,6 @@ public class ChanUrls {
return scheme() + "://boards.4chan.org/" + board + "/catalog"; return scheme() + "://boards.4chan.org/" + board + "/catalog";
} }
public static String getPassUrl() {
return "https://sys.4chan.org/auth";
}
public static String[] getReportCookies(String passId) {
return new String[]{"pass_enabled=1;", "pass_id=" + passId + ";"};
}
public static String getReportUrl(String board, int no) {
return "https://sys.4chan.org/" + board + "/imgboard.php?mode=report&no=" + no;
}
private static String scheme() { private static String scheme() {
return ChanSettings.networkHttps.get() ? "https" : "http"; return ChanSettings.networkHttps.get() ? "https" : "http";
} }

@ -162,7 +162,7 @@ public class WatchManager {
pin.loadable = loadable; pin.loadable = loadable;
pin.loadable.title = PostHelper.getTitle(opPost, loadable); pin.loadable.title = PostHelper.getTitle(opPost, loadable);
PostImage image = opPost.image; PostImage image = opPost.image;
pin.thumbnailUrl = image == null ? "" : image.thumbnailUrl; pin.thumbnailUrl = image == null ? "" : image.thumbnailUrl.toString();
return createPin(pin); return createPin(pin);
} }
@ -746,7 +746,7 @@ public class WatchManager {
pin.isError = false; pin.isError = false;
if (pin.thumbnailUrl == null && thread.op != null && thread.op.image != null) { if (pin.thumbnailUrl == null && thread.op != null && thread.op.image != null) {
pin.thumbnailUrl = thread.op.image.thumbnailUrl; pin.thumbnailUrl = thread.op.image.thumbnailUrl.toString();
} }
// Populate posts list // Populate posts list

@ -67,6 +67,7 @@ public class Board implements SiteReference {
// named value for legacy support // named value for legacy support
@DatabaseField(columnName = "value") @DatabaseField(columnName = "value")
// TODO(sec) force filter this to ascii & numbers.
public String code; public String code;
@DatabaseField @DatabaseField

@ -17,6 +17,8 @@
*/ */
package org.floens.chan.core.model; package org.floens.chan.core.model;
import android.support.annotation.MainThread;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -24,6 +26,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import okhttp3.HttpUrl;
/** /**
* Contains all data needed to represent a single post.<br> * Contains all data needed to represent a single post.<br>
* All {@code final} fields are thread-safe. * All {@code final} fields are thread-safe.
@ -45,6 +49,11 @@ public class Post {
public final String subject; public final String subject;
/**
* Unix timestamp, in seconds.
*/
public final long time;
public final PostImage image; public final PostImage image;
public final String tripcode; public final String tripcode;
@ -57,12 +66,7 @@ public class Post {
public final String countryName; public final String countryName;
/** public final HttpUrl countryUrl;
* Unix timestamp, in seconds.
*/
public final long time;
public final String countryUrl;
public final boolean isSavedReply; public final boolean isSavedReply;
@ -83,24 +87,28 @@ public class Post {
public final CharSequence nameTripcodeIdCapcodeSpan; public final CharSequence nameTripcodeIdCapcodeSpan;
// These members may only mutate on the main thread. /**
public boolean sticky = false; * This post has been deleted (the server isn't sending it anymore).
public boolean closed = false; * <p><b>This boolean is modified in worker threads, use {@code .get()} to access it.</b>
public boolean archived = false; */
public int replies = -1;
public int images = -1;
public int uniqueIps = 1;
public String title = "";
// Atomic, any thread.
public final AtomicBoolean deleted = new AtomicBoolean(false); public final AtomicBoolean deleted = new AtomicBoolean(false);
/** /**
* These ids replied to this post.<br> * These ids replied to this post.
* <b>synchronize on this when accessing.</b> * <p><b>Manual synchronization is needed, since this list can be modified from any thread.
* Wrap all accesses in a {@code synchronized} block.</b>
*/ */
public final List<Integer> repliesFrom = new ArrayList<>(); public final List<Integer> repliesFrom = new ArrayList<>();
// These members may only mutate on the main thread.
private boolean sticky = false;
private boolean closed = false;
private boolean archived = false;
private int replies = -1;
private int images = -1;
private int uniqueIps = 1;
private String title = "";
private Post(Builder builder) { private Post(Builder builder) {
board = builder.board; board = builder.board;
boardId = builder.board.code; boardId = builder.board.code;
@ -142,6 +150,76 @@ public class Post {
repliesTo = Collections.unmodifiableSet(builder.repliesToIds); repliesTo = Collections.unmodifiableSet(builder.repliesToIds);
} }
@MainThread
public boolean isSticky() {
return sticky;
}
@MainThread
public void setSticky(boolean sticky) {
this.sticky = sticky;
}
@MainThread
public boolean isClosed() {
return closed;
}
@MainThread
public void setClosed(boolean closed) {
this.closed = closed;
}
@MainThread
public boolean isArchived() {
return archived;
}
@MainThread
public void setArchived(boolean archived) {
this.archived = archived;
}
@MainThread
public int getReplies() {
return replies;
}
@MainThread
public void setReplies(int replies) {
this.replies = replies;
}
@MainThread
public int getImages() {
return images;
}
@MainThread
public void setImages(int images) {
this.images = images;
}
@MainThread
public int getUniqueIps() {
return uniqueIps;
}
@MainThread
public void setUniqueIps(int uniqueIps) {
this.uniqueIps = uniqueIps;
}
@MainThread
public String getTitle() {
return title;
}
@MainThread
public void setTitle(String title) {
this.title = title;
}
public static final class Builder { public static final class Builder {
public Board board; public Board board;
public int id = -1; public int id = -1;
@ -165,7 +243,7 @@ public class Post {
public String countryCode; public String countryCode;
public String countryName; public String countryName;
public String countryUrl; public HttpUrl countryUrl;
public String posterId = ""; public String posterId = "";
public String moderatorCapcode = ""; public String moderatorCapcode = "";
@ -275,7 +353,7 @@ public class Post {
return this; return this;
} }
public Builder country(String countryCode, String countryName, String countryUrl) { public Builder country(String countryCode, String countryName, HttpUrl countryUrl) {
this.countryCode = countryCode; this.countryCode = countryCode;
this.countryName = countryName; this.countryName = countryName;
this.countryUrl = countryUrl; this.countryUrl = countryUrl;

@ -19,14 +19,16 @@ package org.floens.chan.core.model;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import okhttp3.HttpUrl;
public class PostImage { public class PostImage {
public enum Type { public enum Type {
STATIC, GIF, MOVIE STATIC, GIF, MOVIE
} }
public final String originalName; public final String originalName;
public final String thumbnailUrl; public final HttpUrl thumbnailUrl;
public final String imageUrl; public final HttpUrl imageUrl;
public final String filename; public final String filename;
public final String extension; public final String extension;
public final int imageWidth; public final int imageWidth;
@ -62,8 +64,8 @@ public class PostImage {
public static final class Builder { public static final class Builder {
private String originalName; private String originalName;
private String thumbnailUrl; private HttpUrl thumbnailUrl;
private String imageUrl; private HttpUrl imageUrl;
private String filename; private String filename;
private String extension; private String extension;
private int imageWidth; private int imageWidth;
@ -79,12 +81,12 @@ public class PostImage {
return this; return this;
} }
public Builder thumbnailUrl(String thumbnailUrl) { public Builder thumbnailUrl(HttpUrl thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl; this.thumbnailUrl = thumbnailUrl;
return this; return this;
} }
public Builder imageUrl(String imageUrl) { public Builder imageUrl(HttpUrl imageUrl) {
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
return this; return this;
} }

@ -271,7 +271,7 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
private boolean imageAutoLoad(PostImage postImage) { private boolean imageAutoLoad(PostImage postImage) {
// Auto load the image when it is cached // Auto load the image when it is cached
return fileCache.exists(postImage.imageUrl) || shouldLoadForNetworkType(ChanSettings.imageAutoLoadNetwork.get()); return fileCache.exists(postImage.imageUrl.toString()) || shouldLoadForNetworkType(ChanSettings.imageAutoLoadNetwork.get());
} }
private boolean videoAutoLoad(PostImage postImage) { private boolean videoAutoLoad(PostImage postImage) {

@ -197,18 +197,10 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
callback.loadViewsIntoDraft(draft); callback.loadViewsIntoDraft(draft);
draft.loadable = loadable; draft.loadable = loadable;
if (ChanSettings.passLoggedIn()) {
draft.noVerification = true;
draft.passId = ChanSettings.passId.get();
} else {
draft.noVerification = false;
draft.passId = null;
}
draft.spoilerImage = draft.spoilerImage && board.spoilers; draft.spoilerImage = draft.spoilerImage && board.spoilers;
draft.captchaResponse = null; draft.captchaResponse = null;
if (draft.noVerification) { if (loadable.getSite().isLoggedIn()) {
makeSubmitCall(); makeSubmitCall();
} else { } else {
switchPage(Page.CAPTCHA, true); switchPage(Page.CAPTCHA, true);

@ -612,7 +612,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.boardId, post.no) databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.boardId, post.no)
); );
if (reply != null) { if (reply != null) {
loadable.getSite().delete(new DeleteRequest(loadable.getSite(), post, reply, onlyImageDelete), new Site.DeleteListener() { Site site = loadable.getSite();
site.delete(new DeleteRequest(post, reply, onlyImageDelete), new Site.DeleteListener() {
@Override @Override
public void onDeleteComplete(HttpCall httpPost, DeleteResponse deleteResponse) { public void onDeleteComplete(HttpCall httpPost, DeleteResponse deleteResponse) {
String message; String message;
@ -698,7 +699,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
History history = new History(); History history = new History();
history.loadable = loadable; history.loadable = loadable;
PostImage image = chanLoader.getThread().op.image; PostImage image = chanLoader.getThread().op.image;
history.thumbnailUrl = image == null ? "" : image.thumbnailUrl; history.thumbnailUrl = image == null ? "" : image.thumbnailUrl.toString();
databaseManager.getDatabaseHistoryManager().add(history); databaseManager.getDatabaseHistoryManager().add(history);
} }
} }

@ -118,7 +118,7 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
if (destination.exists()) { if (destination.exists()) {
onDestination(); onDestination();
} else { } else {
FileCache.FileCacheDownloader fileCacheDownloader = fileCache.downloadFile(postImage.imageUrl, this); FileCache.FileCacheDownloader fileCacheDownloader = fileCache.downloadFile(postImage.imageUrl.toString(), this);
// If the fileCacheDownloader is null then the destination already existed and onSuccess() has been called. // If the fileCacheDownloader is null then the destination already existed and onSuccess() has been called.
// Wait otherwise for the download to finish to avoid that the next task is immediately executed. // Wait otherwise for the download to finish to avoid that the next task is immediately executed.
if (fileCacheDownloader != null) { if (fileCacheDownloader != null) {

@ -141,10 +141,6 @@ public class ChanSettings {
public static final BooleanSetting watchPeek; public static final BooleanSetting watchPeek;
public static final StringSetting watchLed; public static final StringSetting watchLed;
public static final StringSetting passToken;
public static final StringSetting passPin;
public static final StringSetting passId;
public static final BooleanSetting historyEnabled; public static final BooleanSetting historyEnabled;
public static final IntegerSetting previousVersion; public static final IntegerSetting previousVersion;
@ -158,22 +154,6 @@ public class ChanSettings {
public static final CounterSetting replyOpenCounter; public static final CounterSetting replyOpenCounter;
public static final CounterSetting threadOpenCounter; public static final CounterSetting threadOpenCounter;
public enum TestOptions implements OptionSettingItem {
ONE("one"),
TWO("two"),
THREE("three");
String name;
TestOptions(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
static { static {
SharedPreferences p = AndroidUtils.getPreferences(); SharedPreferences p = AndroidUtils.getPreferences();
@ -251,10 +231,6 @@ public class ChanSettings {
watchPeek = new BooleanSetting(p, "preference_watch_peek", true); watchPeek = new BooleanSetting(p, "preference_watch_peek", true);
watchLed = new StringSetting(p, "preference_watch_led", "ffffffff"); watchLed = new StringSetting(p, "preference_watch_led", "ffffffff");
passToken = new StringSetting(p, "preference_pass_token", "");
passPin = new StringSetting(p, "preference_pass_pin", "");
passId = new StringSetting(p, "preference_pass_id", "");
historyEnabled = new BooleanSetting(p, "preference_history_enabled", true); historyEnabled = new BooleanSetting(p, "preference_history_enabled", true);
previousVersion = new IntegerSetting(p, "preference_previous_version", 0); previousVersion = new IntegerSetting(p, "preference_previous_version", 0);
@ -295,10 +271,6 @@ public class ChanSettings {
// preference_watch_background_timeout "60" the old timeout background setting in minutes // preference_watch_background_timeout "60" the old timeout background setting in minutes
} }
public static boolean passLoggedIn() {
return passId.get().length() > 0;
}
public static ThemeColor getThemeAndColor() { public static ThemeColor getThemeAndColor() {
String themeRaw = ChanSettings.theme.get(); String themeRaw = ChanSettings.theme.get();

@ -17,6 +17,8 @@
*/ */
package org.floens.chan.core.site; package org.floens.chan.core.site;
import android.support.annotation.Nullable;
import org.floens.chan.chan.ChanLoaderRequest; import org.floens.chan.chan.ChanLoaderRequest;
import org.floens.chan.chan.ChanLoaderRequestParams; import org.floens.chan.chan.ChanLoaderRequestParams;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
@ -56,7 +58,7 @@ public interface Site {
POST_REPORT, POST_REPORT,
/** /**
* This site supports some sort of login (like 4pass). * This site supports some sort of authentication (like 4pass).
* *
* @see #login(LoginRequest, LoginListener) * @see #login(LoginRequest, LoginListener)
* @see SiteEndpoints#login() * @see SiteEndpoints#login()
@ -113,8 +115,12 @@ public interface Site {
SiteEndpoints endpoints(); SiteEndpoints endpoints();
SiteRequestModifier requestModifier();
BoardsType boardsType(); BoardsType boardsType();
String desktopUrl(Loadable loadable, @Nullable Post post);
void boards(BoardsListener boardsListener); void boards(BoardsListener boardsListener);
interface BoardsListener { interface BoardsListener {
@ -141,8 +147,18 @@ public interface Site {
void onDeleteError(HttpCall httpCall); void onDeleteError(HttpCall httpCall);
} }
/* TODO(multi-site) this login mechanism is probably not generic enough right now,
* especially if we're thinking about what a login really is
* We'll expand this later when we have a better idea of what other sites require.
*/
void login(LoginRequest loginRequest, LoginListener loginListener); void login(LoginRequest loginRequest, LoginListener loginListener);
void logout();
boolean isLoggedIn();
LoginRequest getLoginDetails();
interface LoginListener { interface LoginListener {
void onLoginComplete(HttpCall httpCall, LoginResponse loginResponse); void onLoginComplete(HttpCall httpCall, LoginResponse loginResponse);

@ -23,27 +23,29 @@ import org.floens.chan.core.model.Post;
import java.util.Map; import java.util.Map;
import okhttp3.HttpUrl;
/** /**
* Endpoints for {@link Site}. * Endpoints for {@link Site}.
*/ */
public interface SiteEndpoints { public interface SiteEndpoints {
String catalog(Board board); HttpUrl catalog(Board board);
String thread(Board board, Loadable loadable); HttpUrl thread(Board board, Loadable loadable);
String imageUrl(Post.Builder post, Map<String, String> arg); HttpUrl imageUrl(Post.Builder post, Map<String, String> arg);
String thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg); HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg);
String flag(Post.Builder post, String countryCode, Map<String, String> arg); HttpUrl flag(Post.Builder post, String countryCode, Map<String, String> arg);
String boards(); HttpUrl boards();
String reply(Loadable thread); HttpUrl reply(Loadable thread);
String delete(Post post); HttpUrl delete(Post post);
String report(Post post); HttpUrl report(Post post);
String login(); HttpUrl login();
} }

@ -0,0 +1,31 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site;
import android.webkit.WebView;
import org.floens.chan.core.site.http.HttpCall;
import okhttp3.Request;
public interface SiteRequestModifier {
void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder);
void modifyWebView(WebView webView);
}

@ -1,6 +1,9 @@
package org.floens.chan.core.site; package org.floens.chan.core.site;
import android.content.SharedPreferences;
import org.floens.chan.core.site.sites.chan4.Chan4; import org.floens.chan.core.site.sites.chan4.Chan4;
import org.floens.chan.utils.AndroidUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -34,4 +37,8 @@ public class Sites {
public static Site defaultSite() { public static Site defaultSite() {
return CHAN4; return CHAN4;
} }
public static SharedPreferences getPreferences(Site site) {
return AndroidUtils.getPreferences(site.id() + "_site_preferences");
}
} }

@ -20,16 +20,13 @@ package org.floens.chan.core.site.http;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.site.Site;
public class DeleteRequest { public class DeleteRequest {
public final Site site;
public final Post post; public final Post post;
public final SavedReply savedReply; public final SavedReply savedReply;
public final boolean imageOnly; public final boolean imageOnly;
public DeleteRequest(Site site, Post post, SavedReply savedReply, boolean imageOnly) { public DeleteRequest(Post post, SavedReply savedReply, boolean imageOnly) {
this.site = site;
this.post = post; this.post = post;
this.savedReply = savedReply; this.savedReply = savedReply;
this.imageOnly = imageOnly; this.imageOnly = imageOnly;

@ -17,6 +17,7 @@
*/ */
package org.floens.chan.core.site.http; package org.floens.chan.core.site.http;
import org.floens.chan.core.site.Site;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -38,6 +39,8 @@ import okhttp3.Response;
public abstract class HttpCall implements Callback { public abstract class HttpCall implements Callback {
private static final String TAG = "HttpCall"; private static final String TAG = "HttpCall";
protected Site site;
private boolean successful = false; private boolean successful = false;
private HttpCallback callback; private HttpCallback callback;
private Exception exception; private Exception exception;
@ -46,6 +49,10 @@ public abstract class HttpCall implements Callback {
public abstract void process(Response response, String result) throws IOException; public abstract void process(Response response, String result) throws IOException;
public HttpCall(Site site) {
this.site = site;
}
@Override @Override
public void onResponse(Call call, Response response) { public void onResponse(Call call, Response response) {
try { try {

@ -19,6 +19,7 @@ package org.floens.chan.core.site.http;
import org.floens.chan.core.di.UserAgentProvider; import org.floens.chan.core.di.UserAgentProvider;
import org.floens.chan.core.site.Site;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -53,8 +54,13 @@ public class HttpCallManager {
Request.Builder requestBuilder = new Request.Builder(); Request.Builder requestBuilder = new Request.Builder();
Site site = httpCall.site;
httpCall.setup(requestBuilder); httpCall.setup(requestBuilder);
if (site != null) {
site.requestModifier().modifyHttpCall(httpCall, requestBuilder);
}
requestBuilder.header("User-Agent", userAgentProvider.getUserAgent()); requestBuilder.header("User-Agent", userAgentProvider.getUserAgent());
Request request = requestBuilder.build(); Request request = requestBuilder.build();

@ -18,15 +18,11 @@
package org.floens.chan.core.site.http; package org.floens.chan.core.site.http;
import org.floens.chan.core.site.Site;
public class LoginRequest { public class LoginRequest {
public final Site site;
public final String user; public final String user;
public final String pass; public final String pass;
public LoginRequest(Site site, String user, String pass) { public LoginRequest(String user, String pass) {
this.site = site;
this.user = user; this.user = user;
this.pass = pass; this.pass = pass;
} }

@ -35,12 +35,8 @@ public class Reply {
*/ */
public String captchaResponse; public String captchaResponse;
// TODO(multi-site) flip boolean
public boolean noVerification = false;
public Loadable loadable; public Loadable loadable;
public String passId;
public File file; public File file;
public String fileName = ""; public String fileName = "";
public String name = ""; public String name = "";

@ -17,6 +17,11 @@
*/ */
package org.floens.chan.core.site.sites.chan4; package org.floens.chan.core.site.sites.chan4;
import android.content.SharedPreferences;
import android.support.annotation.Nullable;
import android.webkit.CookieManager;
import android.webkit.WebView;
import com.android.volley.RequestQueue; import com.android.volley.RequestQueue;
import com.android.volley.Response; import com.android.volley.Response;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
@ -26,14 +31,18 @@ import org.floens.chan.chan.ChanLoaderRequestParams;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.settings.StringSetting;
import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager; import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
@ -45,6 +54,9 @@ import java.util.Random;
import javax.inject.Inject; import javax.inject.Inject;
import okhttp3.HttpUrl;
import okhttp3.Request;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
public class Chan4 implements Site { public class Chan4 implements Site {
@ -59,68 +71,171 @@ public class Chan4 implements Site {
RequestQueue requestQueue; RequestQueue requestQueue;
private final SiteEndpoints endpoints = new SiteEndpoints() { private final SiteEndpoints endpoints = new SiteEndpoints() {
private final HttpUrl a = new HttpUrl.Builder()
.scheme("https")
.host("a.4cdn.org")
.build();
private final HttpUrl i = new HttpUrl.Builder()
.scheme("https")
.host("i.4cdn.org")
.build();
private final HttpUrl t = new HttpUrl.Builder()
.scheme("https")
.host("t.4cdn.org")
.build();
private final HttpUrl s = new HttpUrl.Builder()
.scheme("https")
.host("s.4cdn.org")
.build();
private final HttpUrl sys = new HttpUrl.Builder()
.scheme("https")
.host("sys.4chan.org")
.build();
@Override @Override
public String catalog(Board board) { public HttpUrl catalog(Board board) {
return "https://a.4cdn.org/" + board.code + "/catalog.json"; return a.newBuilder()
.addPathSegment(board.code)
.addPathSegment("catalog.json")
.build();
} }
@Override @Override
public String thread(Board board, Loadable loadable) { public HttpUrl thread(Board board, Loadable loadable) {
return "https://a.4cdn.org/" + board.code + "/thread/" + loadable.no + ".json"; return a.newBuilder()
.addPathSegment(board.code)
.addPathSegment("thread")
.addPathSegment(loadable.no + ".json")
.build();
} }
@Override @Override
public String imageUrl(Post.Builder post, Map<String, String> arg) { public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) {
return "https://i.4cdn.org/" + post.board.code + "/" + arg.get("tim") + "." + arg.get("ext"); return i.newBuilder()
.addPathSegment(post.board.code)
.addPathSegment(arg.get("tim") + "." + arg.get("ext"))
.build();
} }
@Override @Override
public String thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) { public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
if (spoiler) { if (spoiler) {
HttpUrl.Builder image = s.newBuilder()
.addPathSegment("image");
if (post.board.customSpoilers >= 0) { if (post.board.customSpoilers >= 0) {
int i = random.nextInt(post.board.customSpoilers) + 1; int i = random.nextInt(post.board.customSpoilers) + 1;
return "https://s.4cdn.org/image/spoiler-" + post.board.code + i + ".png"; image.addPathSegment("spoiler-" + post.board.code + i + ".png");
} else { } else {
return "https://s.4cdn.org/image/spoiler.png"; image.addPathSegment("spoiler.png");
} }
return image.build();
} else { } else {
return "https://t.4cdn.org/" + post.board.code + "/" + arg.get("tim") + "s.jpg"; return t.newBuilder()
.addPathSegment(post.board.code)
.addPathSegment(arg.get("tim") + "s.jpg")
.build();
} }
} }
@Override @Override
public String flag(Post.Builder post, String countryCode, Map<String, String> arg) { public HttpUrl flag(Post.Builder post, String countryCode, Map<String, String> arg) {
return "https://s.4cdn.org/image/country/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif"; return s.newBuilder()
.addPathSegment("image")
.addPathSegment("country")
.addPathSegment(countryCode.toLowerCase(Locale.ENGLISH) + ".gif")
.build();
} }
@Override @Override
public String boards() { public HttpUrl boards() {
return "https://a.4cdn.org/boards.json"; return a.newBuilder()
.addPathSegment("boards.json")
.build();
} }
@Override @Override
public String reply(Loadable loadable) { public HttpUrl reply(Loadable loadable) {
return "https://sys.4chan.org/" + loadable.getBoard().code + "/post"; return sys.newBuilder()
.addPathSegment(loadable.board.code)
.addPathSegment("post")
.build();
} }
@Override @Override
public String delete(Post post) { public HttpUrl delete(Post post) {
return "https://sys.4chan.org/" + post.board.code + "/imgboard.php"; return sys.newBuilder()
.addPathSegment(post.board.code)
.addPathSegment("imgboard.php")
.build();
} }
@Override @Override
public String report(Post post) { public HttpUrl report(Post post) {
return "https://sys.4chan.org/" + post.board.code + "/imgboard.php?mode=report&no=" + post.no; return sys.newBuilder()
.addPathSegment(post.board.code)
.addPathSegment("imgboard.php")
.addQueryParameter("mode", "report")
.addQueryParameter("no", String.valueOf(post.no))
.build();
} }
@Override @Override
public String login() { public HttpUrl login() {
return "https://sys.4chan.org/auth"; return sys.newBuilder()
.addPathSegment("auth")
.build();
} }
}; };
private SiteRequestModifier siteRequestModifier = new SiteRequestModifier() {
@Override
public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) {
if (isLoggedIn()) {
requestBuilder.addHeader("Cookie", "pass_id=" + passToken.get());
}
}
@SuppressWarnings("deprecation")
@Override
public void modifyWebView(WebView webView) {
final HttpUrl sys = new HttpUrl.Builder()
.scheme("https")
.host("sys.4chan.org")
.build();
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
if (isLoggedIn()) {
String[] passCookies = {
"pass_enabled=1;",
"pass_id=" + passToken.get() + ";"
};
String domain = sys.scheme() + "://" + sys.host() + "/";
for (String cookie : passCookies) {
cookieManager.setCookie(domain, cookie);
}
}
}
};
// Legacy settings that were global before
private final StringSetting passUser;
private final StringSetting passPass;
private final StringSetting passToken;
public Chan4() { public Chan4() {
getGraph().inject(this); getGraph().inject(this);
SharedPreferences p = AndroidUtils.getPreferences();
passUser = new StringSetting(p, "preference_pass_token", "");
passPass = new StringSetting(p, "preference_pass_pin", "");
// token was renamed, before it meant the username, now it means the token returned
// from the server that the cookie is set to.
passToken = new StringSetting(p, "preference_pass_id", "");
} }
/** /**
@ -160,6 +275,21 @@ public class Chan4 implements Site {
return BoardsType.DYNAMIC; return BoardsType.DYNAMIC;
} }
@Override
public String desktopUrl(Loadable loadable, @Nullable Post post) {
if (loadable.isCatalogMode()) {
return "https://boards.4chan.org/" + loadable.board.code + "/";
} else if (loadable.isThreadMode()) {
String url = "https://boards.4chan.org/" + loadable.board.code + "/thread/" + loadable.no;
if (post != null) {
url += "#p" + post.no;
}
return url;
} else {
throw new IllegalArgumentException();
}
}
@Override @Override
public boolean boardFeature(BoardFeature boardFeature, Board board) { public boolean boardFeature(BoardFeature boardFeature, Board board) {
switch (boardFeature) { switch (boardFeature) {
@ -191,6 +321,11 @@ public class Chan4 implements Site {
return endpoints; return endpoints;
} }
@Override
public SiteRequestModifier requestModifier() {
return siteRequestModifier;
}
@Override @Override
public void boards(final BoardsListener listener) { public void boards(final BoardsListener listener) {
requestQueue.add(new Chan4BoardsRequest(this, new Response.Listener<List<Board>>() { requestQueue.add(new Chan4BoardsRequest(this, new Response.Listener<List<Board>>() {
@ -222,7 +357,7 @@ public class Chan4 implements Site {
@Override @Override
public void post(Reply reply, final PostListener postListener) { public void post(Reply reply, final PostListener postListener) {
httpCallManager.makeHttpCall(new Chan4ReplyHttpCall(reply), new HttpCall.HttpCallback<Chan4ReplyHttpCall>() { httpCallManager.makeHttpCall(new Chan4ReplyHttpCall(this, reply), new HttpCall.HttpCallback<Chan4ReplyHttpCall>() {
@Override @Override
public void onHttpSuccess(Chan4ReplyHttpCall httpPost) { public void onHttpSuccess(Chan4ReplyHttpCall httpPost) {
postListener.onPostComplete(httpPost, httpPost.replyResponse); postListener.onPostComplete(httpPost, httpPost.replyResponse);
@ -237,7 +372,7 @@ public class Chan4 implements Site {
@Override @Override
public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) { public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) {
httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() { httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(this, deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() {
@Override @Override
public void onHttpSuccess(Chan4DeleteHttpCall httpPost) { public void onHttpSuccess(Chan4DeleteHttpCall httpPost) {
deleteListener.onDeleteComplete(httpPost, httpPost.deleteResponse); deleteListener.onDeleteComplete(httpPost, httpPost.deleteResponse);
@ -252,10 +387,17 @@ public class Chan4 implements Site {
@Override @Override
public void login(LoginRequest loginRequest, final LoginListener loginListener) { public void login(LoginRequest loginRequest, final LoginListener loginListener) {
httpCallManager.makeHttpCall(new Chan4PassHttpCall(loginRequest), new HttpCall.HttpCallback<Chan4PassHttpCall>() { passUser.set(loginRequest.user);
passPass.set(loginRequest.pass);
httpCallManager.makeHttpCall(new Chan4PassHttpCall(this, loginRequest), new HttpCall.HttpCallback<Chan4PassHttpCall>() {
@Override @Override
public void onHttpSuccess(Chan4PassHttpCall httpCall) { public void onHttpSuccess(Chan4PassHttpCall httpCall) {
loginListener.onLoginComplete(httpCall, httpCall.loginResponse); LoginResponse loginResponse = httpCall.loginResponse;
if (loginResponse.success) {
passToken.set(loginResponse.token);
}
loginListener.onLoginComplete(httpCall, loginResponse);
} }
@Override @Override
@ -264,4 +406,19 @@ public class Chan4 implements Site {
} }
}); });
} }
@Override
public void logout() {
passToken.set("");
}
@Override
public boolean isLoggedIn() {
return !passToken.get().isEmpty();
}
@Override
public LoginRequest getLoginDetails() {
return new LoginRequest(passUser.get(), passPass.get());
}
} }

@ -43,7 +43,7 @@ public class Chan4BoardsRequest extends JsonReaderRequest<List<Board>> {
private final Site site; private final Site site;
public Chan4BoardsRequest(Site site, Listener<List<Board>> listener, ErrorListener errorListener) { public Chan4BoardsRequest(Site site, Listener<List<Board>> listener, ErrorListener errorListener) {
super(site.endpoints().boards(), listener, errorListener); super(site.endpoints().boards().toString(), listener, errorListener);
this.site = site; this.site = site;
} }

@ -17,6 +17,7 @@
*/ */
package org.floens.chan.core.site.sites.chan4; package org.floens.chan.core.site.sites.chan4;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse; import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
@ -36,7 +37,8 @@ public class Chan4DeleteHttpCall extends HttpCall {
private final DeleteRequest deleteRequest; private final DeleteRequest deleteRequest;
public final DeleteResponse deleteResponse = new DeleteResponse(); public final DeleteResponse deleteResponse = new DeleteResponse();
public Chan4DeleteHttpCall(DeleteRequest deleteRequest) { public Chan4DeleteHttpCall(Site site, DeleteRequest deleteRequest) {
super(site);
this.deleteRequest = deleteRequest; this.deleteRequest = deleteRequest;
} }
@ -50,7 +52,7 @@ public class Chan4DeleteHttpCall extends HttpCall {
formBuilder.add("mode", "usrdel"); formBuilder.add("mode", "usrdel");
formBuilder.add("pwd", deleteRequest.savedReply.password); formBuilder.add("pwd", deleteRequest.savedReply.password);
requestBuilder.url(deleteRequest.site.endpoints().delete(deleteRequest.post)); requestBuilder.url(site.endpoints().delete(deleteRequest.post));
requestBuilder.post(formBuilder.build()); requestBuilder.post(formBuilder.build());
} }

@ -17,6 +17,7 @@
*/ */
package org.floens.chan.core.site.sites.chan4; package org.floens.chan.core.site.sites.chan4;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse; import org.floens.chan.core.site.http.LoginResponse;
@ -33,7 +34,8 @@ public class Chan4PassHttpCall extends HttpCall {
private final LoginRequest loginRequest; private final LoginRequest loginRequest;
public final LoginResponse loginResponse = new LoginResponse(); public final LoginResponse loginResponse = new LoginResponse();
public Chan4PassHttpCall(LoginRequest loginRequest) { public Chan4PassHttpCall(Site site, LoginRequest loginRequest) {
super(site);
this.loginRequest = loginRequest; this.loginRequest = loginRequest;
} }
@ -46,7 +48,7 @@ public class Chan4PassHttpCall extends HttpCall {
formBuilder.add("id", loginRequest.user); formBuilder.add("id", loginRequest.user);
formBuilder.add("pin", loginRequest.pass); formBuilder.add("pin", loginRequest.pass);
requestBuilder.url(loginRequest.site.endpoints().login()); requestBuilder.url(site.endpoints().login());
requestBuilder.post(formBuilder.build()); requestBuilder.post(formBuilder.build());
} }

@ -47,6 +47,8 @@ import java.util.concurrent.Future;
import javax.inject.Inject; import javax.inject.Inject;
import okhttp3.HttpUrl;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
/** /**
@ -84,7 +86,7 @@ public class Chan4ReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
private long startLoad; private long startLoad;
public Chan4ReaderRequest(ChanLoaderRequestParams request) { public Chan4ReaderRequest(ChanLoaderRequestParams request) {
super(getChanUrl(request.loadable), request.listener, request.errorListener); super(getChanUrl(request.loadable).toString(), request.listener, request.errorListener);
getGraph().inject(this); getGraph().inject(this);
// Copy the loadable and cached list. The cached array may changed/cleared by other threads. // Copy the loadable and cached list. The cached array may changed/cleared by other threads.
@ -116,8 +118,8 @@ public class Chan4ReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager(); databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager();
} }
private static String getChanUrl(Loadable loadable) { private static HttpUrl getChanUrl(Loadable loadable) {
String url; HttpUrl url;
if (loadable.site == null) { if (loadable.site == null) {
throw new NullPointerException("Loadable.site == null"); throw new NullPointerException("Loadable.site == null");
@ -497,7 +499,7 @@ public class Chan4ReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
} }
if (countryCode != null && countryName != null) { if (countryCode != null && countryName != null) {
String countryUrl = endpoints.flag(builder, countryCode, Collections.<String, String>emptyMap()); HttpUrl countryUrl = endpoints.flag(builder, countryCode, Collections.<String, String>emptyMap());
builder.country(countryCode, countryName, countryUrl); builder.country(countryCode, countryName, countryUrl);
} }

@ -46,7 +46,8 @@ public class Chan4ReplyHttpCall extends HttpCall {
public final Reply reply; public final Reply reply;
public final ReplyResponse replyResponse = new ReplyResponse(); public final ReplyResponse replyResponse = new ReplyResponse();
public Chan4ReplyHttpCall(Reply reply) { public Chan4ReplyHttpCall(Site site, Reply reply) {
super(site);
this.reply = reply; this.reply = reply;
} }
@ -75,7 +76,7 @@ public class Chan4ReplyHttpCall extends HttpCall {
formBuilder.addFormDataPart("com", reply.comment); formBuilder.addFormDataPart("com", reply.comment);
if (!reply.noVerification) { if (reply.captchaResponse != null) {
if (reply.captchaChallenge != null) { if (reply.captchaChallenge != null) {
formBuilder.addFormDataPart("recaptcha_challenge_field", reply.captchaChallenge); formBuilder.addFormDataPart("recaptcha_challenge_field", reply.captchaChallenge);
formBuilder.addFormDataPart("recaptcha_response_field", reply.captchaResponse); formBuilder.addFormDataPart("recaptcha_response_field", reply.captchaResponse);
@ -94,13 +95,8 @@ public class Chan4ReplyHttpCall extends HttpCall {
formBuilder.addFormDataPart("spoiler", "on"); formBuilder.addFormDataPart("spoiler", "on");
} }
Site site = reply.loadable.getSite();
requestBuilder.url(site.endpoints().reply(reply.loadable)); requestBuilder.url(site.endpoints().reply(reply.loadable));
requestBuilder.post(formBuilder.build()); requestBuilder.post(formBuilder.build());
if (reply.noVerification) {
requestBuilder.addHeader("Cookie", "pass_id=" + reply.passId);
}
} }
@Override @Override

@ -208,7 +208,7 @@ public class TestActivity extends Activity implements View.OnClickListener {
public void onChanLoaderData(ChanThread result) { public void onChanLoaderData(ChanThread result) {
for (Post post : result.posts) { for (Post post : result.posts) {
if (post.image != null) { if (post.image != null) {
final String imageUrl = post.image.imageUrl; final String imageUrl = post.image.imageUrl.toString();
fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() { fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onProgress(long downloaded, long total, boolean done) {

@ -37,14 +37,14 @@ public class PostsFilter {
public static final Comparator<Post> IMAGE_COMPARATOR = new Comparator<Post>() { public static final Comparator<Post> IMAGE_COMPARATOR = new Comparator<Post>() {
@Override @Override
public int compare(Post lhs, Post rhs) { public int compare(Post lhs, Post rhs) {
return rhs.images - lhs.images; return rhs.getImages() - lhs.getImages();
} }
}; };
public static final Comparator<Post> REPLY_COMPARATOR = new Comparator<Post>() { public static final Comparator<Post> REPLY_COMPARATOR = new Comparator<Post>() {
@Override @Override
public int compare(Post lhs, Post rhs) { public int compare(Post lhs, Post rhs) {
return rhs.replies - lhs.replies; return rhs.getReplies() - lhs.getReplies();
} }
}; };

@ -217,7 +217,7 @@ public class CardPostCell extends CardView implements PostCellInterface, View.On
comment.setText(commentText); comment.setText(commentText);
comment.setTextColor(theme.textPrimary); comment.setTextColor(theme.textPrimary);
replies.setText(getResources().getString(R.string.card_stats, post.replies, post.images)); replies.setText(getResources().getString(R.string.card_stats, post.getReplies(), post.getImages()));
} }
private void unbindPost(Post post) { private void unbindPost(Post post) {

@ -51,7 +51,6 @@ import android.widget.TextView;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.ImageLoader;
import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
@ -395,10 +394,10 @@ public class PostCell extends LinearLayout implements PostCellInterface {
title.setText(TextUtils.concat(titleParts.toArray(new CharSequence[titleParts.size()]))); title.setText(TextUtils.concat(titleParts.toArray(new CharSequence[titleParts.size()])));
icons.edit(); icons.edit();
icons.set(PostIcons.STICKY, post.sticky); icons.set(PostIcons.STICKY, post.isSticky());
icons.set(PostIcons.CLOSED, post.closed); icons.set(PostIcons.CLOSED, post.isClosed());
icons.set(PostIcons.DELETED, post.deleted.get()); icons.set(PostIcons.DELETED, post.deleted.get());
icons.set(PostIcons.ARCHIVED, post.archived); icons.set(PostIcons.ARCHIVED, post.isArchived());
if (!isEmpty(post.country)) { if (!isEmpty(post.country)) {
icons.set(PostIcons.COUNTRY, true); icons.set(PostIcons.COUNTRY, true);
@ -445,14 +444,14 @@ public class PostCell extends LinearLayout implements PostCellInterface {
repliesFromSize = post.repliesFrom.size(); repliesFromSize = post.repliesFrom.size();
} }
if ((!threadMode && post.replies > 0) || (repliesFromSize > 0)) { if ((!threadMode && post.getReplies() > 0) || (repliesFromSize > 0)) {
replies.setVisibility(View.VISIBLE); replies.setVisibility(View.VISIBLE);
int replyCount = threadMode ? repliesFromSize : post.replies; int replyCount = threadMode ? repliesFromSize : post.getReplies();
String text = getResources().getQuantityString(R.plurals.reply, replyCount, replyCount); String text = getResources().getQuantityString(R.plurals.reply, replyCount, replyCount);
if (!threadMode && post.images > 0) { if (!threadMode && post.getImages() > 0) {
text += ", " + getResources().getQuantityString(R.plurals.image, post.images, post.images); text += ", " + getResources().getQuantityString(R.plurals.image, post.getImages(), post.getImages());
} }
replies.setText(text); replies.setText(text);
@ -674,7 +673,7 @@ public class PostCell extends LinearLayout implements PostCellInterface {
countryTextColor = theme.detailsColor; countryTextColor = theme.detailsColor;
countryTextSize = textSize; countryTextSize = textSize;
countryIconRequest = getGraph().getImageLoader().get(post.countryUrl, new ImageLoader.ImageListener() { countryIconRequest = getGraph().getImageLoader().get(post.countryUrl.toString(), new ImageLoader.ImageListener() {
@Override @Override
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
if (response.getBitmap() != null) { if (response.getBitmap() != null) {

@ -29,7 +29,6 @@ import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
@ -124,16 +123,16 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen
Board board = op.board; Board board = op.board;
if (board != null) { if (board != null) {
SpannableString replies = new SpannableString(op.replies + "R"); SpannableString replies = new SpannableString(op.getReplies() + "R");
if (op.replies >= board.bumpLimit) { if (op.getReplies() >= board.bumpLimit) {
replies.setSpan(new StyleSpan(Typeface.ITALIC), 0, replies.length(), 0); replies.setSpan(new StyleSpan(Typeface.ITALIC), 0, replies.length(), 0);
} }
SpannableString images = new SpannableString(op.images + "I"); SpannableString images = new SpannableString(op.getImages() + "I");
if (op.images >= board.imageLimit) { if (op.getImages() >= board.imageLimit) {
images.setSpan(new StyleSpan(Typeface.ITALIC), 0, images.length(), 0); images.setSpan(new StyleSpan(Typeface.ITALIC), 0, images.length(), 0);
} }
text.setText(TextUtils.concat(statusText, replies, " / ", images, " / ", String.valueOf(op.uniqueIps) + "P")); text.setText(TextUtils.concat(statusText, replies, " / ", images, " / ", String.valueOf(op.getUniqueIps()) + "P"));
} }
return update; return update;

@ -198,7 +198,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres
saveShare(true, postImage); saveShare(true, postImage);
break; break;
case OPEN_BROWSER_ID: case OPEN_BROWSER_ID:
AndroidUtils.openLinkInBrowser((Activity) context, postImage.imageUrl); AndroidUtils.openLinkInBrowser((Activity) context, postImage.imageUrl.toString());
break; break;
case SEARCH_ID: case SEARCH_ID:
List<FloatingMenuItem> items = new ArrayList<>(); List<FloatingMenuItem> items = new ArrayList<>();
@ -211,7 +211,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) { public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
for (ImageSearch imageSearch : ImageSearch.engines) { for (ImageSearch imageSearch : ImageSearch.engines) {
if (((Integer) item.getId()) == imageSearch.getId()) { if (((Integer) item.getId()) == imageSearch.getId()) {
AndroidUtils.openLinkInBrowser((Activity) context, imageSearch.getUrl(presenter.getCurrentPostImage().imageUrl)); AndroidUtils.openLinkInBrowser((Activity) context, imageSearch.getUrl(presenter.getCurrentPostImage().imageUrl.toString()));
break; break;
} }
} }
@ -234,7 +234,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres
private void saveShare(boolean share, PostImage postImage) { private void saveShare(boolean share, PostImage postImage) {
if (share && ChanSettings.shareUrl.get()) { if (share && ChanSettings.shareUrl.get()) {
AndroidUtils.shareLink(postImage.imageUrl); AndroidUtils.shareLink(postImage.imageUrl.toString());
} else { } else {
ImageSaveTask task = new ImageSaveTask(postImage); ImageSaveTask task = new ImageSaveTask(postImage);
task.setShare(share); task.setShare(share);
@ -373,7 +373,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres
} }
}); });
imageLoader.get(postImage.thumbnailUrl, new ImageLoader.ImageListener() { imageLoader.get(postImage.thumbnailUrl.toString(), new ImageLoader.ImageListener() {
@Override @Override
public void onErrorResponse(VolleyError error) { public void onErrorResponse(VolleyError error) {
Log.e(TAG, "onErrorResponse for preview in transition in ImageViewerController, cannot show correct transition bitmap"); Log.e(TAG, "onErrorResponse for preview in transition in ImageViewerController, cannot show correct transition bitmap");
@ -395,7 +395,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres
return; return;
} }
imageLoader.get(postImage.thumbnailUrl, new ImageLoader.ImageListener() { imageLoader.get(postImage.thumbnailUrl.toString(), new ImageLoader.ImageListener() {
@Override @Override
public void onErrorResponse(VolleyError error) { public void onErrorResponse(VolleyError error) {
Log.e(TAG, "onErrorResponse for preview out transition in ImageViewerController, cannot show correct transition bitmap"); Log.e(TAG, "onErrorResponse for preview out transition in ImageViewerController, cannot show correct transition bitmap");

@ -28,12 +28,12 @@ import android.widget.LinearLayout;
import android.widget.PopupWindow; import android.widget.PopupWindow;
import android.widget.Toast; import android.widget.Toast;
import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Sites;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.helper.RefreshUIMessage; import org.floens.chan.ui.helper.RefreshUIMessage;
@ -115,7 +115,8 @@ public class MainSettingsController extends SettingsController implements Toolba
populatePreferences(); populatePreferences();
onWatchEnabledChanged(ChanSettings.watchEnabled.get()); onWatchEnabledChanged(ChanSettings.watchEnabled.get());
onPassEnabledChanged(ChanSettings.passLoggedIn()); // TODO(multi-site)
onPassEnabledChanged(Sites.defaultSite().isLoggedIn());
buildPreferences(); buildPreferences();

@ -29,7 +29,6 @@ import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.Sites;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
@ -61,6 +60,8 @@ public class PassSettingsController extends Controller implements View.OnClickLi
private EditText inputPin; private EditText inputPin;
private TextView authenticated; private TextView authenticated;
private Site site;
public PassSettingsController(Context context) { public PassSettingsController(Context context) {
super(context); super(context);
} }
@ -70,6 +71,9 @@ public class PassSettingsController extends Controller implements View.OnClickLi
super.onCreate(); super.onCreate();
getGraph().inject(this); getGraph().inject(this);
// TODO(multi-site) some selector of some sorts
site = Sites.defaultSite();
navigationItem.setTitle(R.string.settings_screen_pass); navigationItem.setTitle(R.string.settings_screen_pass);
view = inflateRes(R.layout.controller_pass); view = inflateRes(R.layout.controller_pass);
@ -91,8 +95,9 @@ public class PassSettingsController extends Controller implements View.OnClickLi
bottomDescription.setText(Html.fromHtml(getString(R.string.setting_pass_bottom_description))); bottomDescription.setText(Html.fromHtml(getString(R.string.setting_pass_bottom_description)));
bottomDescription.setMovementMethod(LinkMovementMethod.getInstance()); bottomDescription.setMovementMethod(LinkMovementMethod.getInstance());
inputToken.setText(ChanSettings.passToken.get()); LoginRequest loginDetails = site.getLoginDetails();
inputPin.setText(ChanSettings.passPin.get()); inputToken.setText(loginDetails.user);
inputPin.setText(loginDetails.pass);
// Sanity check // Sanity check
if (parentController.view.getWindowToken() == null) { if (parentController.view.getWindowToken() == null) {
@ -115,7 +120,7 @@ public class PassSettingsController extends Controller implements View.OnClickLi
public void onClick(View v) { public void onClick(View v) {
if (v == button) { if (v == button) {
if (loggedIn()) { if (loggedIn()) {
ChanSettings.passId.set(""); deauth();
crossfadeView.toggle(true, true); crossfadeView.toggle(true, true);
button.setText(R.string.setting_pass_login); button.setText(R.string.setting_pass_login);
hideError(); hideError();
@ -146,7 +151,6 @@ public class PassSettingsController extends Controller implements View.OnClickLi
private void authSuccess(LoginResponse response) { private void authSuccess(LoginResponse response) {
crossfadeView.toggle(false, true); crossfadeView.toggle(false, true);
button.setText(R.string.setting_pass_logout); button.setText(R.string.setting_pass_logout);
ChanSettings.passId.set(response.token);
authenticated.setText(response.message); authenticated.setText(response.message);
((PassSettingControllerListener) previousSiblingController).onPassEnabledChanged(true); ((PassSettingControllerListener) previousSiblingController).onPassEnabledChanged(true);
} }
@ -175,12 +179,14 @@ public class PassSettingsController extends Controller implements View.OnClickLi
button.setText(R.string.setting_pass_logging_in); button.setText(R.string.setting_pass_logging_in);
hideError(); hideError();
ChanSettings.passToken.set(inputToken.getText().toString()); // TODO(multi-site)
ChanSettings.passPin.set(inputPin.getText().toString()); String user = inputToken.getText().toString();
String pass = inputPin.getText().toString();
site.login(new LoginRequest(user, pass), this);
}
// TODO(multi-site) some selector of some sorts private void deauth() {
Site site = Sites.defaultSite(); site.logout();
site.login(new LoginRequest(site, ChanSettings.passToken.get(), ChanSettings.passPin.get()), this);
} }
private void showError(String error) { private void showError(String error) {
@ -194,7 +200,7 @@ public class PassSettingsController extends Controller implements View.OnClickLi
} }
private boolean loggedIn() { private boolean loggedIn() {
return ChanSettings.passId.get().length() > 0; return site.isLoggedIn();
} }
public interface PassSettingControllerListener { public interface PassSettingControllerListener {

@ -19,17 +19,13 @@ package org.floens.chan.ui.controller;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.webkit.CookieManager;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.Sites;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
@ -42,7 +38,6 @@ public class ReportController extends Controller {
this.post = post; this.post = post;
} }
@SuppressWarnings("deprecation")
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@Override @Override
public void onCreate() { public void onCreate() {
@ -50,25 +45,16 @@ public class ReportController extends Controller {
navigationItem.title = context.getString(R.string.report_screen, PostHelper.getTitle(post, null)); navigationItem.title = context.getString(R.string.report_screen, PostHelper.getTitle(post, null));
Site site = post.board.getSite(); Site site = post.board.getSite();
String url = site.endpoints().report(post); HttpUrl url = site.endpoints().report(post);
if (site == Sites.CHAN4) {
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
if (ChanSettings.passLoggedIn()) {
HttpUrl parsed = HttpUrl.parse(url);
String domain = parsed.scheme() + "://" + parsed.host() + "/";
for (String cookie : ChanUrls.getReportCookies(ChanSettings.passId.get())) {
cookieManager.setCookie(domain, cookie);
}
}
}
WebView webView = new WebView(context); WebView webView = new WebView(context);
site.requestModifier().modifyWebView(webView);
WebSettings settings = webView.getSettings(); WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true); settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true); settings.setDomStorageEnabled(true);
webView.loadUrl(url); webView.loadUrl(url.toString());
view = webView; view = webView;
} }
} }

@ -190,10 +190,10 @@ public class WatchNotifier extends Service {
List<CharSequence> expandedLines = new ArrayList<>(); List<CharSequence> expandedLines = new ArrayList<>();
for (Post postForExpandedLine : postsForExpandedLines) { for (Post postForExpandedLine : postsForExpandedLines) {
CharSequence prefix; CharSequence prefix;
if (postForExpandedLine.title.length() <= SUBJECT_LENGTH) { if (postForExpandedLine.getTitle().length() <= SUBJECT_LENGTH) {
prefix = postForExpandedLine.title; prefix = postForExpandedLine.getTitle();
} else { } else {
prefix = postForExpandedLine.title.subSequence(0, SUBJECT_LENGTH); prefix = postForExpandedLine.getTitle().subSequence(0, SUBJECT_LENGTH);
} }
String comment = postForExpandedLine.image != null ? IMAGE_TEXT : ""; String comment = postForExpandedLine.image != null ? IMAGE_TEXT : "";

@ -129,16 +129,16 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
public boolean onMeasured(View view) { public boolean onMeasured(View view) {
switch (newMode) { switch (newMode) {
case LOWRES: case LOWRES:
setThumbnail(postImage.thumbnailUrl); setThumbnail(postImage.thumbnailUrl.toString());
break; break;
case BIGIMAGE: case BIGIMAGE:
setBigImage(postImage.imageUrl); setBigImage(postImage.imageUrl.toString());
break; break;
case GIF: case GIF:
setGif(postImage.imageUrl); setGif(postImage.imageUrl.toString());
break; break;
case MOVIE: case MOVIE:
setVideo(postImage.imageUrl); setVideo(postImage.imageUrl.toString());
break; break;
} }
return true; return true;

@ -52,7 +52,7 @@ public class PostImageThumbnailView extends ThumbnailView {
this.postImage = postImage; this.postImage = postImage;
if (postImage != null) { if (postImage != null) {
setUrl(postImage.thumbnailUrl, width, height); setUrl(postImage.thumbnailUrl.toString(), width, height);
} else { } else {
setUrl(null, width, height); setUrl(null, width, height);
} }

@ -93,6 +93,10 @@ public class AndroidUtils {
return PreferenceManager.getDefaultSharedPreferences(Chan.getInstance()); return PreferenceManager.getDefaultSharedPreferences(Chan.getInstance());
} }
public static SharedPreferences getPreferences(String name) {
return Chan.getInstance().getSharedPreferences(name, Context.MODE_PRIVATE);
}
/** /**
* Tries to open an app that can open the specified URL.<br> * Tries to open an app that can open the specified URL.<br>
* If this app will open the link then show a chooser to the user without this app.<br> * If this app will open the link then show a chooser to the user without this app.<br>

Loading…
Cancel
Save