From ca9afd1e4382b3da39be9adca7c45f398a2e2ac2 Mon Sep 17 00:00:00 2001 From: baka kaba Date: Tue, 20 Nov 2018 19:43:21 +0000 Subject: [PATCH 01/17] Convert AwfulRequest classes to Kotlin, rename some I've converted all the implementations, partly to clean up and partly to make future development a bit easier since we don't need to rely on Java interop. The forum index updater tasks got reworked a bit, including fixing a crash in DropdownParser when a task failed and tried to show a Toast on the wrong thread PostRequest got renamed to ThreadPageRequest since it reflects what the request actually handles better - it gets a page of posts, and also processes thread data like the overall post count. Same for ProfileRequest, renamed to RefreshUserProfileRequest since that's what it actually does (and I removed the unused "username" parameter) I've also renamed some Search requests for clarity, and also renamed the "userTitle" preference and key to reflect the fact it's actually your avatar's URL, *actually* Add default implementation of handleError(), invert logic and reword docs Most requests do exactly the same thing (swallow non-critical errors) so it makes sense to have that as the default method implementation. I've also flipped the logic so the method returns true if the error was handled (which is the usual way of checking if an event has been consumed) --- .../java/com/ferg/awfulapp/AwfulActivity.kt | 4 +- .../java/com/ferg/awfulapp/AwfulFragment.kt | 10 +- .../java/com/ferg/awfulapp/EmoteFragment.kt | 8 +- .../ferg/awfulapp/ForumDisplayFragment.java | 4 +- .../com/ferg/awfulapp/NavigationDrawer.kt | 2 +- .../awfulapp/PrivateMessageListFragment.java | 4 +- .../ferg/awfulapp/ThreadDisplayFragment.java | 18 +- .../ferg/awfulapp/constants/Constants.java | 5 + .../com/ferg/awfulapp/forums/CrawlerTask.java | 237 ---------- .../com/ferg/awfulapp/forums/CrawlerTask.kt | 154 ++++++ .../awfulapp/forums/DropdownParserTask.java | 191 -------- .../awfulapp/forums/DropdownParserTask.kt | 127 +++++ .../ferg/awfulapp/forums/ForumRepository.java | 4 +- .../com/ferg/awfulapp/forums/UpdateTask.java | 333 ------------- .../com/ferg/awfulapp/forums/UpdateTask.kt | 285 +++++++++++ .../preferences/AwfulPreferences.java | 6 +- .../com/ferg/awfulapp/preferences/Keys.java | 4 +- .../fragments/AccountSettings.java | 4 +- .../fragments/ForumIndexSettings.java | 21 +- .../awfulapp/search/SearchForumsFragment.java | 4 +- .../ferg/awfulapp/search/SearchFragment.kt | 10 +- .../com/ferg/awfulapp/sync/SyncManager.java | 8 +- .../awfulapp/task/AnnouncementsRequest.java | 118 ----- .../awfulapp/task/AnnouncementsRequest.kt | 99 ++++ .../com/ferg/awfulapp/task/AwfulRequest.java | 442 ------------------ .../com/ferg/awfulapp/task/AwfulRequest.kt | 331 +++++++++++++ .../awfulapp/task/AwfulStrippedRequest.java | 101 ---- .../awfulapp/task/AwfulStrippedRequest.kt | 85 ++++ .../awfulapp/task/BookmarkColorRequest.java | 39 -- .../awfulapp/task/BookmarkColorRequest.kt | 30 ++ .../ferg/awfulapp/task/BookmarkRequest.java | 52 --- .../com/ferg/awfulapp/task/BookmarkRequest.kt | 40 ++ .../com/ferg/awfulapp/task/EditRequest.java | 45 -- .../com/ferg/awfulapp/task/EditRequest.kt | 42 ++ .../com/ferg/awfulapp/task/EmoteRequest.java | 43 -- .../com/ferg/awfulapp/task/EmoteRequest.kt | 30 ++ .../ferg/awfulapp/task/FeatureRequest.java | 69 --- .../com/ferg/awfulapp/task/FeatureRequest.kt | 46 ++ .../com/ferg/awfulapp/task/IgnoreRequest.java | 40 -- .../com/ferg/awfulapp/task/IgnoreRequest.kt | 31 ++ .../ferg/awfulapp/task/ImageSizeRequest.java | 53 --- .../ferg/awfulapp/task/ImageSizeRequest.kt | 40 ++ .../ferg/awfulapp/task/IndexIconRequest.java | 72 --- .../ferg/awfulapp/task/IndexIconRequest.kt | 62 +++ .../ferg/awfulapp/task/LepersColonyRequest.kt | 18 +- .../com/ferg/awfulapp/task/LoginRequest.java | 55 --- .../com/ferg/awfulapp/task/LoginRequest.kt | 42 ++ .../awfulapp/task/MarkLastReadRequest.java | 83 ---- .../ferg/awfulapp/task/MarkLastReadRequest.kt | 62 +++ .../ferg/awfulapp/task/MarkUnreadRequest.java | 56 --- .../ferg/awfulapp/task/MarkUnreadRequest.kt | 47 ++ .../com/ferg/awfulapp/task/PMListRequest.java | 39 -- .../com/ferg/awfulapp/task/PMListRequest.kt | 25 + .../ferg/awfulapp/task/PMReplyRequest.java | 52 --- .../com/ferg/awfulapp/task/PMReplyRequest.kt | 44 ++ .../com/ferg/awfulapp/task/PMRequest.java | 44 -- .../java/com/ferg/awfulapp/task/PMRequest.kt | 36 ++ .../com/ferg/awfulapp/task/PostRequest.java | 64 --- .../awfulapp/task/PreviewEditRequest.java | 59 --- .../ferg/awfulapp/task/PreviewEditRequest.kt | 50 ++ .../awfulapp/task/PreviewPostRequest.java | 68 --- .../ferg/awfulapp/task/PreviewPostRequest.kt | 52 +++ .../ferg/awfulapp/task/ProfileRequest.java | 74 --- .../com/ferg/awfulapp/task/QuoteRequest.java | 45 -- .../com/ferg/awfulapp/task/QuoteRequest.kt | 41 ++ .../task/RefreshUserProfileRequest.kt | 52 +++ .../com/ferg/awfulapp/task/ReplyRequest.java | 44 -- .../com/ferg/awfulapp/task/ReplyRequest.kt | 41 ++ .../com/ferg/awfulapp/task/ReportRequest.java | 55 --- .../com/ferg/awfulapp/task/ReportRequest.kt | 44 ++ .../task/SearchForumsFilterRequest.kt | 21 + .../awfulapp/task/SearchForumsRequest.java | 36 -- .../com/ferg/awfulapp/task/SearchRequest.java | 45 -- .../com/ferg/awfulapp/task/SearchRequest.kt | 32 ++ .../awfulapp/task/SearchResultPageRequest.kt | 33 ++ .../awfulapp/task/SearchResultRequest.java | 46 -- .../ferg/awfulapp/task/SendEditRequest.java | 61 --- .../com/ferg/awfulapp/task/SendEditRequest.kt | 39 ++ .../ferg/awfulapp/task/SendPostRequest.java | 59 --- .../com/ferg/awfulapp/task/SendPostRequest.kt | 39 ++ .../task/SendPrivateMessageRequest.java | 63 --- .../task/SendPrivateMessageRequest.kt | 52 +++ .../ferg/awfulapp/task/SinglePostRequest.java | 51 -- .../ferg/awfulapp/task/SinglePostRequest.kt | 37 ++ .../ferg/awfulapp/task/ThreadListRequest.java | 82 ---- .../ferg/awfulapp/task/ThreadListRequest.kt | 74 +++ .../task/ThreadLockUnlockRequest.java | 35 -- .../awfulapp/task/ThreadLockUnlockRequest.kt | 24 + .../ferg/awfulapp/task/ThreadPageRequest.kt | 53 +++ .../com/ferg/awfulapp/task/VoteRequest.java | 45 -- .../com/ferg/awfulapp/task/VoteRequest.kt | 28 ++ .../awfulapp/users/LepersColonyFragment.kt | 4 +- .../com/ferg/awfulapp/util/AwfulExtensions.kt | 15 +- 93 files changed, 2459 insertions(+), 3160 deletions(-) delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PostRequest.java delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ProfileRequest.java delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsRequest.java delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultRequest.java delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt delete mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.java create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt index 91de93757..f2e137b30 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt @@ -20,7 +20,7 @@ import com.ferg.awfulapp.provider.AwfulTheme import com.ferg.awfulapp.provider.ColorProvider import com.ferg.awfulapp.task.AwfulRequest import com.ferg.awfulapp.task.FeatureRequest -import com.ferg.awfulapp.task.ProfileRequest +import com.ferg.awfulapp.task.RefreshUserProfileRequest import timber.log.Timber import java.io.File @@ -66,7 +66,7 @@ abstract class AwfulActivity : AppCompatActivity(), AwfulPreferences.AwfulPrefer updateOrientation() when { Authentication.isUserLoggedIn() -> with(mPrefs) { - if (ignoreFormkey == null || userTitle == null) ProfileRequest(this@AwfulActivity).sendBlind() + if (ignoreFormkey == null || userAvatarUrl == null) RefreshUserProfileRequest(this@AwfulActivity).sendBlind() if (ignoreFormkey == null) FeatureRequest(this@AwfulActivity).sendBlind() } // TODO: this interferes with the code in AwfulFragment#reAuthenticate - i.e. it forces "return to this activity" behaviour, but fragments may want to return to the main activity. diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt index b8bf344d6..f90c8a449 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulFragment.kt @@ -54,7 +54,7 @@ import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayoutD import timber.log.Timber abstract class AwfulFragment : Fragment(), AwfulPreferences.AwfulPreferenceUpdate, - AwfulRequest.ProgressListener, ForumsPagerPage, NavigationEventHandler { + AwfulRequest.ProgressListener, ForumsPagerPage, NavigationEventHandler { protected var TAG = "AwfulFragment" protected val prefs: AwfulPreferences by lazy { AwfulPreferences.getInstance(context!!, this) } @@ -169,17 +169,17 @@ abstract class AwfulFragment : Fragment(), AwfulPreferences.AwfulPreferenceUpdat // Reacting to request progress /////////////////////////////////////////////////////////////////////////// - override fun requestStarted(req: AwfulRequest) { + override fun requestStarted(req: AwfulRequest<*>) { // P2R Library is ... awful - part 1 swipyLayout?.direction = TOP swipyLayout?.isRefreshing = true } - override fun requestUpdate(req: AwfulRequest, percent: Int) { + override fun requestUpdate(req: AwfulRequest<*>, percent: Int) { setProgress(percent) } - override fun requestEnded(req: AwfulRequest, error: VolleyError?) { + override fun requestEnded(req: AwfulRequest<*>, error: VolleyError?) { // P2R Library is ... awful - part 2 swipyLayout?.isRefreshing = false swipyLayout?.direction = allowedSwipeRefreshDirections @@ -231,7 +231,7 @@ abstract class AwfulFragment : Fragment(), AwfulPreferences.AwfulPreferenceUpdat * Queue a network [Request]. * Set true to tag the request with the fragment, so it will be cancelled * when the fragment is destroyed. Set false if you want to retain the request's - * default tag, e.g. so pending [com.ferg.awfulapp.task.PostRequest]s can + * default tag, e.g. so pending [com.ferg.awfulapp.task.ThreadPageRequest]s can * be cancelled when starting a new one. * @param request A Volley request * @param cancelOnDestroy Whether to tag with the fragment and automatically cancel diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt index db9a6762c..f0149003a 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt @@ -234,14 +234,14 @@ abstract class EmoteGridFragment : AwfulFragment() { private fun restartLoader() = restartLoader(Constants.EMOTE_LOADER_ID, null, emoteLoader) fun syncEmotes() { - activity?.let { + activity?.let { activity -> queueRequest( EmoteRequest(activity).build( this, - object : AwfulRequest.AwfulResultCallback { + object : AwfulRequest.AwfulResultCallback { override fun success(result: Void?) { loadFailed = false - awfulActivity?.let { restartLoader() } + awfulActivity?.run { restartLoader() } } override fun failure(error: VolleyError?) { @@ -259,7 +259,7 @@ abstract class EmoteGridFragment : AwfulFragment() { /** when true the filter will only match emote codes exactly, otherwise it searches within codes and the emotes' title subtexts */ var filterExactCode = false var currentFilter: String? = null - get() = field.let { if (it != null && it.isNotBlank()) it else null } + get() = field?.takeUnless(String::isNullOrBlank) set(value) { field = value; restartLoader() } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java index b133c418d..6444234d1 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ForumDisplayFragment.java @@ -230,7 +230,7 @@ public void onSaveInstanceState(@NonNull Bundle outState) { @Override protected void cancelNetworkRequests() { super.cancelNetworkRequests(); - NetworkUtils.cancelRequests(ThreadListRequest.REQUEST_TAG); + NetworkUtils.cancelRequests(ThreadListRequest.Companion.getREQUEST_TAG()); } @Override @@ -445,7 +445,7 @@ private void openForum(int id, @Nullable Integer page){ public void syncForum() { if(getActivity() != null && getForumId() > 0){ // cancel pending thread list loading requests - NetworkUtils.cancelRequests(ThreadListRequest.REQUEST_TAG); + NetworkUtils.cancelRequests(ThreadListRequest.Companion.getREQUEST_TAG()); // call this with cancelOnDestroy=false to retain the request's specific type tag queueRequest(new ThreadListRequest(getActivity(), getForumId(), getPage()).build(this, new AwfulRequest.AwfulResultCallback() { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt index 390682805..0dd550a81 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationDrawer.kt @@ -125,7 +125,7 @@ class NavigationDrawer(val activity: AwfulActivity, toolbar: Toolbar, val prefs: private fun refresh() { username.text = prefs.username - prefs.userTitle?.let { customTitle -> + prefs.userAvatarUrl?.let { customTitle -> if (customTitle.isNotBlank()) loadAvatar( customTitle, avatar diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java index 981dd3d71..647b2d604 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/PrivateMessageListFragment.java @@ -82,8 +82,8 @@ public class PrivateMessageListFragment extends AwfulFragment implements SwipeRe private int currentFolder = FOLDER_INBOX; - public final static int FOLDER_INBOX = 0; - public final static int FOLDER_SENT = -1; + public final static int FOLDER_INBOX = Constants.PRIVATE_MESSAGE_DEFAULT_FOLDER; + public final static int FOLDER_SENT = Constants.PRIVATE_MESSAGE_SENT_FOLDER; @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java index cd859e594..8f5a54e65 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java @@ -93,12 +93,12 @@ import com.ferg.awfulapp.task.IgnoreRequest; import com.ferg.awfulapp.task.ImageSizeRequest; import com.ferg.awfulapp.task.MarkLastReadRequest; -import com.ferg.awfulapp.task.PostRequest; -import com.ferg.awfulapp.task.ProfileRequest; import com.ferg.awfulapp.task.RedirectTask; +import com.ferg.awfulapp.task.RefreshUserProfileRequest; import com.ferg.awfulapp.task.ReportRequest; import com.ferg.awfulapp.task.SinglePostRequest; import com.ferg.awfulapp.task.ThreadLockUnlockRequest; +import com.ferg.awfulapp.task.ThreadPageRequest; import com.ferg.awfulapp.task.VoteRequest; import com.ferg.awfulapp.thread.AwfulMessage; import com.ferg.awfulapp.thread.AwfulPagedItem; @@ -448,7 +448,7 @@ public void onPause() { @Override protected void cancelNetworkRequests() { super.cancelNetworkRequests(); - NetworkUtils.cancelRequests(PostRequest.REQUEST_TAG); + NetworkUtils.cancelRequests(ThreadPageRequest.Companion.getREQUEST_TAG()); } @@ -641,7 +641,7 @@ private void rateThread() { new AlertDialog.Builder(activity) .setTitle("Rate this thread") - .setItems(items, (dialog, item) -> queueRequest(new VoteRequest(activity, getThreadId(), item) + .setItems(items, (dialog, item) -> queueRequest(new VoteRequest(activity, getThreadId(), item+1) .build(ThreadDisplayFragment.this, new AwfulRequest.AwfulResultCallback() { @Override public void success(Void result) { @@ -667,7 +667,7 @@ public void failure(VolleyError error) { public void ignoreUser(int userId) { final Activity activity = getActivity(); if (getPrefs().ignoreFormkey == null) { - queueRequest(new ProfileRequest(activity, null).build()); + queueRequest(new RefreshUserProfileRequest(activity).build()); } if (getPrefs().showIgnoreWarning) { @@ -780,14 +780,14 @@ private void syncThread() { if (activity != null) { Timber.i("Syncing - reloading from site (thread %d, page %d) to update DB", getThreadId(), getPageNumber()); // cancel pending post loading requests - NetworkUtils.cancelRequests(PostRequest.REQUEST_TAG); + NetworkUtils.cancelRequests(ThreadPageRequest.Companion.getREQUEST_TAG()); // call this with cancelOnDestroy=false to retain the request's specific type tag final int pageNumber = getPageNumber(); int userId = postFilterUserId == null ? BLANK_USER_ID : postFilterUserId; - queueRequest(new PostRequest(activity, getThreadId(), pageNumber, userId) - .build(this, new AwfulRequest.AwfulResultCallback() { + queueRequest(new ThreadPageRequest(activity, getThreadId(), pageNumber, userId) + .build(this, new AwfulRequest.AwfulResultCallback() { @Override - public void success(Integer result) { + public void success(Void result) { refreshInfo(); setProgress(75); refreshPosts(); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java index f4c5bee4d..b65431113 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java @@ -40,6 +40,7 @@ public class Constants { public static final String FUNCTION_LOGIN = BASE_URL + "/account.php"; public static final String FUNCTION_LOGIN_SSL = "https://forums.somethingawful.com/account.php"; public static final String FUNCTION_BOOKMARK = BASE_URL + "/bookmarkthreads.php"; + public static final String FUNCTION_ANNOUNCEMENTS = BASE_URL + "/announcement.php"; public static final String FUNCTION_USERCP = BASE_URL + "/usercp.php"; public static final String FUNCTION_FORUM = BASE_URL + "/forumdisplay.php"; public static final String FUNCTION_THREAD = BASE_URL + "/showthread.php"; @@ -147,6 +148,10 @@ public class Constants { //we can have up to 80 threads per forum page (SAMart) public static final int THREADS_PER_PAGE = 80; + // private message folder IDs + public static final int PRIVATE_MESSAGE_DEFAULT_FOLDER = 0; + public static final int PRIVATE_MESSAGE_SENT_FOLDER = -1; + // attachments public static final int ATTACHMENT_MAX_BYTES = 1024 * 1024; public static final int ATTACHMENT_MAX_WIDTH = 1280; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.java b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.java deleted file mode 100644 index 426e7f6dc..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.java +++ /dev/null @@ -1,237 +0,0 @@ -package com.ferg.awfulapp.forums; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.util.Log; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.security.InvalidParameterException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static com.ferg.awfulapp.constants.Constants.DEBUG; - -/** - *

Created by baka kaba on 01/04/2016.

- *

- *

A task that parses and updates the forum structure by spidering subforum links. - * This task is heavy (since it loads every forum page, ~75 pages at the time of writing) - * but parses every forum visible to the user, and captures subtitle data.

- *

- *

This basically works by building a tree asynchronously. It loads the main forum page, - * parses it for the section links (Main, Discussion etc) and adds them to the root list as - * {@link Forum} objects. Then each link is followed with a separate request, and each page - * is parsed for its list of subforums. These are all created as Forum objects, added to their - * parents' subforum lists, and then their links are followed.

- */ -public class CrawlerTask extends UpdateTask { - - /** - * Delay constant for throttling low-priority update tasks (in ms) - */ - public static final int PRIORITY_LOW = 2000; - /** - * Delay constant for throttling high-priority update tasks (in ms) - */ - public static final int PRIORITY_HIGH = 0; - - private final List forumSections = Collections.synchronizedList(new ArrayList()); - - { - TAG = "CrawlerTask"; - } - - - public CrawlerTask(@NonNull Context context, int priority) { - super(context); - initialTask = new MainForumRequest(); - taskDelayMillis = (priority == PRIORITY_HIGH) ? PRIORITY_HIGH : PRIORITY_LOW; - } - - - @NonNull - @Override - protected ForumStructure buildForumStructure() { - return ForumStructure.buildFromTree(forumSections, ForumRepository.TOP_LEVEL_PARENT_ID); - } - - - /** - * Parse the category links on the main forum page (Main, Discussion etc). - * This is to ensure all the 'hidden' subforums are picked up, which don't show up - * on the main single-page listing. - * - * @param doc A JSoup document built from the main forum page - */ - private void parseMainSections(Document doc) { - // look for section links on the main page - fail immediately if we can't find them! - Elements sections = doc.getElementsByClass("category"); - if (sections.size() == 0) { - fail("unable to parse main forum page - 0 links found!"); - return; - } - - // parse each section to get its data, and add a 'forum' to the top level list - for (Element section : sections) { - Element link = section.select("a").first(); - String title = link.text(); - String url = link.attr("abs:href"); - - addForumToList(forumSections, ForumRepository.TOP_LEVEL_PARENT_ID, url, title, ""); - } - } - - - /** - * Parse a forum page, and attempt to scrape any subforum links it contains. - * This can be used on category pages (e.g. the 'Main' link) as well as actual - * forum pages (e.g. GBS) - * - * @param forum The Forum object representing the forum being parsed - * @param doc A JSoup document built from the forum's url - */ - private void parseSubforums(Forum forum, Document doc) { - - // look for subforums - Elements subforumElements = doc.getElementsByTag("tr").select(".subforum"); - if (DEBUG) { - String message = "Parsed forum (%s) - found %d subforums"; - Log.d(TAG, String.format(message, forum.title, subforumElements.size())); - } - - // parse details and create subforum objects, and add them to this forum's subforum list - for (Element subforumElement : subforumElements) { - Element link = subforumElement.select("a").first(); - String title = link.text(); - String subtitle = subforumElement.select("dd").text(); - String url = link.attr("abs:href"); - - // strip leading junk on subtitles - final String garbage = "- "; - if (subtitle.startsWith(garbage)) { - subtitle = subtitle.substring(garbage.length()); - } - - addForumToList(forum.subforums, forum.id, url, title, subtitle); - } - } - - - /** - * Parse a forum's url to retrieve its ID - * - * @param url The forum's full url - * @return Its ID - * @throws InvalidParameterException if the ID could not be found - */ - private int getForumId(@NonNull String url) throws InvalidParameterException { - String FORUM_ID_KEY = "forumid"; - try { - Uri uri = Uri.parse(url); - String forumId = uri.getQueryParameter(FORUM_ID_KEY); - return Integer.valueOf(forumId); - } catch (NumberFormatException e) { - String message = "Unable to find forum ID key (%s) in url (%s)\nException: %s"; - throw new InvalidParameterException(String.format(message, FORUM_ID_KEY, url, e.getMessage())); - } - } - - - /** - * Create and add a Forum object to a list. - * - * @param forumList The list to add to - * @param parentId The ID of the parent forum - * @param url The url of this forum - * @param title The title of this forum - * @param subtitle The subtitle of this forum - */ - private void addForumToList(@NonNull List forumList, - int parentId, - @NonNull String url, - @NonNull String title, - @NonNull String subtitle) { - try { - // the subforum list needs to be synchronized since multiple async requests can add to it - List subforums = Collections.synchronizedList(new ArrayList()); - Forum forum = new Forum(getForumId(url), parentId, title, subtitle, subforums); - forumList.add(forum); - - if (!"".equals(url)) { - startTask(new ParseSubforumsRequest(forum, url)); - } - } catch (InvalidParameterException e) { - Log.w(TAG, e.getMessage()); - } - } - - - /////////////////////////////////////////////////////////////////////////// - // Requests - /////////////////////////////////////////////////////////////////////////// - - - /** - * A request that fetches the main forums page and parses it for sections (Main etc) - */ - private class MainForumRequest extends ForumParseTask { - - { - url = Constants.BASE_URL; - } - - @Override - protected void onRequestSucceeded(Document doc) { - Log.i(TAG, "Parsing main page"); - parseMainSections(doc); - } - - - @Override - protected void onRequestFailed(AwfulError error) { - Log.w(TAG, "Failed to get index page!\n" + error.getMessage()); - } - } - - - /** - * A request that fetches a forum page and parses it for subforum data - */ - private class ParseSubforumsRequest extends ForumParseTask { - - @NonNull - private final Forum forum; - - - /** - * Parse a Forum to add any subforums - * - * @param forum A Forum to load and parse, and add any subforums to - */ - private ParseSubforumsRequest(@NonNull Forum forum, @NonNull String url) { - this.forum = forum; - this.url = url; - } - - - @Override - protected void onRequestSucceeded(Document doc) { - parseSubforums(forum, doc); - } - - - @Override - protected void onRequestFailed(AwfulError error) { - Log.w(TAG, String.format("Failed to load forum: %s\n%s", forum.title, error.getMessage())); - } - } - -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt new file mode 100644 index 000000000..1a5597008 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt @@ -0,0 +1,154 @@ +package com.ferg.awfulapp.forums + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document +import timber.log.Timber +import java.util.* + +/** + * + * Created by baka kaba on 19/12/2018. + * + * A task that parses and updates the forum structure by spidering subforum links. + * This task is heavy (since it loads every forum page, ~75 pages at the time of writing) + * but parses every forum visible to the user, and captures subtitle data. + * + * This basically works by building a tree asynchronously. It loads the main forum page, + * parses it for the section links (Main, Discussion etc) and adds them to the root list as + * [Forum] objects. Then each link is followed with a separate request, and each page + * is parsed for its list of subforums. These are all created as Forum objects, added to their + * parents' subforum lists, and then their links (if any) are followed, until there's nothing left. + */ +internal class CrawlerTask(context: Context, priority: Priority) : UpdateTask(context, priority.delayMillis) { + + private val forumSections = Collections.synchronizedList(ArrayList()) + override val initialTask: ForumParseTask = MainForumRequest() + + + override fun buildForumStructure(): ForumStructure { + return ForumStructure.buildFromTree(forumSections, ForumRepository.TOP_LEVEL_PARENT_ID) + } + + + /** + * Parse the category links (Main, Discussion etc) on the main forum page [document]. + * + * This is to ensure all the 'hidden' subforums are picked up, which don't show up + * on the main single-page listing - you have to visit the pages for each category. + * Any categories found will be added to [forumSections]. + */ + private fun parseMainSections(document: Document) { + // look for section links on the main page - fail immediately if we can't find them! + val sections = document.getElementsByClass("category") + if (sections.isEmpty()) { + fail("unable to parse main forum page - 0 links found!") + return + } + + // parse each section to get its data, and add a 'forum' to the top level list + sections.map { it.selectFirst("a") }.forEach { link -> + forumSections.addForum(parentId = ForumRepository.TOP_LEVEL_PARENT_ID, url = link.attr("abs:href"), title = link.text()) + } + } + + + /** + * Parse a forum page [document], and attempt to scrape any subforum links it contains. + * + * This can be used on category pages (e.g. the 'Main' link) as well as actual + * forum pages (e.g. GBS). The [forum] object represents the page being parsed, and will + * have its subforums updated as appropriate. + */ + private fun parseSubforums(forum: Forum, document: Document) { + val subforumElements = document.select("tr.subforum") + if (DEBUG) Timber.d("Parsed forum ${forum.title} - found ${subforumElements.size} subforums") + + // parse details and create subforum objects, and add them to this forum's subforum list + for (element in subforumElements) { + val link = element.selectFirst("a") + val subtitle = element.select("dd").text().removePrefix("- ") // strip leading junk on subtitles + forum.subforums.addForum(parentId = forum.id, url = link.attr("abs:href"), title = link.text(), subtitle = subtitle) + } + } + + + /** + * Parse a forum's url to retrieve its ID, or null if it couldn't be found + */ + private fun getForumId(url: String): Int? = + Uri.parse(url).getQueryParameter(PARAM_FORUM_ID)?.toIntOrNull() + + + /** + * Create and add a Forum object to a list. + * + * @param parentId The ID of the parent forum + * @param url The url of this forum + * @param title The title of this forum + * @param subtitle The subtitle of this forum + */ + private fun MutableList.addForum(parentId: Int, url: String, title: String, subtitle: String = "") { + // the subforum list needs to be synchronized since multiple async requests can add to it + val subforums = Collections.synchronizedList(ArrayList()) + val forumId = getForumId(url) + if (forumId == null) { + Timber.w("Unable to find forum ID key $PARAM_FORUM_ID in url $url") + return + } + val forum = Forum(forumId, parentId, title, subtitle, subforums) + add(forum) + if (url.isNotEmpty()) startTask(ParseSubforumsRequest(forum, url)) + } + + + /////////////////////////////////////////////////////////////////////////// + // Requests + /////////////////////////////////////////////////////////////////////////// + + + /** + * A request that fetches the main forums page and parses it for sections (Main etc) + */ + private inner class MainForumRequest : UpdateTask.ForumParseTask() { + + override val url: String + get() = BASE_URL + + override fun onRequestSucceeded(doc: Document) { + Timber.i("Parsing main page") + parseMainSections(doc) + } + + override fun onRequestFailed(error: AwfulError) { + Timber.w("Failed to get index page!\n${error.message}") + } + } + + + /** + * A request that fetches a forum page and parses it for subforum data + * + * This loads a [url] representing a [forum], and parses the resulting page, adding the data to + * the [forum] object. + */ + private inner class ParseSubforumsRequest(private val forum: Forum, override val url: String) : UpdateTask.ForumParseTask() { + + override fun onRequestSucceeded(doc: Document) { + parseSubforums(forum, doc) + } + + override fun onRequestFailed(error: AwfulError) { + Timber.w("Failed to load forum: ${forum.title}\n${error.message}") + } + } + + /** + * Priority used to throttle update tasks with a given delay + */ + enum class Priority(val delayMillis: Int) { + LOW(2000), HIGH(0) + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.java b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.java deleted file mode 100644 index 6ce248508..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.ferg.awfulapp.forums; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; -import android.widget.Toast; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import java.util.Stack; - -/** - *

Created by baka kaba on 18/04/2016.

- *

- *

Forum update task that parses the forum jump dropdown. - * This task only requires a single page load, but can't scrape subtitles - * and misses certain forums.

- *

- *

It works by finding the set of option tags in the dropdown - * box, then looks for the items that are formatted like forum entries. - * It keeps a running model of the current branch hierarchy, so it can identify - * what level a forum should be on and which forum is its parent

- */ -public class DropdownParserTask extends UpdateTask { - - private final List parsedForums = new ArrayList<>(); - - { - TAG = "DropdownParserTask"; - } - - - public DropdownParserTask(@NonNull Context context) { - super(context); - initialTask = new DropdownParseRequest(); - } - - - private class DropdownParseRequest extends ForumParseTask { - - { - url = Constants.FUNCTION_FORUM + "?" + Constants.PARAM_FORUM_ID + "=" + Constants.FORUM_ID_GOLDMINE; - } - - @Override - protected void onRequestSucceeded(Document doc) { - Log.i(TAG, "Got page - parsing dropdown to get forum hierarchy"); - parsePage(doc); - } - - - @Override - protected void onRequestFailed(AwfulError error) { - Log.w(TAG, "Request error - couldn't get page to parse"); - } - } - - - private void parsePage(Document doc) { - // can't do anything without a page - fail immediately - if (doc == null) { - fail("no document to parse"); - return; - } - - class ParsedForum { - final int id; - final int depth; - - - ParsedForum(int id, int depth) { - this.id = id; - this.depth = depth; - } - } - - try { - // this stack works like a breadcrumb trail, so we can compare to the last forum - // added at the current depth, and work out the new forum's relationship to it - Stack parsedForumStack = new Stack<>(); - // this is a dummy forum representing the root - every other forum will be below this (depth >= 0) - parsedForumStack.push(new ParsedForum(ForumRepository.TOP_LEVEL_PARENT_ID, -1)); - ParsedForum previousForum; - - // get the items from the dropdown - Elements forums = doc.select("form.forum_jump option"); - Log.d(TAG, "Found " + forums.size() + " elements"); - for (Element option : forums) { - String idString = option.val(); - String rawTitle; - try { - // we need the raw text so we can preserve the leading space that identifies sections like Main - rawTitle = option.textNodes().get(0).getWholeText(); - } catch (IndexOutOfBoundsException e) { - throw new ParseException("Couldn't get TextNode for title", 0); - } - - Integer forumDepth = getForumDepth(rawTitle); - if (forumDepth == null) { - // not a forum - continue; - } - // strip off the depth-marking prefix (dashes plus a leading space) - String title = rawTitle.substring(forumDepth + 1); - - // if this new forum isn't as deep as the previous one on the stack, it's a new branch - // and we need to back up. Pop them off until we find a forum on the same or higher level - previousForum = parsedForumStack.peek(); - while (previousForum.depth > forumDepth) { - parsedForumStack.pop(); - previousForum = parsedForumStack.peek(); - } - - if (forumDepth == previousForum.depth) { - // the new forum is a sibling of the one on the stack - remove that so - // this will take its place as the last added on this level (and the one - // on the stack is its parent) - parsedForumStack.pop(); - previousForum = parsedForumStack.peek(); - } - - - int id; - try { - id = Integer.parseInt(idString); - } catch (NumberFormatException e) { - throw new ParseException("Can't parse forum ID as int! Got: " + idString, 0); - } - // we're going to push the new forum onto the stack - the one currently on top will be its parent - int parentId = previousForum.id; - parsedForums.add(new Forum(id, parentId, title, "")); - parsedForumStack.push(new ParsedForum(id, forumDepth)); - - if (Constants.DEBUG) { - Log.d(TAG, String.format("(ID:%s Parent:%d) %s", id, parentId, title)); - } - } - } catch (ParseException e) { - Log.e(TAG, "Unexpected parse failure - has the page formatting changed?", e); - if (Constants.DEBUG) { - Toast.makeText(context, "DROPDOWN PARSE ERROR\n" + e.getMessage(), Toast.LENGTH_LONG).show(); - } - } - } - - - /** - * Get the depth of a forum by its title prefix in the dropdown. - * The value is the number of leading dash characters, so the actual - * depth is relative (e.g. one level below -- is ----, so one level deeper than 2 is 4) - * - * @param rawTitleText The raw, unnormalised text from the dropdown (the whitespace is important) - * @return The depth value, which is the number of leading dashes (so you can strip them for display) - */ - @Nullable - private Integer getForumDepth(@NonNull String rawTitleText) { - if (rawTitleText.isEmpty()) { - return null; - } - - // Valid forum titles have some number of - chars followed by a space, so get the number of -s - for (int i = 0; i < rawTitleText.length(); i++) { - char c = rawTitleText.charAt(i); - if (c == ' ') { - return i; - } else if (c != '-') { - // if we run into a non-dash before we find a space, give up - break; - } - } - - // we didn't find a space after some dashes, so this isn't a valid forum title! - return null; - } - - - @NonNull - @Override - protected ForumStructure buildForumStructure() { - return ForumStructure.buildFromOrderedList(parsedForums, ForumRepository.TOP_LEVEL_PARENT_ID); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt new file mode 100644 index 000000000..1a357785d --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt @@ -0,0 +1,127 @@ +package com.ferg.awfulapp.forums + +import android.content.Context +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import timber.log.Timber +import java.text.ParseException +import java.util.* + +/** + * + * Created by baka kaba on 18/04/2016. + * + * Forum update task that parses the forum jump dropdown. + * This task only requires a single page load, but can't scrape subtitles + * and misses certain forums. + * + * It works by finding the set of `option` tags in the dropdown + * box, then looks for the items that are formatted like forum entries. + * It keeps a running model of the current branch hierarchy, so it can identify + * what level a forum should be on and which forum is its parent + */ +internal class DropdownParserTask(context: Context) : UpdateTask(context) { + + override val initialTask: ForumParseTask = DropdownParseRequest() + private val parsedForums = ArrayList() + + + private inner class DropdownParseRequest : UpdateTask.ForumParseTask() { + + override val url: String + get() = "$FUNCTION_FORUM?$PARAM_FORUM_ID=$FORUM_ID_GOLDMINE" + + override fun onRequestSucceeded(doc: Document) { + Timber.i("Got page - parsing dropdown to get forum hierarchy") + parsePage(doc) + } + + override fun onRequestFailed(error: AwfulError) { + Timber.w("Request error - couldn't get page to parse") + } + } + + + /** + * Parse a forum hierarchy from a page containing a dropdown picker, populating [parsedForums] + */ + private fun parsePage(doc: Document?) { + // can't do anything without a page - fail immediately + if (doc == null) { + fail("no document to parse") + return + } + + try { + // this stack works like a breadcrumb trail, so we can compare to the last forum + // added at the current depth, and work out the new forum's relationship to it + val forumHierarchy = Stack().apply { + // this is a dummy forum representing the root - every other forum will be below this (depth >= 0) + push(ParsedForum(ForumRepository.TOP_LEVEL_PARENT_ID, "root", -1)) + } + + fun getPreviousForum(): ParsedForum = forumHierarchy.peek() + + // get the items from the dropdown + val items = doc.select("form.forum_jump option") + Timber.d("Found ${items.size} elements") + if (items.isEmpty()) throw ParseException("No dropdown items found!", 0) + + items.mapNotNull(::parseForum).forEach { forum -> + // backtrack until we get to a forum at a higher level than this forum, so we can insert it below + while (getPreviousForum().depth >= forum.depth) { + forumHierarchy.pop() + } + + // we're going to push the new forum onto the stack - the one currently on top will be its parent + val parentId = getPreviousForum().id + parsedForums.add(Forum(forum.id, parentId, forum.title, "")) + forumHierarchy.push(forum) + if (DEBUG) Timber.d(" ${forum.title} (ID:${forum.id} Parent:$parentId)") + } + } catch (e: ParseException) { + Timber.e(e, "Unexpected parse failure - has the page formatting changed?") + } + + } + + + // Regex for identifying a forum title and extracting its indent(if any) and title content + // format is [optional indent dashes] [required space] [required title text] + private val forumTitleRegex = Regex("""^(-*) +(.+)""") + + /** + * Parse forum details from an item in the dropdown picker. + * + * This will return null for non-standard forum items (e.g. Private Messages) and things like + * dividers. Depth is signified by the number of dashes preceding the title - right now this is + * 2 per level (so "---- Forum 2" is below "-- Forum 1") but this could change, so we just use the + * raw value and treat the numbers as relative (if it's equal it's the same level, if it's bigger + * it's a lower level, etc) + */ + @Throws(ParseException::class) + private fun parseForum(dropdownItem: Element): ParsedForum? { + // we need the raw text so we can preserve the leading space that identifies sections like Main + val rawTitle = dropdownItem.textNodes().firstOrNull()?.wholeText + ?: throw ParseException("Couldn't get TextNode for title", 0) + // we're expecting leading dashes followed by a space, and then some characters + val result = forumTitleRegex.find(rawTitle) + ?: return null + // get the forum ID - this will fail for non-forums ("Private Messages" etc) as they have non-int IDs, + // which is why we do this after verifying it is a forum + val idString = dropdownItem.`val`() + val forumId = idString.toIntOrNull() + ?: throw ParseException("Can't parse forum ID as int! Got: $idString", 0) + return result.destructured.let { (dashes, title) -> ParsedForum(forumId, title, depth = dashes.length) } + } + + + override fun buildForumStructure(): ForumStructure = + ForumStructure.buildFromOrderedList(parsedForums, ForumRepository.TOP_LEVEL_PARENT_ID) + + private data class ParsedForum(val id: Int, val title: String, val depth: Int) +} + + diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumRepository.java b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumRepository.java index a0ccd8302..7632fbb5f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumRepository.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/ForumRepository.java @@ -28,8 +28,6 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -120,7 +118,7 @@ public void cancelUpdate() { UpdateTask cancelledTask = currentUpdateTask; currentUpdateTask = null; cancelledTask.cancel(); - NetworkUtils.cancelRequests(IndexIconRequest.REQUEST_TAG); + NetworkUtils.cancelRequests(IndexIconRequest.Companion.getREQUEST_TAG()); } for (ForumsUpdateListener listener : listeners) { listener.onForumsUpdateCancelled(); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.java b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.java deleted file mode 100644 index 546c10738..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.java +++ /dev/null @@ -1,333 +0,0 @@ -package com.ferg.awfulapp.forums; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.util.Log; - -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.task.AwfulRequest; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static com.ferg.awfulapp.constants.Constants.DEBUG; - -/** - * Created by baka kaba on 18/04/2016. - *

- *

Base class for async update tasks.

- *

- *

Subclasses need to provide an initial {@link ForumParseTask} to run. This should add - * further tasks as necessary using {@link #startTask(ForumParseTask)}. Once all started tasks - * have ended (or the task times out), the supplied {@link ResultListener} is called with - * the result status and the collected forums (if successful)

* - */ -abstract class UpdateTask { - - protected String TAG = "UpdateTask"; - - // give up if the task hasn't completed after this length of time: - private static final int TIMEOUT = 10; - private static final TimeUnit TIMEOUT_UNITS = TimeUnit.MINUTES; - // execution delay for added tasks, used to throttle multiple requests - protected int taskDelayMillis = 0; - - /** - * Single thread, basically processes request tasks sequentially, so they can be delayed - * The bulk of the work is done on a networking thread that the request result comes in on - */ - private final ScheduledExecutorService taskExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() { - @Override - public Thread newThread(@NonNull Runnable r) { - Thread thread = new Thread(r, "Forum update"); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; - } - }); - - /** - * Counter that tracks the number of outstanding tasks - */ - private final AtomicInteger openTasks = new AtomicInteger(); - - /** - * flag to ensure the UpdateTask can only be started once - */ - private volatile boolean executed = false; - /** - * flag to ensure the wrap-up and result code can only be called once - */ - private volatile boolean finishCalled = false; - /** - * catch-all failure flag for when something has gone wrong - */ - private volatile boolean failed = false; - - protected final Context context; - /** - * The initial task to run - this may (or may not) add additional tasks to the queue - */ - protected ForumParseTask initialTask; - private volatile ResultListener resultListener = null; - - - interface ResultListener { - /** - * Called when the UpdateTask has finished. - * - * @param task The finished task - * @param success Whether the task finished successfully, or hit a fatal error - * @param forumStructure The forums data produced by the task - will be null if success is false - */ - void onRefreshCompleted(@NonNull UpdateTask task, boolean success, @Nullable ForumStructure forumStructure); - } - - - UpdateTask(@NonNull Context context) { - this.context = context; - } - - - /** - * Start the refresh task. - * The task and its callback will be executed on a worker thread. - * - * @param callback A listener to deliver the result to - */ - public void execute(@NonNull final ResultListener callback) { - if (executed) { - throw new IllegalStateException("Task already executed - you need to create a new one!"); - } - executed = true; - resultListener = callback; - - // queue up a timeout task to shut everything down - not counted in #openTasks - taskExecutor.schedule(new Runnable() { - @Override - public void run() { - fail("timed out"); - } - }, TIMEOUT, TIMEOUT_UNITS); - - // kick off the updates! - startTask(initialTask); - } - - - /** - * Interrupt and cancel this task. - */ - public void cancel() { - // set the executed flag in case someone tries cancelling before executing - executed = true; - fail("cancel was called"); - } - - - /** - * Finish the UpdateTask, and return a result to the listener. - */ - private synchronized void finish() { - if (finishCalled) { - return; - } - finishCalled = true; - taskExecutor.shutdownNow(); - - boolean success = !failed; - Log.d(TAG, String.format("finish() called, success: %s, outstanding tasks: %d", success ? "true" : "false", openTasks.get())); - // only build the structure if we succeeded (otherwise it'll be incomplete) - ForumStructure forumStructure = null; - if (success) { - forumStructure = buildForumStructure(); - // always treat zero forums as a failure (this is definitely bad data) - if (forumStructure.getNumberOfForums() == 0) { - Log.w(TAG, "All tasks completed successfully, but got 0 forums!"); - success = false; - } - } - resultListener.onRefreshCompleted(this, success, forumStructure); - - // print some debug infos about what was produced - if (DEBUG && success && forumStructure != null) { - List allForums = forumStructure.getAsList().formatAs(ForumStructure.FLAT).build(); - Log.w(TAG, String.format("Forums parsed! %d sections found:\n\n", allForums.size())); - for (String line : printForums(forumStructure).split("\\n")) { - Log.w(TAG, line); - } - } - - } - - - /** - * Called on success - this is where the task should create the final forum structure. - * - * @return The complete parsed forums hierarchy - */ - @NonNull - protected abstract ForumStructure buildForumStructure(); - - - /** - * Mark the task as failed. - * Call this to end the task and return a failed result - every failure condition - * (timeout, failed network request, interruption etc) should call this! - */ - void fail(@NonNull String reason) { - failed = true; - Log.w(TAG, "Forum update task failed! (" + reason + ")"); - finish(); - } - - - /** - * Add a new parse task to the queue, incrementing the number of pending tasks. - * This call will be ignored if the main task has already been flagged as failed, - * so it can wind down without generating new (pointless) work. - * New tasks will be run with a delay of {@link #taskDelayMillis} - * - * @param requestTask The task to queue - */ - protected void startTask(@NonNull final ForumParseTask requestTask) { - if (failed) { - Log.d(TAG, "Forum update has failed - dropping new task"); - return; - } - openTasks.incrementAndGet(); - try { - taskExecutor.execute(new Runnable() { - @Override - public void run() { - // run the task with any required delay - immediately fail it if interrupted - try { - Thread.sleep(taskDelayMillis); - NetworkUtils.queueRequest(requestTask.build()); - } catch (InterruptedException e) { - finishTask(false); - } - } - }); - } catch (RejectedExecutionException e) { - // executor has probably been shut down between the failure check and submitting the new task - e.printStackTrace(); - finishTask(false); - } - } - - - /** - * Remove a finished task from the outstanding list, and handle its result. - * Every task initiated with {@link #startTask(ForumParseTask)} must call this! - * If called with success = false, the main task will be flagged as failed and terminate. - * - * @param success true if the task completed, false if it failed somehow - */ - private void finishTask(boolean success) { - int remaining = openTasks.decrementAndGet(); - if (!success) { - fail("one of this update's tasks failed"); - } else if (remaining <= 0) { - finish(); - } - } - - - /////////////////////////////////////////////////////////////////////////// - // Requests - /////////////////////////////////////////////////////////////////////////// - - - /** - * Abstract superclass ensuring {@link #finishTask(boolean)} is always called appropriately - */ - @WorkerThread - protected abstract class ForumParseTask extends AwfulRequest { - - /** - * The url of the page to retrieve, which is returned in the handle* methods - */ - String url; - - - public ForumParseTask() { - super(context, null); - } - - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return url; - } - - // TODO: request errors aren't being handled properly, e.g. failed page loads when you're not logged in don't call here, and the task times out - - - @Override - protected final Void handleResponse(Document doc) throws AwfulError { - onRequestSucceeded(doc); - finishTask(true); - return null; - } - - - @Override - protected final boolean handleError(AwfulError error, Document doc) { - onRequestFailed(error); - finishTask(false); - return false; - } - - - abstract protected void onRequestSucceeded(Document doc); - - abstract protected void onRequestFailed(AwfulError error); - } - - - /////////////////////////////////////////////////////////////////////////// - // Output stuff for logging - /////////////////////////////////////////////////////////////////////////// - - - private String printForums(ForumStructure forums) { - StringBuilder sb = new StringBuilder(); - List forumList = forums.getAsList().formatAs(ForumStructure.FULL_TREE).build(); - for (Forum section : forumList) { - printForum(section, 0, sb); - } - return sb.toString(); - } - - - private void printForum(Forum forum, int depth, StringBuilder sb) { - appendPadded(sb, forum.title, depth).append(":\n"); - if (!"".equals(forum.subtitle)) { - appendPadded(sb, forum.subtitle, depth).append("\n"); - } - for (Forum subforum : forum.subforums) { - printForum(subforum, depth + 1, sb); - } - } - - - private StringBuilder appendPadded(StringBuilder sb, String message, int pad) { - for (int i = 0; i < pad; i++) { - sb.append("-"); - } - sb.append(message); - return sb; - } - -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt new file mode 100644 index 000000000..768429902 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt @@ -0,0 +1,285 @@ +package com.ferg.awfulapp.forums + +import android.content.Context +import android.net.Uri +import android.support.annotation.WorkerThread +import com.ferg.awfulapp.constants.Constants.DEBUG +import com.ferg.awfulapp.forums.UpdateTask.ResultListener +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.task.AwfulRequest +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +/** + * Created by baka kaba on 18/04/2016. + * + * Base class for async update tasks. + * + * Subclasses need to set an [initialTask] to run. This should add further tasks as necessary + * using [startTask]. Once all started tasks have ended (or the task times out), + * the supplied [ResultListener] is called with the result status and the collected forums (if successful) + * + * Task processing can be throttled by passing a value for [taskDelayMillis], which will delay each + * added task by this amount of time, allowing you to space out requests and avoid hitting the site + * too heavily. + */ +internal abstract class UpdateTask(protected val context: Context, private val taskDelayMillis: Int = 0) { + + /** + * Single thread, basically processes request tasks sequentially, so they can be delayed + * The bulk of the work is done on a networking thread that the request result comes in on + */ + private val taskExecutor = Executors.newScheduledThreadPool(1) { r -> + Thread(r, "Forum update").apply { priority = Thread.MIN_PRIORITY } + } + + /** Counter that tracks the number of outstanding tasks */ + private val openTasks = AtomicInteger() + + /** flag to ensure the UpdateTask can only be started once */ + @Volatile + private var executed = false + + /** flag to ensure the wrap-up and result code can only be called once */ + @Volatile + private var finishCalled = false + + /** catch-all failure flag for when something has gone wrong */ + @Volatile + private var failed = false + + /** The initial task to run - this may (or may not) add additional tasks to the queue */ + protected abstract val initialTask: ForumParseTask + + @Volatile + private var resultListener: ResultListener? = null + + + internal interface ResultListener { + /** + * Called when the UpdateTask has finished. + * + * @param task The finished task + * @param success Whether the task finished successfully, or hit a fatal error + * @param forumStructure The forums data produced by the task - will be null if success is false + */ + fun onRefreshCompleted(task: UpdateTask, success: Boolean, forumStructure: ForumStructure?) + } + + + /** + * Start the refresh task. + * The task and its callback will be executed on a worker thread. + * + * @param callback A listener to deliver the result to + */ + fun execute(callback: ResultListener) { + check(!executed) { "Task already executed - you need to create a new one!" } + executed = true + resultListener = callback + + // queue up a timeout task to shut everything down - not counted in #openTasks + taskExecutor.schedule({ fail("timed out") }, TIMEOUT.toLong(), TIMEOUT_UNITS) + + // kick off the updates! + startTask(initialTask) + } + + + /** + * Interrupt and cancel this task. + */ + fun cancel() { + // set the executed flag in case someone tries cancelling before executing + executed = true + fail("cancel was called") + } + + + /** + * Finish the UpdateTask, and return a result to the listener. + */ + @Synchronized + private fun finish() { + if (finishCalled) return + + finishCalled = true + taskExecutor.shutdownNow() + + var success = !failed + Timber.d("finish() called, success: $success, outstanding tasks: ${openTasks.get()}") + // only build the structure if we succeeded (otherwise it'll be incomplete) + var forumStructure: ForumStructure? = null + if (success) { + forumStructure = buildForumStructure() + // always treat zero forums as a failure (this is definitely bad data) + if (forumStructure.numberOfForums == 0) { + Timber.w("All tasks completed successfully, but got 0 forums!") + success = false + } + } + resultListener!!.onRefreshCompleted(this, success, forumStructure) + + // print some debug infos about what was produced + if (DEBUG && forumStructure != null) { + val allForums = forumStructure.asList.formatAs(ForumStructure.FLAT).build() + Timber.w("Forums parsed! ${allForums.size} sections found:\n\n") + for (line in printForums(forumStructure).split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + Timber.w(line) + } + } + + } + + + /** + * Called on success - this is where the task should create the final forum structure. + * + * @return The complete parsed forums hierarchy + */ + protected abstract fun buildForumStructure(): ForumStructure + + + /** + * Mark the task as failed. + * Call this to end the task and return a failed result - every failure condition + * (timeout, failed network request, interruption etc) should call this! + */ + fun fail(reason: String) { + failed = true + Timber.w("Forum update task failed! ($reason)") + finish() + } + + + /** + * Add a new parse task to the queue, incrementing the number of pending tasks. + * This call will be ignored if the main task has already been flagged as failed, + * so it can wind down without generating new (pointless) work. + * New tasks will be run with a delay of [taskDelayMillis] + * + * @param requestTask The task to queue + */ + protected fun startTask(requestTask: ForumParseTask) { + if (failed) { + Timber.d("Forum update has failed - dropping new task") + return + } + openTasks.incrementAndGet() + try { + taskExecutor.execute { + // run the task with any required delay - immediately fail it if interrupted + try { + Thread.sleep(taskDelayMillis.toLong()) + NetworkUtils.queueRequest(requestTask.build()) + } catch (e: InterruptedException) { + finishTask(false) + } + } + } catch (e: RejectedExecutionException) { + // executor has probably been shut down between the failure check and submitting the new task + e.printStackTrace() + finishTask(false) + } + + } + + + /** + * Remove a finished task from the outstanding list, and handle its result. + * Every task initiated with [.startTask] must call this! + * If called with success = false, the main task will be flagged as failed and terminate. + * + * @param success true if the task completed, false if it failed somehow + */ + private fun finishTask(success: Boolean) { + val remaining = openTasks.decrementAndGet() + if (!success) { + fail("one of this update's tasks failed") + } else if (remaining <= 0) { + finish() + } + } + + + /////////////////////////////////////////////////////////////////////////// + // Requests + /////////////////////////////////////////////////////////////////////////// + + + /** + * Abstract superclass ensuring [.finishTask] is always called appropriately + */ + @WorkerThread + protected abstract inner class ForumParseTask : AwfulRequest(context, null) { + + /** + * The url of the page to retrieve, which is returned in the handle* methods + */ + abstract val url: String + + + override fun generateUrl(urlBuilder: Uri.Builder?): String = url + + // TODO: request errors aren't being handled properly, e.g. failed page loads when you're not logged in don't call here, and the task times out + + + override fun handleResponse(doc: Document): Void? { + onRequestSucceeded(doc) + finishTask(true) + return null + } + + + override fun handleError(error: AwfulError, doc: Document): Boolean { + onRequestFailed(error) + finishTask(false) + return true + } + + + protected abstract fun onRequestSucceeded(doc: Document) + + protected abstract fun onRequestFailed(error: AwfulError) + } + + + /////////////////////////////////////////////////////////////////////////// + // Output stuff for logging + /////////////////////////////////////////////////////////////////////////// + + + private fun printForums(forums: ForumStructure): String { + val topLevelForums = forums.asList.formatAs(ForumStructure.FULL_TREE).build() + return buildString { + topLevelForums.forEach { printForum(it, 0) } + } + } + + + private fun StringBuilder.printForum(forum: Forum, depth: Int) { + with(forum) { + appendPadded(title, depth) + if (subtitle.isNotBlank()) appendPadded(subtitle, depth) + subforums.forEach { printForum(it, depth + 1) } + } + } + + + private fun StringBuilder.appendPadded(message: String, pad: Int) { + repeat(pad) { append('-') } + appendln(message) + } + + companion object { + // give up if the task hasn't completed after this length of time: + private const val TIMEOUT = 10 + private val TIMEOUT_UNITS = TimeUnit.MINUTES + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java index b4710d939..718bccab5 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/AwfulPreferences.java @@ -47,7 +47,6 @@ import com.ferg.awfulapp.R; import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulUtils; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -89,7 +88,7 @@ public class AwfulPreferences implements OnSharedPreferenceChangeListener { //GENERAL STUFF public String username; - public String userTitle; + public String userAvatarUrl; /** this is only set when the user is on probation! See {@link com.ferg.awfulapp.util.AwfulError#checkPageErrors(Document, AwfulPreferences)} */ public int userId; public boolean hasPlatinum; @@ -155,6 +154,7 @@ public class AwfulPreferences implements OnSharedPreferenceChangeListener { public boolean disablePullNext; public long probationTime; public boolean showIgnoreWarning; + /** some user-specific validation key that's required when sending a request to ignore a user */ public String ignoreFormkey; public Set markedUsers; public Float p2rDistance; @@ -241,7 +241,7 @@ private void updateValues() { Resources res = mContext.getResources(); scaleFactor = res.getDisplayMetrics().density; username = getPreference(Keys.USERNAME, "Username"); - userTitle = getPreference(Keys.USER_TITLE, (String) null); + userAvatarUrl = getPreference(Keys.USER_AVATAR_URL, (String) null); hasPlatinum = getPreference(Keys.HAS_PLATINUM, false); hasArchives = getPreference(Keys.HAS_ARCHIVES, false); hasNoAds = getPreference(Keys.HAS_NO_ADS, false); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java index 870a78909..3826d064c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/Keys.java @@ -24,7 +24,7 @@ public abstract class Keys { // strings @IntDef({ USERNAME, - USER_TITLE, + USER_AVATAR_URL, THEME, LAUNCHER_ICON, LAYOUT, @@ -138,7 +138,7 @@ public abstract class Keys { public static final int USERNAME = R.string.pref_key_username; - public static final int USER_TITLE = R.string.pref_key_user_title; + public static final int USER_AVATAR_URL = R.string.pref_key_user_title; public static final int THEME = R.string.pref_key_theme; public static final int LAUNCHER_ICON = R.string.pref_key_launcher_icon; public static final int LAYOUT = R.string.pref_key_layout; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java index 11718d212..ff17155ee 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/AccountSettings.java @@ -12,7 +12,7 @@ import com.ferg.awfulapp.network.NetworkUtils; import com.ferg.awfulapp.task.AwfulRequest; import com.ferg.awfulapp.task.FeatureRequest; -import com.ferg.awfulapp.task.ProfileRequest; +import com.ferg.awfulapp.task.RefreshUserProfileRequest; /** * Created by baka kaba on 04/05/2015. @@ -56,7 +56,7 @@ public boolean onPreferenceClick(Preference preference) { public void success(Void result) { dialog.dismiss(); setSummaries(); - NetworkUtils.queueRequest(new ProfileRequest(getActivity()).build(null, null)); + NetworkUtils.queueRequest(new RefreshUserProfileRequest(getActivity()).build(null, null)); } @Override diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java index bca726422..7d352f560 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ForumIndexSettings.java @@ -2,6 +2,7 @@ import android.preference.Preference; import android.support.annotation.NonNull; +import android.support.annotation.UiThread; import com.ferg.awfulapp.R; import com.ferg.awfulapp.constants.Constants; @@ -76,25 +77,31 @@ public void onPause() { @Override public void onForumsUpdateStarted() { - updateRunning = true; - setUpdateForumsSummary(); + handleForumUpdateCallback(true); } @Override public void onForumsUpdateCompleted(boolean success) { - updateRunning = false; - setUpdateForumsSummary(); + handleForumUpdateCallback(false); } @Override public void onForumsUpdateCancelled() { - updateRunning = false; - setUpdateForumsSummary(); + handleForumUpdateCallback(false); + } + + + private void handleForumUpdateCallback(boolean running) { + updateRunning = running; + if (getActivity() != null) { + getActivity().runOnUiThread(this::setUpdateForumsSummary); + } } + @UiThread private void setUpdateForumsSummary() { Preference updatePref = findPrefById(R.string.pref_key_update_forums_menu_item); if (updatePref != null) { @@ -115,7 +122,7 @@ private class UpdateForumsListener implements Preference.OnPreferenceClickListen @Override public boolean onPreferenceClick(Preference preference) { // TODO: maybe move this into a full sync button somewhere, that does forum features etc - forumRepo.updateForums(new CrawlerTask(getActivity(), CrawlerTask.PRIORITY_HIGH)); + forumRepo.updateForums(new CrawlerTask(getActivity(), CrawlerTask.Priority.HIGH)); return true; } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java index 1d260b079..66c26034a 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchForumsFragment.java @@ -44,7 +44,7 @@ import com.ferg.awfulapp.R; import com.ferg.awfulapp.network.NetworkUtils; import com.ferg.awfulapp.task.AwfulRequest; -import com.ferg.awfulapp.task.SearchForumsRequest; +import com.ferg.awfulapp.task.SearchForumsFilterRequest; import com.ferg.awfulapp.thread.AwfulSearchForum; import java.util.ArrayList; @@ -149,7 +149,7 @@ public boolean volumeScroll(KeyEvent event) { } public void getForums(){ - NetworkUtils.queueRequest(new SearchForumsRequest(this.getContext()).build(null, new AwfulRequest.AwfulResultCallback>() { + NetworkUtils.queueRequest(new SearchForumsFilterRequest(this.getContext()).build(null, new AwfulRequest.AwfulResultCallback>() { @Override public void success(ArrayList result) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt index 923ff23b8..a78d6e478 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/search/SearchFragment.kt @@ -52,7 +52,7 @@ import com.ferg.awfulapp.preferences.AwfulPreferences import com.ferg.awfulapp.provider.ColorProvider import com.ferg.awfulapp.task.AwfulRequest import com.ferg.awfulapp.task.SearchRequest -import com.ferg.awfulapp.task.SearchResultRequest +import com.ferg.awfulapp.task.SearchResultPageRequest import com.ferg.awfulapp.thread.AwfulSearch import com.ferg.awfulapp.thread.AwfulSearchResult import com.ferg.awfulapp.thread.AwfulURL @@ -135,7 +135,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl private fun search() { mDialog = ProgressDialog.show(activity, getString(R.string.search_forums_active_dialog_title), getString(R.string.search_forums_active_dialog_message), true, false) val searchForumsPrimitive = ArrayUtils.toPrimitive(searchForums.toTypedArray()) - NetworkUtils.queueRequest(SearchRequest(this.context, mSearchQuery.text.toString().toLowerCase(), searchForumsPrimitive) + NetworkUtils.queueRequest(SearchRequest(this.context!!, mSearchQuery.text.toString().toLowerCase(), searchForumsPrimitive) .build(null, object : AwfulRequest.AwfulResultCallback { override fun success(result: AwfulSearchResult) { removeDialog() @@ -153,7 +153,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl } } - override fun failure(error: VolleyError) { + override fun failure(error: VolleyError?) { removeDialog() Snackbar.make(view!!, R.string.search_forums_failure_message, Snackbar.LENGTH_LONG).setAction("Retry") { search() }.show() } @@ -212,7 +212,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl override fun onRefresh(direction: SwipyRefreshLayoutDirection) { Timber.i("onRefresh: %s", mMaxPageQueried) val preItemCount = mSearchResultList.adapter?.itemCount ?: 0 - NetworkUtils.queueRequest(SearchResultRequest(this.context, mQueryId, mMaxPageQueried + 1).build(null, object : AwfulRequest.AwfulResultCallback> { + NetworkUtils.queueRequest(SearchResultPageRequest(this.context!!, mQueryId, mMaxPageQueried + 1).build(null, object : AwfulRequest.AwfulResultCallback> { // TODO: combine this with #search since they share functionality - maybe a SearchQuery object for the current query that holds this state we're changing override fun success(result: ArrayList) { @@ -226,7 +226,7 @@ class SearchFragment : AwfulFragment(), com.orangegangsters.github.swipyrefreshl mSearchResultList.smoothScrollToPosition(preItemCount + 1) } - override fun failure(error: VolleyError) { + override fun failure(error: VolleyError?) { mSRL.isRefreshing = false } })) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/sync/SyncManager.java b/Awful.apk/src/main/java/com/ferg/awfulapp/sync/SyncManager.java index 54f1e2d1b..f2243a472 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/sync/SyncManager.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/sync/SyncManager.java @@ -16,7 +16,7 @@ import com.ferg.awfulapp.network.NetworkUtils; import com.ferg.awfulapp.preferences.AwfulPreferences; import com.ferg.awfulapp.task.FeatureRequest; -import com.ferg.awfulapp.task.ProfileRequest; +import com.ferg.awfulapp.task.RefreshUserProfileRequest; import com.ferg.awfulapp.util.AwfulUtils; import java.util.concurrent.TimeUnit; @@ -68,7 +68,7 @@ private static void updateAnnouncements(@NonNull Context context) { private static void updateProfile(@NonNull Context context) { - NetworkUtils.queueRequest(new ProfileRequest(context).build(null, null)); + NetworkUtils.queueRequest(new RefreshUserProfileRequest(context).build(null, null)); } @@ -97,9 +97,9 @@ private static void updateForums(@NonNull final Context context) { Timber.d("Not updating forums - %s %s since last update", timeSinceUpdate, timeUnits); return; } - int updatePriority = hasForumData ? CrawlerTask.PRIORITY_LOW : CrawlerTask.PRIORITY_HIGH; + CrawlerTask.Priority updatePriority = hasForumData ? CrawlerTask.Priority.LOW : CrawlerTask.Priority.HIGH; Timber.d("Updating forums (%s priority) - %s forum data, %d %s since last update", - updatePriority == CrawlerTask.PRIORITY_HIGH ? "high" : "low", + updatePriority.name(), hasForumData ? "we have old" : "no existing", timeSinceUpdate, timeUnits); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.java deleted file mode 100644 index a6cd67884..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.util.Log; - -import com.ferg.awfulapp.preferences.AwfulPreferences; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.util.ArrayList; -import java.util.List; - -import static com.ferg.awfulapp.thread.AwfulPost.tryConvertToHttps; - -/** - * Created by baka kaba on 24/01/2017. - *

- * Request to load announcements and parse them as AwfulPosts. - * - * This performs no database caching! - * - * Announcements are like a weird variation on posts in a thread, as such they only have a few - * of the usual page elements, and only a few properties set in AwfulPost. - */ - -public class AnnouncementsRequest extends AwfulRequest> { - - public AnnouncementsRequest(Context context) { - super(context, null); - } - - @NonNull - private static List parseAnnouncement(@NonNull Document aThread) { - List results = new ArrayList<>(); - AwfulPreferences prefs = AwfulPreferences.getInstance(); - - // grab all the main announcement sections - these contain *most* of the data we need :/ - Elements mainAnnouncements = aThread.select("#main_full tr[valign='top']"); - Log.d(TAG, "parseAnnouncement: found" + mainAnnouncements.size() + " announcements"); - for (Element announcementSection : mainAnnouncements) { - AwfulPost announcement = new AwfulPost(); - - Element author = announcementSection.select(".author").first(); - if (author != null) { - announcement.setUsername(author.text()); - } - - Element regDate = announcementSection.select(".registered").first(); - if (regDate != null) { - announcement.setRegDate(regDate.text()); - } - - Element avatar = announcementSection.select(".title img").first(); - if (avatar != null) { - tryConvertToHttps(avatar); - announcement.setAvatar(avatar.attr("src")); - } - - // not sure if this ever appears for announcements but whatever, may as well - Element editedBy = announcementSection.select(".editedby").first(); - if (editedBy != null) { - announcement.setEdited("" + editedBy.text() + ""); - } - - // announcements have their post date in a whole other section directly after the announcement section - Element postDateSection = announcementSection.nextElementSibling(); - if (postDateSection != null) { - Element postDate = postDateSection.select(".postdate").first(); - if (postDate != null) { - announcement.setDate(postDate.text()); - } - } - - - Element postBody = announcementSection.select(".postbody").first(); - if (postBody != null) { - // process videos, images and links and store the resulting post HTML - AwfulPost.convertVideos(postBody, prefs.inlineYoutube); - for (Element image : postBody.getElementsByTag("img")) { - AwfulPost.processPostImage(image, false, prefs); - } - for (Element link : postBody.getElementsByTag("a")) { - tryConvertToHttps(link); - } - announcement.setContent(postBody.html()); - } - // I guess this is important...? - announcement.setEditable(false); - results.add(announcement); - Log.i(TAG, Integer.toString(mainAnnouncements.size()) + " posts found, " + results.size() + " posts parsed."); - } - return results; - } - - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return "https://forums.somethingawful.com/announcement.php?forumid=1"; - } - - @Override - protected List handleResponse(Document doc) throws AwfulError { - return parseAnnouncement(doc); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - Log.d(TAG, "handleError: " + error.getMessage() + "\n" + error.getSubMessage()); - return error.isCritical(); - } - -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt new file mode 100644 index 000000000..5aff0b5a3 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt @@ -0,0 +1,99 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.preferences.AwfulPreferences +import com.ferg.awfulapp.thread.AwfulPost +import com.ferg.awfulapp.thread.AwfulPost.tryConvertToHttps +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document +import timber.log.Timber +import java.util.* + +/** + * Created by baka kaba on 24/01/2017. + * + * + * Request to load announcements and parse them as AwfulPosts. + * + * This performs no database caching! + * + * Announcements are like a weird variation on posts in a thread, as such they only have a few + * of the usual page elements, and only a few properties set in AwfulPost. + */ +@JvmSuppressWildcards +class AnnouncementsRequest(context: Context) : AwfulRequest>(context, null) { + + private fun parseAnnouncement(aThread: Document): List { + val results = ArrayList() + val prefs = AwfulPreferences.getInstance() + + // TODO: tidy up when there's an announcement to test against + // grab all the main announcement sections - these contain *most* of the data we need :/ + val mainAnnouncements = aThread.select("#main_full tr[valign='top']") + Timber.d("parseAnnouncement: found ${mainAnnouncements.size} announcements") + for (announcementSection in mainAnnouncements) { + val announcement = AwfulPost() + + val author = announcementSection.selectFirst(".author") + if (author != null) { + announcement.username = author.text() + } + + val regDate = announcementSection.selectFirst(".registered") + if (regDate != null) { + announcement.regDate = regDate.text() + } + + val avatar = announcementSection.selectFirst(".title img") + if (avatar != null) { + tryConvertToHttps(avatar) + announcement.avatar = avatar.attr("src") + } + + // not sure if this ever appears for announcements but whatever, may as well + val editedBy = announcementSection.selectFirst(".editedby") + if (editedBy != null) { + announcement.edited = "" + editedBy.text() + "" + } + + // announcements have their post date in a whole other section directly after the announcement section + val postDateSection = announcementSection.nextElementSibling() + if (postDateSection != null) { + val postDate = postDateSection.selectFirst(".postdate") + if (postDate != null) { + announcement.date = postDate.text() + } + } + + + val postBody = announcementSection.selectFirst(".postbody") + if (postBody != null) { + // process videos, images and links and store the resulting post HTML + AwfulPost.convertVideos(postBody, prefs.inlineYoutube) + for (image in postBody.getElementsByTag("img")) { + AwfulPost.processPostImage(image, false, prefs) + } + for (link in postBody.getElementsByTag("a")) { + tryConvertToHttps(link) + } + announcement.content = postBody.html() + } + // I guess this is important...? + announcement.isEditable = false + results.add(announcement) + Timber.i("${mainAnnouncements.size} posts found, ${results.size} posts parsed.") + } + return results + } + + + override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_ANNOUNCEMENTS + "?forumid=1" + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): List { + return parseAnnouncement(doc) + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.java deleted file mode 100644 index 3873075ce..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.java +++ /dev/null @@ -1,442 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.widget.Toast; - -import com.android.volley.AuthFailureError; -import com.android.volley.DefaultRetryPolicy; -import com.android.volley.NetworkResponse; -import com.android.volley.ParseError; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response; -import com.android.volley.RetryPolicy; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.HttpHeaderParser; -import com.crashlytics.android.Crashlytics; -import com.ferg.awfulapp.AwfulApplication; -import com.ferg.awfulapp.R; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.preferences.AwfulPreferences; -import com.ferg.awfulapp.util.AwfulError; - -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.mime.MultipartEntityBuilder; -import org.apache.http.entity.mime.content.FileBody; -import org.apache.http.entity.mime.content.StringBody; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import timber.log.Timber; - -import static com.ferg.awfulapp.constants.Constants.BASE_URL; -import static com.ferg.awfulapp.constants.Constants.SITE_HTML_ENCODING; - -/** - * Created by Matt Shepard on 8/7/13. - */ -public abstract class AwfulRequest { - - /** Used for identifying request types when cancelling, reassign this in subclasses */ - public static final Object REQUEST_TAG = new Object(); - public static final String TAG = "AwfulRequest"; - - private Context cont; - private String baseUrl; - private Handler handle; - private Map params = null; - private MultipartEntityBuilder attachParams = MultipartEntityBuilder.create(); - private HttpEntity httpEntity = null; - private ProgressListener progressListener; - public AwfulRequest(Context context, String apiUrl) { - cont = context; - baseUrl = apiUrl; - handle = new Handler(Looper.getMainLooper()); - } - - public interface AwfulResultCallback{ - /** - * Called whenever a queued request successfully completes. - * The return value is optional and will likely be null depending on request type. - * @param result Response result or null if request does not provide direct result (most requests won't). - */ - void success(T result); - - /** - * Called whenever a network request fails, parsing was not successful, or if a forums issue is detected. - * If AwfulRequest.build() is provided an AwfulFragment ProgressListener, it will automatically pass the error to the AwfulFragment's displayAlert function. - * @param error - */ - void failure(VolleyError error); - } - - - public Object getRequestTag() { - return REQUEST_TAG; - } - - - protected void addPostParam(String key, String value){ - if(key == null || value == null){ - //intentionally triggering that NPE here, so we can log it now instead of when it hits the volley queue - //noinspection ConstantConditions,RedundantStringToString - Timber.e("PARAM NULL: %s - v: %s", key.toString(), value.toString()); - } - if(attachParams != null){ - try { - attachParams.addPart(key, new StringBody(value, ContentType.TEXT_PLAIN)); - } catch (Exception e) { - e.printStackTrace(); - } - } - if(params == null){ - params = new HashMap(); - } - params.put(key, value); - } - - protected void attachFile(String key, String filename){ - if(attachParams == null){ - attachParams = MultipartEntityBuilder.create(); - if(params != null){ - for(Map.Entry item : params.entrySet()){ - try { - attachParams.addPart(item.getKey(), new StringBody(item.getValue(), ContentType.TEXT_PLAIN)); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - attachParams.addPart(key, new FileBody(new File(filename))); - } - - protected void buildFinalRequest() - { - httpEntity = attachParams.build(); - } - - protected void setPostParams(Map post){ - params = post; - } - - /** - * Build request with no status/success/failure callbacks. Useful for fire-and-forget calls. - * @return The final request, to pass into queueRequest. - */ - public Request build(){ - return build(null, null, null); - } - - /** - * Build request, using the ProgressListener (AwfulFragment already implements this) - * and the AwfulResultCallback (for success/failure messages). - * @param prog A ProgressListener, typically the current AwfulFragment instance. A null value disables progress updates. - * @param resultListener AwfulResultCallback interface for success/failure callbacks. These will always be called on the UI thread. - * @return A result to pass into queueRequest. (AwfulApplication implements queueRequest, AwfulActivity provides a convenience shortcut to access it) - */ - public Request build(ProgressListener prog, final AwfulResultCallback resultListener){ - return build(prog, new Response.Listener() { - @Override - public void onResponse(T response) { - if(resultListener != null){ - resultListener.success(response); - } - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - // TODO: 29/10/2017 this is a temporary warning/advice for people on older devices who can't connect - remove it once there's something better for recommending security updates - if (error != null && StringUtils.contains(error.getMessage(), "SSLProtocolException")) { - Toast.makeText(cont, R.string.ssl_connection_error_message, Toast.LENGTH_LONG).show(); - } - if(resultListener != null){ - resultListener.failure(error); - } - } - }); - } - - /** - * Build request, same as build(ProgressListener, AwfulResultCallback) but provides direct access to volley callbacks. - * There is no real reason to use this over the other version. - * @param prog - * @param successListener - * @param errorListener - * @return - */ - private Request build(ProgressListener prog, Response.Listener successListener, Response.ErrorListener errorListener){ - progressListener = prog; - Uri.Builder helper = null; - if(baseUrl != null){ - try{ - helper = Uri.parse(baseUrl).buildUpon(); - }catch(Exception e){ - helper = null; - } - } - final ActualRequest actualRequest = new ActualRequest(generateUrl(helper), successListener, errorListener); - actualRequest.setTag(getRequestTag()); - return actualRequest; - } - - /** - * Generate the URL to use in the request here. This includes any query arguments. - * A Uri.Builder is provided with the base URL already processed if a base URL is provided in the constructor. - * @param urlBuilder A Uri.Builder instance with the provided base URL. If no URL is provided in the constructor, this will be null. - * @return String containing the full request URL. - */ - protected abstract String generateUrl(Uri.Builder urlBuilder); - - /** - * Handle the server response here, process any data and return any values if needed. - * The return value is optional, you can specify Void for the type and return null. - * @param doc The document containing the data from the request. - * @return Any value returned will be provided to the Response.Listener callback. - */ - protected abstract T handleResponse(Document doc) throws AwfulError; - - /** - * Before a response is handled, it will be checked against AwfulError.checkPageErrors(). - * If that check fails, this function will be called and will include the error information. - * You can choose to stop the request, or allow it to proceed to handleResponse(). - * If the request is stopped, the AwfulError class will be provided to the error listener. Otherwise the process will continue normally. - * NOTE: Automatic error checking can be disabled by overriding shouldCheckErrors(). - * @param error An AwfulError object containing the cause for this error. - * @param doc The full document file of the response, which will be provided to the handleResponse() if you allow the request to continue. - * @return false to allow the request to continue to the handleResponse() stage, true will stop the request and return the AwfulError to the error handler callback. - */ - protected abstract boolean handleError(AwfulError error, Document doc); - - protected AwfulPreferences getPreferences(){ - return AwfulPreferences.getInstance(cont); - } - protected Context getContext(){ - return cont; - } - protected ContentResolver getContentResolver(){ - return cont.getContentResolver(); - } - - /** - * Customize the error a request delivers in its {@link ProgressListener#requestEnded(AwfulRequest, VolleyError)} callback. - * - * You can use this to provide a more meaningful error, e.g. for the automatic user alerts that - * fragments display - be aware that returning a different error (instead of just changing the - * message) may affect error handling, e.g. code that looks for a {@link AwfulError#ERROR_LOGGED_OUT} - * - * @param error The actual error, typically network failure or whatever. - * @return the error to pass to listeners, or null for no error (and no alert) - */ - protected VolleyError customizeProgressListenerError(VolleyError error){ - return error; - } - - /** - * Whether or not to automatically check for common page errors during the request process. - * Override it and return false to disable these checks. - * See {@link #handleError} and {@link AwfulError#checkPageErrors} for more details. - * @return true to automatically check, false to disable. - */ - protected boolean shouldCheckErrors(){ - return true; - } - - protected void updateProgress(final int percent){ - if(progressListener != null){ - //updateProgress() will be called from a secondary thread, so run these on the UI thread. - handle.post(new Runnable() { - @Override - public void run() { - progressListener.requestUpdate(AwfulRequest.this, percent); - } - }); - } - } - - private static final RetryPolicy lenientRetryPolicy = new DefaultRetryPolicy(20000, 1, 1); - - - protected Document parseAsHtml(@NonNull NetworkResponse response) throws IOException { - long jsoupParseStart = System.currentTimeMillis(); - Document doc = Jsoup.parse(new ByteArrayInputStream(response.data), SITE_HTML_ENCODING, BASE_URL); - Timber.d("Jsoup parsing finished (took " + (System.currentTimeMillis() - jsoupParseStart) + "ms)"); - return doc; - } - - /** - * Allows subclasses (i.e. AwfulStrippedRequest) to direct the document to the appropriate handler function. - * Feels clunky to have this (don't override it in concrete classes!) as well as #handleResponse (do override that!) - */ - protected T handleResponseDocument(@NonNull Document document) throws AwfulError { - return handleResponse(document); - } - - private class ActualRequest extends Request{ - - private Response.Listener success; - - ActualRequest(String url, Response.Listener successListener, Response.ErrorListener errorListener) { - super(params != null? Method.POST : Method.GET, url, errorListener); - Timber.i("Created request: %s", url); - success = successListener; - setRetryPolicy(lenientRetryPolicy); - } - - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - long startTime = System.currentTimeMillis(); - Timber.i("Starting parse: %s", getUrl()); - updateProgress(25); - try { - Document doc = parseAsHtml(response); - updateProgress(50); - if (shouldCheckErrors()) { - AwfulError error = AwfulError.checkPageErrors(doc, getPreferences()); - if (error != null && handleError(error, doc)) { - throw error; - } - } - - T result = handleResponseDocument(doc); - Timber.d("Successful parse: %s\nTook %dms", getUrl(), System.currentTimeMillis() - startTime); - return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)); - } catch (AwfulError ae) { - return Response.error(ae); - } catch (OutOfMemoryError e) { - if (AwfulApplication.crashlyticsEnabled()) { - Crashlytics.setString("Response URL", getUrl()); - Crashlytics.setLong("Response data size", response.data.length); - } - throw e; - } catch (Exception e) { - Timber.e(e, "Failed parse: %s", getUrl()); - return Response.error(new ParseError(e)); - } finally { - updateProgress(100); - } - } - - - @Override - protected VolleyError parseNetworkError(VolleyError volleyError) { - String errorMessage = "Network error: "; - if (volleyError == null) { - errorMessage += "(null VolleyError)"; - } else { - Timber.e(volleyError); - if (volleyError.getCause() != null) { - String causeMessage = volleyError.getCause().getMessage(); - errorMessage += (causeMessage == null) ? "unknown" : causeMessage; - } - if (volleyError.networkResponse != null) { - errorMessage += "\nStatus code: " + volleyError.networkResponse.statusCode; - } - } - Timber.e(errorMessage); - return volleyError;// new AwfulError(errorMessage); - } - - - @Override - public Request setRequestQueue(RequestQueue requestQueue) { - super.setRequestQueue(requestQueue); - if(progressListener != null){ - handle.post(new Runnable() { - @Override - public void run() { - if(progressListener != null){ - progressListener.requestStarted(AwfulRequest.this); - } - } - }); - } - return this; - } - - @Override - protected void deliverResponse(T response) { - if(success != null){ - success.onResponse(response); - } - if(progressListener != null){ - progressListener.requestEnded(AwfulRequest.this, null); - } - } - - @Override - public void deliverError(VolleyError error) { - super.deliverError(error); - if(progressListener != null){ - progressListener.requestEnded(AwfulRequest.this, customizeProgressListenerError(error)); - } - } - - @Override - public Map getHeaders() throws AuthFailureError { - Map headers = super.getHeaders(); - if(headers == null || headers.size() < 1){ - headers = new HashMap<>(); - } - NetworkUtils.setCookieHeaders(headers); - Timber.i("getHeaders: %s", headers); - return headers; - } - - @Override - protected Map getParams() throws AuthFailureError { - return params; - } - - @Override - public byte[] getBody() throws AuthFailureError { - if(attachParams != null){ - if (httpEntity == null) { - buildFinalRequest(); - } - try{ - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - httpEntity.writeTo(bytes); - return bytes.toByteArray(); - }catch(IOException ioe){ - Timber.e("Failed to convert body bytestream"); - } - } - return super.getBody(); - } - - @Override - public String getBodyContentType() { - if(attachParams != null){ - if (httpEntity == null) { - buildFinalRequest(); - } - return httpEntity.getContentType().getValue(); - } - return super.getBodyContentType(); - } - } - - - public interface ProgressListener{ - void requestStarted(AwfulRequest req); - void requestUpdate(AwfulRequest req, int percent); - void requestEnded(AwfulRequest req, VolleyError error); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt new file mode 100644 index 000000000..78aa05bd8 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt @@ -0,0 +1,331 @@ +package com.ferg.awfulapp.task + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import com.android.volley.* +import com.android.volley.toolbox.HttpHeaderParser +import com.crashlytics.android.Crashlytics +import com.ferg.awfulapp.AwfulApplication +import com.ferg.awfulapp.R +import com.ferg.awfulapp.constants.Constants.BASE_URL +import com.ferg.awfulapp.constants.Constants.SITE_HTML_ENCODING +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.preferences.AwfulPreferences +import com.ferg.awfulapp.util.AwfulError +import org.apache.http.HttpEntity +import org.apache.http.entity.ContentType +import org.apache.http.entity.mime.MultipartEntityBuilder +import org.apache.http.entity.mime.content.FileBody +import org.apache.http.entity.mime.content.StringBody +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.util.* + +/** + * Created by Matt Shepard on 8/7/13. + */ +abstract class AwfulRequest(protected val context: Context, private val baseUrl: String?) { + private val handler: Handler = Handler(Looper.getMainLooper()) + private var params: MutableMap? = null + private var attachParams: MultipartEntityBuilder? = MultipartEntityBuilder.create() + private var httpEntity: HttpEntity? = null + private var progressListener: ProgressListener? = null + + + open val requestTag: Any + get() = REQUEST_TAG + + protected val preferences: AwfulPreferences + get() = AwfulPreferences.getInstance(context) + protected val contentResolver: ContentResolver + get() = context.contentResolver + + interface AwfulResultCallback { + /** + * Called whenever a queued request successfully completes. + * The return value is optional and will likely be null depending on request type. + * @param result Response result or null if request does not provide direct result (most requests won't). + */ + fun success(result: T) + + /** + * Called whenever a network request fails, parsing was not successful, or if a forums issue is detected. + * If AwfulRequest.build() is provided an AwfulFragment ProgressListener, it will automatically pass the error to the AwfulFragment's displayAlert function. + * @param error + */ + fun failure(error: VolleyError?) + } + + + protected fun addPostParam(key: String, value: String) { + attachParams?.attachParam(key, value) + params = (params ?: HashMap()).apply { this[key] = value } + } + + protected fun attachFile(key: String, filename: String) { + attachParams = (attachParams ?: MultipartEntityBuilder.create()).apply { + params?.forEach { (k, v) -> attachParam(k, v) } + addPart(key, FileBody(File(filename))) + } + } + + private fun MultipartEntityBuilder.attachParam(key: String, value: String) { + addPart(key, StringBody(value, ContentType.TEXT_PLAIN)) + } + + protected fun buildFinalRequest() { + httpEntity = attachParams!!.build() + } + + protected fun setPostParams(post: MutableMap) { + params = post + } + + /** + * Build request with no status/success/failure callbacks. Useful for fire-and-forget calls. + * @return The final request, to pass into queueRequest. + */ + fun build(): Request { + return build(null, null, null) + } + + /** + * Build request, using the ProgressListener (AwfulFragment already implements this) + * and the AwfulResultCallback (for success/failure messages). + * @param prog A ProgressListener, typically the current AwfulFragment instance. A null value disables progress updates. + * @param resultListener AwfulResultCallback interface for success/failure callbacks. These will always be called on the UI thread. + * @return A result to pass into queueRequest. (AwfulApplication implements queueRequest, AwfulActivity provides a convenience shortcut to access it) + */ + fun build(prog: ProgressListener?, resultListener: AwfulResultCallback?): Request { + return build(prog, Response.Listener { response -> + resultListener?.success(response) + }, + Response.ErrorListener { error -> + // TODO: 29/10/2017 this is a temporary warning/advice for people on older devices who can't connect - remove it once there's something better for recommending security updates + error?.message?.contains("SSLProtocolException")?.let { + Toast.makeText(context, R.string.ssl_connection_error_message, Toast.LENGTH_LONG).show() + } + resultListener?.failure(error) + }) + } + + /** + * Build request, same as build(ProgressListener, AwfulResultCallback) but provides direct access to volley callbacks. + * There is no real reason to use this over the other version. + * @param prog + * @param successListener + * @param errorListener + * @return + */ + private fun build(prog: ProgressListener?, successListener: Response.Listener?, errorListener: Response.ErrorListener?): Request { + progressListener = prog + val helper = baseUrl?.run(Uri::parse)?.run(Uri::buildUpon) + val actualRequest = ActualRequest(generateUrl(helper), successListener, errorListener) + actualRequest.tag = requestTag + return actualRequest + } + + /** + * Generate the URL to use in the request here. This includes any query arguments. + * A Uri.Builder is provided with the base URL already processed if a base URL is provided in the constructor. + * @param urlBuilder A Uri.Builder instance with the provided base URL. If no URL is provided in the constructor, this will be null. + * @return String containing the full request URL. + */ + protected abstract fun generateUrl(urlBuilder: Uri.Builder?): String + + /** + * Handle the parsed response [doc]ument here, process any data and return any values if needed. + * + * The return value is optional, you can specify Void? for the type and return null. This result + * will be passed to the response listener. + */ + @Throws(AwfulError::class) + protected abstract fun handleResponse(doc: Document): T + + + /** + * Handler for [error]s thrown by [AwfulError.checkPageErrors] when parsing a response. + * + * The main response-handler logic calls this when it encounters an [AwfulError], to check whether + * the request implementation will handle it (and processing can proceed to [handleResponse]). + * Returns true if the error was handled. + * + * By default this swallows non-critical errors, and allows everything else through. If you need + * different behaviour for some reason, override this! + */ + protected open fun handleError(error: AwfulError, doc: Document): Boolean = !error.isCritical + + /** + * Customize the error a request delivers in its [ProgressListener.requestEnded] callback. + * + * You can use this to provide a more meaningful error, e.g. for the automatic user alerts that + * fragments display - be aware that returning a different error (instead of just changing the + * message) may affect error handling, e.g. code that looks for a [AwfulError.ERROR_LOGGED_OUT] + * + * @param error The actual error, typically network failure or whatever. + * @return the error to pass to listeners, or null for no error (and no alert) + */ + protected open fun customizeProgressListenerError(error: VolleyError): VolleyError = error + + + protected fun updateProgress(percent: Int) { + //updateProgress() will be called from a secondary thread, so run these on the UI thread. + progressListener?.let { handler.post { it.requestUpdate(this@AwfulRequest, percent) } } + } + + + @Throws(IOException::class) + protected open fun parseAsHtml(response: NetworkResponse): Document { + val jsoupParseStart = System.currentTimeMillis() + val doc = Jsoup.parse(ByteArrayInputStream(response.data), SITE_HTML_ENCODING, BASE_URL) + Timber.d("Jsoup parsing finished (took ${System.currentTimeMillis() - jsoupParseStart}ms)") + return doc + } + + /** + * Allows subclasses (i.e. AwfulStrippedRequest) to direct the document to the appropriate handler function. + * Feels clunky to have this (don't override it in concrete classes!) as well as #handleResponse (do override that!) + */ + @Throws(AwfulError::class) + protected open fun handleResponseDocument(document: Document): T { + return handleResponse(document) + } + + private inner class ActualRequest internal constructor( + url: String, + private val success: Response.Listener?, + errorListener: Response.ErrorListener? + ) : Request( + if (params != null) Request.Method.POST else Request.Method.GET, + url, + errorListener + ) { + + init { + Timber.i("Created request: $url") + retryPolicy = lenientRetryPolicy + } + + + override fun parseNetworkResponse(response: NetworkResponse): Response { + val startTime = System.currentTimeMillis() + Timber.i("Starting parse: $url") + updateProgress(25) + try { + val doc = parseAsHtml(response) + updateProgress(50) + val error = AwfulError.checkPageErrors(doc, preferences) + if (error != null && handleError(error, doc)) { + throw error + } + + val result = handleResponseDocument(doc) + Timber.d("Successful parse: $url\nTook ${System.currentTimeMillis() - startTime}ms") + return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)) + } catch (ae: AwfulError) { + return Response.error(ae) + } catch (e: OutOfMemoryError) { + if (AwfulApplication.crashlyticsEnabled()) { + Crashlytics.setString("Response URL", url) + Crashlytics.setLong("Response data size", response.data.size.toLong()) + } + throw e + } catch (e: Exception) { + Timber.e(e, "Failed parse: $url") + return Response.error(ParseError(e)) + } finally { + updateProgress(100) + } + } + + + override fun parseNetworkError(volleyError: VolleyError?): VolleyError? { + return volleyError.apply { + with(StringBuilder()) { + append("Network error: ") + if (this@apply == null) { + append("(null VolleyError)") + } else { + Timber.e(volleyError) + append(cause?.message ?: "unknown cause") + networkResponse?.let { append("\nStatus code: ${networkResponse.statusCode}") } + } + Timber.e(toString()) + } + } + } + + + override fun setRequestQueue(requestQueue: RequestQueue): Request<*> { + super.setRequestQueue(requestQueue) + progressListener?.let { handler.post { it.requestStarted(this@AwfulRequest) } } + return this + } + + override fun deliverResponse(response: T) { + success?.onResponse(response) + progressListener?.requestEnded(this@AwfulRequest, null) + } + + override fun deliverError(error: VolleyError) { + super.deliverError(error) + progressListener?.requestEnded(this@AwfulRequest, customizeProgressListenerError(error)) + } + + @Throws(AuthFailureError::class) + override fun getHeaders(): Map { + return (super.getHeaders()?.takeIf { it.isNotEmpty() } ?: HashMap()) + .also(NetworkUtils::setCookieHeaders) + .also { Timber.i("getHeaders: %s", this) } + } + + @Throws(AuthFailureError::class) + override fun getParams(): Map? = this@AwfulRequest.params + + @Throws(AuthFailureError::class) + override fun getBody(): ByteArray { + attachParams?.let { + if (httpEntity == null) buildFinalRequest() + try { + return ByteArrayOutputStream().apply(httpEntity!!::writeTo).toByteArray() + } catch (ioe: IOException) { + Timber.e(ioe, "Failed to convert response body byte stream") + } + } + return super.getBody() + } + + override fun getBodyContentType(): String { + attachParams?.let { + if (httpEntity == null) buildFinalRequest() + return httpEntity!!.contentType.value + } + return super.getBodyContentType() + } + } + + + interface ProgressListener { + fun requestStarted(req: AwfulRequest<*>) + fun requestUpdate(req: AwfulRequest<*>, percent: Int) + fun requestEnded(req: AwfulRequest<*>, error: VolleyError?) + } + + companion object { + + /** Used for identifying request types when cancelling, reassign this in subclasses */ + val REQUEST_TAG = Any() + val TAG = "AwfulRequest" + + private val lenientRetryPolicy = DefaultRetryPolicy(20000, 1, 1f) + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.java deleted file mode 100644 index ecdd3906d..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.android.volley.NetworkResponse; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; - -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import timber.log.Timber; - -import static com.ferg.awfulapp.constants.Constants.BASE_URL; -import static com.ferg.awfulapp.constants.Constants.SITE_HTML_ENCODING; - -/** - * Created by baka kaba on 11/11/2018. - * - * Wrapper class for AwfulRequests, allowing a request to receive and handle a response with the - * page selector elements stripped out (which can speed up HTML parsing considerably) - * - * Ideally this is just temporary until all the outstanding requests can be moved over to using it - */ -public abstract class AwfulStrippedRequest extends AwfulRequest { - - public AwfulStrippedRequest(Context context, String apiUrl) { - super(context, apiUrl); - } - - /** - * Handle the HTML document parsed from the response, which has had the page selector elements - * stripped out. - * - * Values for the currently selected and last page are passed in - usually this is the only - * information you'd need from the page selectors anyway. These may be null if the values - * couldn't be parsed, e.g. if the site's page structure changes - * @param doc the parsed HTML document, with the page selector elements removed - * @param currentPage the value for the current page, according to the page selector - * @param totalPages the value for the last page number, according to the page selector - */ - abstract T handleStrippedResponse(Document doc, @Nullable Integer currentPage, @Nullable Integer totalPages) throws AwfulError; - - - // TODO: can/should this be done with the outer

tag instead? - // matches a single page select block (usually 2 on a page) - private final static Pattern pageSelectorRegex = Pattern.compile(""); - - // data pulled after stripping unwanted elements from the source HTML in #parseAsHtml - @Nullable - private Integer selectedPage = null; - @Nullable - private Integer lastPage = null; - - @Override - protected Document parseAsHtml(@NonNull NetworkResponse response) throws IOException { - // TODO: fall back to superclass implementation on error, set retry flag - long startTime = System.currentTimeMillis(); - Timber.d("Stripping page selectors from HTML to speed up parsing"); - // grab the data as a string, and match the select blocks - String data = new String(response.data, SITE_HTML_ENCODING); - Matcher pageSelectMatcher = pageSelectorRegex.matcher(data); - - // try and pull out the useful data before we throw the blocks away - if (pageSelectMatcher.find()) { - // separate matchers so one can fail without breaking the other - Matcher selectedPageMatcher = selectedPageRegex.matcher(pageSelectMatcher.group()); - Matcher lastPageMatcher = lastPageRegex.matcher(pageSelectMatcher.group()); - if (selectedPageMatcher.find()) { - try { selectedPage = Integer.parseInt(selectedPageMatcher.group(1)); } - catch (NumberFormatException e) { } - } - if (lastPageMatcher.find()) { - try { lastPage = Integer.parseInt(lastPageMatcher.group(1)); } - catch (NumberFormatException e) { } - } - } - - // now dump the select blocks and parse what's left - String smaller = pageSelectMatcher.replaceAll(""); - Timber.d("Garbage stripped (took " + (System.currentTimeMillis() - startTime) + "ms) - starting Jsoup parse"); - long jsoupParseStart = System.currentTimeMillis(); - Document doc = Jsoup.parse(smaller, BASE_URL); - Timber.d("Jsoup parsing finished (took " + (System.currentTimeMillis() - jsoupParseStart) + "ms)"); - return doc; - } - - @Override - protected T handleResponseDocument(@NonNull Document document) throws AwfulError { - return handleStrippedResponse(document, selectedPage, lastPage); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.kt new file mode 100644 index 000000000..25d340633 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulStrippedRequest.kt @@ -0,0 +1,85 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import com.android.volley.NetworkResponse +import com.ferg.awfulapp.constants.Constants.BASE_URL +import com.ferg.awfulapp.constants.Constants.SITE_HTML_ENCODING +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import timber.log.Timber +import java.io.IOException +import java.nio.charset.Charset + +/** + * Created by baka kaba on 11/11/2018. + * + * Wrapper class for AwfulRequests, allowing a request to receive and handle a response with the + * page selector elements stripped out (which can speed up HTML parsing considerably) + * + * Ideally this is just temporary until all the outstanding requests can be moved over to using it + */ +abstract class AwfulStrippedRequest(context: Context, apiUrl: String) : AwfulRequest(context, apiUrl) { + + // data pulled after stripping unwanted elements from the source HTML in #parseAsHtml + private var selectedPage: Int? = null + private var lastPage: Int? = null + + /** + * Handle the HTML [document] parsed from the response, which has had the page selector elements + * stripped out. + * + * Values for the currently selected and last page are passed in - usually this is the only + * information you'd need from the page selectors anyway. These may be null if the values + * couldn't be parsed, e.g. if the site's page structure changes + * @param currentPage the value for the current page, according to the page selector + * @param totalPages the value for the last page number, according to the page selector + */ + @Throws(AwfulError::class) + internal abstract fun handleStrippedResponse(document: Document, currentPage: Int?, totalPages: Int?): T + + @Throws(IOException::class) + override fun parseAsHtml(response: NetworkResponse): Document { + // TODO: fall back to superclass implementation on error, set retry flag + val startTime = System.currentTimeMillis() + Timber.d("Stripping page selectors from HTML to speed up parsing") + // grab the data as a string, and match the select blocks + val html = String(response.data, SITE_CHARSET) + + // try and pull out the useful data before we throw the blocks away + pageSelectorRegex.find(html)?.value?.let { selectBlock -> + // separate matchers so one can fail without breaking the other + selectedPage = selectedPageRegex.find(selectBlock)?.tryParseInt() + lastPage = lastPageRegex.find(selectBlock)?.tryParseInt() + } + + // now dump the select blocks and parse what's left + val smaller = pageSelectorRegex.replace(html, "") + Timber.d("Garbage stripped (took ${startTime.elapsed}ms) - starting Jsoup parse") + val jsoupParseStart = System.currentTimeMillis() + return Jsoup.parse(smaller, BASE_URL).also { + Timber.d("jsoup parsing finished (took ${jsoupParseStart.elapsed}ms)") + } + } + + private val Long.elapsed get() = System.currentTimeMillis() - this + private fun MatchResult.tryParseInt() = this.groupValues[1].toIntOrNull() + + @Throws(AwfulError::class) + override fun handleResponseDocument(document: Document): T { + return handleStrippedResponse(document, selectedPage, lastPage) + } + + companion object { + private val SITE_CHARSET = Charset.forName(SITE_HTML_ENCODING) + + // TODO: can/should this be done with the outer
tag instead? + // matches a single page select block (usually 2 on a page) + private val pageSelectorRegex = Regex("""""") + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.java deleted file mode 100644 index fb2953a27..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by Matthew on 8/9/13. - */ -public class BookmarkColorRequest extends AwfulRequest { - public BookmarkColorRequest(Context context, int threadId) { - super(context, null); - addPostParam(Constants.PARAM_ACTION, "cat_toggle"); - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_BOOKMARK; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - //nothing to do - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } - -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt new file mode 100644 index 000000000..79eda535d --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt @@ -0,0 +1,30 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document + +/** + * Created by Matthew on 8/9/13. + */ +class BookmarkColorRequest(context: Context, threadId: Int) : AwfulRequest(context, null) { + init { + addPostParam(Constants.PARAM_ACTION, "cat_toggle") + addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + // TODO wat + //since we aren't adding query arguments to a POST request, + //we can just pass null in the constructor URL field and it'll skip this Uri.Builder + return Constants.FUNCTION_BOOKMARK + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? = null + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.java deleted file mode 100644 index 8b639e629..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.provider.AwfulProvider; -import com.ferg.awfulapp.thread.AwfulThread; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class BookmarkRequest extends AwfulRequest { - private int threadId; - private boolean add; - public BookmarkRequest(Context context, int threadId, boolean add) { - super(context, null); - this.add = add; - this.threadId = threadId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - if(add){ - addPostParam(Constants.PARAM_ACTION, "add"); - }else{ - addPostParam(Constants.PARAM_ACTION, "remove"); - } - return Constants.FUNCTION_BOOKMARK; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - ContentValues cv = new ContentValues(); - cv.put(AwfulThread.BOOKMARKED, add?1:0); - getContentResolver().update(AwfulThread.CONTENT_URI, cv, AwfulThread.ID+"=?", AwfulProvider.int2StrArray(threadId)); - if(!add){ - getContentResolver().delete(AwfulThread.CONTENT_URI_UCP, AwfulThread.ID+"=?", AwfulProvider.int2StrArray(threadId)); - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt new file mode 100644 index 000000000..ccd7c632e --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt @@ -0,0 +1,40 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.thread.AwfulThread +import com.ferg.awfulapp.util.AwfulError +import com.ferg.awfulapp.util.toSqlBoolean +import org.jsoup.nodes.Document + +/** + * Created by matt on 8/8/13. + */ +class BookmarkRequest(context: Context, private val threadId: Int, private val add: Boolean) : AwfulRequest(context, null) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) + if (add) { + addPostParam(Constants.PARAM_ACTION, "add") + } else { + addPostParam(Constants.PARAM_ACTION, "remove") + } + return Constants.FUNCTION_BOOKMARK + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + val cv = ContentValues() + cv.put(AwfulThread.BOOKMARKED, add.toSqlBoolean) + val id = arrayOf(threadId.toString()) + + with(contentResolver) { + update(AwfulThread.CONTENT_URI, cv, "${AwfulThread.ID}=?", id) + if (!add) delete(AwfulThread.CONTENT_URI_UCP, "${AwfulThread.ID}=?", id) + } + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.java deleted file mode 100644 index b56f629e6..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.provider.DatabaseHelper; -import com.ferg.awfulapp.reply.Reply; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.sql.Timestamp; - -/** - * Created by matt on 8/8/13. - */ -public class EditRequest extends AwfulRequest { - private int threadId, postId; - public EditRequest(Context context, int threadId, int postId) { - super(context, Constants.FUNCTION_EDIT_POST); - this.threadId = threadId; - this.postId = postId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "editpost"); - urlBuilder.appendQueryParameter(Constants.PARAM_POST_ID, Integer.toString(postId)); - return urlBuilder.build().toString(); - } - - @Override - protected ContentValues handleResponse(Document doc) throws AwfulError { - ContentValues newReply = Reply.processEdit(doc, threadId, postId); - newReply.put(DatabaseHelper.UPDATED_TIMESTAMP, new Timestamp(System.currentTimeMillis()).toString()); - return newReply; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt new file mode 100644 index 000000000..10f2d2501 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt @@ -0,0 +1,42 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.provider.DatabaseHelper +import com.ferg.awfulapp.reply.Reply +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document + +import java.sql.Timestamp + +/** + * Request the data you get by starting a post edit on the main site. + * + * This provides you with the text and BBCode added to the post composer, + * the form key and cookie (for authentication?) as well as any selected + * options (see [Reply.processEdit]) and a current timestamp. + */ +class EditRequest(context: Context, private val threadId: Int, private val postId: Int) + : AwfulRequest(context, Constants.FUNCTION_EDIT_POST) { + + // TODO: this and the quote/reply requests are all very similar - they all just load the "start replying" page and grab any existing contents. Combine them maybe? + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!) { + appendQueryParameter(Constants.PARAM_ACTION, "editpost") + appendQueryParameter(Constants.PARAM_POST_ID, postId.toString()) + return build().toString() + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): ContentValues { + return Reply.processEdit(doc, threadId, postId).apply { + put(DatabaseHelper.UPDATED_TIMESTAMP, Timestamp(System.currentTimeMillis()).toString()) + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.java deleted file mode 100644 index 9aedcd8b0..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulEmote; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.util.ArrayList; - -/** - * Created by matt on 8/8/13. - */ -public class EmoteRequest extends AwfulRequest { - public EmoteRequest(Context context) { - super(context, Constants.FUNCTION_MISC); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "showsmilies"); - return urlBuilder.build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - ArrayList emotes = AwfulEmote.parseEmotes(doc); - int inserted = getContentResolver().bulkInsert(AwfulEmote.CONTENT_URI, emotes.toArray(new ContentValues[emotes.size()])); - if(inserted < 0){ - throw new AwfulError(); - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt new file mode 100644 index 000000000..c3604faca --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt @@ -0,0 +1,30 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.thread.AwfulEmote +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * Created by matt on 8/8/13. + */ +class EmoteRequest(context: Context) : AwfulRequest(context, Constants.FUNCTION_MISC) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!) { + appendQueryParameter(Constants.PARAM_ACTION, "showsmilies") + return build().toString() + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + val emotes = AwfulEmote.parseEmotes(doc) + val inserted = contentResolver.bulkInsert(AwfulEmote.CONTENT_URI, emotes.toTypedArray()) + if (inserted < 0) throw AwfulError("Inserted $inserted emotes") + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.java deleted file mode 100644 index 6d3313fb7..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.preferences.Keys; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -/** - * Created by matt on 8/8/13. - */ -public class FeatureRequest extends AwfulRequest { - public FeatureRequest(Context context) { - super(context, Constants.FUNCTION_MEMBER); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "accountfeatures").build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - Element features = doc.getElementsByClass("features").first(); - boolean premium = false; - boolean archives = false; - boolean noads = false; - if (features != null) { - - Elements feature_dts = features.getElementsByTag("dt"); - if (feature_dts.size() == 3) { - premium = feature_dts.get(0).hasClass("enabled"); - archives = feature_dts.get(1).hasClass("enabled"); - noads = feature_dts.get(2).hasClass("enabled"); - try { - getPreferences().setPreference(Keys.HAS_PLATINUM, premium); - } catch (Exception e) { - e.printStackTrace(); - } - try { - getPreferences().setPreference(Keys.HAS_ARCHIVES, archives); - } catch (Exception e) { - e.printStackTrace(); - } - try { - getPreferences().setPreference(Keys.HAS_NO_ADS, noads); - } catch (Exception e) { - e.printStackTrace(); - } - } - } else { - throw new AwfulError("Feature page did not load"); - } - Log.i("FeatureRequest", "Updated account features P:" + premium + " A:" - + archives + " NA:" + noads); - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt new file mode 100644 index 000000000..8130816ed --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt @@ -0,0 +1,46 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.preferences.Keys +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import timber.log.Timber + +/** + * An AwfulRequest that fetches the features active on the user's account (platinum etc.), + * and stores that data in AwfulPreferences. + */ +class FeatureRequest(context: Context) : AwfulRequest(context, Constants.FUNCTION_MEMBER) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + return urlBuilder!!.appendQueryParameter(Constants.PARAM_ACTION, "accountfeatures").build().toString() + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + // grab the element containing the features info and validate its structure + val features = doc.selectFirst(".features")?.select("dt") + if (features == null) { + throw AwfulError("Couldn't find features element") + } else if (features.size != 3) { + throw AwfulError("Unexpected number of feature elements (wanted 3, got ${features.size}") + } + + mapOf( + Keys.HAS_PLATINUM to features[0].enabled, + Keys.HAS_ARCHIVES to features[1].enabled, + Keys.HAS_NO_ADS to features[2].enabled + ).forEach { (k, v) -> preferences.setPreference(k, v) } + + Timber.i("Updated account features\nPlatinum:${preferences.hasPlatinum} Archives:${preferences.hasArchives} NoAds:${preferences.hasNoAds}") + return null + } + + private val Element.enabled get() = this.hasClass("enabled") + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.java deleted file mode 100644 index a39b291c8..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by Matthew on 8/8/13. - */ -public class IgnoreRequest extends AwfulRequest { - public IgnoreRequest(Context context, int userId) { - super(context, null);//member2? heh, ~radium~ - addPostParam(Constants.PARAM_ACTION, Constants.ACTION_ADDLIST); - addPostParam(Constants.PARAM_USERLIST, Constants.USERLIST_IGNORE); - addPostParam(Constants.FORMKEY, getPreferences().ignoreFormkey); - addPostParam(Constants.PARAM_USER_ID, Integer.toString(userId)); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_MEMBER2; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - //nothin a doin' here, if we fail we'll see in the failed callback - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt new file mode 100644 index 000000000..004db1700 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt @@ -0,0 +1,31 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document + +/** + * An AwfulRequest that adds a userId to the user's ignore list. + */ +class IgnoreRequest(context: Context, userId: Int) : AwfulRequest(context, null) { + init { + addPostParam(Constants.PARAM_ACTION, Constants.ACTION_ADDLIST) + addPostParam(Constants.PARAM_USERLIST, Constants.USERLIST_IGNORE) + addPostParam(Constants.FORMKEY, preferences.ignoreFormkey) + addPostParam(Constants.PARAM_USER_ID, Integer.toString(userId)) + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + //since we aren't adding query arguments to a POST request, + //we can just pass null in the constructor URL field and it'll skip this Uri.Builder + return Constants.FUNCTION_MEMBER2 + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? = null // nothing to handle, just fire and forget + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.java deleted file mode 100644 index b63920606..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.support.annotation.NonNull; - -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.HttpHeaderParser; - -/** - * Basic request to get the size of a resource at a given URL. - *

- * In case of an error, the listener will be called with a null result. - */ -public class ImageSizeRequest extends Request { - private final Response.Listener listener; - - /** - * A Volley Request to get the size of a resource at a given URL. - * - * @param url the url of the resource - * @param listener receives a response containing the resource size, or null if there was an error - */ - public ImageSizeRequest(@NonNull String url, Response.Listener listener) { - super(Method.HEAD, url, null); - this.listener = listener; - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - String length = response.headers.get("Content-Length"); - if (length == null) { - return null; - } - try { - return Response.success(Integer.parseInt(length), HttpHeaderParser.parseCacheHeaders(response)); - } catch (NumberFormatException e) { - return null; - } - } - - @Override - protected void deliverResponse(Integer response) { - listener.onResponse(response); - } - - @Override - public void deliverError(VolleyError error) { - // just return a 'nope' size value, doesn't really matter why we failed - listener.onResponse(null); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.kt new file mode 100644 index 000000000..d522c8469 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ImageSizeRequest.kt @@ -0,0 +1,40 @@ +package com.ferg.awfulapp.task + +import com.android.volley.NetworkResponse +import com.android.volley.Request +import com.android.volley.Response +import com.android.volley.VolleyError +import com.android.volley.toolbox.HttpHeaderParser + +/** + * Basic request to get the size of a resource at a given URL. + * + * In case of an error, the listener will be called with a null result. + */ +class ImageSizeRequest +/** + * A Volley Request to get the size of a resource at a given URL. + * + * @param url the url of the resource + * @param listener receives a response containing the resource size, or null if there was an error + */ +(url: String, private val listener: Response.Listener) : Request(Request.Method.HEAD, url, null) { + + override fun parseNetworkResponse(response: NetworkResponse): Response? { + val length = response.headers["Content-Length"] ?: return null + return try { + Response.success(Integer.parseInt(length), HttpHeaderParser.parseCacheHeaders(response)) + } catch (e: NumberFormatException) { + null + } + } + + override fun deliverResponse(response: Int?) { + listener.onResponse(response) + } + + override fun deliverError(error: VolleyError) { + // just return a 'nope' size value, doesn't really matter why we failed + listener.onResponse(null) + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.java deleted file mode 100644 index 924a676e2..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.preferences.Keys; -import com.ferg.awfulapp.thread.AwfulForum; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.select.Elements; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class IndexIconRequest extends AwfulRequest { - - public static final Object REQUEST_TAG = new Object(); - - public IndexIconRequest(Context context) { - super(context, null); - } - - - @Override - public Object getRequestTag() { - return REQUEST_TAG; - } - - @Override - protected String generateUrl(Uri.Builder build) { - return Constants.BASE_URL + "/"; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - AwfulForum.processForumIcons(doc, getContentResolver()); - updateProgress(80); - - //optional section, parses username from PM notification field. - Elements pmBlock = doc.getElementsByAttributeValue("id", "pm"); - try { - if (pmBlock.size() > 0) { - Elements bolded = pmBlock.first().getElementsByTag("b"); - if (bolded.size() > 1) { - String name = bolded.first().text().split("'")[0]; - String unread = bolded.get(1).text(); - Pattern findUnread = Pattern.compile("(\\d+)\\s+unread"); - Matcher matchUnread = findUnread.matcher(unread); - int unreadCount = -1; - if (matchUnread.find()) { - unreadCount = Integer.parseInt(matchUnread.group(1)); - } - Log.v("IndexRequest", "text: " + name + " - " + unreadCount); - if (name != null && name.length() > 0) { - getPreferences().setPreference(Keys.USERNAME, name); - } - } - } - } catch (Exception e) { - //this chunk is optional, no need to fail everything if it doesn't work out. - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt new file mode 100644 index 000000000..54d42d6a1 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt @@ -0,0 +1,62 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.preferences.Keys +import com.ferg.awfulapp.thread.AwfulForum +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document +import timber.log.Timber +import java.util.regex.Pattern + +/** + * An AwfulRequest that parses forum icons from the main forums page, and stores them in the database. + */ +class IndexIconRequest(context: Context) : AwfulRequest(context, null) { + + //TODO: do we even need this anymore? We don't display these forum icons, but we do parse the colours for our custom icons, so check what's needed + companion object { + val REQUEST_TAG = Any() + } + + override val requestTag: Any + get() = REQUEST_TAG + + + override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.BASE_URL + "/" + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + AwfulForum.processForumIcons(doc, contentResolver) + updateProgress(80) + + //optional section, parses username from PM notification field. + // TODO: this has nothing to do with parsing forum icons - if we need to update the username, do it separately. Also it's broken when there's an apostrophe in the username? + val pmBlock = doc.getElementsByAttributeValue("id", "pm") + try { + if (pmBlock.size > 0) { + val bolded = pmBlock.first().getElementsByTag("b") + if (bolded.size > 1) { + val name = bolded.first()?.text()?.split("'".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()?.get(0) + val unread = bolded[1].text() + val findUnread = Pattern.compile("(\\d+)\\s+unread") + val matchUnread = findUnread.matcher(unread) + var unreadCount = -1 + if (matchUnread.find()) { + unreadCount = Integer.parseInt(matchUnread.group(1)) + } + Timber.v("text: $name - $unreadCount") + if (name != null && name.isNotEmpty()) { + preferences.setPreference(Keys.USERNAME, name) + } + } + } + } catch (e: Exception) { + //this chunk is optional, no need to fail everything if it doesn't work out. + } + + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt index 0ef5dcac0..60453d9bd 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt @@ -22,7 +22,9 @@ class LepersColonyRequest(context: Context, val page: Int = 1, val userId: Strin companion object { val REQUEST_TAG = Any() } - override fun getRequestTag(): Any = REQUEST_TAG + // TODO: fix this + override val requestTag: Any + get() = REQUEST_TAG override fun generateUrl(urlBuilder: Uri.Builder?): String { with(urlBuilder!!) { @@ -32,8 +34,8 @@ class LepersColonyRequest(context: Context, val page: Int = 1, val userId: Strin } } - override fun handleResponse(doc: Document?): LepersColonyPage? { - with (doc!!) { + override fun handleResponse(doc: Document): LepersColonyPage { + with (doc) { val thisPage = selectFirst(".pages option[selected]")?.text()?.toIntOrNull() ?: FIRST_PAGE val lastPage = selectFirst(".pages a[title='Last page']") ?.attr("href") @@ -48,14 +50,14 @@ class LepersColonyRequest(context: Context, val page: Int = 1, val userId: Strin } } - override fun handleStrippedResponse(doc: Document, currentPage: Int?, lastPage: Int?): LepersColonyPage { + override fun handleStrippedResponse(document: Document, currentPage: Int?, totalPages: Int?): LepersColonyPage { val thisPage = currentPage ?: FIRST_PAGE - val totalPages = lastPage ?: thisPage - val punishments = doc.select("table.standard.full tr").drop(1).map(Punishment.Companion::parse) - return LepersColonyPage(punishments, thisPage, totalPages, userId) + val lastPage = totalPages ?: thisPage + val punishments = document.select("table.standard.full tr").drop(1).map(Punishment.Companion::parse) + return LepersColonyPage(punishments, thisPage, lastPage, userId) } - override fun handleError(error: AwfulError?, doc: Document?) = true + override fun handleError(error: AwfulError, doc: Document) = false /** * Represents the contents and metadata for a page from the Leper's Colony diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.java deleted file mode 100644 index 9f4f3e5a9..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.preferences.AwfulPreferences; -import com.ferg.awfulapp.preferences.Keys; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class LoginRequest extends AwfulRequest { - private String username; - public LoginRequest(Context context, String username, String password) { - super(context, null); - this.username = username; - addPostParam(Constants.PARAM_ACTION, "login"); - addPostParam(Constants.PARAM_USERNAME, username); - addPostParam(Constants.PARAM_PASSWORD, password); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_LOGIN_SSL; - } - - @Override - protected Boolean handleResponse(Document doc) throws AwfulError { - Boolean result = NetworkUtils.saveLoginCookies(getContext()); - if(result){ - AwfulPreferences prefs = AwfulPreferences.getInstance(getContext()); - prefs.setPreference(Keys.USERNAME, username); - } - return result; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - if(error.networkResponse != null && error.networkResponse.statusCode == 302){ - Boolean result = NetworkUtils.saveLoginCookies(getContext()); - if(result){ - AwfulPreferences prefs = AwfulPreferences.getInstance(getContext()); - prefs.setPreference(Keys.USERNAME, username); - } - return result; - }else{ - return error.isCritical(); - } - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt new file mode 100644 index 000000000..357aa4e3d --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt @@ -0,0 +1,42 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.android.volley.NetworkResponse +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.preferences.Keys +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * AwfulRequest that sends a login request to the site, and stores login cookies and sets the current username if successful. + */ +class LoginRequest(context: Context, private val username: String, password: String) : AwfulRequest(context, null) { + init { + addPostParam(Constants.PARAM_ACTION, "login") + addPostParam(Constants.PARAM_USERNAME, username) + addPostParam(Constants.PARAM_PASSWORD, password) + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_LOGIN_SSL + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Boolean = validateLoginState() + + override fun handleError(error: AwfulError, doc: Document): Boolean = + error.networkResponse?.isRedirect == true || !error.isCritical + + + private val NetworkResponse.isRedirect get() = this.statusCode == 302 + + /** + * Check if we've received login cookies, and if so store the username we used to log in + */ + private fun validateLoginState(): Boolean { + return NetworkUtils.saveLoginCookies(context).also { success -> + if (success) preferences.setPreference(Keys.USERNAME, username) + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.java deleted file mode 100644 index 221c08cb5..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -import com.android.volley.VolleyError; -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.provider.AwfulProvider; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.thread.AwfulThread; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by Matthew on 8/9/13. - */ -public class MarkLastReadRequest extends AwfulRequest { - private int threadId, postIndex; - public MarkLastReadRequest(Context context, int threadId, int postIndex) { - super(context, null); - this.threadId = threadId; - this.postIndex = postIndex; - addPostParam(Constants.PARAM_ACTION, "setseen"); - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - addPostParam(Constants.PARAM_INDEX, Integer.toString(postIndex)); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_THREAD; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - //set unread posts (> unreadIndex) - ContentValues last_read = new ContentValues(); - last_read.put(AwfulPost.PREVIOUSLY_READ, 0); - ContentResolver resolv = getContentResolver(); - resolv.update(AwfulPost.CONTENT_URI, - last_read, - AwfulPost.THREAD_ID+"=? AND "+AwfulPost.POST_INDEX+">?", - AwfulProvider.int2StrArray(threadId, postIndex)); - - //set previously read posts (< unreadIndex) - last_read.put(AwfulPost.PREVIOUSLY_READ, 1); - resolv.update(AwfulPost.CONTENT_URI, - last_read, - AwfulPost.THREAD_ID+"=? AND "+AwfulPost.POST_INDEX+"<=?", - AwfulProvider.int2StrArray(threadId, postIndex)); - - //update unread count - Cursor threadData = resolv.query(ContentUris.withAppendedId(AwfulThread.CONTENT_URI, threadId), AwfulProvider.ThreadProjection, null, null, null); - if(threadData != null && threadData.moveToFirst()){ - ContentValues thread_update = new ContentValues(); - thread_update.put(AwfulThread.UNREADCOUNT, threadData.getInt(threadData.getColumnIndex(AwfulThread.POSTCOUNT)) - postIndex); - resolv.update(AwfulThread.CONTENT_URI, - thread_update, - AwfulThread.ID + "=?", - AwfulProvider.int2StrArray(threadId)); - } - if (threadData != null) { - threadData.close(); - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } - - @Override - protected VolleyError customizeProgressListenerError(VolleyError error) { - return new AwfulError("Failed to mark post!"); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt new file mode 100644 index 000000000..d1d136ee1 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt @@ -0,0 +1,62 @@ +package com.ferg.awfulapp.task + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri + +import com.android.volley.VolleyError +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.provider.AwfulProvider +import com.ferg.awfulapp.thread.AwfulPost +import com.ferg.awfulapp.thread.AwfulThread +import com.ferg.awfulapp.util.AwfulError +import com.ferg.awfulapp.util.toSqlBoolean + +import org.jsoup.nodes.Document + +/** + * An AwfulRequest that sets a given post as the last one read in a particular thread, updating + * the database to reflect this when the request is successful. + */ +class MarkLastReadRequest(context: Context, private val threadId: Int, private val postIndex: Int) : AwfulRequest(context, null) { + init { + addPostParam(Constants.PARAM_ACTION, "setseen") + addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) + addPostParam(Constants.PARAM_INDEX, Integer.toString(postIndex)) + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_THREAD + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + var cv = ContentValues() + with(contentResolver) { + fun where(greaterThan: Boolean) = + "${AwfulPost.THREAD_ID}=? AND ${AwfulPost.POST_INDEX} ${if (greaterThan) ">" else "<="}?" + + val params = listOf(threadId, postIndex).map(Int::toString).toTypedArray() + + // set later posts to unread, and this post (and all previous) to read + cv.put(AwfulPost.PREVIOUSLY_READ, false.toSqlBoolean) + update(AwfulPost.CONTENT_URI, cv, where(greaterThan = true), params) + + cv.put(AwfulPost.PREVIOUSLY_READ, true.toSqlBoolean) + update(AwfulPost.CONTENT_URI, cv, where(greaterThan = false), params) + + // update the thread's unread count + val threadData = query(ContentUris.withAppendedId(AwfulThread.CONTENT_URI, threadId.toLong()), AwfulProvider.ThreadProjection, null, null, null) + threadData?.use { cursor -> + if (cursor.moveToFirst()) { + val newPostCount = cursor.getInt(cursor.getColumnIndex(AwfulThread.POSTCOUNT)) - postIndex + cv = ContentValues().apply { put(AwfulThread.UNREADCOUNT, newPostCount) } + update(AwfulThread.CONTENT_URI, cv, AwfulThread.ID + "=?", arrayOf(threadId.toString())) + } + } + } + return null + } + + + override fun customizeProgressListenerError(error: VolleyError) = AwfulError("Failed to mark post!") +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.java deleted file mode 100644 index e5793d3f8..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.android.volley.VolleyError; -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.thread.AwfulThread; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by Matthew on 8/9/13. - */ -public class MarkUnreadRequest extends AwfulRequest { - private int threadId; - public MarkUnreadRequest(Context context, int threadId) { - super(context, null); - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - addPostParam(Constants.PARAM_ACTION, "resetseen"); - this.threadId = threadId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_THREAD; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - ContentValues last_read = new ContentValues(); - last_read.put(AwfulPost.PREVIOUSLY_READ, 0); - getContentResolver().update(AwfulPost.CONTENT_URI, last_read, AwfulPost.THREAD_ID+"=?", new String[]{Integer.toString(threadId)}); - ContentValues unread = new ContentValues(); - unread.put(AwfulThread.UNREADCOUNT, 0); - unread.put(AwfulThread.HAS_VIEWED_THREAD, 0); - getContentResolver().update(ContentUris.withAppendedId(AwfulThread.CONTENT_URI, threadId), unread, null, null); - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } - - @Override - protected VolleyError customizeProgressListenerError(VolleyError error) { - return new AwfulError("Failed to mark unread!"); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt new file mode 100644 index 000000000..0defaa3b1 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt @@ -0,0 +1,47 @@ +package com.ferg.awfulapp.task + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri + +import com.android.volley.VolleyError +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.thread.AwfulPost +import com.ferg.awfulapp.thread.AwfulThread +import com.ferg.awfulapp.util.AwfulError +import com.ferg.awfulapp.util.toSqlBoolean + +import org.jsoup.nodes.Document + +/** + * A request to mark a thread as unread. + */ +class MarkUnreadRequest(context: Context, private val threadId: Int) : AwfulRequest(context, null) { + init { + addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) + addPostParam(Constants.PARAM_ACTION, "resetseen") + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_THREAD + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + with (contentResolver) { + // set all posts in the thread as unread + val unreadPost = ContentValues().apply { put(AwfulPost.PREVIOUSLY_READ, false.toSqlBoolean) } + update(AwfulPost.CONTENT_URI, unreadPost, AwfulPost.THREAD_ID + "=?", arrayOf(threadId.toString())) + + // update the thread data to reflect an unread state + val unreadThread = ContentValues().apply { + put(AwfulThread.UNREADCOUNT, 0) + put(AwfulThread.HAS_VIEWED_THREAD, false.toSqlBoolean) + } + update(ContentUris.withAppendedId(AwfulThread.CONTENT_URI, threadId.toLong()), unreadThread, null, null) + } + return null + } + + + override fun customizeProgressListenerError(error: VolleyError) = AwfulError("Failed to mark unread!") +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.java deleted file mode 100644 index 3c921be48..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class PMListRequest extends AwfulRequest { - - int folder = 0; - - public PMListRequest(Context context, int folder) { - super(context, null); - this.folder = folder; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_PRIVATE_MESSAGE+"?"+Constants.PARAM_FOLDERID+"="+folder; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - AwfulMessage.processMessageList(getContentResolver(), doc, folder); - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt new file mode 100644 index 000000000..3660f1737 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt @@ -0,0 +1,25 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * A request that gets and updates the stored list of Private Messages in a particular [folder] + */ +class PMListRequest(context: Context, private val folder: Int = PRIVATE_MESSAGE_DEFAULT_FOLDER) + : AwfulRequest(context, null) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String = + "$FUNCTION_PRIVATE_MESSAGE?$PARAM_FOLDERID=$folder" + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + AwfulMessage.processMessageList(contentResolver, doc, folder) + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.java deleted file mode 100644 index 42343c045..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class PMReplyRequest extends AwfulRequest { - private int id; - public PMReplyRequest(Context context, int id) { - super(context, Constants.FUNCTION_PRIVATE_MESSAGE); - this.id = id; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "newmessage"); - urlBuilder.appendQueryParameter(Constants.PARAM_PRIVATE_MESSAGE_ID, Integer.toString(id)); - return urlBuilder.build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - ContentValues reply = AwfulMessage.processReplyMessage(doc, id); - //we remove the reply content so as not to override the existing reply. - String replyContent = reply.getAsString(AwfulMessage.REPLY_CONTENT); - reply.remove(AwfulMessage.REPLY_CONTENT); - String replyTitle = reply.getAsString(AwfulMessage.TITLE); - reply.remove(AwfulMessage.TITLE); - if(getContentResolver().update(ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_REPLY, id), reply, null, null)<1){ - //but if the reply doesn't already exist, re-add that reply and insert it. - reply.put(AwfulMessage.REPLY_CONTENT, replyContent); - reply.put(AwfulMessage.TITLE, replyTitle); - getContentResolver().insert(AwfulMessage.CONTENT_URI_REPLY, reply); - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt new file mode 100644 index 000000000..453644fb0 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt @@ -0,0 +1,44 @@ +package com.ferg.awfulapp.task + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.thread.AwfulMessage +import org.jsoup.nodes.Document + +/** + * A request that fetches and stores data for a reply to a Private Message. + * + * The request takes the [id] of the message being replied to, fetches the reply page + * from the site, and parses the username, title and reply contents from that page. + * This is stored in the database as a message draft, unless a draft for a reply to + * this PM already exists, in which case the title and reply contents are unchanged. + */ +class PMReplyRequest(context: Context, private val id: Int) : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String = with(urlBuilder!!) { + appendQueryParameter(PARAM_ACTION, "newmessage") + appendQueryParameter(PARAM_PRIVATE_MESSAGE_ID, id.toString()) + build().toString() + } + + override fun handleResponse(doc: Document): Void? { + // parse the reply data, and create a version that doesn't overwrite the title or reply content, for updating any existing draft + val newReply = AwfulMessage.processReplyMessage(doc, id) + val updateDraft = ContentValues(newReply).apply { + remove(AwfulMessage.TITLE) + remove(AwfulMessage.REPLY_CONTENT) + } + + // try and update the draft - if it fails, insert the full reply data (including title and message content) as a new draft + val currentDraft = ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_REPLY, id.toLong()) + if (contentResolver.update(currentDraft, updateDraft, null, null) < 1) { + // no update so the draft didn't exist + contentResolver.insert(AwfulMessage.CONTENT_URI_REPLY, newReply) + } + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.java deleted file mode 100644 index 165f10ff6..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class PMRequest extends AwfulRequest { - private int id; - public PMRequest(Context context, int id) { - super(context, Constants.FUNCTION_PRIVATE_MESSAGE); - this.id = id; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "show"); - urlBuilder.appendQueryParameter(Constants.PARAM_PRIVATE_MESSAGE_ID, Integer.toString(id)); - return urlBuilder.build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - ContentValues message = AwfulMessage.processMessage(doc, id); - if(getContentResolver().update(ContentUris.withAppendedId(AwfulMessage.CONTENT_URI, id), message, null, null)<1){ - getContentResolver().insert(AwfulMessage.CONTENT_URI, message); - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt new file mode 100644 index 000000000..4606a6ba2 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt @@ -0,0 +1,36 @@ +package com.ferg.awfulapp.task + +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.FUNCTION_PRIVATE_MESSAGE +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * A request that fetches a Private Message by its [id], and parses and stores + * its data in the database. + */ +class PMRequest(context: Context, private val id: Int) : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String = with (urlBuilder!!) { + appendQueryParameter(Constants.PARAM_ACTION, "show") + appendQueryParameter(Constants.PARAM_PRIVATE_MESSAGE_ID, id.toString()) + build().toString() + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + with (contentResolver) { + val message = AwfulMessage.processMessage(doc, id) + val messageUri = ContentUris.withAppendedId(AwfulMessage.CONTENT_URI, id.toLong()) + if (update(messageUri, message, null, null) < 1) { + insert(AwfulMessage.CONTENT_URI, message) + } + } + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PostRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PostRequest.java deleted file mode 100644 index 34f473c03..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PostRequest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulThread; -import com.ferg.awfulapp.util.AwfulError; - -import org.jetbrains.annotations.Nullable; -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/7/13. - */ -public class PostRequest extends AwfulStrippedRequest { - - public static final Object REQUEST_TAG = new Object(); - - private int threadId, page, userId; - public PostRequest(Context context, int threadId, int page, int userId) { - super(context, Constants.FUNCTION_THREAD); - this.threadId = threadId; - this.page = page; - this.userId = userId; - } - - - @Override - public Object getRequestTag() { - return REQUEST_TAG; - } - - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - urlBuilder.appendQueryParameter(Constants.PARAM_PER_PAGE, Integer.toString(getPreferences().postPerPage)); - urlBuilder.appendQueryParameter(Constants.PARAM_PAGE, Integer.toString(page)); - if(userId > 0){ - urlBuilder.appendQueryParameter(Constants.PARAM_USER_ID, Integer.toString(userId)); - } - return urlBuilder.build().toString(); - } - - @Override - protected Integer handleResponse(Document doc) throws AwfulError { - AwfulThread.parseThreadPage(getContentResolver(), doc, threadId, page, -1, getPreferences().postPerPage, getPreferences(), userId); - return page; - } - - @Override - public Integer handleStrippedResponse(Document doc, @Nullable Integer currentPage, @Nullable Integer totalPages) { - // TODO: this is all kinda janky, best to use the passed data from the response, right? Instead of relying on 'page' from the request - int lastPage = totalPages != null ? totalPages : page; - AwfulThread.parseThreadPage(getContentResolver(), doc, threadId, page, lastPage, getPreferences().postPerPage, getPreferences(), userId); - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.java deleted file mode 100644 index 3f34bde66..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.thread.PostPreviewParseTask; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import timber.log.Timber; - -/** - * Created by matt on 8/8/13. - */ -public class PreviewEditRequest extends AwfulRequest { - public PreviewEditRequest(Context context, ContentValues reply) { - super(context, null); - addPostParam(Constants.PARAM_ACTION, "updatepost"); - addPostParam(Constants.PARAM_POST_ID, Integer.toString(reply.getAsInteger(AwfulPost.EDIT_POST_ID))); - Timber.e(Constants.PARAM_POST_ID + ": " + Integer.toString(reply.getAsInteger(AwfulPost.EDIT_POST_ID))); - addPostParam(Constants.PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.REPLY_CONTENT))); - addPostParam(Constants.PARAM_PARSEURL, Constants.YES); - if(reply.containsKey(AwfulPost.FORM_BOOKMARK) && reply.getAsString(AwfulPost.FORM_BOOKMARK).equalsIgnoreCase("checked")){ - addPostParam(Constants.PARAM_BOOKMARK, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_SIGNATURE)){ - addPostParam(AwfulMessage.REPLY_SIGNATURE, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_DISABLE_SMILIES)){ - addPostParam(AwfulMessage.REPLY_DISABLE_SMILIES, Constants.YES); - } - addPostParam(Constants.PARAM_SUBMIT, Constants.SUBMIT_REPLY); - addPostParam(Constants.PARAM_PREVIEW, Constants.PREVIEW_REPLY); - - buildFinalRequest(); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_EDIT_POST; - } - - @Override - protected String handleResponse(Document doc) throws AwfulError { - return new PostPreviewParseTask(doc).call(); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - Timber.e(error); - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt new file mode 100644 index 000000000..15f662703 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt @@ -0,0 +1,50 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.thread.AwfulPost +import com.ferg.awfulapp.thread.PostPreviewParseTask +import org.jsoup.nodes.Document +import timber.log.Timber + +/** + * A request that fetches a preview of an edited post, providing the parsed HTML. + * + * This functions like the Preview button when editing a post on the site, which + * takes the current content in the edit window and renders it as HTML. You supply + * the data on the edit page via the ContentValues parameter, which is sent to + * the site to retrieve the preview. + */ +class PreviewEditRequest(context: Context, reply: ContentValues) : AwfulRequest(context, null) { + init { + with(reply) { + val postId = getAsInteger(AwfulPost.EDIT_POST_ID)?.toString() + ?: throw IllegalArgumentException("No post ID included") + addPostParam(PARAM_ACTION, "updatepost") + addPostParam(PARAM_POST_ID, postId) + Timber.i("$PARAM_POST_ID: $postId") + addPostParam(PARAM_MESSAGE, getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) + addPostParam(PARAM_PARSEURL, YES) + // TODO: this bookmarks every thread you edit a post in, unless you turn it off in a browser - seems bad for replies, worse for edits? + if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + addPostParam(PARAM_BOOKMARK, YES) + } + + listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) + .forEach { if (containsKey(it)) addPostParam(it, YES) } + addPostParam(PARAM_SUBMIT, SUBMIT_REPLY) + addPostParam(PARAM_PREVIEW, PREVIEW_REPLY) + } + + buildFinalRequest() + } + + override fun generateUrl(urlBuilder: Uri.Builder?) = FUNCTION_EDIT_POST + + override fun handleResponse(doc: Document): String = PostPreviewParseTask(doc).call() + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.java deleted file mode 100644 index 515413e38..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; -import android.widget.Toast; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.thread.PostPreviewParseTask; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import timber.log.Timber; - -/** - * Created by matt on 8/8/13. - */ -public class PreviewPostRequest extends AwfulRequest { - - // TODO: 18/12/2017 this and PreviewEditRequest are almost identical, merge 'em - - public PreviewPostRequest(Context context, ContentValues reply) { - super(context, null); - addPostParam(Constants.PARAM_ACTION, "postreply"); - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(reply.getAsInteger(AwfulMessage.ID))); - addPostParam(Constants.PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)); - addPostParam(Constants.PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)); - addPostParam(Constants.PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.REPLY_CONTENT))); - addPostParam(Constants.PARAM_PARSEURL, Constants.YES); - if(reply.containsKey(AwfulPost.FORM_BOOKMARK) && reply.getAsString(AwfulPost.FORM_BOOKMARK).equalsIgnoreCase("checked")){ - addPostParam(Constants.PARAM_BOOKMARK, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_SIGNATURE)){ - addPostParam(AwfulMessage.REPLY_SIGNATURE, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_DISABLE_SMILIES)){ - addPostParam(AwfulMessage.REPLY_DISABLE_SMILIES, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_ATTACHMENT)){ - Toast.makeText(context, "Attaching: " + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT), Toast.LENGTH_LONG).show(); - attachFile(Constants.PARAM_ATTACHMENT, reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)); - } - addPostParam(Constants.PARAM_SUBMIT, Constants.SUBMIT_REPLY); - addPostParam(Constants.PARAM_PREVIEW, Constants.PREVIEW_REPLY); - - buildFinalRequest(); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_POST_REPLY; - } - - @Override - protected String handleResponse(Document doc) throws AwfulError { - return new PostPreviewParseTask(doc).call(); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - Timber.e(error); - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt new file mode 100644 index 000000000..b666d33d4 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt @@ -0,0 +1,52 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.thread.AwfulPost +import com.ferg.awfulapp.thread.PostPreviewParseTask +import org.jsoup.nodes.Document + +/** + * A request that fetches a preview of a reply, providing the parsed HTML. + * + * This functions like the Preview button when composing a post on the site, which + * takes the current content in the post window and renders it as HTML. You supply + * the data on the edit page via the ContentValues parameter, which is sent to + * the site to retrieve the preview. + */ +class PreviewPostRequest (context: Context, reply: ContentValues) : AwfulRequest(context, null) { +// TODO: 18/12/2017 this and PreviewEditRequest are almost identical, merge 'em + init { + with(reply) { + val threadId = getAsInteger(AwfulMessage.ID)?.toString() + ?: throw IllegalArgumentException("No thread ID included") + addPostParam(PARAM_ACTION, "postreply") + addPostParam(PARAM_THREAD_ID, threadId) + addPostParam(PARAM_FORMKEY, getAsString(AwfulPost.FORM_KEY)) + addPostParam(PARAM_FORM_COOKIE, getAsString(AwfulPost.FORM_COOKIE)) + addPostParam(PARAM_MESSAGE, getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) + + addPostParam(PARAM_PARSEURL, YES) + // TODO: this bookmarks every thread you post in, unless you turn it off in a browser - seems bad? + if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + addPostParam(PARAM_BOOKMARK, YES) + } + listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) + .forEach { if (containsKey(it)) addPostParam(it, YES) } + + getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + addPostParam(PARAM_SUBMIT, SUBMIT_REPLY) + addPostParam(PARAM_PREVIEW, PREVIEW_REPLY) + } + buildFinalRequest() + } + + override fun generateUrl(urlBuilder: Uri.Builder?) = FUNCTION_POST_REPLY + + override fun handleResponse(doc: Document): String = PostPreviewParseTask(doc).call() + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ProfileRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ProfileRequest.java deleted file mode 100644 index 212ad6065..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ProfileRequest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; -import android.text.TextUtils; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.preferences.Keys; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -/** - * Created by matt on 8/8/13. - */ -public class ProfileRequest extends AwfulRequest { - private String username; - public ProfileRequest(Context context, String username) { - super(context, Constants.FUNCTION_MEMBER); - this.username = username; - } - - public ProfileRequest(Context context) { - this(context,null); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, Constants.ACTION_PROFILE); - urlBuilder.appendQueryParameter(Constants.PARAM_USERNAME, (TextUtils.isEmpty(username) || username.equals("0")) ? getPreferences().username : username); - return urlBuilder.build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - Element formkey = doc.getElementsByAttributeValue("name", "formkey").first(); - if (formkey != null) { - try { - getPreferences().setPreference(Keys.IGNORE_FORMKEY, formkey.val()); - } catch (Exception e) { - e.printStackTrace(); - } - } else { - throw new AwfulError("Profile page did not load"); - } - Element title = doc.getElementsByClass("title").first(); - if(null != title){ - String userTitle = ""; - Elements titleTags = title.getAllElements(); - for(Element tag : titleTags){ - String tagName = tag.tagName(); - if("br".equals(tagName)){ - break; - }else if("img".equals(tagName)){ - userTitle = tag.attr("src"); - break; - } - } - try { - getPreferences().setPreference(Keys.USER_TITLE, userTitle); - } catch (Exception e) { - e.printStackTrace(); - } - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.java deleted file mode 100644 index 4b4aed5b8..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.provider.DatabaseHelper; -import com.ferg.awfulapp.reply.Reply; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.sql.Timestamp; - -/** - * Created by matt on 8/8/13. - */ -public class QuoteRequest extends AwfulRequest { - private int threadId, postId; - public QuoteRequest(Context context, int threadId, int postId) { - super(context, Constants.FUNCTION_POST_REPLY); - this.threadId = threadId; - this.postId = postId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "newreply"); - urlBuilder.appendQueryParameter(Constants.PARAM_POST_ID, Integer.toString(postId)); - return urlBuilder.build().toString(); - } - - @Override - protected ContentValues handleResponse(Document doc) throws AwfulError { - ContentValues newReply = Reply.processQuote(doc, threadId, postId); - newReply.put(DatabaseHelper.UPDATED_TIMESTAMP, new Timestamp(System.currentTimeMillis()).toString()); - return newReply; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt new file mode 100644 index 000000000..b2ef3fd3e --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt @@ -0,0 +1,41 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.provider.DatabaseHelper +import com.ferg.awfulapp.reply.Reply +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document + +import java.sql.Timestamp + +/** + * Request the data you get by quoting a post on the main site. + * + * This provides you with the text and BBCode added to the reply composer, + * the form key and cookie (for authentication?) as well as any selected + * options (see [Reply.processQuote]) and a current timestamp. + */ +class QuoteRequest(context: Context, private val threadId: Int, private val postId: Int) + : AwfulRequest(context, Constants.FUNCTION_POST_REPLY) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with (urlBuilder!!) { + appendQueryParameter(Constants.PARAM_ACTION, "newreply") + appendQueryParameter(Constants.PARAM_POST_ID, postId.toString()) + return build().toString() + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): ContentValues { + return Reply.processQuote(doc, threadId, postId).apply { + put(DatabaseHelper.UPDATED_TIMESTAMP, Timestamp(System.currentTimeMillis()).toString()) + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt new file mode 100644 index 000000000..9eb58dadb --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt @@ -0,0 +1,52 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.preferences.AwfulPreferences +import com.ferg.awfulapp.preferences.Keys.IGNORE_FORMKEY +import com.ferg.awfulapp.preferences.Keys.USER_AVATAR_URL +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * Created by baka kaba on 21/12/18. + * + * Request to pull current data from the user's profile, and update the local app state. + * + * This currently updates the user's avatar URL, and the [AwfulPreferences.ignoreFormkey] used + * to validate (I guess!?) attempts to ignore a user. + */ +class RefreshUserProfileRequest(context: Context) : AwfulRequest(context, FUNCTION_MEMBER) { + + companion object { + private const val PROFILE_ID_FOR_THIS_USER = "0" + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!) { + appendQueryParameter(PARAM_ACTION, ACTION_PROFILE) + appendQueryParameter(PARAM_USER_ID, PROFILE_ID_FOR_THIS_USER) + return build().toString() + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + val formKey = doc.selectFirst("[name=formkey]") + ?: throw AwfulError("Couldn't read profile page") + preferences.setPreference(IGNORE_FORMKEY, formKey.`val`()) + + // TODO: set the username here, and have any "update username" actions use this request to do it + // the user's avatar (if any) is the image before the first
tag - + // any images after that are gang tags, extra images to make the avatar longer, etc + val avatarUrl = doc.selectFirst(".title") + ?.allElements + ?.takeWhile { it.tagName() != "br" } + ?.firstOrNull { it.tagName() == "img" } + ?.attr("src") + preferences.setPreference(USER_AVATAR_URL, avatarUrl ?: "") + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.java deleted file mode 100644 index d52e9ec99..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.provider.DatabaseHelper; -import com.ferg.awfulapp.reply.Reply; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.sql.Timestamp; - -/** - * Created by matt on 8/8/13. - */ -public class ReplyRequest extends AwfulRequest { - private int threadId; - public ReplyRequest(Context context, int threadId) { - super(context, Constants.FUNCTION_POST_REPLY); - this.threadId = threadId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, "newreply"); - urlBuilder.appendQueryParameter(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - return urlBuilder.build().toString(); - } - - @Override - protected ContentValues handleResponse(Document doc) throws AwfulError { - ContentValues newReply = Reply.processReply(doc, threadId); - newReply.put(DatabaseHelper.UPDATED_TIMESTAMP, new Timestamp(System.currentTimeMillis()).toString()); - return newReply; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt new file mode 100644 index 000000000..98f508042 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt @@ -0,0 +1,41 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.provider.DatabaseHelper +import com.ferg.awfulapp.reply.Reply +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document + +import java.sql.Timestamp + +/** + * Request the data you get when starting a new reply on the site. + * + * This provides you with any initial reply contents, the form key + * and cookie (for authentication?) as well as any selected + * options (see [Reply.processReply]) and a current timestamp. + */ +class ReplyRequest(context: Context, private val threadId: Int) + : AwfulRequest(context, Constants.FUNCTION_POST_REPLY) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!) { + appendQueryParameter(Constants.PARAM_ACTION, "newreply") + appendQueryParameter(Constants.PARAM_THREAD_ID, threadId.toString()) + return build().toString() + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): ContentValues { + return Reply.processReply(doc, threadId).apply { + put(DatabaseHelper.UPDATED_TIMESTAMP, Timestamp(System.currentTimeMillis()).toString()) + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.java deleted file mode 100644 index 75c94cd4d..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; - -/** - * Created by matt on 8/8/13. - */ -public class ReportRequest extends AwfulRequest { - private int postId; - private String mComments; - public ReportRequest(Context context, int postId, String mComments) { - super(context, null); - this.mComments = mComments; - this.postId = postId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - addPostParam(Constants.PARAM_COMMENTS, mComments); - addPostParam(Constants.PARAM_POST_ID, Integer.toString(postId)); - addPostParam(Constants.PARAM_ACTION, "submit"); - - return Constants.FUNCTION_REPORT; - } - - @Override - protected String handleResponse(Document doc) throws AwfulError { - - if(doc.getElementById("content") != null){ - Element standard = doc.getElementsByClass("standard").first(); - if(standard != null && standard.hasText()){ - if(standard.text().contains("Thank you, but this thread has already been reported recently!")){ - throw new AwfulError("Someone has already reported this thread recently"); - - }else if(standard.text().contains("Your alert has been submitted to the Moderators.")){ - return "Your alert has been submitted to the Moderators."; //"Thank you for your report"; - } - } - throw new AwfulError("An error occurred while trying to process your report"); - } - throw new AwfulError("An error occurred while trying to send your report"); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt new file mode 100644 index 000000000..28a047c3c --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt @@ -0,0 +1,44 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri + +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * A request that submits a report for a post. + * + * This sends a report to the mods for a given [postId], providing [comments] as + * the report reason. The response is a status message when the report was + * successful, otherwise an error is thrown. + */ +class ReportRequest(context: Context, private val postId: Int, private val comments: String) + : AwfulRequest(context, null) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + addPostParam(Constants.PARAM_COMMENTS, comments) + addPostParam(Constants.PARAM_POST_ID, postId.toString()) + addPostParam(Constants.PARAM_ACTION, "submit") + return Constants.FUNCTION_REPORT + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): String { + val message = doc.selectFirst("standard")?.takeIf(Element::hasText)?.text() + fun responseSays(text: String) = message?.contains(text, ignoreCase = true) ?: false + // TODO: when I tested this I got a toast with a different success message - no clue where it came from! + return when { + responseSays("your alert has been submitted") -> + "Report sent!" + responseSays("this thread has already been reported") -> + "Someone has already reported this thread recently" + else -> + throw AwfulError("An error occurred while trying to send your report") + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt new file mode 100644 index 000000000..945fd3d88 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt @@ -0,0 +1,21 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.thread.AwfulSearchForum +import org.jsoup.nodes.Document +import java.util.* + +/** + * Get a list of forums that can be searched, and their default selection states. + */ +class SearchForumsFilterRequest(context: Context) : AwfulRequest>(context, null) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_SEARCH + + override fun handleResponse(doc: Document): ArrayList { + return AwfulSearchForum.parseSearchForums(doc) + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsRequest.java deleted file mode 100644 index 5084febc5..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulSearchForum; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.util.ArrayList; - -/** - * Created by matt on 8/8/13. - */ -public class SearchForumsRequest extends AwfulRequest> { - public SearchForumsRequest(Context context) { - super(context, null); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_SEARCH; - } - - @Override - protected ArrayList handleResponse(Document doc) throws AwfulError { - return AwfulSearchForum.parseSearchForums(doc); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.java deleted file mode 100644 index 38427be89..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulSearch; -import com.ferg.awfulapp.thread.AwfulSearchResult; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class SearchRequest extends AwfulRequest { - public SearchRequest(Context context, String query, int[] forums) { - super(context, null); - addPostParam(Constants.PARAM_ACTION, Constants.ACTION_QUERY); - addPostParam(Constants.PARAM_QUERY, query); - if(forums !=null) { - for (int i = 0; i < forums.length; i++) { - addPostParam(String.format(Constants.PARAM_FORUMS, i), String.valueOf(forums[i])); - } - } - buildFinalRequest(); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_SEARCH; - } - - @Override - protected AwfulSearchResult handleResponse(Document doc) throws AwfulError { - AwfulSearchResult result = AwfulSearchResult.parseSearch(doc); - result.setResultList(AwfulSearch.parseSearchResult(doc)); - return result; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt new file mode 100644 index 000000000..110c2454f --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt @@ -0,0 +1,32 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.thread.AwfulSearch +import com.ferg.awfulapp.thread.AwfulSearchResult +import org.jsoup.nodes.Document + +/** + * Run a search query, optionally limiting the search to a set of forum IDs. + */ +class SearchRequest(context: Context, query: String, forums: IntArray?) + : AwfulRequest(context, null) { + init { + addPostParam(PARAM_ACTION, ACTION_QUERY) + addPostParam(PARAM_QUERY, query) + forums?.forEachIndexed { index, forumId -> + addPostParam(String.format(PARAM_FORUMS, index), forumId.toString()) + } + buildFinalRequest() + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_SEARCH + + override fun handleResponse(doc: Document): AwfulSearchResult { + return AwfulSearchResult.parseSearch(doc).apply { + resultList = AwfulSearch.parseSearchResult(doc) + } + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt new file mode 100644 index 000000000..26ca5dd0d --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt @@ -0,0 +1,33 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.thread.AwfulSearch +import org.jsoup.nodes.Document +import java.util.* + +/** + * Fetch a [page] of results from an existing search query by providing its [queryId]. + */ +class SearchResultPageRequest(context: Context, private val queryId: Int, private val page: Int) + : AwfulRequest>(context, FUNCTION_SEARCH) { + + init { + buildFinalRequest() + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!){ + appendQueryParameter(PARAM_ACTION, ACTION_RESULTS) + appendQueryParameter(PARAM_QID, queryId.toString()) + appendQueryParameter(PAGE, page.toString()) + return build().toString() + } + } + + override fun handleResponse(doc: Document): ArrayList { + return AwfulSearch.parseSearchResult(doc) + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultRequest.java deleted file mode 100644 index 0f5fb8b3e..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultRequest.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulSearch; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -import java.util.ArrayList; - -/** - * Created by matt on 8/8/13. - */ -public class SearchResultRequest extends AwfulRequest> { - - int mQuery, mPage; - - public SearchResultRequest(Context context, int query, int page) { - super(context, Constants.FUNCTION_SEARCH); - mQuery = query; - mPage = page; - - buildFinalRequest(); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, Constants.ACTION_RESULTS); - urlBuilder.appendQueryParameter(Constants.PARAM_QID, String.valueOf(mQuery)); - urlBuilder.appendQueryParameter(Constants.PAGE, String.valueOf(mPage)); - return urlBuilder.build().toString(); - } - - @Override - protected ArrayList handleResponse(Document doc) throws AwfulError { - return AwfulSearch.parseSearchResult(doc); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.java deleted file mode 100644 index 8dafd16da..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class SendEditRequest extends AwfulRequest { - private ContentValues replyData; - public SendEditRequest(Context context, ContentValues reply) { - super(context, null); - replyData = reply; - addPostParam(Constants.PARAM_ACTION, "updatepost"); - addPostParam(Constants.PARAM_THREAD_ID, reply.getAsString(AwfulMessage.ID)); - addPostParam(Constants.PARAM_POST_ID, reply.getAsString(AwfulPost.EDIT_POST_ID)); - //edits don't have form keys/cookies - //addPostParam(Constants.PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)); - //addPostParam(Constants.PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)); - addPostParam(Constants.PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.REPLY_CONTENT))); - addPostParam(Constants.PARAM_PARSEURL, Constants.YES); - if(reply.containsKey(AwfulPost.FORM_BOOKMARK) && reply.getAsString(AwfulPost.FORM_BOOKMARK).equalsIgnoreCase("checked")){ - addPostParam(Constants.PARAM_BOOKMARK, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_SIGNATURE)){ - addPostParam(AwfulMessage.REPLY_SIGNATURE, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_DISABLE_SMILIES)){ - addPostParam(AwfulMessage.REPLY_DISABLE_SMILIES, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_ATTACHMENT)){ - attachFile(Constants.PARAM_ATTACHMENT, reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)); - } - - buildFinalRequest(); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_EDIT_POST; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt new file mode 100644 index 000000000..aece22efd --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt @@ -0,0 +1,39 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.thread.AwfulPost +import org.jsoup.nodes.Document + +/** + * Submit a post edit described by a set of ContentValues + */ +class SendEditRequest(context: Context, reply: ContentValues) : AwfulRequest(context, null) { + // TODO: again this is v similar to the Edit/PostPreview requests - better to merge them? + // TODO: it's also probably neater to ditch the ContentValues and just use normal params instead of all this wrangling + init { + with(reply) { + addPostParam(PARAM_ACTION, "updatepost") + addPostParam(PARAM_POST_ID, getAsString(AwfulPost.EDIT_POST_ID)) + addPostParam(PARAM_MESSAGE, getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) + addPostParam(PARAM_PARSEURL, YES) + if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + addPostParam(PARAM_BOOKMARK, YES) + } + listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) + .forEach { key -> if (containsKey(key)) addPostParam(key, YES) } + getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + } + + buildFinalRequest() + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_EDIT_POST + + override fun handleResponse(doc: Document): Void? = null + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.java deleted file mode 100644 index c06f13680..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; -import android.widget.Toast; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.thread.AwfulPost; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/8/13. - */ -public class SendPostRequest extends AwfulRequest { - public SendPostRequest(Context context, ContentValues reply) { - super(context, null); - addPostParam(Constants.PARAM_ACTION, "postreply"); - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(reply.getAsInteger(AwfulMessage.ID))); - addPostParam(Constants.PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)); - addPostParam(Constants.PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)); - addPostParam(Constants.PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.REPLY_CONTENT))); - addPostParam(Constants.PARAM_PARSEURL, Constants.YES); - if(reply.containsKey(AwfulPost.FORM_BOOKMARK) && reply.getAsString(AwfulPost.FORM_BOOKMARK).equalsIgnoreCase("checked")){ - addPostParam(Constants.PARAM_BOOKMARK, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_SIGNATURE)){ - addPostParam(AwfulMessage.REPLY_SIGNATURE, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_DISABLE_SMILIES)){ - addPostParam(AwfulMessage.REPLY_DISABLE_SMILIES, Constants.YES); - } - if(reply.containsKey(AwfulMessage.REPLY_ATTACHMENT)){ - Toast.makeText(context, "Attaching: " + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT), Toast.LENGTH_LONG).show(); - attachFile(Constants.PARAM_ATTACHMENT, reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)); - } - - buildFinalRequest(); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_POST_REPLY; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt new file mode 100644 index 000000000..045ea0260 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt @@ -0,0 +1,39 @@ +package com.ferg.awfulapp.task + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.thread.AwfulMessage +import com.ferg.awfulapp.thread.AwfulPost +import org.jsoup.nodes.Document + +/** + * Submit a post described by a set of ContentValues + */ +class SendPostRequest(context: Context, reply: ContentValues) : AwfulRequest(context, null) { + init { + with(reply) { + addPostParam(PARAM_ACTION, "postreply") + addPostParam(PARAM_THREAD_ID, Integer.toString(getAsInteger(AwfulMessage.ID)!!)) + addPostParam(PARAM_FORMKEY, getAsString(AwfulPost.FORM_KEY)) + addPostParam(PARAM_FORM_COOKIE, getAsString(AwfulPost.FORM_COOKIE)) + addPostParam(PARAM_MESSAGE, NetworkUtils.encodeHtml(getAsString(AwfulMessage.REPLY_CONTENT))) + addPostParam(PARAM_PARSEURL, YES) + if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + addPostParam(PARAM_BOOKMARK, YES) + } + listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) + .forEach { key -> if (containsKey(key)) addPostParam(key, YES) } + getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + } + + buildFinalRequest() + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_POST_REPLY + + override fun handleResponse(doc: Document): Void? = null + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.java deleted file mode 100644 index 5b5b270b1..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.widget.Toast; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.network.NetworkUtils; -import com.ferg.awfulapp.provider.AwfulProvider; -import com.ferg.awfulapp.thread.AwfulMessage; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 9/11/13. - */ -public class SendPrivateMessageRequest extends AwfulRequest { - private int pmId; - public SendPrivateMessageRequest(Context context, int replyId) { - super(context, Constants.FUNCTION_PRIVATE_MESSAGE); - pmId = replyId; - //TODO note: this is a quick-and-dirty conversion to the new request system. - //we need to revamp this draft system to match the changes made in the post reply system. - Cursor pmInfo = context.getContentResolver().query(ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_REPLY, replyId), AwfulProvider.DraftProjection, null, null, null); - if (pmInfo != null && pmInfo.moveToFirst()) { - addPostParam(Constants.PARAM_PRIVATE_MESSAGE_ID, Integer.toString(replyId)); - addPostParam(Constants.PARAM_ACTION, Constants.ACTION_DOSEND); - addPostParam(Constants.DESTINATION_TOUSER, pmInfo.getString(pmInfo.getColumnIndex(AwfulMessage.RECIPIENT))); - addPostParam(Constants.PARAM_TITLE, NetworkUtils.encodeHtml(pmInfo.getString(pmInfo.getColumnIndex(AwfulMessage.TITLE)))); - if (replyId > 0) { - addPostParam("prevmessageid", Integer.toString(replyId)); - } - addPostParam(Constants.PARAM_PARSEURL, Constants.YES); - addPostParam("savecopy", "yes"); - addPostParam("iconid", "0"); - addPostParam(Constants.PARAM_MESSAGE, NetworkUtils.encodeHtml(pmInfo.getString(pmInfo.getColumnIndex(AwfulMessage.REPLY_CONTENT)))); - } else { - Toast.makeText(context, "Unable to send private message!", Toast.LENGTH_LONG).show(); - } - if (pmInfo != null) { - pmInfo.close(); - } - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - return Constants.FUNCTION_PRIVATE_MESSAGE; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - getContentResolver().delete(AwfulMessage.CONTENT_URI, AwfulMessage.ID+"=?", AwfulProvider.int2StrArray(pmId)); - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt new file mode 100644 index 000000000..be3eafe58 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt @@ -0,0 +1,52 @@ +package com.ferg.awfulapp.task + +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.widget.Toast +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.network.NetworkUtils +import com.ferg.awfulapp.provider.AwfulProvider +import com.ferg.awfulapp.thread.AwfulMessage +import org.jsoup.nodes.Document + +/** + * Submit a Private Message. + * + * This takes the ID of a private message draft stored in the database, and uses that data to + * submit the message. + */ +class SendPrivateMessageRequest(context: Context, private val pmId: Int) + : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { + init { + // TODO: do this extraction elsewhere, handle failure there, just pass in valid data + // TODO: pmId is being used as a draft ID AND the ID of a PM you're replying to ("prevmessageid")??? what's that about + // try and extract a stored draft with the given ID + val uri = ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_REPLY, pmId.toLong()) + val storedDraft = contentResolver.query(uri, AwfulProvider.DraftProjection, null, null, null) + var failed = true + + storedDraft?.takeIf(Cursor::moveToFirst)?.apply { + addPostParam(PARAM_ACTION, ACTION_DOSEND) + addPostParam(DESTINATION_TOUSER, getString(getColumnIndex(AwfulMessage.RECIPIENT))) + addPostParam(PARAM_TITLE, getString(getColumnIndex(AwfulMessage.TITLE)).run(NetworkUtils::encodeHtml)) + if (pmId > 0) addPostParam("prevmessageid", pmId.toString()) + addPostParam(PARAM_PARSEURL, YES) + addPostParam("savecopy", YES) + addPostParam("iconid", "0") // we don't have an icon picker yet, so use the default + addPostParam(PARAM_MESSAGE, getString(getColumnIndex(AwfulMessage.REPLY_CONTENT)).run(NetworkUtils::encodeHtml)) + failed = false + } + storedDraft?.close() + if (failed) Toast.makeText(context, "Unable to send private message!", Toast.LENGTH_LONG).show() + } + + override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_PRIVATE_MESSAGE + + override fun handleResponse(doc: Document): Void? { + contentResolver.delete(AwfulMessage.CONTENT_URI, "${AwfulMessage.ID}=?", arrayOf(pmId.toString())) + return null + } + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.java deleted file mode 100644 index fc83a0524..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.thread.AwfulThread; -import com.ferg.awfulapp.util.AwfulError; -import com.ferg.awfulapp.util.AwfulUtils; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; - -/** - * Created by matt on 8/7/13. - */ -public class SinglePostRequest extends AwfulRequest { - - public static final Object REQUEST_TAG = new Object(); - - private String postId; - public SinglePostRequest(Context context, String postId) { - super(context, Constants.FUNCTION_THREAD); - this.postId = postId; - } - - - @Override - public Object getRequestTag() { - return REQUEST_TAG; - } - - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, Constants.ACTION_SHOWPOST); - urlBuilder.appendQueryParameter(Constants.PARAM_POST_ID, this.postId); - return urlBuilder.build().toString(); - } - - @Override - protected String handleResponse(Document doc) throws AwfulError { - Element postbody = doc.getElementsByClass("postbody").first(); - return postbody.html(); - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt new file mode 100644 index 000000000..274bb7d9a --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt @@ -0,0 +1,37 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * Fetch and parse a single post, using the given [postId]. + * + * This returns the post content as raw HTML. + */ +class SinglePostRequest(context: Context, private val postId: String) + : AwfulRequest(context, FUNCTION_THREAD) { + + + override val requestTag: Any + get() = REQUEST_TAG + + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!) { + appendQueryParameter(PARAM_ACTION, ACTION_SHOWPOST) + appendQueryParameter(PARAM_POST_ID, postId) + return build().toString() + } + } + + override fun handleResponse(doc: Document): String = + doc.selectFirst(".postbody")?.html() ?: throw AwfulError("Couldn't find post content") + + + companion object { + private val REQUEST_TAG = Any() + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.java deleted file mode 100644 index 04b4a7aaf..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.Nullable; - -import com.ferg.awfulapp.announcements.AnnouncementsManager; -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.messages.PmManager; -import com.ferg.awfulapp.thread.AwfulForum; -import com.ferg.awfulapp.thread.AwfulPagedItem; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by matt on 8/7/13. - */ -public class ThreadListRequest extends AwfulStrippedRequest { - - public static final Object REQUEST_TAG = new Object(); - - private int forumId, page; - - public ThreadListRequest(Context context, int forumId, int page) { - // TODO: 19/09/2016 decide whether to handle all USERCP requests as bookmark urls (and do the PmManager calls a different way) - // I'm so sorry, thanks for forcing me to do this JAVA - super(context, forumId != Constants.USERCP_ID ? Constants.FUNCTION_FORUM - : page == 1 ? Constants.FUNCTION_USERCP - : Constants.FUNCTION_BOOKMARK); - this.forumId = forumId; - this.page = page; - } - - - @Override - public Object getRequestTag() { - return REQUEST_TAG; - } - - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - if (forumId != Constants.USERCP_ID) { - urlBuilder.appendQueryParameter(Constants.PARAM_FORUM_ID, Integer.toString(forumId)); - } - urlBuilder.appendQueryParameter(Constants.PARAM_PAGE, Integer.toString(page)); - return urlBuilder.build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - int lastPage = AwfulPagedItem.parseLastPage(doc); - handleStrippedResponse(doc, page, lastPage); - return null; - } - - @Override - Void handleStrippedResponse(Document doc, @Nullable Integer currentPage, @Nullable Integer totalPages) throws AwfulError { - int thisPage = (currentPage == null) ? page : currentPage; - int lastPage = (totalPages == null) ? thisPage : totalPages; - // TODO: legacy try/catch - work out what this is meant to be catching exactly, and if we can ditch it - try { - if (forumId == Constants.USERCP_ID) { - AwfulForum.parseUCPThreads(doc, page, lastPage, getContentResolver()); - PmManager.parseUcpPage(doc); - } else { - AwfulForum.parseThreads(forumId, page, lastPage, doc, getContentResolver()); - AnnouncementsManager.getInstance().parseForumPage(doc); - } - } catch (Exception e) { - e.printStackTrace(); - throw new AwfulError(); - } - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt new file mode 100644 index 000000000..b3ca99f29 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt @@ -0,0 +1,74 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.announcements.AnnouncementsManager +import com.ferg.awfulapp.constants.Constants.* +import com.ferg.awfulapp.messages.PmManager +import com.ferg.awfulapp.thread.AwfulForum +import com.ferg.awfulapp.thread.AwfulPagedItem +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * Fetch the threads on a certain [page] of the forum with the given [forumId]. + * + * This loads and parses the page, updating the cache database as appropriate, + * depending on whether a normal or bookmarks page was loaded. + * + * This request also hands the page off to other parsers, e.g. for announcements + * and private messages, to scrape any updated information the page contains. + */ +class ThreadListRequest(context: Context, private val forumId: Int, private val page: Int) + : AwfulStrippedRequest(context, when { + forumId != USERCP_ID -> FUNCTION_FORUM + page == 1 -> FUNCTION_USERCP + else -> FUNCTION_BOOKMARK + }) { +// TODO: 19/09/2016 decide whether to handle all USERCP requests as bookmark urls (and do the PmManager calls a different way) + + override val requestTag: Any + get() = REQUEST_TAG + + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + with(urlBuilder!!) { + if (forumId != USERCP_ID) appendQueryParameter(PARAM_FORUM_ID, forumId.toString()) + appendQueryParameter(PARAM_PAGE, page.toString()) + return build().toString() + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): Void? { + val lastPage = AwfulPagedItem.parseLastPage(doc) + handleStrippedResponse(doc, page, lastPage) + return null + } + + @Throws(AwfulError::class) + override fun handleStrippedResponse(document: Document, currentPage: Int?, totalPages: Int?): Void? { + val thisPage = currentPage ?: page + val lastPage = totalPages ?: thisPage + // TODO: legacy try/catch - work out what this is meant to be catching exactly, and if we can ditch it + try { + // parse the threads on the page, and also check for announcements/PMs depending on where they appear + if (forumId == USERCP_ID) { + AwfulForum.parseUCPThreads(document, page, lastPage, contentResolver) + PmManager.parseUcpPage(document) + } else { + AwfulForum.parseThreads(forumId, page, lastPage, document, contentResolver) + AnnouncementsManager.getInstance().parseForumPage(document) + } + } catch (e: Exception) { + e.printStackTrace() + throw AwfulError() + } + return null + } + + + companion object { + val REQUEST_TAG = Any() + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.java deleted file mode 100644 index cbb4da946..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -public class ThreadLockUnlockRequest extends AwfulRequest { - private int threadId; - public ThreadLockUnlockRequest(Context context, int threadId) { - super(context, Constants.FUNCTION_POSTINGS); - this.threadId = threadId; - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)); - - urlBuilder.appendQueryParameter(Constants.PARAM_ACTION, Constants.ACTION_TOGGLE_THREAD_LOCKED); - return urlBuilder.build().toString(); - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt new file mode 100644 index 000000000..dcb201017 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt @@ -0,0 +1,24 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants.* +import org.jsoup.nodes.Document + +/** + * Attempts to toggle the locked/unlocked state of a thread. + */ +class ThreadLockUnlockRequest(context: Context, private val threadId: Int) + : AwfulRequest(context, FUNCTION_POSTINGS) { + + override fun generateUrl(urlBuilder: Uri.Builder?): String { + addPostParam(PARAM_THREAD_ID, threadId.toString()) + with(urlBuilder!!) { + appendQueryParameter(PARAM_ACTION, ACTION_TOGGLE_THREAD_LOCKED) + return build().toString() + } + } + + override fun handleResponse(doc: Document): Void? = null + +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt new file mode 100644 index 000000000..0e763d330 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt @@ -0,0 +1,53 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.thread.AwfulThread +import org.jsoup.nodes.Document + +/** + * A request to fetch and parse the data on a thread page, updating the database with the results. + * + * Supplying a valid [userId] will fetch from the "show posts by this user" thread view, i.e. the + * thread will consist entirely of their posts, and will have as many pages as those posts can fill. + * + * Currently this overwrites the "normal" thread view in the database, so viewing page 1 of a thread, + * and then page 1 with a given [userId] will result in page 1 being fully or partially overwritten + * with that user's posts, depending on how many there are. This is only a problem when viewing the + * cached data (since usually the page will be reloaded and rewritten when you view it) but it's + * something to be aware of. + */ +class ThreadPageRequest(context: Context, private val threadId: Int, private val page: Int, private val userId: Int = 0) + : AwfulStrippedRequest(context, Constants.FUNCTION_THREAD) { + + + override val requestTag: Any + get() = REQUEST_TAG + + + override fun generateUrl(urlBuilder: Uri.Builder?): String = with(urlBuilder!!) { + appendQueryParameter(Constants.PARAM_THREAD_ID, threadId.toString()) + appendQueryParameter(Constants.PARAM_PER_PAGE, preferences.postPerPage.toString()) + appendQueryParameter(Constants.PARAM_PAGE, page.toString()) + if (userId > 0) appendQueryParameter(Constants.PARAM_USER_ID, userId.toString()) + build().toString() + } + + override fun handleResponse(doc: Document): Void? { + AwfulThread.parseThreadPage(contentResolver, doc, threadId, page, -1, preferences.postPerPage, preferences, userId) + return null + } + + public override fun handleStrippedResponse(doc: Document, currentPage: Int?, totalPages: Int?): Void? { + // TODO: this is all kinda janky, best to use the passed data from the response, right? Instead of relying on 'page' from the request + val lastPage = totalPages ?: page + AwfulThread.parseThreadPage(contentResolver, doc, threadId, page, lastPage, preferences.postPerPage, preferences, userId) + return null + } + + + companion object { + val REQUEST_TAG = Any() + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.java b/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.java deleted file mode 100644 index 6dfffd9c5..000000000 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ferg.awfulapp.task; - -import android.content.Context; -import android.net.Uri; - -import com.android.volley.VolleyError; -import com.ferg.awfulapp.R; -import com.ferg.awfulapp.constants.Constants; -import com.ferg.awfulapp.util.AwfulError; - -import org.jsoup.nodes.Document; - -/** - * Created by Matthew on 8/9/13. - */ -public class VoteRequest extends AwfulRequest { - public VoteRequest(Context context, int threadId, int vote) { - super(context, null); - addPostParam(Constants.PARAM_THREAD_ID, String.valueOf(threadId)); - addPostParam(Constants.PARAM_VOTE, String.valueOf(vote + 1)); - } - - @Override - protected String generateUrl(Uri.Builder urlBuilder) { - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_RATE_THREAD; - } - - @Override - protected Void handleResponse(Document doc) throws AwfulError { - //nothin a doin' here, if we fail we'll see in the failed callback - return null; - } - - @Override - protected boolean handleError(AwfulError error, Document doc) { - return error.isCritical(); - } - - @Override - protected VolleyError customizeProgressListenerError(VolleyError error) { - return new AwfulError(getContext().getString(R.string.vote_failed)); - } -} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt new file mode 100644 index 000000000..c4eeb15a7 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt @@ -0,0 +1,28 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import android.net.Uri + +import com.android.volley.VolleyError +import com.ferg.awfulapp.R +import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.util.AwfulError + +import org.jsoup.nodes.Document + +/** + * A request that submits a rating for a thread. + */ +class VoteRequest(context: Context, threadId: Int, vote: Int) : AwfulRequest(context, null) { + init { + addPostParam(Constants.PARAM_THREAD_ID, threadId.toString()) + addPostParam(Constants.PARAM_VOTE, vote.toString()) + } + + override fun generateUrl(urlBuilder: Uri.Builder?) = Constants.FUNCTION_RATE_THREAD + + override fun handleResponse(doc: Document): Void? = null + + override fun customizeProgressListenerError(error: VolleyError): VolleyError = + AwfulError(context.getString(R.string.vote_failed)) +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/users/LepersColonyFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/users/LepersColonyFragment.kt index af0f3c0d3..4f5c336a7 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/users/LepersColonyFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/users/LepersColonyFragment.kt @@ -191,8 +191,8 @@ class LepersColonyFragment : AwfulFragment(), SwipyRefreshLayout.OnRefreshListen val request = LepersColonyRequest(context, navigationState.page, navigationState.userId?.toString()) .build(this, object : AwfulRequest.AwfulResultCallback { - override fun success(result: LepersColonyRequest.LepersColonyPage?) { - result?.run { + override fun success(result: LepersColonyRequest.LepersColonyPage) { + result.run { lastPage = totalPages pageBar.updatePagePosition(navigationState.page, lastPage) showData(punishments) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulExtensions.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulExtensions.kt index 735ecdb54..8009c0f07 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulExtensions.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/util/AwfulExtensions.kt @@ -31,4 +31,17 @@ fun Intent.tryGetIntExtra(key: String): Int? = this::getIntExtra.tryGet(key, MIS private fun ((String, T) -> T).tryGet(key: String, missing: T): T? { this(key, missing).let { result -> return if (result == missing) null else result } -} \ No newline at end of file +} + + +/* + Database/content values stuff + */ + +/** This Boolean's SQLite representation (an int) */ +val Boolean.toSqlBoolean: Int + get() = if (this) 1 else 0 + +/** This Int's Boolean equivalent, when treated as a SQLite boolean */ +val Int.fromSqlBoolean: Boolean + get() = this >= 1 \ No newline at end of file From d9db1a4a8896ba33a9e17bd3f4af47242f716abd Mon Sep 17 00:00:00 2001 From: baka kaba Date: Sun, 23 Dec 2018 18:44:59 +0000 Subject: [PATCH 02/17] Rework AwfulRequests' POST/GET handling, update Kotlin to 1.3.11 Now there's no need to mess around building URLs or whatever - you pass a base URL into the constructor, specify that it's a POST request if necessary (it defaults to GET), and then add any parameters to the "parameters" member object. The request will handle generating the URL or building a Multipart Entity as appropriate, so the Request classes can be a lot cleaner and simpler --- Awful.apk/build.gradle | 2 +- .../java/com/ferg/awfulapp/EmoteFragment.kt | 2 +- .../com/ferg/awfulapp/forums/CrawlerTask.kt | 9 +- .../awfulapp/forums/DropdownParserTask.kt | 7 +- .../com/ferg/awfulapp/forums/UpdateTask.kt | 11 +- .../awfulapp/task/AnnouncementsRequest.kt | 13 +- .../com/ferg/awfulapp/task/AwfulRequest.kt | 280 ++++++++++-------- .../awfulapp/task/BookmarkColorRequest.kt | 23 +- .../com/ferg/awfulapp/task/BookmarkRequest.kt | 20 +- .../com/ferg/awfulapp/task/EditRequest.kt | 17 +- .../com/ferg/awfulapp/task/EmoteRequest.kt | 15 +- .../com/ferg/awfulapp/task/FeatureRequest.kt | 12 +- .../com/ferg/awfulapp/task/IgnoreRequest.kt | 24 +- .../ferg/awfulapp/task/IndexIconRequest.kt | 9 +- .../ferg/awfulapp/task/LepersColonyRequest.kt | 15 +- .../com/ferg/awfulapp/task/LoginRequest.kt | 20 +- .../ferg/awfulapp/task/MarkLastReadRequest.kt | 17 +- .../ferg/awfulapp/task/MarkUnreadRequest.kt | 16 +- .../com/ferg/awfulapp/task/PMListRequest.kt | 9 +- .../com/ferg/awfulapp/task/PMReplyRequest.kt | 19 +- .../java/com/ferg/awfulapp/task/PMRequest.kt | 17 +- .../ferg/awfulapp/task/PreviewEditRequest.kt | 31 +- .../ferg/awfulapp/task/PreviewPostRequest.kt | 34 +-- .../com/ferg/awfulapp/task/QuoteRequest.kt | 17 +- .../task/RefreshUserProfileRequest.kt | 10 +- .../com/ferg/awfulapp/task/ReplyRequest.kt | 17 +- .../com/ferg/awfulapp/task/ReportRequest.kt | 18 +- .../task/SearchForumsFilterRequest.kt | 13 +- .../com/ferg/awfulapp/task/SearchRequest.kt | 15 +- .../awfulapp/task/SearchResultPageRequest.kt | 19 +- .../com/ferg/awfulapp/task/SendEditRequest.kt | 28 +- .../com/ferg/awfulapp/task/SendPostRequest.kt | 33 +-- .../task/SendPrivateMessageRequest.kt | 24 +- .../ferg/awfulapp/task/SinglePostRequest.kt | 10 +- .../ferg/awfulapp/task/ThreadListRequest.kt | 10 +- .../awfulapp/task/ThreadLockUnlockRequest.kt | 10 +- .../ferg/awfulapp/task/ThreadPageRequest.kt | 23 +- .../com/ferg/awfulapp/task/VoteRequest.kt | 16 +- 38 files changed, 432 insertions(+), 453 deletions(-) diff --git a/Awful.apk/build.gradle b/Awful.apk/build.gradle index 1193df282..1c4254a05 100644 --- a/Awful.apk/build.gradle +++ b/Awful.apk/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.2.71' + ext.kotlin_version = '1.3.11' repositories { google() diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt index f0149003a..aeef31715 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/EmoteFragment.kt @@ -98,7 +98,7 @@ class EmotePicker : DialogFragment() { viewPager!!.adapter = this tabLayout!!.setupWithViewPager(viewPager) pages.forEachIndexed { i, page -> - tabLayout.getTabAt(i)!!.setIcon(page.iconResId).contentDescription = page.title + tabLayout!!.getTabAt(i)!!.setIcon(page.iconResId).contentDescription = page.title } } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt index 1a5597008..24c258bb2 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/CrawlerTask.kt @@ -112,10 +112,7 @@ internal class CrawlerTask(context: Context, priority: Priority) : UpdateTask(co /** * A request that fetches the main forums page and parses it for sections (Main etc) */ - private inner class MainForumRequest : UpdateTask.ForumParseTask() { - - override val url: String - get() = BASE_URL + private inner class MainForumRequest : UpdateTask.ForumParseTask(BASE_URL) { override fun onRequestSucceeded(doc: Document) { Timber.i("Parsing main page") @@ -131,10 +128,10 @@ internal class CrawlerTask(context: Context, priority: Priority) : UpdateTask(co /** * A request that fetches a forum page and parses it for subforum data * - * This loads a [url] representing a [forum], and parses the resulting page, adding the data to + * This loads a URL representing a [forum], and parses the resulting page, adding the data to * the [forum] object. */ - private inner class ParseSubforumsRequest(private val forum: Forum, override val url: String) : UpdateTask.ForumParseTask() { + private inner class ParseSubforumsRequest(private val forum: Forum, url: String) : UpdateTask.ForumParseTask(url) { override fun onRequestSucceeded(doc: Document) { parseSubforums(forum, doc) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt index 1a357785d..b604a740e 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/DropdownParserTask.kt @@ -28,10 +28,11 @@ internal class DropdownParserTask(context: Context) : UpdateTask(context) { private val parsedForums = ArrayList() - private inner class DropdownParseRequest : UpdateTask.ForumParseTask() { + private inner class DropdownParseRequest : UpdateTask.ForumParseTask(FUNCTION_FORUM) { - override val url: String - get() = "$FUNCTION_FORUM?$PARAM_FORUM_ID=$FORUM_ID_GOLDMINE" + init { + parameters.add(PARAM_FORUM_ID, FORUM_ID_GOLDMINE.toString()) + } override fun onRequestSucceeded(doc: Document) { Timber.i("Got page - parsing dropdown to get forum hierarchy") diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt index 768429902..2ca2a35bd 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/forums/UpdateTask.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.forums import android.content.Context -import android.net.Uri import android.support.annotation.WorkerThread import com.ferg.awfulapp.constants.Constants.DEBUG import com.ferg.awfulapp.forums.UpdateTask.ResultListener @@ -216,15 +215,15 @@ internal abstract class UpdateTask(protected val context: Context, private val t * Abstract superclass ensuring [.finishTask] is always called appropriately */ @WorkerThread - protected abstract inner class ForumParseTask : AwfulRequest(context, null) { + protected abstract inner class ForumParseTask(url: String) : AwfulRequest(context, url) { /** * The url of the page to retrieve, which is returned in the handle* methods */ - abstract val url: String - - - override fun generateUrl(urlBuilder: Uri.Builder?): String = url +// abstract val url: String +// +// +// override fun generateUrl(urlBuilder: Uri.Builder?): String = url // TODO: request errors aren't being handled properly, e.g. failed page loads when you're not logged in don't call here, and the task times out diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt index 5aff0b5a3..7d12ae866 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AnnouncementsRequest.kt @@ -1,8 +1,7 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.FUNCTION_ANNOUNCEMENTS import com.ferg.awfulapp.preferences.AwfulPreferences import com.ferg.awfulapp.thread.AwfulPost import com.ferg.awfulapp.thread.AwfulPost.tryConvertToHttps @@ -23,7 +22,12 @@ import java.util.* * of the usual page elements, and only a few properties set in AwfulPost. */ @JvmSuppressWildcards -class AnnouncementsRequest(context: Context) : AwfulRequest>(context, null) { +class AnnouncementsRequest(context: Context) + : AwfulRequest>(context, FUNCTION_ANNOUNCEMENTS) { + + init { + parameters.add("forumid", "1") + } private fun parseAnnouncement(aThread: Document): List { val results = ArrayList() @@ -88,9 +92,6 @@ class AnnouncementsRequest(context: Context) : AwfulRequest { return parseAnnouncement(doc) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt index 78aa05bd8..31b97521a 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/AwfulRequest.kt @@ -5,6 +5,7 @@ import android.content.Context import android.net.Uri import android.os.Handler import android.os.Looper +import android.support.annotation.UiThread import android.widget.Toast import com.android.volley.* import com.android.volley.toolbox.HttpHeaderParser @@ -15,6 +16,8 @@ import com.ferg.awfulapp.constants.Constants.BASE_URL import com.ferg.awfulapp.constants.Constants.SITE_HTML_ENCODING import com.ferg.awfulapp.network.NetworkUtils import com.ferg.awfulapp.preferences.AwfulPreferences +import com.ferg.awfulapp.task.AwfulRequest.Parameters.GetParams +import com.ferg.awfulapp.task.AwfulRequest.Parameters.PostParams import com.ferg.awfulapp.util.AwfulError import org.apache.http.HttpEntity import org.apache.http.entity.ContentType @@ -28,119 +31,121 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException -import java.util.* /** - * Created by Matt Shepard on 8/7/13. + * Base class for requests to the Something Awful forums site, with HTML response and error handling. + * + * You can create a request by subclassing this, specifying the [baseUrl] you want to call + * (and [isPostRequest] if necessary), and then adding data to the [parameters] object to define + * the GET/POST parameters, any attachments etc. To send the request, call [build] with any required + * callback listeners, and pass the resulting [Request] to a Volley queue (e.g. through + * [NetworkUtils.queueRequest]. + * + * The only method you need to implement is [handleResponse], where you process the response's HTML + * [Document] and produce a return value for any result listeners. If you don't actually need to + * do anything with the response (e.g. a fire-and-forget message to the site) you can just set [T] + * to a nullable type (Void? makes most sense if there's no meaningful result) and return null here. + * + * [handleError] and [customizeProgressListenerError] both have default implementations, but can be + * overridden for special error handling and creating custom notification messages. You probably + * won't want to change [handleError] in most cases, and if you do add any handler code (e.g. if + * a network failure requires the app to update some state) you'll probably just want to call the + * super method to get the standard error handling when you're done. */ -abstract class AwfulRequest(protected val context: Context, private val baseUrl: String?) { +abstract class AwfulRequest(protected val context: Context, private val baseUrl: String, private val isPostRequest: Boolean = false) { private val handler: Handler = Handler(Looper.getMainLooper()) - private var params: MutableMap? = null - private var attachParams: MultipartEntityBuilder? = MultipartEntityBuilder.create() - private var httpEntity: HttpEntity? = null private var progressListener: ProgressListener? = null + /** + * Represents parameters to be added to the final request. + * The concrete type depends on whether this is a GET or POST request. + */ + protected val parameters: Parameters - open val requestTag: Any - get() = REQUEST_TAG + init { + parameters = if (isPostRequest) PostParams() else GetParams() + } - protected val preferences: AwfulPreferences - get() = AwfulPreferences.getInstance(context) - protected val contentResolver: ContentResolver - get() = context.contentResolver - interface AwfulResultCallback { - /** - * Called whenever a queued request successfully completes. - * The return value is optional and will likely be null depending on request type. - * @param result Response result or null if request does not provide direct result (most requests won't). - */ - fun success(result: T) + protected sealed class Parameters { + /** add a simple key/value parameter to this request */ + abstract fun add(key: String, value: String) - /** - * Called whenever a network request fails, parsing was not successful, or if a forums issue is detected. - * If AwfulRequest.build() is provided an AwfulFragment ProgressListener, it will automatically pass the error to the AwfulFragment's displayAlert function. - * @param error - */ - fun failure(error: VolleyError?) - } + /** add a file to this request by specifying its path */ + abstract fun attachFile(key: String, filePath: String) + class PostParams : Parameters() { + val params: MultipartEntityBuilder = MultipartEntityBuilder.create() + val httpEntity: HttpEntity by lazy { params.build() } - protected fun addPostParam(key: String, value: String) { - attachParams?.attachParam(key, value) - params = (params ?: HashMap()).apply { this[key] = value } - } + override fun add(key: String, value: String) { + params.addPart(key, StringBody(value, ContentType.TEXT_PLAIN)) + } - protected fun attachFile(key: String, filename: String) { - attachParams = (attachParams ?: MultipartEntityBuilder.create()).apply { - params?.forEach { (k, v) -> attachParam(k, v) } - addPart(key, FileBody(File(filename))) + override fun attachFile(key: String, filePath: String) { + params.addPart(key, FileBody(File(filePath))) + } } - } - private fun MultipartEntityBuilder.attachParam(key: String, value: String) { - addPart(key, StringBody(value, ContentType.TEXT_PLAIN)) - } + class GetParams : Parameters() { + val params = HashMap() - protected fun buildFinalRequest() { - httpEntity = attachParams!!.build() - } + override fun add(key: String, value: String) { + params[key] = value + } - protected fun setPostParams(post: MutableMap) { - params = post + override fun attachFile(key: String, filePath: String) { + throw RuntimeException("Can't attach a file with a GET request - use a POST one instead") + } + } } - /** - * Build request with no status/success/failure callbacks. Useful for fire-and-forget calls. - * @return The final request, to pass into queueRequest. - */ - fun build(): Request { - return build(null, null, null) - } + + open val requestTag: Any get() = REQUEST_TAG + + protected val preferences: AwfulPreferences get() = AwfulPreferences.getInstance(context) + protected val contentResolver: ContentResolver get() = context.contentResolver + /** - * Build request, using the ProgressListener (AwfulFragment already implements this) - * and the AwfulResultCallback (for success/failure messages). - * @param prog A ProgressListener, typically the current AwfulFragment instance. A null value disables progress updates. - * @param resultListener AwfulResultCallback interface for success/failure callbacks. These will always be called on the UI thread. - * @return A result to pass into queueRequest. (AwfulApplication implements queueRequest, AwfulActivity provides a convenience shortcut to access it) + * Build this request, for passing into [NetworkUtils.queueRequest]. + * + * You can provide an optional [progressListener] for progress updates on the UI thread + * (e.g. to update a loading bar). This will typically be an AwfulFragment. + * + * Passing in a [resultListener] will give you success and failure callbacks on the UI thread - + * if you don't care about these (e.g. for fire-and-forget requests) this can be left null. + * + * Since both listeners receive 'finished' callbacks, with any resulting errors, you'll probably + * want to handle any UI activity through [progressListener] and do any app logic through the + * [resultListener] callback. By passing in an AwfulFragment as the progress listener, you'll + * get progress bar updates and error message display for free! */ - fun build(prog: ProgressListener?, resultListener: AwfulResultCallback?): Request { - return build(prog, Response.Listener { response -> - resultListener?.success(response) - }, - Response.ErrorListener { error -> - // TODO: 29/10/2017 this is a temporary warning/advice for people on older devices who can't connect - remove it once there's something better for recommending security updates - error?.message?.contains("SSLProtocolException")?.let { - Toast.makeText(context, R.string.ssl_connection_error_message, Toast.LENGTH_LONG).show() - } - resultListener?.failure(error) - }) - } + @JvmOverloads + fun build(progressListener: ProgressListener? = null, resultListener: AwfulResultCallback? = null): Request { + this@AwfulRequest.progressListener = progressListener + // if it's a GET request, we need to build the full parameterised URL here + val requestUrl = + if (parameters is GetParams) { + val builder = Uri.parse(baseUrl).buildUpon() + parameters.params.entries + .fold(builder) { uri, (k, v) -> uri.appendQueryParameter(k, v) } + .build().toString() + } else baseUrl + + val successListener = resultListener?.let { Response.Listener(it::success) } + + val errorListener = Response.ErrorListener { error -> + // TODO: 29/10/2017 this is a temporary warning/advice for people on older devices who can't connect - remove it once there's something better for recommending security updates + if (error?.message?.contains("SSLProtocolException") == true) { + Toast.makeText(context, R.string.ssl_connection_error_message, Toast.LENGTH_LONG).show() + } + resultListener?.failure(error) + } - /** - * Build request, same as build(ProgressListener, AwfulResultCallback) but provides direct access to volley callbacks. - * There is no real reason to use this over the other version. - * @param prog - * @param successListener - * @param errorListener - * @return - */ - private fun build(prog: ProgressListener?, successListener: Response.Listener?, errorListener: Response.ErrorListener?): Request { - progressListener = prog - val helper = baseUrl?.run(Uri::parse)?.run(Uri::buildUpon) - val actualRequest = ActualRequest(generateUrl(helper), successListener, errorListener) - actualRequest.tag = requestTag - return actualRequest + return ActualRequest(requestUrl, successListener, errorListener).apply { tag = requestTag } } - /** - * Generate the URL to use in the request here. This includes any query arguments. - * A Uri.Builder is provided with the base URL already processed if a base URL is provided in the constructor. - * @param urlBuilder A Uri.Builder instance with the provided base URL. If no URL is provided in the constructor, this will be null. - * @return String containing the full request URL. - */ - protected abstract fun generateUrl(urlBuilder: Uri.Builder?): String /** * Handle the parsed response [doc]ument here, process any data and return any values if needed. @@ -159,7 +164,7 @@ abstract class AwfulRequest(protected val context: Context, private val baseU * the request implementation will handle it (and processing can proceed to [handleResponse]). * Returns true if the error was handled. * - * By default this swallows non-critical errors, and allows everything else through. If you need + * By default this swallows non-critical errors, and returns false for everything else. If you need * different behaviour for some reason, override this! */ protected open fun handleError(error: AwfulError, doc: Document): Boolean = !error.isCritical @@ -175,14 +180,23 @@ abstract class AwfulRequest(protected val context: Context, private val baseU * @return the error to pass to listeners, or null for no error (and no alert) */ protected open fun customizeProgressListenerError(error: VolleyError): VolleyError = error + // TODO: check if any request classes should be using this, for better error feedback - protected fun updateProgress(percent: Int) { + /** + * Pass a progress [percent]age to any progress listener attached to this request. + */ + private fun updateProgress(percent: Int) { //updateProgress() will be called from a secondary thread, so run these on the UI thread. progressListener?.let { handler.post { it.requestUpdate(this@AwfulRequest, percent) } } } + /** + * Parse a HTML [Document] from this request's [response]. + * + * Don't override this, it's an internal function that's handled differently by [AwfulStrippedRequest] + */ @Throws(IOException::class) protected open fun parseAsHtml(response: NetworkResponse): Document { val jsoupParseStart = System.currentTimeMillis() @@ -191,6 +205,7 @@ abstract class AwfulRequest(protected val context: Context, private val baseU return doc } + /** * Allows subclasses (i.e. AwfulStrippedRequest) to direct the document to the appropriate handler function. * Feels clunky to have this (don't override it in concrete classes!) as well as #handleResponse (do override that!) @@ -200,12 +215,20 @@ abstract class AwfulRequest(protected val context: Context, private val baseU return handleResponse(document) } + + /** + * Final Volley Request class, created when the AwfulRequest is complete and ready to be queued. + * + * Since GET requests (apparently?) require their full parameterised URL to be passed into + * the constructor here, we can't just make AwfulRequest a subclass of this, since its subclasses + * add their GET parameters in the init blocks + */ private inner class ActualRequest internal constructor( url: String, private val success: Response.Listener?, - errorListener: Response.ErrorListener? + errorListener: Response.ErrorListener ) : Request( - if (params != null) Request.Method.POST else Request.Method.GET, + if (isPostRequest) Request.Method.POST else Request.Method.GET, url, errorListener ) { @@ -223,10 +246,10 @@ abstract class AwfulRequest(protected val context: Context, private val baseU try { val doc = parseAsHtml(response) updateProgress(50) - val error = AwfulError.checkPageErrors(doc, preferences) - if (error != null && handleError(error, doc)) { - throw error - } + val error = AwfulError.checkPageErrors(doc, preferences) + if (error != null && handleError(error, doc)) { + throw error + } val result = handleResponseDocument(doc) Timber.d("Successful parse: $url\nTook ${System.currentTimeMillis() - startTime}ms") @@ -240,6 +263,7 @@ abstract class AwfulRequest(protected val context: Context, private val baseU } throw e } catch (e: Exception) { + // TODO: find out what else this is meant to be catching, because it's swallowing every exception Timber.e(e, "Failed parse: $url") return Response.error(ParseError(e)) } finally { @@ -283,48 +307,72 @@ abstract class AwfulRequest(protected val context: Context, private val baseU @Throws(AuthFailureError::class) override fun getHeaders(): Map { - return (super.getHeaders()?.takeIf { it.isNotEmpty() } ?: HashMap()) - .also(NetworkUtils::setCookieHeaders) + return mutableMapOf().apply(NetworkUtils::setCookieHeaders) .also { Timber.i("getHeaders: %s", this) } } - @Throws(AuthFailureError::class) - override fun getParams(): Map? = this@AwfulRequest.params @Throws(AuthFailureError::class) override fun getBody(): ByteArray { - attachParams?.let { - if (httpEntity == null) buildFinalRequest() - try { - return ByteArrayOutputStream().apply(httpEntity!!::writeTo).toByteArray() - } catch (ioe: IOException) { - Timber.e(ioe, "Failed to convert response body byte stream") - } + check(parameters is PostParams) + return try { + ByteArrayOutputStream().apply(parameters.httpEntity::writeTo).toByteArray() + } catch (e: IOException) { + Timber.w(e, "Failed to convert response body byte stream") + super.getBody() } - return super.getBody() } override fun getBodyContentType(): String { - attachParams?.let { - if (httpEntity == null) buildFinalRequest() - return httpEntity!!.contentType.value - } - return super.getBodyContentType() + check(parameters is PostParams) + return parameters.httpEntity.contentType.value } } + /** + * Receives callbacks when a request succeeds or fails. + */ + interface AwfulResultCallback { + /** + * Called when the queued request successfully completes. + * + * If the request returns a [result] it will be passed here - most requests don't, in which + * case this will be null. + */ + @UiThread + fun success(result: T) + + /** + * Called when the network request fails, parsing was not successful, or if a forums issue was detected. + * + * Any generated [error] will be provided here, which may provide useful information! + */ + @UiThread + fun failure(error: VolleyError?) + } + + + /** + * Receives callbacks on the request's lifecycle. + */ interface ProgressListener { + /** Called when the request has been queued */ + @UiThread fun requestStarted(req: AwfulRequest<*>) + + /** Called when the request is announcing a progress [percent]age update */ + @UiThread fun requestUpdate(req: AwfulRequest<*>, percent: Int) + + /** Called when the request has finished, with a possible [error] if it failed */ + @UiThread fun requestEnded(req: AwfulRequest<*>, error: VolleyError?) } companion object { - - /** Used for identifying request types when cancelling, reassign this in subclasses */ + /** Used for identifying request types when cancelling, reassign this in subclasses */ val REQUEST_TAG = Any() - val TAG = "AwfulRequest" private val lenientRetryPolicy = DefaultRetryPolicy(20000, 1, 1f) } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt index 79eda535d..49f539cc3 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkColorRequest.kt @@ -1,27 +1,20 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document /** - * Created by Matthew on 8/9/13. + * Cycle the bookmark colour set on the site for a given thread. */ -class BookmarkColorRequest(context: Context, threadId: Int) : AwfulRequest(context, null) { +class BookmarkColorRequest(context: Context, threadId: Int) + : AwfulRequest(context, FUNCTION_BOOKMARK, isPostRequest = true) { init { - addPostParam(Constants.PARAM_ACTION, "cat_toggle") - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) - } - - override fun generateUrl(urlBuilder: Uri.Builder?): String { - // TODO wat - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_BOOKMARK + with (parameters) { + add(PARAM_ACTION, "cat_toggle") + add(PARAM_THREAD_ID, Integer.toString(threadId)) + } } @Throws(AwfulError::class) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt index ccd7c632e..67994f6bb 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/BookmarkRequest.kt @@ -2,28 +2,26 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulThread import com.ferg.awfulapp.util.AwfulError import com.ferg.awfulapp.util.toSqlBoolean import org.jsoup.nodes.Document /** - * Created by matt on 8/8/13. + * Add or remove a bookmark on the site for the given [threadId], updating the local database. */ -class BookmarkRequest(context: Context, private val threadId: Int, private val add: Boolean) : AwfulRequest(context, null) { +class BookmarkRequest(context: Context, private val threadId: Int, private val add: Boolean) + : AwfulRequest(context, FUNCTION_BOOKMARK, isPostRequest = true) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) - if (add) { - addPostParam(Constants.PARAM_ACTION, "add") - } else { - addPostParam(Constants.PARAM_ACTION, "remove") + init { + with(parameters) { + add(PARAM_THREAD_ID, threadId.toString()) + add(PARAM_ACTION, if (add) "add" else "remove") } - return Constants.FUNCTION_BOOKMARK } + @Throws(AwfulError::class) override fun handleResponse(doc: Document): Void? { val cv = ContentValues() diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt index 10f2d2501..afb14454d 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EditRequest.kt @@ -2,15 +2,11 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.provider.DatabaseHelper import com.ferg.awfulapp.reply.Reply import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document - import java.sql.Timestamp /** @@ -21,14 +17,13 @@ import java.sql.Timestamp * options (see [Reply.processEdit]) and a current timestamp. */ class EditRequest(context: Context, private val threadId: Int, private val postId: Int) - : AwfulRequest(context, Constants.FUNCTION_EDIT_POST) { + : AwfulRequest(context, FUNCTION_EDIT_POST) { // TODO: this and the quote/reply requests are all very similar - they all just load the "start replying" page and grab any existing contents. Combine them maybe? - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - appendQueryParameter(Constants.PARAM_ACTION, "editpost") - appendQueryParameter(Constants.PARAM_POST_ID, postId.toString()) - return build().toString() + init { + with(parameters) { + add(PARAM_ACTION, "editpost") + add(PARAM_POST_ID, postId.toString()) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt index c3604faca..d6df19d00 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/EmoteRequest.kt @@ -1,22 +1,19 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.FUNCTION_MISC +import com.ferg.awfulapp.constants.Constants.PARAM_ACTION import com.ferg.awfulapp.thread.AwfulEmote import com.ferg.awfulapp.util.AwfulError import org.jsoup.nodes.Document /** - * Created by matt on 8/8/13. + * Request the current set of site emotes, updating the local database. */ -class EmoteRequest(context: Context) : AwfulRequest(context, Constants.FUNCTION_MISC) { +class EmoteRequest(context: Context) : AwfulRequest(context, FUNCTION_MISC) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - appendQueryParameter(Constants.PARAM_ACTION, "showsmilies") - return build().toString() - } + init { + parameters.add(PARAM_ACTION, "showsmilies") } @Throws(AwfulError::class) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt index 8130816ed..2cb46bd0f 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/FeatureRequest.kt @@ -1,12 +1,10 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.FUNCTION_MEMBER +import com.ferg.awfulapp.constants.Constants.PARAM_ACTION import com.ferg.awfulapp.preferences.Keys import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document import org.jsoup.nodes.Element import timber.log.Timber @@ -15,10 +13,10 @@ import timber.log.Timber * An AwfulRequest that fetches the features active on the user's account (platinum etc.), * and stores that data in AwfulPreferences. */ -class FeatureRequest(context: Context) : AwfulRequest(context, Constants.FUNCTION_MEMBER) { +class FeatureRequest(context: Context) : AwfulRequest(context, FUNCTION_MEMBER) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - return urlBuilder!!.appendQueryParameter(Constants.PARAM_ACTION, "accountfeatures").build().toString() + init { + parameters.add(PARAM_ACTION, "accountfeatures") } @Throws(AwfulError::class) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt index 004db1700..a242692f5 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IgnoreRequest.kt @@ -1,28 +1,22 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document /** * An AwfulRequest that adds a userId to the user's ignore list. */ -class IgnoreRequest(context: Context, userId: Int) : AwfulRequest(context, null) { +class IgnoreRequest(context: Context, userId: Int) + : AwfulRequest(context, FUNCTION_MEMBER2, isPostRequest = true) { init { - addPostParam(Constants.PARAM_ACTION, Constants.ACTION_ADDLIST) - addPostParam(Constants.PARAM_USERLIST, Constants.USERLIST_IGNORE) - addPostParam(Constants.FORMKEY, preferences.ignoreFormkey) - addPostParam(Constants.PARAM_USER_ID, Integer.toString(userId)) - } - - override fun generateUrl(urlBuilder: Uri.Builder?): String { - //since we aren't adding query arguments to a POST request, - //we can just pass null in the constructor URL field and it'll skip this Uri.Builder - return Constants.FUNCTION_MEMBER2 + with(parameters) { + add(PARAM_ACTION, ACTION_ADDLIST) + add(PARAM_USERLIST, USERLIST_IGNORE) + add(FORMKEY, preferences.ignoreFormkey) + add(PARAM_USER_ID, Integer.toString(userId)) + } } @Throws(AwfulError::class) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt index 54d42d6a1..0b95976db 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/IndexIconRequest.kt @@ -1,8 +1,7 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.BASE_URL import com.ferg.awfulapp.preferences.Keys import com.ferg.awfulapp.thread.AwfulForum import com.ferg.awfulapp.util.AwfulError @@ -13,7 +12,7 @@ import java.util.regex.Pattern /** * An AwfulRequest that parses forum icons from the main forums page, and stores them in the database. */ -class IndexIconRequest(context: Context) : AwfulRequest(context, null) { +class IndexIconRequest(context: Context) : AwfulRequest(context, BASE_URL) { //TODO: do we even need this anymore? We don't display these forum icons, but we do parse the colours for our custom icons, so check what's needed companion object { @@ -23,13 +22,9 @@ class IndexIconRequest(context: Context) : AwfulRequest(context, null) { override val requestTag: Any get() = REQUEST_TAG - - override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.BASE_URL + "/" - @Throws(AwfulError::class) override fun handleResponse(doc: Document): Void? { AwfulForum.processForumIcons(doc, contentResolver) - updateProgress(80) //optional section, parses username from PM notification field. // TODO: this has nothing to do with parsing forum icons - if we need to update the username, do it separately. Also it's broken when there's an apostrophe in the username? diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt index 60453d9bd..482301b17 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LepersColonyRequest.kt @@ -2,7 +2,7 @@ package com.ferg.awfulapp.task import android.content.Context import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.task.LepersColonyRequest.LepersColonyPage import com.ferg.awfulapp.users.LepersColonyFragment.Companion.FIRST_PAGE import com.ferg.awfulapp.users.Punishment @@ -16,7 +16,7 @@ import org.jsoup.nodes.Document */ class LepersColonyRequest(context: Context, val page: Int = 1, val userId: String? = null): - AwfulStrippedRequest(context, Constants.FUNCTION_BANLIST) { + AwfulStrippedRequest(context, FUNCTION_BANLIST) { // allow queued requests to be cancelled when a new one starts, e.g. skipping quickly through pages companion object { @@ -26,11 +26,10 @@ class LepersColonyRequest(context: Context, val page: Int = 1, val userId: Strin override val requestTag: Any get() = REQUEST_TAG - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - appendQueryParameter(Constants.PARAM_PAGE, page.toString()) - userId?.let { appendQueryParameter(Constants.PARAM_USER_ID, userId) } - return build().toString() + init { + with(parameters) { + add(PARAM_PAGE, page.toString()) + userId?.let { add(PARAM_USER_ID, userId) } } } @@ -40,7 +39,7 @@ class LepersColonyRequest(context: Context, val page: Int = 1, val userId: Strin val lastPage = selectFirst(".pages a[title='Last page']") ?.attr("href") ?.let(Uri::parse) - ?.getQueryParameter(Constants.PARAM_PAGE) + ?.getQueryParameter(PARAM_PAGE) ?.toIntOrNull() ?: thisPage diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt index 357aa4e3d..df29c0ce1 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/LoginRequest.kt @@ -1,26 +1,28 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.android.volley.NetworkResponse -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.network.NetworkUtils import com.ferg.awfulapp.preferences.Keys import com.ferg.awfulapp.util.AwfulError import org.jsoup.nodes.Document /** - * AwfulRequest that sends a login request to the site, and stores login cookies and sets the current username if successful. + * AwfulRequest that sends a login request to the site, and stores login cookies and sets + * the current username if successful. */ -class LoginRequest(context: Context, private val username: String, password: String) : AwfulRequest(context, null) { +class LoginRequest(context: Context, private val username: String, password: String) + : AwfulRequest(context, FUNCTION_LOGIN_SSL, isPostRequest = true) { + init { - addPostParam(Constants.PARAM_ACTION, "login") - addPostParam(Constants.PARAM_USERNAME, username) - addPostParam(Constants.PARAM_PASSWORD, password) + with(parameters) { + add(PARAM_ACTION, "login") + add(PARAM_USERNAME, username) + add(PARAM_PASSWORD, password) + } } - override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_LOGIN_SSL - @Throws(AwfulError::class) override fun handleResponse(doc: Document): Boolean = validateLoginState() diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt index d1d136ee1..70ae093dd 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkLastReadRequest.kt @@ -3,30 +3,29 @@ package com.ferg.awfulapp.task import android.content.ContentUris import android.content.ContentValues import android.content.Context -import android.net.Uri - import com.android.volley.VolleyError -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.provider.AwfulProvider import com.ferg.awfulapp.thread.AwfulPost import com.ferg.awfulapp.thread.AwfulThread import com.ferg.awfulapp.util.AwfulError import com.ferg.awfulapp.util.toSqlBoolean - import org.jsoup.nodes.Document /** * An AwfulRequest that sets a given post as the last one read in a particular thread, updating * the database to reflect this when the request is successful. */ -class MarkLastReadRequest(context: Context, private val threadId: Int, private val postIndex: Int) : AwfulRequest(context, null) { +class MarkLastReadRequest(context: Context, private val threadId: Int, private val postIndex: Int) + : AwfulRequest(context, FUNCTION_THREAD, isPostRequest = true) { init { - addPostParam(Constants.PARAM_ACTION, "setseen") - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) - addPostParam(Constants.PARAM_INDEX, Integer.toString(postIndex)) + with(parameters) { + add(PARAM_ACTION, "setseen") + add(PARAM_THREAD_ID, threadId.toString()) + add(PARAM_INDEX, postIndex.toString()) + } } - override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_THREAD @Throws(AwfulError::class) override fun handleResponse(doc: Document): Void? { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt index 0defaa3b1..77d5b1b83 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/MarkUnreadRequest.kt @@ -3,28 +3,26 @@ package com.ferg.awfulapp.task import android.content.ContentUris import android.content.ContentValues import android.content.Context -import android.net.Uri - import com.android.volley.VolleyError -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulPost import com.ferg.awfulapp.thread.AwfulThread import com.ferg.awfulapp.util.AwfulError import com.ferg.awfulapp.util.toSqlBoolean - import org.jsoup.nodes.Document /** * A request to mark a thread as unread. */ -class MarkUnreadRequest(context: Context, private val threadId: Int) : AwfulRequest(context, null) { +class MarkUnreadRequest(context: Context, private val threadId: Int) + : AwfulRequest(context, FUNCTION_THREAD, isPostRequest = true) { init { - addPostParam(Constants.PARAM_THREAD_ID, Integer.toString(threadId)) - addPostParam(Constants.PARAM_ACTION, "resetseen") + with(parameters) { + add(PARAM_THREAD_ID, threadId.toString()) + add(PARAM_ACTION, "resetseen") + } } - override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_THREAD - @Throws(AwfulError::class) override fun handleResponse(doc: Document): Void? { with (contentResolver) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt index 3660f1737..0934c9147 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMListRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulMessage import com.ferg.awfulapp.util.AwfulError @@ -11,10 +10,12 @@ import org.jsoup.nodes.Document * A request that gets and updates the stored list of Private Messages in a particular [folder] */ class PMListRequest(context: Context, private val folder: Int = PRIVATE_MESSAGE_DEFAULT_FOLDER) - : AwfulRequest(context, null) { + : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { + + init { + parameters.add(PARAM_FOLDERID, folder.toString()) + } - override fun generateUrl(urlBuilder: Uri.Builder?): String = - "$FUNCTION_PRIVATE_MESSAGE?$PARAM_FOLDERID=$folder" @Throws(AwfulError::class) override fun handleResponse(doc: Document): Void? { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt index 453644fb0..95e8444da 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMReplyRequest.kt @@ -3,7 +3,6 @@ package com.ferg.awfulapp.task import android.content.ContentUris import android.content.ContentValues import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulMessage import org.jsoup.nodes.Document @@ -16,23 +15,27 @@ import org.jsoup.nodes.Document * This is stored in the database as a message draft, unless a draft for a reply to * this PM already exists, in which case the title and reply contents are unchanged. */ -class PMReplyRequest(context: Context, private val id: Int) : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { +class PMReplyRequest(context: Context, private val id: Int) + : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { - override fun generateUrl(urlBuilder: Uri.Builder?): String = with(urlBuilder!!) { - appendQueryParameter(PARAM_ACTION, "newmessage") - appendQueryParameter(PARAM_PRIVATE_MESSAGE_ID, id.toString()) - build().toString() + init { + with(parameters) { + add(PARAM_ACTION, "newmessage") + add(PARAM_PRIVATE_MESSAGE_ID, id.toString()) + } } override fun handleResponse(doc: Document): Void? { - // parse the reply data, and create a version that doesn't overwrite the title or reply content, for updating any existing draft + // parse the reply data, and create a version that doesn't overwrite the title or reply content, + // for updating any existing draft val newReply = AwfulMessage.processReplyMessage(doc, id) val updateDraft = ContentValues(newReply).apply { remove(AwfulMessage.TITLE) remove(AwfulMessage.REPLY_CONTENT) } - // try and update the draft - if it fails, insert the full reply data (including title and message content) as a new draft + // try and update the draft - if it fails, insert the full reply data + // (including title and message content) as a new draft val currentDraft = ContentUris.withAppendedId(AwfulMessage.CONTENT_URI_REPLY, id.toLong()) if (contentResolver.update(currentDraft, updateDraft, null, null) < 1) { // no update so the draft didn't exist diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt index 4606a6ba2..59010a66c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PMRequest.kt @@ -2,9 +2,7 @@ package com.ferg.awfulapp.task import android.content.ContentUris import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants -import com.ferg.awfulapp.constants.Constants.FUNCTION_PRIVATE_MESSAGE +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulMessage import com.ferg.awfulapp.util.AwfulError import org.jsoup.nodes.Document @@ -15,15 +13,18 @@ import org.jsoup.nodes.Document */ class PMRequest(context: Context, private val id: Int) : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { - override fun generateUrl(urlBuilder: Uri.Builder?): String = with (urlBuilder!!) { - appendQueryParameter(Constants.PARAM_ACTION, "show") - appendQueryParameter(Constants.PARAM_PRIVATE_MESSAGE_ID, id.toString()) - build().toString() + init { + with(parameters) { + add(PARAM_ACTION, "show") + add(PARAM_PRIVATE_MESSAGE_ID, id.toString()) + + } } + @Throws(AwfulError::class) override fun handleResponse(doc: Document): Void? { - with (contentResolver) { + with(contentResolver) { val message = AwfulMessage.processMessage(doc, id) val messageUri = ContentUris.withAppendedId(AwfulMessage.CONTENT_URI, id.toLong()) if (update(messageUri, message, null, null) < 1) { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt index 15f662703..f5f99be84 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewEditRequest.kt @@ -2,7 +2,6 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.network.NetworkUtils import com.ferg.awfulapp.thread.AwfulMessage @@ -19,31 +18,31 @@ import timber.log.Timber * the data on the edit page via the ContentValues parameter, which is sent to * the site to retrieve the preview. */ -class PreviewEditRequest(context: Context, reply: ContentValues) : AwfulRequest(context, null) { +class PreviewEditRequest(context: Context, reply: ContentValues) + : AwfulRequest(context, FUNCTION_EDIT_POST, isPostRequest = true) { + init { - with(reply) { - val postId = getAsInteger(AwfulPost.EDIT_POST_ID)?.toString() + with(parameters) { + val postId = reply.getAsInteger(AwfulPost.EDIT_POST_ID)?.toString() ?: throw IllegalArgumentException("No post ID included") - addPostParam(PARAM_ACTION, "updatepost") - addPostParam(PARAM_POST_ID, postId) Timber.i("$PARAM_POST_ID: $postId") - addPostParam(PARAM_MESSAGE, getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) - addPostParam(PARAM_PARSEURL, YES) + + add(PARAM_ACTION, "updatepost") + add(PARAM_POST_ID, postId) + add(PARAM_MESSAGE, reply.getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) + add(PARAM_PARSEURL, YES) // TODO: this bookmarks every thread you edit a post in, unless you turn it off in a browser - seems bad for replies, worse for edits? - if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { - addPostParam(PARAM_BOOKMARK, YES) + if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + parameters.add(PARAM_BOOKMARK, YES) } listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) - .forEach { if (containsKey(it)) addPostParam(it, YES) } - addPostParam(PARAM_SUBMIT, SUBMIT_REPLY) - addPostParam(PARAM_PREVIEW, PREVIEW_REPLY) + .forEach { if (reply.containsKey(it)) parameters.add(it, YES) } + add(PARAM_SUBMIT, SUBMIT_REPLY) + add(PARAM_PREVIEW, PREVIEW_REPLY) } - - buildFinalRequest() } - override fun generateUrl(urlBuilder: Uri.Builder?) = FUNCTION_EDIT_POST override fun handleResponse(doc: Document): String = PostPreviewParseTask(doc).call() diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt index b666d33d4..973beaf68 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/PreviewPostRequest.kt @@ -2,7 +2,6 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.network.NetworkUtils import com.ferg.awfulapp.thread.AwfulMessage @@ -18,34 +17,33 @@ import org.jsoup.nodes.Document * the data on the edit page via the ContentValues parameter, which is sent to * the site to retrieve the preview. */ -class PreviewPostRequest (context: Context, reply: ContentValues) : AwfulRequest(context, null) { +class PreviewPostRequest (context: Context, reply: ContentValues) + : AwfulRequest(context, FUNCTION_POST_REPLY, isPostRequest = true) { // TODO: 18/12/2017 this and PreviewEditRequest are almost identical, merge 'em init { - with(reply) { - val threadId = getAsInteger(AwfulMessage.ID)?.toString() + with(parameters) { + val threadId = reply.getAsInteger(AwfulMessage.ID)?.toString() ?: throw IllegalArgumentException("No thread ID included") - addPostParam(PARAM_ACTION, "postreply") - addPostParam(PARAM_THREAD_ID, threadId) - addPostParam(PARAM_FORMKEY, getAsString(AwfulPost.FORM_KEY)) - addPostParam(PARAM_FORM_COOKIE, getAsString(AwfulPost.FORM_COOKIE)) - addPostParam(PARAM_MESSAGE, getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) + add(PARAM_ACTION, "postreply") + add(PARAM_THREAD_ID, threadId) + add(PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)) + add(PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)) + add(PARAM_MESSAGE, reply.getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) - addPostParam(PARAM_PARSEURL, YES) + add(PARAM_PARSEURL, YES) // TODO: this bookmarks every thread you post in, unless you turn it off in a browser - seems bad? - if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { - addPostParam(PARAM_BOOKMARK, YES) + if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + add(PARAM_BOOKMARK, YES) } listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) - .forEach { if (containsKey(it)) addPostParam(it, YES) } + .forEach { if (reply.containsKey(it)) add(it, YES) } - getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } - addPostParam(PARAM_SUBMIT, SUBMIT_REPLY) - addPostParam(PARAM_PREVIEW, PREVIEW_REPLY) + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + add(PARAM_SUBMIT, SUBMIT_REPLY) + add(PARAM_PREVIEW, PREVIEW_REPLY) } - buildFinalRequest() } - override fun generateUrl(urlBuilder: Uri.Builder?) = FUNCTION_POST_REPLY override fun handleResponse(doc: Document): String = PostPreviewParseTask(doc).call() diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt index b2ef3fd3e..218de033a 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/QuoteRequest.kt @@ -2,15 +2,11 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.provider.DatabaseHelper import com.ferg.awfulapp.reply.Reply import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document - import java.sql.Timestamp /** @@ -21,13 +17,12 @@ import java.sql.Timestamp * options (see [Reply.processQuote]) and a current timestamp. */ class QuoteRequest(context: Context, private val threadId: Int, private val postId: Int) - : AwfulRequest(context, Constants.FUNCTION_POST_REPLY) { + : AwfulRequest(context, FUNCTION_POST_REPLY) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with (urlBuilder!!) { - appendQueryParameter(Constants.PARAM_ACTION, "newreply") - appendQueryParameter(Constants.PARAM_POST_ID, postId.toString()) - return build().toString() + init { + with(parameters) { + add(PARAM_ACTION, "newreply") + add(PARAM_POST_ID, postId.toString()) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt index 9eb58dadb..7ffc1018d 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/RefreshUserProfileRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.preferences.AwfulPreferences import com.ferg.awfulapp.preferences.Keys.IGNORE_FORMKEY @@ -23,11 +22,10 @@ class RefreshUserProfileRequest(context: Context) : AwfulRequest(context, private const val PROFILE_ID_FOR_THIS_USER = "0" } - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - appendQueryParameter(PARAM_ACTION, ACTION_PROFILE) - appendQueryParameter(PARAM_USER_ID, PROFILE_ID_FOR_THIS_USER) - return build().toString() + init { + with(parameters) { + add(PARAM_ACTION, ACTION_PROFILE) + add(PARAM_USER_ID, PROFILE_ID_FOR_THIS_USER) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt index 98f508042..9fa065e54 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReplyRequest.kt @@ -2,15 +2,11 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.provider.DatabaseHelper import com.ferg.awfulapp.reply.Reply import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document - import java.sql.Timestamp /** @@ -21,13 +17,12 @@ import java.sql.Timestamp * options (see [Reply.processReply]) and a current timestamp. */ class ReplyRequest(context: Context, private val threadId: Int) - : AwfulRequest(context, Constants.FUNCTION_POST_REPLY) { + : AwfulRequest(context, FUNCTION_POST_REPLY) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - appendQueryParameter(Constants.PARAM_ACTION, "newreply") - appendQueryParameter(Constants.PARAM_THREAD_ID, threadId.toString()) - return build().toString() + init { + with(parameters) { + add(PARAM_ACTION, "newreply") + add(PARAM_THREAD_ID, threadId.toString()) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt index 28a047c3c..c1d616ff0 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ReportRequest.kt @@ -1,11 +1,8 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri - -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -17,13 +14,14 @@ import org.jsoup.nodes.Element * successful, otherwise an error is thrown. */ class ReportRequest(context: Context, private val postId: Int, private val comments: String) - : AwfulRequest(context, null) { + : AwfulRequest(context, FUNCTION_REPORT, isPostRequest = true) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - addPostParam(Constants.PARAM_COMMENTS, comments) - addPostParam(Constants.PARAM_POST_ID, postId.toString()) - addPostParam(Constants.PARAM_ACTION, "submit") - return Constants.FUNCTION_REPORT + init { + with(parameters) { + add(PARAM_COMMENTS, comments) + add(PARAM_POST_ID, postId.toString()) + add(PARAM_ACTION, "submit") + } } @Throws(AwfulError::class) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt index 945fd3d88..381967f6c 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchForumsFilterRequest.kt @@ -1,8 +1,7 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.FUNCTION_SEARCH import com.ferg.awfulapp.thread.AwfulSearchForum import org.jsoup.nodes.Document import java.util.* @@ -10,12 +9,10 @@ import java.util.* /** * Get a list of forums that can be searched, and their default selection states. */ -class SearchForumsFilterRequest(context: Context) : AwfulRequest>(context, null) { +class SearchForumsFilterRequest(context: Context) + : AwfulRequest>(context, FUNCTION_SEARCH) { - override fun generateUrl(urlBuilder: Uri.Builder?): String = Constants.FUNCTION_SEARCH - - override fun handleResponse(doc: Document): ArrayList { - return AwfulSearchForum.parseSearchForums(doc) - } + override fun handleResponse(doc: Document): ArrayList = + AwfulSearchForum.parseSearchForums(doc) } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt index 110c2454f..18cd76086 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulSearch import com.ferg.awfulapp.thread.AwfulSearchResult @@ -11,17 +10,17 @@ import org.jsoup.nodes.Document * Run a search query, optionally limiting the search to a set of forum IDs. */ class SearchRequest(context: Context, query: String, forums: IntArray?) - : AwfulRequest(context, null) { + : AwfulRequest(context, FUNCTION_SEARCH, isPostRequest = true) { init { - addPostParam(PARAM_ACTION, ACTION_QUERY) - addPostParam(PARAM_QUERY, query) - forums?.forEachIndexed { index, forumId -> - addPostParam(String.format(PARAM_FORUMS, index), forumId.toString()) + with(parameters) { + add(PARAM_ACTION, ACTION_QUERY) + add(PARAM_QUERY, query) + forums?.forEachIndexed { index, forumId -> + add(String.format(PARAM_FORUMS, index), forumId.toString()) + } } - buildFinalRequest() } - override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_SEARCH override fun handleResponse(doc: Document): AwfulSearchResult { return AwfulSearchResult.parseSearch(doc).apply { diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt index 26ca5dd0d..4bab6cf43 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SearchResultPageRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulSearch import org.jsoup.nodes.Document @@ -14,20 +13,14 @@ class SearchResultPageRequest(context: Context, private val queryId: Int, privat : AwfulRequest>(context, FUNCTION_SEARCH) { init { - buildFinalRequest() - } - - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!){ - appendQueryParameter(PARAM_ACTION, ACTION_RESULTS) - appendQueryParameter(PARAM_QID, queryId.toString()) - appendQueryParameter(PAGE, page.toString()) - return build().toString() + with(parameters){ + add(PARAM_ACTION, ACTION_RESULTS) + add(PARAM_QID, queryId.toString()) + add(PAGE, page.toString()) } } - override fun handleResponse(doc: Document): ArrayList { - return AwfulSearch.parseSearchResult(doc) - } + override fun handleResponse(doc: Document): ArrayList = + AwfulSearch.parseSearchResult(doc) } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt index aece22efd..12a0cc8ba 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendEditRequest.kt @@ -2,7 +2,6 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.network.NetworkUtils import com.ferg.awfulapp.thread.AwfulMessage @@ -12,28 +11,27 @@ import org.jsoup.nodes.Document /** * Submit a post edit described by a set of ContentValues */ -class SendEditRequest(context: Context, reply: ContentValues) : AwfulRequest(context, null) { +class SendEditRequest(context: Context, reply: ContentValues) + : AwfulRequest(context, FUNCTION_EDIT_POST, isPostRequest = true) { // TODO: again this is v similar to the Edit/PostPreview requests - better to merge them? // TODO: it's also probably neater to ditch the ContentValues and just use normal params instead of all this wrangling init { - with(reply) { - addPostParam(PARAM_ACTION, "updatepost") - addPostParam(PARAM_POST_ID, getAsString(AwfulPost.EDIT_POST_ID)) - addPostParam(PARAM_MESSAGE, getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) - addPostParam(PARAM_PARSEURL, YES) - if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { - addPostParam(PARAM_BOOKMARK, YES) + with(parameters) { + add(PARAM_ACTION, "updatepost") + add(PARAM_POST_ID, reply.getAsString(AwfulPost.EDIT_POST_ID)) + add(PARAM_MESSAGE, reply.getAsString(AwfulMessage.REPLY_CONTENT).run(NetworkUtils::encodeHtml)) + add(PARAM_PARSEURL, YES) + if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + add(PARAM_BOOKMARK, YES) } listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) - .forEach { key -> if (containsKey(key)) addPostParam(key, YES) } - getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + .forEach { key -> if (reply.containsKey(key)) add(key, YES) } + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> + attachFile(PARAM_ATTACHMENT, filePath) + } } - - buildFinalRequest() } - override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_EDIT_POST - override fun handleResponse(doc: Document): Void? = null } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt index 045ea0260..00b4e98de 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPostRequest.kt @@ -2,7 +2,6 @@ package com.ferg.awfulapp.task import android.content.ContentValues import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.network.NetworkUtils import com.ferg.awfulapp.thread.AwfulMessage @@ -12,28 +11,28 @@ import org.jsoup.nodes.Document /** * Submit a post described by a set of ContentValues */ -class SendPostRequest(context: Context, reply: ContentValues) : AwfulRequest(context, null) { +class SendPostRequest(context: Context, reply: ContentValues) + : AwfulRequest(context, FUNCTION_POST_REPLY, isPostRequest = true) { + init { - with(reply) { - addPostParam(PARAM_ACTION, "postreply") - addPostParam(PARAM_THREAD_ID, Integer.toString(getAsInteger(AwfulMessage.ID)!!)) - addPostParam(PARAM_FORMKEY, getAsString(AwfulPost.FORM_KEY)) - addPostParam(PARAM_FORM_COOKIE, getAsString(AwfulPost.FORM_COOKIE)) - addPostParam(PARAM_MESSAGE, NetworkUtils.encodeHtml(getAsString(AwfulMessage.REPLY_CONTENT))) - addPostParam(PARAM_PARSEURL, YES) - if (getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { - addPostParam(PARAM_BOOKMARK, YES) + with(parameters) { + add(PARAM_ACTION, "postreply") + add(PARAM_THREAD_ID, Integer.toString(reply.getAsInteger(AwfulMessage.ID)!!)) + add(PARAM_FORMKEY, reply.getAsString(AwfulPost.FORM_KEY)) + add(PARAM_FORM_COOKIE, reply.getAsString(AwfulPost.FORM_COOKIE)) + add(PARAM_MESSAGE, NetworkUtils.encodeHtml(reply.getAsString(AwfulMessage.REPLY_CONTENT))) + add(PARAM_PARSEURL, YES) + if (reply.getAsString(AwfulPost.FORM_BOOKMARK).equals("checked", ignoreCase = true)) { + add(PARAM_BOOKMARK, YES) } listOf(AwfulMessage.REPLY_SIGNATURE, AwfulMessage.REPLY_DISABLE_SMILIES) - .forEach { key -> if (containsKey(key)) addPostParam(key, YES) } - getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> attachFile(PARAM_ATTACHMENT, filePath) } + .forEach { key -> if (reply.containsKey(key)) add(key, YES) } + reply.getAsString(AwfulMessage.REPLY_ATTACHMENT)?.let { filePath -> + attachFile(PARAM_ATTACHMENT, filePath) + } } - - buildFinalRequest() } - override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_POST_REPLY - override fun handleResponse(doc: Document): Void? = null } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt index be3eafe58..1e8fe6274 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SendPrivateMessageRequest.kt @@ -3,7 +3,6 @@ package com.ferg.awfulapp.task import android.content.ContentUris import android.content.Context import android.database.Cursor -import android.net.Uri import android.widget.Toast import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.network.NetworkUtils @@ -18,7 +17,8 @@ import org.jsoup.nodes.Document * submit the message. */ class SendPrivateMessageRequest(context: Context, private val pmId: Int) - : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE) { + : AwfulRequest(context, FUNCTION_PRIVATE_MESSAGE, isPostRequest = true) { + init { // TODO: do this extraction elsewhere, handle failure there, just pass in valid data // TODO: pmId is being used as a draft ID AND the ID of a PM you're replying to ("prevmessageid")??? what's that about @@ -28,22 +28,22 @@ class SendPrivateMessageRequest(context: Context, private val pmId: Int) var failed = true storedDraft?.takeIf(Cursor::moveToFirst)?.apply { - addPostParam(PARAM_ACTION, ACTION_DOSEND) - addPostParam(DESTINATION_TOUSER, getString(getColumnIndex(AwfulMessage.RECIPIENT))) - addPostParam(PARAM_TITLE, getString(getColumnIndex(AwfulMessage.TITLE)).run(NetworkUtils::encodeHtml)) - if (pmId > 0) addPostParam("prevmessageid", pmId.toString()) - addPostParam(PARAM_PARSEURL, YES) - addPostParam("savecopy", YES) - addPostParam("iconid", "0") // we don't have an icon picker yet, so use the default - addPostParam(PARAM_MESSAGE, getString(getColumnIndex(AwfulMessage.REPLY_CONTENT)).run(NetworkUtils::encodeHtml)) + with(parameters) { + add(PARAM_ACTION, ACTION_DOSEND) + add(DESTINATION_TOUSER, getString(getColumnIndex(AwfulMessage.RECIPIENT))) + add(PARAM_TITLE, getString(getColumnIndex(AwfulMessage.TITLE)).run(NetworkUtils::encodeHtml)) + if (pmId > 0) add("prevmessageid", pmId.toString()) + add(PARAM_PARSEURL, YES) + add("savecopy", YES) + add("iconid", "0") // we don't have an icon picker yet, so use the default + add(PARAM_MESSAGE, getString(getColumnIndex(AwfulMessage.REPLY_CONTENT)).run(NetworkUtils::encodeHtml)) + } failed = false } storedDraft?.close() if (failed) Toast.makeText(context, "Unable to send private message!", Toast.LENGTH_LONG).show() } - override fun generateUrl(urlBuilder: Uri.Builder?): String = FUNCTION_PRIVATE_MESSAGE - override fun handleResponse(doc: Document): Void? { contentResolver.delete(AwfulMessage.CONTENT_URI, "${AwfulMessage.ID}=?", arrayOf(pmId.toString())) return null diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt index 274bb7d9a..7fcd6a6e5 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/SinglePostRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.util.AwfulError import org.jsoup.nodes.Document @@ -19,11 +18,10 @@ class SinglePostRequest(context: Context, private val postId: String) get() = REQUEST_TAG - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - appendQueryParameter(PARAM_ACTION, ACTION_SHOWPOST) - appendQueryParameter(PARAM_POST_ID, postId) - return build().toString() + init { + with(parameters) { + add(PARAM_ACTION, ACTION_SHOWPOST) + add(PARAM_POST_ID, postId) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt index b3ca99f29..cfce230ec 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadListRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.announcements.AnnouncementsManager import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.messages.PmManager @@ -31,11 +30,10 @@ class ThreadListRequest(context: Context, private val forumId: Int, private val get() = REQUEST_TAG - override fun generateUrl(urlBuilder: Uri.Builder?): String { - with(urlBuilder!!) { - if (forumId != USERCP_ID) appendQueryParameter(PARAM_FORUM_ID, forumId.toString()) - appendQueryParameter(PARAM_PAGE, page.toString()) - return build().toString() + init { + with(parameters) { + if (forumId != USERCP_ID) add(PARAM_FORUM_ID, forumId.toString()) + add(PARAM_PAGE, page.toString()) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt index dcb201017..1c9b11faa 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadLockUnlockRequest.kt @@ -1,7 +1,6 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri import com.ferg.awfulapp.constants.Constants.* import org.jsoup.nodes.Document @@ -11,11 +10,10 @@ import org.jsoup.nodes.Document class ThreadLockUnlockRequest(context: Context, private val threadId: Int) : AwfulRequest(context, FUNCTION_POSTINGS) { - override fun generateUrl(urlBuilder: Uri.Builder?): String { - addPostParam(PARAM_THREAD_ID, threadId.toString()) - with(urlBuilder!!) { - appendQueryParameter(PARAM_ACTION, ACTION_TOGGLE_THREAD_LOCKED) - return build().toString() + init { + with(parameters) { + add(PARAM_THREAD_ID, threadId.toString()) + add(PARAM_ACTION, ACTION_TOGGLE_THREAD_LOCKED) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt index 0e763d330..ad1b58398 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ThreadPageRequest.kt @@ -1,8 +1,7 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.thread.AwfulThread import org.jsoup.nodes.Document @@ -19,19 +18,19 @@ import org.jsoup.nodes.Document * something to be aware of. */ class ThreadPageRequest(context: Context, private val threadId: Int, private val page: Int, private val userId: Int = 0) - : AwfulStrippedRequest(context, Constants.FUNCTION_THREAD) { + : AwfulStrippedRequest(context, FUNCTION_THREAD) { override val requestTag: Any get() = REQUEST_TAG - - override fun generateUrl(urlBuilder: Uri.Builder?): String = with(urlBuilder!!) { - appendQueryParameter(Constants.PARAM_THREAD_ID, threadId.toString()) - appendQueryParameter(Constants.PARAM_PER_PAGE, preferences.postPerPage.toString()) - appendQueryParameter(Constants.PARAM_PAGE, page.toString()) - if (userId > 0) appendQueryParameter(Constants.PARAM_USER_ID, userId.toString()) - build().toString() + init { + with(parameters) { + add(PARAM_THREAD_ID, threadId.toString()) + add(PARAM_PER_PAGE, preferences.postPerPage.toString()) + add(PARAM_PAGE, page.toString()) + if (userId > 0) add(PARAM_USER_ID, userId.toString()) + } } override fun handleResponse(doc: Document): Void? { @@ -39,10 +38,10 @@ class ThreadPageRequest(context: Context, private val threadId: Int, private val return null } - public override fun handleStrippedResponse(doc: Document, currentPage: Int?, totalPages: Int?): Void? { + public override fun handleStrippedResponse(document: Document, currentPage: Int?, totalPages: Int?): Void? { // TODO: this is all kinda janky, best to use the passed data from the response, right? Instead of relying on 'page' from the request val lastPage = totalPages ?: page - AwfulThread.parseThreadPage(contentResolver, doc, threadId, page, lastPage, preferences.postPerPage, preferences, userId) + AwfulThread.parseThreadPage(contentResolver, document, threadId, page, lastPage, preferences.postPerPage, preferences, userId) return null } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt index c4eeb15a7..81a8ceec2 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/VoteRequest.kt @@ -1,26 +1,24 @@ package com.ferg.awfulapp.task import android.content.Context -import android.net.Uri - import com.android.volley.VolleyError import com.ferg.awfulapp.R -import com.ferg.awfulapp.constants.Constants +import com.ferg.awfulapp.constants.Constants.* import com.ferg.awfulapp.util.AwfulError - import org.jsoup.nodes.Document /** * A request that submits a rating for a thread. */ -class VoteRequest(context: Context, threadId: Int, vote: Int) : AwfulRequest(context, null) { +class VoteRequest(context: Context, threadId: Int, vote: Int) + : AwfulRequest(context, FUNCTION_RATE_THREAD) { init { - addPostParam(Constants.PARAM_THREAD_ID, threadId.toString()) - addPostParam(Constants.PARAM_VOTE, vote.toString()) + with(parameters) { + add(PARAM_THREAD_ID, threadId.toString()) + add(PARAM_VOTE, vote.toString()) + } } - override fun generateUrl(urlBuilder: Uri.Builder?) = Constants.FUNCTION_RATE_THREAD - override fun handleResponse(doc: Document): Void? = null override fun customizeProgressListenerError(error: VolleyError): VolleyError = From 9dff887a0e9d6d221e4146dc8a113ca033c36b30 Mon Sep 17 00:00:00 2001 From: baka kaba Date: Sun, 20 Jan 2019 00:04:21 +0000 Subject: [PATCH 03/17] Fix blank initial pages when the WebView is (re)created This basically does a refresh call when the HTML is loaded and the container is first initialised. If the container wasn't ready when a "display content" call came in, then it ends up loading it here It's *possible* that some content might refresh the display twice, if the container initialised refresh coincides with the app setting the content, so there are two JS #showPageHtml calls after bodyHtml is set. I'm only mentioning it in case it comes up as a "why's that happening" issue - it should be rare if it ever happens, and it would only be a single glitch when the WebView is first initialised Fixes #653 --- .../src/main/assets/javascript/thread.js | 3 +++ .../awfulapp/webview/WebViewJsInterface.java | 20 ++++--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Awful.apk/src/main/assets/javascript/thread.js b/Awful.apk/src/main/assets/javascript/thread.js index 54dc2af16..26ae03dcd 100644 --- a/Awful.apk/src/main/assets/javascript/thread.js +++ b/Awful.apk/src/main/assets/javascript/thread.js @@ -83,6 +83,9 @@ function containerInit() { window.addEventListener('awful-scroll-post', function scrollToPost() { window.topScrollID = window.requestAnimationFrame(scrollPost.bind(null, null)); }); + + // trigger a page content load, in case some was sent before the container was ready to handle it + loadPageHtml(); } /** diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java b/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java index cd040e5d9..058dbd294 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java @@ -2,8 +2,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.util.Log; import android.webkit.JavascriptInterface; import com.ferg.awfulapp.preferences.AwfulPreferences; @@ -12,6 +10,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import timber.log.Timber; + /** * Created by baka kaba on 23/01/2017. *

@@ -21,27 +21,17 @@ * be used in a {@link com.ferg.awfulapp.ThreadDisplayFragment} and needs to react to other UI clicks. */ // TODO: 12/02/2017 JS interface methods are called on a separate thread apparently - none of our implementations are thread-safe at all -@WorkerThread public class WebViewJsInterface { - private static final String TAG = "WebViewJsInterface"; - private final Map preferences = new ConcurrentHashMap<>(); @NonNull - private String bodyHtml = ""; - - @Nullable - private AwfulWebView webView = null; + private volatile String bodyHtml = ""; public WebViewJsInterface() { updatePreferences(); } - void setWebView(@NonNull AwfulWebView webView) { - this.webView = webView; - } - /** * Updates the JavaScript-accessible preference store from the current values in AwfulPreferences. */ @@ -77,8 +67,6 @@ public final void updatePreferences() { protected void setCustomPreferences(Map preferences) { } - // TODO: sync for threads? check html -> update html needs to be atomic? - @NonNull @JavascriptInterface public final String getBodyHtml() { @@ -97,7 +85,7 @@ public String getPreference(String preference) { @JavascriptInterface public void debugMessage(final String msg) { - Log.d(TAG, "Awful DEBUG: " + msg); + Timber.d("Awful DEBUG: %s", msg); } // TODO: 28/01/2017 work out if any other common interface methods can go in here From 259818bb1265129200637f5833ec12e0d8219f1c Mon Sep 17 00:00:00 2001 From: baka kaba Date: Sun, 20 Jan 2019 00:05:09 +0000 Subject: [PATCH 04/17] Bump version and update changelog --- Awful.apk/src/main/AndroidManifest.xml | 4 ++-- Awful.apk/src/main/assets/changelog.html | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 0eb26f075..6f5d78e16 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@

+
+

3.6.2

+
    +
  • Removed zen "blank page" feature bestowed by the cosmos 🧘
  • +
+
+

3.6.1

  • Had a serious talk with the post menu button about breaking the layout. It is very sorry and promised to not do it again.
+
+ +

3.6.0

  • Now with leper's colony. Enjoy reading about why you were banned (hint: it's for posting badly)
  • @@ -29,6 +39,7 @@

    3.6.0

  • That changelog you've been reading? I guess that's new as well. It's a christmas miracle!
+

3.5.2

    From fa722c2205328475abe213ada6d0c2b1be307b4b Mon Sep 17 00:00:00 2001 From: baka kaba Date: Sun, 20 Jan 2019 00:04:21 +0000 Subject: [PATCH 05/17] Fix blank initial pages when the WebView is (re)created This basically does a refresh call when the HTML is loaded and the container is first initialised. If the container wasn't ready when a "display content" call came in, then it ends up loading it here It's *possible* that some content might refresh the display twice, if the container initialised refresh coincides with the app setting the content, so there are two JS #showPageHtml calls after bodyHtml is set. I'm only mentioning it in case it comes up as a "why's that happening" issue - it should be rare if it ever happens, and it would only be a single glitch when the WebView is first initialised Fixes #653 --- .../src/main/assets/javascript/thread.js | 3 +++ .../awfulapp/webview/WebViewJsInterface.java | 20 ++++--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Awful.apk/src/main/assets/javascript/thread.js b/Awful.apk/src/main/assets/javascript/thread.js index 54dc2af16..26ae03dcd 100644 --- a/Awful.apk/src/main/assets/javascript/thread.js +++ b/Awful.apk/src/main/assets/javascript/thread.js @@ -83,6 +83,9 @@ function containerInit() { window.addEventListener('awful-scroll-post', function scrollToPost() { window.topScrollID = window.requestAnimationFrame(scrollPost.bind(null, null)); }); + + // trigger a page content load, in case some was sent before the container was ready to handle it + loadPageHtml(); } /** diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java b/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java index cd040e5d9..058dbd294 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/webview/WebViewJsInterface.java @@ -2,8 +2,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.util.Log; import android.webkit.JavascriptInterface; import com.ferg.awfulapp.preferences.AwfulPreferences; @@ -12,6 +10,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import timber.log.Timber; + /** * Created by baka kaba on 23/01/2017. *

    @@ -21,27 +21,17 @@ * be used in a {@link com.ferg.awfulapp.ThreadDisplayFragment} and needs to react to other UI clicks. */ // TODO: 12/02/2017 JS interface methods are called on a separate thread apparently - none of our implementations are thread-safe at all -@WorkerThread public class WebViewJsInterface { - private static final String TAG = "WebViewJsInterface"; - private final Map preferences = new ConcurrentHashMap<>(); @NonNull - private String bodyHtml = ""; - - @Nullable - private AwfulWebView webView = null; + private volatile String bodyHtml = ""; public WebViewJsInterface() { updatePreferences(); } - void setWebView(@NonNull AwfulWebView webView) { - this.webView = webView; - } - /** * Updates the JavaScript-accessible preference store from the current values in AwfulPreferences. */ @@ -77,8 +67,6 @@ public final void updatePreferences() { protected void setCustomPreferences(Map preferences) { } - // TODO: sync for threads? check html -> update html needs to be atomic? - @NonNull @JavascriptInterface public final String getBodyHtml() { @@ -97,7 +85,7 @@ public String getPreference(String preference) { @JavascriptInterface public void debugMessage(final String msg) { - Log.d(TAG, "Awful DEBUG: " + msg); + Timber.d("Awful DEBUG: %s", msg); } // TODO: 28/01/2017 work out if any other common interface methods can go in here From 694c25145465bc45e6ad8558afea65fe7c1020a8 Mon Sep 17 00:00:00 2001 From: baka kaba Date: Sun, 20 Jan 2019 00:05:09 +0000 Subject: [PATCH 06/17] Bump version and update changelog --- Awful.apk/src/main/AndroidManifest.xml | 4 ++-- Awful.apk/src/main/assets/changelog.html | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 0eb26f075..6f5d78e16 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@

    +
    +

    3.6.2

    +
      +
    • Removed zen "blank page" feature bestowed by the cosmos 🧘
    • +
    +
    +

    3.6.1

    • Had a serious talk with the post menu button about breaking the layout. It is very sorry and promised to not do it again.
    +
    + +

    3.6.0

    • Now with leper's colony. Enjoy reading about why you were banned (hint: it's for posting badly)
    • @@ -29,6 +39,7 @@

      3.6.0

    • That changelog you've been reading? I guess that's new as well. It's a christmas miracle!
    +

    3.5.2

      From c124dd2dd9ec04500ccabd12a88bcb8ea6898dc2 Mon Sep 17 00:00:00 2001 From: Jonathan Mathews Date: Mon, 28 Jan 2019 22:18:12 +0000 Subject: [PATCH 07/17] Cleanup (#672) Move font handling to new FontManager singleton, some cleanup Font handling is now encapsulated in a dedicated FontManager class. Cleaned up some code, removed some unused parameters and fixed indentation. Switched some Timber calls from error to warning level --- .../java/com/ferg/awfulapp/AwfulActivity.kt | 2 +- .../com/ferg/awfulapp/AwfulApplication.java | 237 +++++----------- .../java/com/ferg/awfulapp/FontManager.java | 217 +++++++++++++++ .../ferg/awfulapp/network/NetworkUtils.java | 262 +++++++++--------- .../preferences/fragments/ThemeSettings.java | 33 +-- 5 files changed, 430 insertions(+), 321 deletions(-) create mode 100644 Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt index f2e137b30..82a8a5278 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulActivity.kt @@ -187,7 +187,7 @@ abstract class AwfulActivity : AppCompatActivity(), AwfulPreferences.AwfulPrefer @JvmOverloads fun setPreferredFont(view: View?, flags: Int = -1) = - view?.let { (application as AwfulApplication).setPreferredFont(view, flags) } + view?.let { FontManager.getInstance().setTypefaceToCurrentFont(view, flags) } protected fun updateTheme() = setTheme(AwfulTheme.forForum(null).themeResId) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java index 6893c9a75..83c71cda6 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/AwfulApplication.java @@ -4,11 +4,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.graphics.Typeface; import android.os.StrictMode; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; import com.crashlytics.android.Crashlytics; import com.ferg.awfulapp.announcements.AnnouncementsManager; @@ -19,78 +15,80 @@ import com.jakewharton.threetenabp.AndroidThreeTen; import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Set; import java.util.concurrent.TimeUnit; import io.fabric.sdk.android.Fabric; import timber.log.Timber; -public class AwfulApplication extends Application implements AwfulPreferences.AwfulPreferenceUpdate { - private static final String APP_STATE_PREFERENCES = "app_state_prefs"; - /** - * Used for storing misc app data, separate from user preferences, so onPreferenceChange callbacks aren't triggered - */ - private static SharedPreferences appStatePrefs; +public class AwfulApplication extends Application { + private static final String APP_STATE_PREFERENCES = "app_state_prefs"; + /** + * Used for storing misc app data, separate from user preferences, so onPreferenceChange callbacks aren't triggered + */ + private static SharedPreferences appStatePrefs; private static boolean crashlyticsEnabled = false; - private AwfulPreferences mPref; - private final HashMap fonts = new HashMap<>(); - private Typeface currentFont; - @Override public void onCreate() { super.onCreate(); - // initialise the AwfulPreferences singleton first since a lot of things rely on it for a Context - mPref = AwfulPreferences.getInstance(this, this); - appStatePrefs = this.getSharedPreferences(APP_STATE_PREFERENCES, MODE_PRIVATE); - - NetworkUtils.init(this); - AndroidThreeTen.init(this); - AnnouncementsManager.init(); - onPreferenceChange(mPref,null); - - // work out how long it's been since the app was updated - long hoursSinceInstall = Long.MAX_VALUE; - try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); - long millisSinceInstall = System.currentTimeMillis() - packageInfo.lastUpdateTime; - hoursSinceInstall = TimeUnit.HOURS.convert(millisSinceInstall, TimeUnit.MILLISECONDS); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - Timber.i("App installed %d hours ago", hoursSinceInstall); - - // enable Crashlytics on non-debug builds, or debug builds that have been installed for a while - crashlyticsEnabled = !BuildConfig.DEBUG || hoursSinceInstall > 4; + + // initialise the AwfulPreferences singleton first since a lot of things rely on it for a Context + AwfulPreferences mPref = AwfulPreferences.getInstance(this); + + appStatePrefs = this.getSharedPreferences(APP_STATE_PREFERENCES, MODE_PRIVATE); + + NetworkUtils.init(this); + AndroidThreeTen.init(this); + AnnouncementsManager.init(); + FontManager.createInstance(mPref, getAssets()); + + long hoursSinceInstall = getHoursSinceInstall(); + + // enable Crashlytics on non-debug builds, or debug builds that have been installed for a while + crashlyticsEnabled = !BuildConfig.DEBUG || hoursSinceInstall > 4; + if (crashlyticsEnabled) { - Fabric.with(this, new Crashlytics()); - Timber.plant(new CrashlyticsReportingTree()); - if (mPref.sendUsernameInReport) { - Crashlytics.setUserName(mPref.username); - } - } else { - Timber.plant(new Timber.DebugTree()); - } - - if (Constants.DEBUG) { - Timber.d("*\n*\n*Debug active\n*\n*"); + Fabric.with(this, new Crashlytics()); + + if (mPref.sendUsernameInReport) + Crashlytics.setUserName(mPref.username); + } + + Timber.plant(crashlyticsEnabled ? new CrashlyticsReportingTree() : new Timber.DebugTree()); + + Timber.i("App installed %d hours ago", hoursSinceInstall); + + if (Constants.DEBUG) { + Timber.d("*\n*\n*Debug active\n*\n*"); /* This checks destroyed cursors aren't left open, and crashes (with a log) if it finds one Really this is here to avoid introducing any more leaks, since there are some issues with too many open cursors */ - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() - .detectLeakedSqlLiteObjects() - .penaltyLog() - .penaltyDeath() - .build()); - } - - SyncManager.sync(this); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .penaltyLog() + .penaltyDeath() + .build()); + } + + SyncManager.sync(this); } + /** + * @return how long it's been since the app was updated + */ + private long getHoursSinceInstall() { + long hoursSinceInstall = Long.MAX_VALUE; + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + long millisSinceInstall = System.currentTimeMillis() - packageInfo.lastUpdateTime; + hoursSinceInstall = TimeUnit.HOURS.convert(millisSinceInstall, TimeUnit.MILLISECONDS); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return hoursSinceInstall; + } /** * Returns true if the Crashlytics singleton has been initialised and can be used. @@ -99,121 +97,34 @@ public static boolean crashlyticsEnabled() { return crashlyticsEnabled; } - - /** - * Get the SharedPreferences used for storing basic app state. - *

      - * These are separate from the default shared preferences, and won't trigger onPreferenceChange callbacks. - * - * @see AwfulPreferences.AwfulPreferenceUpdate#onPreferenceChange(AwfulPreferences, String) - */ - public static SharedPreferences getAppStatePrefs() { - return appStatePrefs; - } - - - public void setFontFromPreference(TextView textView, int flags){ - if(flags < 0 && textView.getTypeface() != null){ - flags = textView.getTypeface().getStyle(); - }else{ - flags = Typeface.NORMAL; - } - if(fonts.size() == 0){ - processFonts(); - } - if(currentFont != null){ - if(mPref.preferredFont.contains("mono")){ - switch(flags){ - case Typeface.BOLD: - textView.setTypeface(currentFont, Typeface.MONOSPACE.BOLD); - break; - case Typeface.ITALIC: - textView.setTypeface(currentFont, Typeface.MONOSPACE.ITALIC); - break; - case Typeface.BOLD_ITALIC: - textView.setTypeface(currentFont, Typeface.MONOSPACE.BOLD_ITALIC); - break; - case Typeface.NORMAL: - default: - textView.setTypeface(currentFont, Typeface.MONOSPACE.NORMAL); - break; - } - }else{ - textView.setTypeface(currentFont, flags); - } - } - } - - public void setFontFromPreferenceRecurse(ViewGroup viewGroup, int flags){ - for(int x=0;x + * These are separate from the default shared preferences, and won't trigger onPreferenceChange callbacks. + * + * @see AwfulPreferences.AwfulPreferenceUpdate#onPreferenceChange(AwfulPreferences, String) + */ + public static SharedPreferences getAppStatePrefs() { + return appStatePrefs; } - public void setPreferredFont(View view, int flags) { - if(view instanceof TextView){ - setFontFromPreference((TextView)view, flags); - }else if(view instanceof ViewGroup){ - setFontFromPreferenceRecurse((ViewGroup)view, flags); - } - } - - @Override - public void onPreferenceChange(AwfulPreferences prefs, String key) { - currentFont = fonts.get(mPref.preferredFont); - Timber.i("FONT SELECTED: "+mPref.preferredFont); - } - - public String[] getFontList() { - if(fonts.size() == 0){ - processFonts(); - } - Set keys = fonts.keySet(); - for(String key : keys){ - Timber.i("Font: "+key); - } - return keys.toArray(new String[keys.size()]); - } - - private void processFonts(){ - fonts.clear(); - fonts.put("default",Typeface.defaultFromStyle(Typeface.NORMAL)); - try { - String[] files = getAssets().list("fonts"); - for(String file : files){ - String fileName = "fonts/"+file; - fonts.put(fileName, Typeface.createFromAsset(getAssets(), fileName)); - Timber.i("Processed Font: "+fileName); - } - } catch (IOException | RuntimeException e) { - e.printStackTrace(); - } - onPreferenceChange(mPref, null); - } - - @Override - public File getCacheDir() { - Timber.i("getCacheDir(): " + super.getCacheDir()); - return super.getCacheDir(); - } - + @Override + public File getCacheDir() { + Timber.i("getCacheDir(): %s", super.getCacheDir()); + return super.getCacheDir(); + } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); - if(level != Application.TRIM_MEMORY_UI_HIDDEN && level != Application.TRIM_MEMORY_BACKGROUND){ - NetworkUtils.clearImageCache(); + if (level != Application.TRIM_MEMORY_UI_HIDDEN && level != Application.TRIM_MEMORY_BACKGROUND) { + NetworkUtils.clearImageCache(); } } @Override public void onLowMemory() { - super.onLowMemory(); - NetworkUtils.clearImageCache(); + super.onLowMemory(); + NetworkUtils.clearImageCache(); } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java b/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java new file mode 100644 index 000000000..2906b65d2 --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/FontManager.java @@ -0,0 +1,217 @@ +package com.ferg.awfulapp; + +import android.content.res.AssetManager; +import android.graphics.Typeface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.ferg.awfulapp.preferences.AwfulPreferences; + +import org.apache.commons.lang3.text.WordUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import timber.log.Timber; + +/** + * Handles accessing font files from the assets + */ +public class FontManager implements AwfulPreferences.AwfulPreferenceUpdate { + private static FontManager instance; + private static final String FONT_PATH = "fonts"; + private Typeface currentFont; + private final Map fonts = new HashMap<>(); + + /** + * Get the singleton instance of FontManager. + *

      + * Note: Will be null if it hasn't been built using {@link #createInstance(AwfulPreferences, AssetManager)} + * + * @return The instance of FontManager or null. + */ + public static FontManager getInstance() { + return instance; + } + + /** + * Create the singleton instance of FontManager. + * + * @param preferences The AwfulPreferences + * @param assets An AssetManager for accessing the font files + */ + public static void createInstance(@NonNull AwfulPreferences preferences, @NonNull AssetManager assets) { + instance = new FontManager(preferences.preferredFont, assets); + preferences.registerCallback(instance); + } + + /** + * Constructor for FontManager + * + * @param preferredFont The filename of the selected font + * @param assets An AssetManager for accessing the font files + */ + private FontManager(String preferredFont, AssetManager assets) { + buildFontList(preferredFont, assets); + } + + /** + * Get the list of font filenames. + * + * @return The list of font filenames as a String array + */ + public String[] getFontFilenames() { + Timber.i("Font list: %s", fonts.keySet()); + return fonts.keySet().toArray(new String[0]); + } + + /** + * Get the list of clean font names + * + * @return The list of font filenames as a String array + */ + public String[] getFontNames() { + return extractFontNames(getFontFilenames()); + } + + /** + * Called to update the current font when the AwfulPreferences have changed. + * + * @param preferences The new AwfulPreferences + * @param key Not used + */ + @Override + public void onPreferenceChange(AwfulPreferences preferences, @Nullable String key) { + setCurrentFont(preferences.preferredFont); + } + + /** + * Set the current font from the fonts map. + * + * @param fontName Filename of the current font + */ + public void setCurrentFont(String fontName) { + currentFont = fonts.get(fontName); + + if (currentFont != null) + Timber.i("Font Selected: %s", fontName); + else + Timber.w("Couldn't select font: %s", fontName); + } + + /** + * Set typeface of TextViews and all child TextViews to the current font. + * + * @param view View to be processed + * @param flags {@link Typeface#NORMAL}, {@link Typeface#BOLD}, + * {@link Typeface#ITALIC}, or {@link Typeface#BOLD_ITALIC}, + */ + public void setTypefaceToCurrentFont(View view, int flags) { + if (view instanceof TextView) + setTextViewTypefaceToCurrentFont((TextView) view, flags); + else if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + + for (int i = 0; i < viewGroup.getChildCount(); i++) + setTypefaceToCurrentFont(viewGroup.getChildAt(i), flags); + } + } + + /** + * Recreate the font Map from the asset files. + * + * @param preferredFont The filename of the currently selected font + * @param assets An AssetManager for accessing the font files + */ + public void buildFontList(String preferredFont, AssetManager assets) { + fonts.clear(); + fonts.put("default", Typeface.defaultFromStyle(Typeface.NORMAL)); + + String[] files = null; + + try { + files = assets.list(FONT_PATH); + } catch (IOException | RuntimeException e) { + e.printStackTrace(); + } + + if (files == null) { + Timber.w("Couldn't load font assets from %s", FONT_PATH); + return; + } + + for (String file : files) { + String fileName = String.format("%s/%s", FONT_PATH, file); + fonts.put(fileName, Typeface.createFromAsset(assets, fileName)); + Timber.i("Processed Font: %s", fileName); + } + + setCurrentFont(preferredFont); + } + + /** + * Set a TextView's typeface to the current font. + * + * @param textView TextView to set + * @param textStyle {@link Typeface#NORMAL}, {@link Typeface#BOLD}, + * {@link Typeface#ITALIC}, or {@link Typeface#BOLD_ITALIC}, + */ + private void setTextViewTypefaceToCurrentFont(TextView textView, int textStyle) { + if (!isValidTextStyle(textStyle)) { + textStyle = textView.getTypeface() != null ? + textView.getTypeface().getStyle() : Typeface.NORMAL; + } + + if (currentFont != null) + textView.setTypeface(currentFont, textStyle); + else + Timber.w("Couldn't set typeface as currentFont is null"); + } + + /** + * Check if the passed text style is valid. + * + * @param textStyle A text style + * @return True iff textStyle is valid + */ + private static boolean isValidTextStyle(int textStyle) { + return textStyle == Typeface.NORMAL || textStyle == Typeface.BOLD || + textStyle == Typeface.ITALIC || textStyle == Typeface.BOLD_ITALIC; + } + + /** + * Create clean font names from the given file names. + * + * @param fontList An array of font file names + * @return An array of font names + */ + private static String[] extractFontNames(@NonNull String[] fontList) { + String[] fontNames = new String[fontList.length]; + + Pattern pattern = Pattern.compile(FONT_PATH + "/(.*).ttf.mp3", Pattern.CASE_INSENSITIVE); + + for (int i = 0; i < fontList.length; i++) { + String fontName; + Matcher matcher = pattern.matcher(fontList[i]); + + if (matcher.find()) { + fontName = matcher.group(1).replaceAll("_", " "); + } else { + //if the regex fails, try our best to clean up the filename. + fontName = fontList[i].replaceAll(".ttf.mp3", "") + .replaceAll("fonts/", "") + .replaceAll("_", " "); + } + + fontNames[i] = WordUtils.capitalize(fontName); + } + + return fontNames; + } +} \ No newline at end of file diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java b/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java index 69516efe8..f6d422f28 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/network/NetworkUtils.java @@ -31,10 +31,8 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.http.HttpResponseCache; -import android.os.Messenger; import android.support.annotation.NonNull; import android.text.TextUtils; -import android.util.Log; import com.android.volley.Request; import com.android.volley.RequestQueue; @@ -49,6 +47,7 @@ import org.jsoup.nodes.Document; import java.io.File; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.CookieHandler; import java.net.CookieManager; @@ -60,39 +59,37 @@ import java.util.Calendar; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import timber.log.Timber; + public class NetworkUtils { - private static final String TAG = "NetworkUtils"; private static final String CHARSET = "windows-1252"; private static final Pattern unencodeCharactersPattern = Pattern.compile("&#(\\d+);"); private static final Pattern encodeCharactersPattern = Pattern.compile("([^\\x00-\\x7F])"); - - private static RequestQueue mNetworkQueue; - private static LRUImageCache mImageCache; - private static ImageLoader mImageLoader; + private static RequestQueue mNetworkQueue; + private static LRUImageCache mImageCache; + private static ImageLoader mImageLoader; private static CookieManager ckmngr; private static String cookie = null; private static final String COOKIE_HEADER = "Cookie"; - static { ckmngr = new CookieManager(null, CookiePolicy.ACCEPT_ALL); CookieHandler.setDefault(ckmngr); } - /** * Initialise request handling and caching - call this early! - * @param context A context used to create a cache dir + * + * @param context A context used to create a cache dir */ public static void init(Context context) { // update the security provider first, to ensure we fix SSL errors before setting anything else up @@ -119,32 +116,30 @@ public static void clearImageCache() { } } - - public static void queueRequest(Request request){ + public static void queueRequest(Request request) { if (mNetworkQueue != null) { mNetworkQueue.add(request); } else { - Log.w(TAG, "Can't queue request - NetworkQueue is null, has NetworkUtils been initialised?"); + Timber.w("Can't queue request - NetworkQueue is null, has NetworkUtils been initialised?"); } } - - public static void cancelRequests(Object tag){ + public static void cancelRequests(Object tag) { if (mNetworkQueue != null) { mNetworkQueue.cancelAll(tag); } else { - Log.w(TAG, "Can't cancel requests - NetworkQueue is null, has NetworkUtils been initialised?"); + Timber.w("Can't cancel requests - NetworkQueue is null, has NetworkUtils been initialised?"); } } /** * Add the current session cookie's data to a header map. - * + *

      * The data is provided as a single header - see {@link #restoreLoginCookies(Context)} for the format. */ public static void setCookieHeaders(@NonNull Map headers) { - if(cookie == null){ - Log.e(TAG,"Cookie was empty for some reason, trying to restore cookie"); + if (cookie == null) { + Timber.w("Cookie was empty for some reason, trying to restore cookie"); restoreLoginCookies(AwfulPreferences.getInstance().getContext()); } if (!cookie.isEmpty()) { @@ -170,51 +165,54 @@ public static synchronized boolean restoreLoginCookies(Context ctx) { long expiry = prefs.getLong(Constants.COOKIE_PREF_EXPIRY_DATE, -1); int cookieVersion = prefs.getInt(Constants.COOKIE_PREF_VERSION, 0); - if (useridCookieValue != null && passwordCookieValue != null && expiry != -1) { - cookie = String.format("%s=%s;%s=%s;%s=%s;%s=%s;", - Constants.COOKIE_NAME_USERID, useridCookieValue, - Constants.COOKIE_NAME_PASSWORD, passwordCookieValue, - Constants.COOKIE_NAME_SESSIONID, sessionidCookieValue, - Constants.COOKIE_NAME_SESSIONHASH, sessionhashCookieValue); - - HttpCookie useridCookie = - new HttpCookie(Constants.COOKIE_NAME_USERID, useridCookieValue); - HttpCookie passwordCookie = - new HttpCookie(Constants.COOKIE_NAME_PASSWORD, passwordCookieValue); - HttpCookie sessionidCookie = - new HttpCookie(Constants.COOKIE_NAME_SESSIONID, sessionidCookieValue); - HttpCookie sessionhashCookie = - new HttpCookie(Constants.COOKIE_NAME_SESSIONHASH, sessionhashCookieValue); - - Date expiryDate = new Date(expiry); - Date now = new Date(); - HttpCookie[] allCookies = {useridCookie, passwordCookie, sessionidCookie, sessionhashCookie}; - - Log.e(TAG, "now.compareTo(expiryDate):" + (expiryDate.getTime() - now.getTime())); - for (HttpCookie tempCookie : allCookies) { - tempCookie.setVersion(cookieVersion); - tempCookie.setDomain(Constants.COOKIE_DOMAIN); - tempCookie.setMaxAge(expiryDate.getTime() - now.getTime()); - tempCookie.setPath(Constants.COOKIE_PATH); - } - ckmngr.getCookieStore().add(URI.create(Constants.COOKIE_DOMAIN), useridCookie); - ckmngr.getCookieStore().add(URI.create(Constants.COOKIE_DOMAIN), passwordCookie); - ckmngr.getCookieStore().add(URI.create(Constants.COOKIE_DOMAIN), sessionhashCookie); - if(Constants.DEBUG) { - Log.w(TAG, "Cookies restored from prefs"); - Log.w(TAG, "Cookie dump: " + TextUtils.join("\n", ckmngr.getCookieStore().getCookies())); + if (useridCookieValue == null || passwordCookieValue == null || expiry == -1) { + if (Constants.DEBUG) { + Timber.w("Unable to restore cookies! Reasons:\n" + + (useridCookieValue == null ? "USER_ID is NULL\n" : "") + + (passwordCookieValue == null ? "PASSWORD is NULL\n" : "") + + (expiry == -1 ? "EXPIRY is -1" : "")); } - return true; - } else { - String logMsg = "Unable to restore cookies! Reasons:\n"; - logMsg += (useridCookieValue == null) ? "USER_ID is NULL\n" : ""; - logMsg += (passwordCookieValue == null) ? "PASSWORD is NULL\n" : ""; - logMsg += (expiry == -1) ? "EXPIRY is -1" : ""; - if(Constants.DEBUG) Log.w(TAG, logMsg); + cookie = ""; + return false; } - return false; + cookie = String.format("%s=%s;%s=%s;%s=%s;%s=%s;", + Constants.COOKIE_NAME_USERID, useridCookieValue, + Constants.COOKIE_NAME_PASSWORD, passwordCookieValue, + Constants.COOKIE_NAME_SESSIONID, sessionidCookieValue, + Constants.COOKIE_NAME_SESSIONHASH, sessionhashCookieValue); + + HttpCookie useridCookie = + new HttpCookie(Constants.COOKIE_NAME_USERID, useridCookieValue); + HttpCookie passwordCookie = + new HttpCookie(Constants.COOKIE_NAME_PASSWORD, passwordCookieValue); + HttpCookie sessionidCookie = + new HttpCookie(Constants.COOKIE_NAME_SESSIONID, sessionidCookieValue); + HttpCookie sessionhashCookie = + new HttpCookie(Constants.COOKIE_NAME_SESSIONHASH, sessionhashCookieValue); + + Date expiryDate = new Date(expiry); + Date now = new Date(); + HttpCookie[] allCookies = {useridCookie, passwordCookie, sessionidCookie, sessionhashCookie}; + + Timber.e("now.compareTo(expiryDate):%s", (expiryDate.getTime() - now.getTime())); + for (HttpCookie tempCookie : allCookies) { + tempCookie.setVersion(cookieVersion); + tempCookie.setDomain(Constants.COOKIE_DOMAIN); + tempCookie.setMaxAge(expiryDate.getTime() - now.getTime()); + tempCookie.setPath(Constants.COOKIE_PATH); + } + ckmngr.getCookieStore().add(URI.create(Constants.COOKIE_DOMAIN), useridCookie); + ckmngr.getCookieStore().add(URI.create(Constants.COOKIE_DOMAIN), passwordCookie); + ckmngr.getCookieStore().add(URI.create(Constants.COOKIE_DOMAIN), sessionhashCookie); + + if (Constants.DEBUG) { + Timber.w("Cookies restored from prefs"); + Timber.w("Cookie dump: %s", TextUtils.join("\n", ckmngr.getCookieStore().getCookies())); + } + + return true; } /** @@ -223,7 +221,7 @@ public static synchronized boolean restoreLoginCookies(Context ctx) { */ public static synchronized void clearLoginCookies(Context ctx) { // First clear out the persistent preferences... - if(null == ctx){ + if (null == ctx) { ctx = AwfulPreferences.getInstance().getContext(); } SharedPreferences prefs = ctx.getSharedPreferences( @@ -255,35 +253,35 @@ public static synchronized boolean saveLoginCookies(Context ctx) { Date expires = null; Integer version = null; - List cookies = ckmngr.getCookieStore().getCookies(); - for (HttpCookie cookie : cookies) { - if (cookie.getDomain().contains(Constants.COOKIE_DOMAIN)) { - final String cookieName = cookie.getName(); - switch (cookieName) { - case Constants.COOKIE_NAME_USERID: - useridValue = cookie.getValue(); - break; - case Constants.COOKIE_NAME_PASSWORD: - passwordValue = cookie.getValue(); - break; - case Constants.COOKIE_NAME_SESSIONID: - sessionId = cookie.getValue(); - break; - case Constants.COOKIE_NAME_SESSIONHASH: - sessionHash = cookie.getValue(); - break; - } - // keep the soonest valid expiry in case they don't match - Calendar c = Calendar.getInstance(); - c.add(Calendar.SECOND, ((int) cookie.getMaxAge())); - Date cookieExpiryDate = c.getTime(); - if (expires == null || (cookieExpiryDate != null && cookieExpiryDate.before(expires))) { - expires = cookieExpiryDate; - } - // fall back to the lowest cookie spec version - if (version == null || cookie.getVersion() < version) { - version = cookie.getVersion(); - } + for (HttpCookie cookie : ckmngr.getCookieStore().getCookies()) { + if (!cookie.getDomain().contains(Constants.COOKIE_DOMAIN)) + continue; + + switch (cookie.getName()) { + case Constants.COOKIE_NAME_USERID: + useridValue = cookie.getValue(); + break; + case Constants.COOKIE_NAME_PASSWORD: + passwordValue = cookie.getValue(); + break; + case Constants.COOKIE_NAME_SESSIONID: + sessionId = cookie.getValue(); + break; + case Constants.COOKIE_NAME_SESSIONHASH: + sessionHash = cookie.getValue(); + break; + } + + // keep the soonest valid expiry in case they don't match + Calendar c = Calendar.getInstance(); + c.add(Calendar.SECOND, ((int) cookie.getMaxAge())); + Date cookieExpiryDate = c.getTime(); + if (expires == null || (cookieExpiryDate != null && cookieExpiryDate.before(expires))) { + expires = cookieExpiryDate; + } + // fall back to the lowest cookie spec version + if (version == null || cookie.getVersion() < version) { + version = cookie.getVersion(); } } @@ -318,42 +316,40 @@ public static synchronized String getCookieString(String type) { } } } - Log.w(TAG, "getCookieString couldn't find type: " + type); + Timber.w("getCookieString couldn't find type: %s", type); return ""; } public static Document get(String aUrl) throws Exception { - return get(new URI(aUrl), null, 0); + return get(new URI(aUrl)); } - public static Document get(URI location, Messenger statusCallback, int midpointPercent) throws Exception { - Document response = null; - String responseString = ""; - - Log.i(TAG, "Fetching " + location); + public static Document get(URI location) throws Exception { + Timber.i("Fetching %s", location); HttpURLConnection urlConnection = (HttpURLConnection) location.toURL().openConnection(); + + if (urlConnection == null) { + Timber.e("Couldn't open connection"); + return null; + } + + Document response; + try { - if (urlConnection != null) { - response = Jsoup.parse(urlConnection.getInputStream(), CHARSET, Constants.BASE_URL); - } - }finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } + InputStream inputStream = urlConnection.getInputStream(); + response = Jsoup.parse(inputStream, CHARSET, Constants.BASE_URL); + } finally { + urlConnection.disconnect(); } - Log.i(TAG, "Fetched " + location); + + Timber.i("Fetched %s", location); return response; } - public static String getRedirect(String aUrl, HashMap aParams) throws Exception { - URI location; - if (aParams != null) { - location = new URI(aUrl + getQueryStringParameters(aParams)); - } else { - location = new URI(aUrl); - } + URI location = new URI(aUrl + getQueryStringParameters(aParams)); + String redirectLocation; HttpURLConnection urlConnection = (HttpURLConnection) location.toURL().openConnection(); try { @@ -371,42 +367,38 @@ public static String getRedirect(String aUrl, HashMap aParams) t return null; } - public static String getQueryStringParameters(HashMap aParams) { - StringBuilder result = new StringBuilder("?"); + if (aParams == null) + return ""; - if (aParams != null) { - try { - // Loop over each parameter and add it to the query string - Iterator> iter = aParams.entrySet().iterator(); + StringBuilder result = new StringBuilder("?"); - while (iter.hasNext()) { - Map.Entry param = iter.next(); + try { + String separator = ""; - result.append(param.getKey()).append("=").append(URLEncoder.encode(param.getValue(), "UTF-8")); + for (Map.Entry entry : aParams.entrySet()) { + result.append(separator) + .append(entry.getKey()) + .append("=") + .append(URLEncoder.encode(entry.getValue(), "UTF-8")); - if (iter.hasNext()) { - result.append("&"); - } - } - } catch (UnsupportedEncodingException e) { - Log.i(TAG, e.toString()); + separator = "&"; } - } else { - return ""; + } catch (UnsupportedEncodingException e) { + Timber.i(e.toString()); } return result.toString(); } public static void logCookies() { - if(Constants.DEBUG) { - Log.i(TAG, "---BEGIN COOKIE DUMP---"); + if (Constants.DEBUG) { + Timber.i("---BEGIN COOKIE DUMP---"); List cookies = ckmngr.getCookieStore().getCookies(); for (HttpCookie c : cookies) { - Log.i(TAG, c.toString()); + Timber.i(c.toString()); } - Log.i(TAG, "---END COOKIE DUMP---"); + Timber.i("---END COOKIE DUMP---"); } } @@ -444,6 +436,4 @@ public static String encodeHtml(String str) { fixCharMatch.appendTail(unencodedContent); return unencodedContent.toString(); } - - } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java index 5fc5079db..dc4660bb3 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/preferences/fragments/ThemeSettings.java @@ -15,15 +15,14 @@ import android.support.v7.app.AlertDialog; import android.util.Log; -import com.ferg.awfulapp.AwfulApplication; import com.ferg.awfulapp.BuildConfig; +import com.ferg.awfulapp.FontManager; import com.ferg.awfulapp.R; import com.ferg.awfulapp.constants.Constants; import com.ferg.awfulapp.provider.AwfulTheme; import com.ferg.awfulapp.util.AwfulUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.text.WordUtils; import java.io.File; import java.util.ArrayList; @@ -61,7 +60,6 @@ public String getTitle() { protected void initialiseSettings() { super.initialiseSettings(); findPrefById(R.string.pref_key_launcher_icon).setOnPreferenceChangeListener(new IconListener()); - Pattern fontFilename = Pattern.compile("fonts/(.*).ttf.mp3", Pattern.CASE_INSENSITIVE); Activity activity = getActivity(); // TODO: 25/04/2017 a separate permissions class would probably be good, keep all this garbage in one place if (AwfulUtils.isMarshmallow()) { @@ -81,29 +79,12 @@ protected void initialiseSettings() { } } refreshListPreferences(); - - // completely replace all entries in the font ListPreference - ListPreference f = (ListPreference) findPrefById(R.string.pref_key_preferred_font); - String[] fontList = ((AwfulApplication) activity.getApplication()).getFontList(); - String[] fontNames = new String[fontList.length]; - String thisFontName; - for (int x = 0; x < fontList.length; x++) { - Matcher fontName = fontFilename.matcher(fontList[x]); - if (fontName.find()) { - thisFontName = fontName.group(1).replaceAll("_", " "); - } else {//if the regex fails, try our best to clean up the filename. - thisFontName = fontList[x].replaceAll(".ttf.mp3", "").replaceAll("fonts/", "").replaceAll("_", " "); - } - fontNames[x] = WordUtils.capitalize(thisFontName); - } - //noinspection ConstantConditions - let it crash if the preference is missing, someone screwed up - f.setEntries(fontNames); - f.setEntryValues(fontList); } private void refreshListPreferences() { refreshLayoutPreference(); refreshThemePreference(); + refreshFontListPreference(); } /** @@ -199,6 +180,16 @@ private void setListPreferenceChoices(@NonNull ListPreference pref, pref.setEntryValues(values.toArray(new CharSequence[values.size()])); } + private void refreshFontListPreference() { + ListPreference listPreference = (ListPreference) findPrefById(R.string.pref_key_preferred_font); + + // reload the font files + FontManager.getInstance().buildFontList(mPrefs.preferredFont, getActivity().getAssets()); + + // noinspection ConstantConditions - let it crash if the preference is missing, someone screwed up + listPreference.setEntries(FontManager.getInstance().getFontNames()); + listPreference.setEntryValues(FontManager.getInstance().getFontFilenames()); + } @RequiresApi(api = Build.VERSION_CODES.M) private void requestStoragePermissions() { From ebb2d875cdc196b95629380bd21d958ba06868b6 Mon Sep 17 00:00:00 2001 From: baka-kaba Date: Tue, 29 Jan 2019 16:00:17 +0000 Subject: [PATCH 08/17] Fix thread entry in navigation drawer always opening page 1 (#674) Re-added the check that prevents a page load if the navigation event refers to the current thread (and page number, if one is specified) Fixes #668 --- .../com/ferg/awfulapp/ThreadDisplayFragment.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java index 8f5a54e65..40da39ece 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java @@ -43,7 +43,6 @@ import android.graphics.Color; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -1598,13 +1597,11 @@ private void deferNavigation(@NonNull NavigationEvent event) { private void openThread(int id, @Nullable Integer page, @Nullable String postJump){ Timber.i("Opening thread (old/new) ID:%d/%d, PAGE:%s/%s, JUMP:%s/%s", getThreadId(), id, getPageNumber(), page, getPostJump(), postJump); - // removed because it included (if !forceReload) and that param was always set to true -// if (id == currentThreadId && (page == null || page == currentPage)) { -// // do nothing if there's no change -// // TODO: 15/01/2018 handle a change in postJump though? Right now this reflects the old logic from ForumsIndexActivity -// return; -// } - // TODO: 15/01/2018 a call to display a thread may come before the fragment has been properly created - if so, store the request details and perform it when ready. Handle that here or in #loadThread? + if (id == currentThreadId && (page == null || page == currentPage)) { + // do nothing if there's no change + // TODO: 15/01/2018 handle a change in postJump though? Right now this reflects the old logic from ForumsIndexActivity + return; + } clearBackStack(); int threadPage = (page == null) ? FIRST_PAGE : page; loadThread(id, threadPage, postJump, true); From 5f3b60019404b014f226297676f0d814ee667076 Mon Sep 17 00:00:00 2001 From: baka-kaba Date: Tue, 29 Jan 2019 21:30:52 +0000 Subject: [PATCH 09/17] Fix cleartext content in API 28+, use stricter mixed content policy (#673) HTTP content (rather than HTTPS) used to be the default in Android, but in API 28 it's become opt-in instead of opt-out. I've explicitly allowed it in the manifest. I've also switched the WebView's MixedContentMode from ALWAYS_ALLOW to COMPATIBILITY_MODE. The latter should still allow cleartext content that people post, like images, while blocking any dodgier filetypes. It always felt like a security hole leaving it on "allow anything", ideally this will let the WebView people manage the security side without being restrictive --- Awful.apk/src/main/AndroidManifest.xml | 4 +++- .../src/main/java/com/ferg/awfulapp/webview/AwfulWebView.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 6f5d78e16..6d4f55f8c 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -23,7 +23,9 @@ android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" - android:theme="@style/Theme.AwfulTheme.Launcher"> + android:theme="@style/Theme.AwfulTheme.Launcher" + android:usesCleartextTraffic="true" + >

      +
      +

      3.6.3

      +
        +
      • Fixed broken stuff from the terrible non-https internet badlands
      • +
      • WebP is an image format, we knew that, so do our menus
      • +
      • "Keep screen on" keeps on keeping the screen on (should be more reliable)
      • +
      +
      +

      3.6.2

        From abb0089284a1175b9b4c95f14641824c3c75b15c Mon Sep 17 00:00:00 2001 From: baka kaba Date: Mon, 1 Apr 2019 00:45:56 +0100 Subject: [PATCH 15/17] Update contributor list --- Awful.apk/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/Awful.apk/src/main/res/values/strings.xml b/Awful.apk/src/main/res/values/strings.xml index ceba34ed2..eb55d5bf8 100644 --- a/Awful.apk/src/main/res/values/strings.xml +++ b/Awful.apk/src/main/res/values/strings.xml @@ -302,6 +302,7 @@ Guavanaut JingleBells Literal Hamster + Oben Sereri spanky the dolphin The Dave From b961a99b74e03e99614514d9484d6454db99d3e3 Mon Sep 17 00:00:00 2001 From: baka-kaba Date: Tue, 2 Apr 2019 00:45:13 +0100 Subject: [PATCH 16/17] Fix matching for username highlighting (#683) The username was being added to the regex unescaped, which is bad, so I borrowed some code from Mozilla to escape strings closes #682 --- Awful.apk/src/main/assets/javascript/thread.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Awful.apk/src/main/assets/javascript/thread.js b/Awful.apk/src/main/assets/javascript/thread.js index 26ae03dcd..6e9e9e04b 100644 --- a/Awful.apk/src/main/assets/javascript/thread.js +++ b/Awful.apk/src/main/assets/javascript/thread.js @@ -584,10 +584,19 @@ function highlightOwnUsername(scopeElement) { return textNodeArray; } + /** + * Escapes a string for inserting into a regex. + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ + function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } + var selector = 'article:not(self) .postcontent'; + var username = listener.getPreference('username'); - var regExp = new RegExp('\\b' + listener.getPreference('username') + '\\b', 'g'); - var styled = '' + listener.getPreference('username') + ''; + var regExp = new RegExp('\\b' + escapeRegExp(username) + '\\b', 'g'); + var styled = '' + username + ''; scopeElement.querySelectorAll(selector).forEach(function eachPost(post) { getTextNodesIn(post).forEach(function eachTextNode(node) { if (node.wholeText.match(regExp)) { From 79ed6b6150f89fe52689680be02df03a42760533 Mon Sep 17 00:00:00 2001 From: baka kaba Date: Tue, 2 Apr 2019 00:48:22 +0100 Subject: [PATCH 17/17] Bump version to 3.6.4 --- Awful.apk/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Awful.apk/src/main/AndroidManifest.xml b/Awful.apk/src/main/AndroidManifest.xml index 2ac0472cc..d4b142a2e 100644 --- a/Awful.apk/src/main/AndroidManifest.xml +++ b/Awful.apk/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@