From d4c097f6540686c94f78cccc3672e07b59e27437 Mon Sep 17 00:00:00 2001 From: Thodoris1999 Date: Sat, 21 Jul 2018 22:15:18 +0300 Subject: [PATCH] viewmodel progress --- app/build.gradle | 6 + .../activities/topic/GetQuoteTextTask.java | 4 + .../activities/topic/TopicActivity.java | 387 +++++------------- .../mthmmy/activities/topic/TopicTask.java | 213 ++++++++++ .../activities/topic/TopicTaskResult.java | 87 ++++ .../gr/thmmy/mthmmy/base/BaseActivity.java | 6 + .../java/gr/thmmy/mthmmy/utils/HTMLUtils.java | 76 ++++ .../thmmy/mthmmy/viewmodel/BaseViewModel.java | 18 + .../mthmmy/viewmodel/TopicViewModel.java | 40 ++ 9 files changed, 549 insertions(+), 288 deletions(-) create mode 100644 app/src/main/java/gr/thmmy/mthmmy/activities/topic/GetQuoteTextTask.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTask.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTaskResult.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java diff --git a/app/build.gradle b/app/build.gradle index cd627a53..4e9bb2f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,11 @@ android { archivesBaseName = archivesBaseName + "-$date" } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { @@ -56,6 +61,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.0' implementation 'net.gotev:uploadservice:3.4.2' implementation 'net.gotev:uploadservice-okhttp:3.4.2' + implementation 'android.arch.lifecycle:extensions:1.1.1' } apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/GetQuoteTextTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/GetQuoteTextTask.java new file mode 100644 index 00000000..0b29087f --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/GetQuoteTextTask.java @@ -0,0 +1,4 @@ +package gr.thmmy.mthmmy.activities.topic; + +public class GetQuoteTextTask { +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java index 009c8cf2..a1ff2822 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java @@ -2,6 +2,8 @@ package gr.thmmy.mthmmy.activities.topic; import android.annotation.SuppressLint; import android.app.NotificationManager; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -52,8 +54,10 @@ import gr.thmmy.mthmmy.model.Bookmark; import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.utils.CustomLinearLayoutManager; +import gr.thmmy.mthmmy.utils.HTMLUtils; import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import okhttp3.MultipartBody; import okhttp3.Request; @@ -130,14 +134,10 @@ public class TopicActivity extends BaseActivity { * Holds a list of this topic's posts */ private ArrayList postsList; - /** - * Gets assigned to {@link #postFocus} when there is no post focus information in the url - */ - private static final int NO_POST_FOCUS = -1; /** * Holds the index of the post that has focus */ - private int postFocus = NO_POST_FOCUS; + /** * Holds the position in the {@link #postsList} of the post with focus */ @@ -195,6 +195,7 @@ public class TopicActivity extends BaseActivity { private TextView pageIndicator; private ImageButton nextPage; private ImageButton lastPage; + private TopicViewModel viewModel; //Topic's info related private SpannableStringBuilder topicTreeAndMods = new SpannableStringBuilder("Loading..."), @@ -209,6 +210,7 @@ public class TopicActivity extends BaseActivity { Bundle extras = getIntent().getExtras(); topicTitle = extras.getString(BUNDLE_TOPIC_TITLE); + String maybeTopicTitle = topicTitle; topicPageUrl = extras.getString(BUNDLE_TOPIC_URL); ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory( Uri.parse(topicPageUrl)); @@ -262,7 +264,7 @@ public class TopicActivity extends BaseActivity { CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager( getApplicationContext(), loadedPageUrl); recyclerView.setLayoutManager(layoutManager); - topicAdapter = new TopicAdapter(this, postsList, base_url, topicTask); + topicAdapter = new TopicAdapter(this, postsList, viewModel.getTopicTaskResultMutableLiveData().getValue().getBaseUrl(), topicTask); recyclerView.setAdapter(topicAdapter); replyFAB = findViewById(R.id.topic_fab); @@ -295,8 +297,82 @@ public class TopicActivity extends BaseActivity { paginationEnabled(false); //Gets posts - topicTask = new TopicTask(); - topicTask.execute(topicPageUrl); //Attempt data parsing + //topicTask = new TopicTask(); + //topicTask.execute(topicPageUrl); //Attempt data parsing + + viewModel = ViewModelProviders.of(this).get(TopicViewModel.class); + viewModel.getTopicTaskResultMutableLiveData().observe(this, topicTaskResult -> { + if (topicTaskResult == null) { + hideControls(); + } else { + switch (topicTaskResult.getResultCode()) { + case SUCCESS: + if (topicTitle == null || Objects.equals(topicTitle, "") + || !Objects.equals(topicTitle, topicTaskResult.getTopicTitle())) { + toolbarTitle.setText(topicTaskResult.getTopicTitle()); + } + + if (!postsList.isEmpty()) { + recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug + postsList.clear(); + if (topicTitle != null) toolbarTitle.setText(topicTitle); + topicAdapter.notifyItemRangeRemoved(0, postsList.size() - 1); + } + postsList.addAll(topicTaskResult.getNewPostsList()); + topicAdapter.notifyItemRangeInserted(0, postsList.size()); + topicAdapter.prepareForDelete(new TopicActivity.DeleteTask()); + topicAdapter.prepareForPrepareForEdit(new TopicActivity.PrepareForEdit()); + + pageIndicator.setText(String.valueOf(thisPage) + "/" + String.valueOf(numberOfPages)); + pageRequestValue = topicTaskResult.getCurrentPageIndex(); + + if (topicTaskResult.getCurrentPageIndex() == topicTaskResult.getPageCount()) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) + notificationManager.cancel(NEW_POST_TAG, loadedPageTopicId); + } + + showControls(); + case NETWORK_ERROR: + Toast.makeText(getBaseContext(), "Network Error", Toast.LENGTH_SHORT).show(); + break; + case SAME_PAGE: + showControls(); + Toast.makeText(getBaseContext(), "That's the same page", Toast.LENGTH_SHORT).show(); + //TODO change focus + break; + case UNAUTHORIZED: + showControls(); + Toast.makeText(getBaseContext(), "This topic is either missing or off limits to you", Toast.LENGTH_SHORT).show(); + break; + default: + //Parse failed - should never happen + Timber.d("Parse failed!"); //TODO report ParseException!!! + Toast.makeText(getBaseContext(), "Fatal Error", Toast.LENGTH_SHORT).show(); + finish(); + break; + } + } + + }); + viewModel.initialLoad(topicPageUrl); + + } + + public void hideControls() { + progressBar.setVisibility(ProgressBar.VISIBLE); + paginationEnabled(false); + if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(false); + } + + public void showControls() { + progressBar.setVisibility(ProgressBar.INVISIBLE); + if (replyPageUrl == null) { + replyFAB.hide(); + topicAdapter.resetTopic(viewModel.getTopicTaskResultMutableLiveData().getValue().getBaseUrl(), new TopicTask(), false); + } else topicAdapter.resetTopic(viewModel.getTopicTaskResultMutableLiveData().getValue().getBaseUrl(), new TopicTask(), true); + paginationEnabled(true); + if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(true); } @Override @@ -321,11 +397,22 @@ public class TopicActivity extends BaseActivity { LinearLayout infoDialog = (LinearLayout) inflater.inflate(R.layout.dialog_topic_info , null); TextView treeAndMods = infoDialog.findViewById(R.id.topic_tree_and_mods); - treeAndMods.setText(topicTreeAndMods); + //treeAndMods.setText(new SpannableStringBuilder("Loading...")); treeAndMods.setMovementMethod(LinkMovementMethod.getInstance()); TextView usersViewing = infoDialog.findViewById(R.id.users_viewing); - usersViewing.setText(topicViewers); + //usersViewing.setText(new SpannableStringBuilder("Loading...")); usersViewing.setMovementMethod(LinkMovementMethod.getInstance()); + viewModel.getTopicTaskResultMutableLiveData().observe(this, topicTaskResult -> { + if (topicTaskResult == null) { + usersViewing.setText(new SpannableStringBuilder("Loading...")); + treeAndMods.setText(new SpannableStringBuilder("Loading...")); + } else { + String treeAndModsString = topicTaskResult.getTopicTreeAndMods(); + treeAndMods.setText(HTMLUtils.getSpannableFromHtml(this, treeAndModsString)); + String topicViewersString = topicTaskResult.getTopicViewers(); + usersViewing.setText(HTMLUtils.getSpannableFromHtml(this, topicViewersString)); + } + }); builder.setView(infoDialog); AlertDialog dialog = builder.create(); @@ -336,7 +423,7 @@ public class TopicActivity extends BaseActivity { sendIntent.setType("text/plain"); sendIntent.putExtra(android.content.Intent.EXTRA_TEXT, topicPageUrl); startActivity(Intent.createChooser(sendIntent, "Share via")); - return true; + return true; //invalidateOptionsMenu(); default: return super.onOptionsItemSelected(item); } @@ -564,282 +651,6 @@ public class TopicActivity extends BaseActivity { } //------------------------------------BOTTOM NAV BAR METHODS END------------------------------------ - private enum ResultCode { - SUCCESS, NETWORK_ERROR, PARSING_ERROR, OTHER_ERROR, SAME_PAGE, UNAUTHORIZED - } - - - /** - * An {@link AsyncTask} that handles asynchronous fetching of this topic page and parsing of its - * data. - *

TopicTask's {@link AsyncTask#execute execute} method needs a topic's url as String - * parameter.

- */ - class TopicTask extends AsyncTask { - ArrayList localPostsList; - - @Override - protected void onPreExecute() { - progressBar.setVisibility(ProgressBar.VISIBLE); - paginationEnabled(false); - if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(false); - } - - protected ResultCode doInBackground(String... strings) { - Document document = null; - String newPageUrl = strings[0]; - - //Finds the index of message focus if present - { - postFocus = NO_POST_FOCUS; - if (newPageUrl.contains("msg")) { - String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); - if (tmp.contains(";")) - postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(";"))); - else if (tmp.contains("#")) - postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf("#"))); - } - } - //Checks if the page to be loaded is the one already shown - if (!reloadingPage && !Objects.equals(loadedPageUrl, "") && newPageUrl.contains(base_url)) { - if (newPageUrl.contains("topicseen#new") || newPageUrl.contains("#new")) - if (thisPage == numberOfPages) - return ResultCode.SAME_PAGE; - if (newPageUrl.contains("msg")) { - String tmpUrlSbstr = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); - if (tmpUrlSbstr.contains("msg")) - tmpUrlSbstr = tmpUrlSbstr.substring(0, tmpUrlSbstr.indexOf("msg") - 1); - int testAgainst = Integer.parseInt(tmpUrlSbstr); - for (Post post : postsList) { - if (post.getPostIndex() == testAgainst) { - return ResultCode.SAME_PAGE; - } - } - } else if ((Objects.equals(newPageUrl, base_url) && thisPage == 1) || - Integer.parseInt(newPageUrl.substring(base_url.length() + 1)) / 15 + 1 == thisPage) - return ResultCode.SAME_PAGE; - } else if (!Objects.equals(loadedPageUrl, "")) topicTitle = null; - if (reloadingPage) reloadingPage = !reloadingPage; - - loadedPageUrl = newPageUrl; - if (strings[0].substring(0, strings[0].lastIndexOf(".")).contains("topic=")) - base_url = strings[0].substring(0, strings[0].lastIndexOf(".")); //New topic's base url - replyPageUrl = null; - Request request = new Request.Builder() - .url(newPageUrl) - .build(); - try { - Response response = client.newCall(request).execute(); - document = Jsoup.parse(response.body().string()); - localPostsList = parse(document); - - loadedPageTopicId = Integer.parseInt(ThmmyPage.getTopicId(loadedPageUrl)); - - //Finds the position of the focused message if present - for (int i = 0; i < localPostsList.size(); ++i) { - if (localPostsList.get(i).getPostIndex() == postFocus) { - postFocusPosition = i; - break; - } - } - return ResultCode.SUCCESS; - } catch (IOException e) { - Timber.i(e, "IO Exception"); - return ResultCode.NETWORK_ERROR; - } catch (ParseException e) { - if (isUnauthorized(document)) - return ResultCode.UNAUTHORIZED; - Timber.e(e, "Parsing Error"); - return ResultCode.PARSING_ERROR; - } catch (Exception e) { - Timber.e(e, "Exception"); - return ResultCode.OTHER_ERROR; - } - } - - protected void onPostExecute(ResultCode parseResult) { - switch (parseResult) { - case SUCCESS: - if (topicTitle == null || Objects.equals(topicTitle, "") - || !Objects.equals(topicTitle, parsedTitle)) { - toolbarTitle.setText(parsedTitle); - topicTitle = parsedTitle; - thisPageBookmark = new Bookmark(parsedTitle, Integer.toString(loadedPageTopicId), true); - invalidateOptionsMenu(); - } - - if (!postsList.isEmpty()) { - recyclerView.getRecycledViewPool().clear(); //Avoid inconsistency detected bug - postsList.clear(); - topicAdapter.notifyItemRangeRemoved(0, postsList.size() - 1); - } - postsList.addAll(localPostsList); - topicAdapter.notifyItemRangeInserted(0, postsList.size()); - topicAdapter.prepareForDelete(new DeleteTask()); - topicAdapter.prepareForPrepareForEdit(new PrepareForEdit()); - progressBar.setVisibility(ProgressBar.INVISIBLE); - - if (replyPageUrl == null) { - replyFAB.hide(); - topicAdapter.resetTopic(base_url, new TopicTask(), false); - } else topicAdapter.resetTopic(base_url, new TopicTask(), true); - - if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(true); - - //Set current page - pageIndicator.setText(String.valueOf(thisPage) + "/" + String.valueOf(numberOfPages)); - pageRequestValue = thisPage; - - if (thisPage == numberOfPages) { - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager != null) - notificationManager.cancel(NEW_POST_TAG, loadedPageTopicId); - } - - paginationEnabled(true); - break; - case NETWORK_ERROR: - Toast.makeText(getBaseContext(), "Network Error", Toast.LENGTH_SHORT).show(); - break; - case SAME_PAGE: - stopLoading(); - Toast.makeText(getBaseContext(), "That's the same page", Toast.LENGTH_SHORT).show(); - //TODO change focus - break; - case UNAUTHORIZED: - stopLoading(); - Toast.makeText(getBaseContext(), "This topic is either missing or off limits to you", Toast.LENGTH_SHORT).show(); - break; - default: - //Parse failed - should never happen - Timber.d("Parse failed!"); //TODO report ParseException!!! - Toast.makeText(getBaseContext(), "Fatal Error", Toast.LENGTH_SHORT).show(); - finish(); - break; - } - } - - private void stopLoading() { - progressBar.setVisibility(ProgressBar.INVISIBLE); - if (replyPageUrl == null) { - replyFAB.hide(); - topicAdapter.resetTopic(base_url, new TopicTask(), false); - } else topicAdapter.resetTopic(base_url, new TopicTask(), true); - if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(true); - paginationEnabled(true); - } - - /** - * All the parsing a topic needs. - * - * @param topic {@link Document} object containing this topic's source code - * @see org.jsoup.Jsoup Jsoup - */ - private ArrayList parse(Document topic) throws ParseException { - try { - ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic); - - //Finds topic's tree, mods and users viewing - { - topicTreeAndMods = getSpannableFromHtml(topic.select("div.nav").first().html()); - topicViewers = getSpannableFromHtml(TopicParser.parseUsersViewingThisTopic(topic, language)); - } - - //Finds reply page url - { - Element replyButton = topic.select("a:has(img[alt=Reply])").first(); - if (replyButton == null) - replyButton = topic.select("a:has(img[alt=Απάντηση])").first(); - if (replyButton != null) replyPageUrl = replyButton.attr("href"); - } - - //Finds topic title if missing - { - parsedTitle = topic.select("td[id=top_subject]").first().text(); - if (parsedTitle.contains("Topic:")) { - parsedTitle = parsedTitle.substring(parsedTitle.indexOf("Topic:") + 7 - , parsedTitle.indexOf("(Read") - 2); - } else { - parsedTitle = parsedTitle.substring(parsedTitle.indexOf("Θέμα:") + 6 - , parsedTitle.indexOf("(Αναγνώστηκε") - 2); - Timber.d("Parsed title: %s", parsedTitle); - } - } - - { //Finds current page's index - thisPage = TopicParser.parseCurrentPageIndex(topic, language); - } - { //Finds number of pages - numberOfPages = TopicParser.parseTopicNumberOfPages(topic, thisPage, language); - - for (int i = 0; i < numberOfPages; i++) { - //Generate each page's url from topic's base url +".15*numberOfPage" - pagesUrls.put(i, base_url + "." + String.valueOf(i * 15)); - } - } - - return TopicParser.parseTopic(topic, language); - } catch (Exception e) { - throw new ParseException("Parsing failed (TopicTask)"); - } - } - - private boolean isUnauthorized(Document document) { - return document != null && document.select("body:contains(The topic or board you" + - " are looking for appears to be either missing or off limits to you.)," + - "body:contains(Το θέμα ή πίνακας που ψάχνετε ή δεν υπάρχει ή δεν " + - "είναι προσβάσιμο από εσάς.)").size() > 0; - } - - private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) { - int start = strBuilder.getSpanStart(span); - int end = strBuilder.getSpanEnd(span); - int flags = strBuilder.getSpanFlags(span); - ClickableSpan clickable = new ClickableSpan() { - @Override - public void onClick(View view) { - ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(Uri.parse(span.getURL())); - if (target.is(ThmmyPage.PageCategory.BOARD)) { - Intent intent = new Intent(getApplicationContext(), BoardActivity.class); - Bundle extras = new Bundle(); - extras.putString(BUNDLE_BOARD_URL, span.getURL()); - extras.putString(BUNDLE_BOARD_TITLE, ""); - intent.putExtras(extras); - intent.setFlags(FLAG_ACTIVITY_NEW_TASK); - getApplicationContext().startActivity(intent); - } else if (target.is(ThmmyPage.PageCategory.PROFILE)) { - Intent intent = new Intent(getApplicationContext(), ProfileActivity.class); - Bundle extras = new Bundle(); - extras.putString(BUNDLE_PROFILE_URL, span.getURL()); - extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); - extras.putString(BUNDLE_PROFILE_USERNAME, ""); - intent.putExtras(extras); - intent.setFlags(FLAG_ACTIVITY_NEW_TASK); - getApplicationContext().startActivity(intent); - } else if (target.is(ThmmyPage.PageCategory.INDEX)) - finish(); - } - }; - strBuilder.setSpan(clickable, start, end, flags); - strBuilder.removeSpan(span); - } - - private SpannableStringBuilder getSpannableFromHtml(String html) { - CharSequence sequence; - if (Build.VERSION.SDK_INT >= 24) { - sequence = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); - } else { - //noinspection deprecation - sequence = Html.fromHtml(html); - } - SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence); - URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class); - for (URLSpan span : urls) { - makeLinkClickable(strBuilder, span); - } - return strBuilder; - } - } class PrepareForReply extends AsyncTask, Void, Boolean> { String numReplies, seqnum, sc, topic, buildedQuotes = ""; @@ -976,7 +787,7 @@ public class TopicActivity extends BaseActivity { if (result) { topicTask = new TopicTask(); if ((postsList.get(postsList.size() - 1).getPostNumber() + 1) % 15 == 0) - topicTask.execute(base_url + "." + 2147483647); + topicTask.execute(viewModel.getTopicTaskResultMutableLiveData().getValue().getBaseUrl() + "." + 2147483647); else { reloadingPage = true; topicTask.execute(loadedPageUrl); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTask.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTask.java new file mode 100644 index 00000000..6c654475 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTask.java @@ -0,0 +1,213 @@ +package gr.thmmy.mthmmy.activities.topic; + +import android.os.AsyncTask; +import android.util.SparseArray; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Objects; + +import gr.thmmy.mthmmy.base.BaseApplication; +import gr.thmmy.mthmmy.model.Post; +import gr.thmmy.mthmmy.model.ThmmyPage; +import gr.thmmy.mthmmy.utils.parsing.ParseException; +import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; +import gr.thmmy.mthmmy.viewmodel.TopicViewModel; +import okhttp3.Request; +import okhttp3.Response; +import timber.log.Timber; + +/** + * An {@link AsyncTask} that handles asynchronous fetching of this topic page and parsing of its + * data. + *

TopicTask's {@link AsyncTask#execute execute} method needs a topic's url as String + * parameter.

+ */ +public class TopicTask extends AsyncTask { + //input data + private TopicViewModel viewModel; + private boolean reloadingPage; + private ArrayList lastPostsList; + //output data + private ResultCode resultCode; + private String topicTitle, replyPageUrl, topicTreeAndMods, topicViewers; + private ArrayList newPostsList; + private int loadedPageTopicId = -1; + private int focusedPostIndex = 0; + private SparseArray pagesUrls = new SparseArray<>(); + //(possibly) update data + private int currentPageIndex, pageCount; + private String baseUrl, lastPageLoadAttemptedUrl; + + //consecutive load constructor + public TopicTask(boolean reloadingPage, String baseUrl, int currentPageIndex, int pageCount, + String lastPageLoadAttemptedUrl, ArrayList lastPostsList) { + this.viewModel = viewModel; + this.reloadingPage = reloadingPage; + this.baseUrl = baseUrl; + this.currentPageIndex = currentPageIndex; + this.pageCount = pageCount; + this.lastPageLoadAttemptedUrl = lastPageLoadAttemptedUrl; + this.lastPostsList = lastPostsList; + } + + //first load constructor + public TopicTask() { + this.viewModel = viewModel; + this.reloadingPage = false; + this.baseUrl = ""; + this.currentPageIndex = 1; + this.pageCount = 1; + this.lastPageLoadAttemptedUrl = ""; + this.lastPostsList = null; + } + + @Override + protected TopicTaskResult doInBackground(String... strings) { + Document document = null; + String newPageUrl = strings[0]; + + //Finds the index of message focus if present + int postFocus = -1; + { + if (newPageUrl.contains("msg")) { + String tmp = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); + if (tmp.contains(";")) + postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf(";"))); + else if (tmp.contains("#")) + postFocus = Integer.parseInt(tmp.substring(0, tmp.indexOf("#"))); + } + } + //Checks if the page to be loaded is the one already shown + if (!reloadingPage && !Objects.equals(lastPageLoadAttemptedUrl, "") && newPageUrl.contains(baseUrl)) { + if (newPageUrl.contains("topicseen#new") || newPageUrl.contains("#new")) + if (currentPageIndex == pageCount) + resultCode = ResultCode.SAME_PAGE; + if (newPageUrl.contains("msg")) { + String tmpUrlSbstr = newPageUrl.substring(newPageUrl.indexOf("msg") + 3); + if (tmpUrlSbstr.contains("msg")) + tmpUrlSbstr = tmpUrlSbstr.substring(0, tmpUrlSbstr.indexOf("msg") - 1); + int testAgainst = Integer.parseInt(tmpUrlSbstr); + for (Post post : lastPostsList) { + if (post.getPostIndex() == testAgainst) { + resultCode = ResultCode.SAME_PAGE; + } + } + } else if ((Objects.equals(newPageUrl, baseUrl) && currentPageIndex == 1) || + Integer.parseInt(newPageUrl.substring(baseUrl.length() + 1)) / 15 + 1 ==currentPageIndex) + resultCode = ResultCode.SAME_PAGE; + } else if (!Objects.equals(lastPageLoadAttemptedUrl, "")) topicTitle = null; + if (reloadingPage) reloadingPage = !reloadingPage; + + lastPageLoadAttemptedUrl = newPageUrl; + if (strings[0].substring(0, strings[0].lastIndexOf(".")).contains("topic=")) + baseUrl = strings[0].substring(0, strings[0].lastIndexOf(".")); //New topic's base url + replyPageUrl = null; + Request request = new Request.Builder() + .url(newPageUrl) + .build(); + try { + Response response = BaseApplication.getInstance().getClient().newCall(request).execute(); + document = Jsoup.parse(response.body().string()); + newPostsList = parse(document); + + loadedPageTopicId = Integer.parseInt(ThmmyPage.getTopicId(lastPageLoadAttemptedUrl)); + + //Finds the position of the focused message if present + for (int i = 0; i < newPostsList.size(); ++i) { + if (newPostsList.get(i).getPostIndex() == postFocus) { + focusedPostIndex = i; + break; + } + } + resultCode = ResultCode.SUCCESS; + } catch (IOException e) { + Timber.i(e, "IO Exception"); + resultCode = ResultCode.NETWORK_ERROR; + } catch (ParseException e) { + if (isUnauthorized(document)) + resultCode = ResultCode.UNAUTHORIZED; + Timber.e(e, "Parsing Error"); + resultCode = ResultCode.PARSING_ERROR; + } catch (Exception e) { + Timber.e(e, "Exception"); + resultCode = ResultCode.OTHER_ERROR; + } + return new TopicTaskResult(resultCode, baseUrl, topicTitle, replyPageUrl, newPostsList, + loadedPageTopicId, currentPageIndex, pageCount, focusedPostIndex, topicTreeAndMods, + topicViewers, lastPageLoadAttemptedUrl, pagesUrls); + } + + /** + * All the parsing a topic needs. + * + * @param topic {@link Document} object containing this topic's source code + * @see org.jsoup.Jsoup Jsoup + */ + private ArrayList parse(Document topic) throws ParseException { + try { + ParseHelpers.Language language = ParseHelpers.Language.getLanguage(topic); + + //Finds topic's tree, mods and users viewing + { + topicTreeAndMods = topic.select("div.nav").first().html(); + topicViewers = TopicParser.parseUsersViewingThisTopic(topic, language); + } + + //Finds reply page url + { + Element replyButton = topic.select("a:has(img[alt=Reply])").first(); + if (replyButton == null) + replyButton = topic.select("a:has(img[alt=Απάντηση])").first(); + if (replyButton != null) replyPageUrl = replyButton.attr("href"); + } + + //Finds topic title if missing + { + topicTitle = topic.select("td[id=top_subject]").first().text(); + if (topicTitle.contains("Topic:")) { + topicTitle = topicTitle.substring(topicTitle.indexOf("Topic:") + 7 + , topicTitle.indexOf("(Read") - 2); + } else { + topicTitle = topicTitle.substring(topicTitle.indexOf("Θέμα:") + 6 + , topicTitle.indexOf("(Αναγνώστηκε") - 2); + Timber.d("Parsed title: %s", topicTitle); + } + } + + { //Finds current page's index + currentPageIndex = TopicParser.parseCurrentPageIndex(topic, language); + } + { //Finds number of pages + pageCount = TopicParser.parseTopicNumberOfPages(topic, currentPageIndex, language); + + for (int i = 0; i < pageCount; i++) { + //Generate each page's url from topic's base url +".15*numberOfPage" + pagesUrls.put(i, baseUrl + "." + String.valueOf(i * 15)); + } + } + return TopicParser.parseTopic(topic, language); + } catch (Exception e) { + throw new ParseException("Parsing failed (TopicTask)"); + } + } + + private boolean isUnauthorized(Document document) { + return document != null && document.select("body:contains(The topic or board you" + + " are looking for appears to be either missing or off limits to you.)," + + "body:contains(Το θέμα ή πίνακας που ψάχνετε ή δεν υπάρχει ή δεν " + + "είναι προσβάσιμο από εσάς.)").size() > 0; + } + + public enum ResultCode { + SUCCESS, NETWORK_ERROR, PARSING_ERROR, OTHER_ERROR, SAME_PAGE, UNAUTHORIZED + } + + public interface OnTopicTaskCompleted { + void onTopicTaskCompleted(TopicTaskResult result); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTaskResult.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTaskResult.java new file mode 100644 index 00000000..4756c7e0 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicTaskResult.java @@ -0,0 +1,87 @@ +package gr.thmmy.mthmmy.activities.topic; + +import android.util.SparseArray; + +import java.util.ArrayList; + +import gr.thmmy.mthmmy.model.Post; + +public class TopicTaskResult { + private final TopicTask.ResultCode resultCode; + private final String baseUrl, topicTitle, replyPageUrl; + private final ArrayList newPostsList; + private final int loadedPageTopicId, currentPageIndex, pageCount, focusedPostIndex; + private final String topicTreeAndMods, topicViewers, lastPageLoadAttemptedUrl; + private final SparseArray pagesUrls; + + public TopicTaskResult(TopicTask.ResultCode resultCode, String baseUrl, String topicTitle, + String replyPageUrl, ArrayList newPostsList, int loadedPageTopicId, + int currentPageIndex, int pageCount, int focusedPostIndex, String topicTreeAndMods, + String topicViewers, String lastPageLoadAttemptedUrl, SparseArray pagesUrls) { + this.resultCode = resultCode; + this.baseUrl = baseUrl; + this.topicTitle = topicTitle; + this.replyPageUrl = replyPageUrl; + this.newPostsList = newPostsList; + this.loadedPageTopicId = loadedPageTopicId; + this.currentPageIndex = currentPageIndex; + this.pageCount = pageCount; + this.focusedPostIndex = focusedPostIndex; + this.topicTreeAndMods = topicTreeAndMods; + this.topicViewers = topicViewers; + this.lastPageLoadAttemptedUrl = lastPageLoadAttemptedUrl; + this.pagesUrls = pagesUrls; + } + + public TopicTask.ResultCode getResultCode() { + return resultCode; + } + + public String getBaseUrl() { + return baseUrl; + } + + public String getTopicTitle() { + return topicTitle; + } + + public String getReplyPageUrl() { + return replyPageUrl; + } + + public ArrayList getNewPostsList() { + return newPostsList; + } + + public int getLoadedPageTopicId() { + return loadedPageTopicId; + } + + public int getCurrentPageIndex() { + return currentPageIndex; + } + + public int getPageCount() { + return pageCount; + } + + public int getFocusedPostIndex() { + return focusedPostIndex; + } + + public String getTopicTreeAndMods() { + return topicTreeAndMods; + } + + public String getTopicViewers() { + return topicViewers; + } + + public String getLastPageLoadAttemptedUrl() { + return lastPageLoadAttemptedUrl; + } + + public SparseArray getPagesUrls() { + return pagesUrls; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java index 27002423..7d75d42e 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java @@ -2,6 +2,7 @@ package gr.thmmy.mthmmy.base; import android.Manifest; import android.app.ProgressDialog; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -54,6 +55,7 @@ import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.services.DownloadHelper; import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.utils.FileUtils; +import gr.thmmy.mthmmy.viewmodel.BaseViewModel; import okhttp3.OkHttpClient; import timber.log.Timber; @@ -118,6 +120,10 @@ public abstract class BaseActivity extends AppCompatActivity { loadSavedBookmarks(); } + BaseViewModel baseViewModel = ViewModelProviders.of(this).get(BaseViewModel.class); + baseViewModel.getCurrentPageBookmark().observe(this, thisPageBookmark -> { + setTopicBookmark(thisPageBookmarkMenuButton); + }); } @Override diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java b/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java new file mode 100644 index 00000000..2713a2a8 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/HTMLUtils.java @@ -0,0 +1,76 @@ +package gr.thmmy.mthmmy.utils; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.view.View; + +import gr.thmmy.mthmmy.activities.board.BoardActivity; +import gr.thmmy.mthmmy.activities.profile.ProfileActivity; +import gr.thmmy.mthmmy.model.ThmmyPage; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_TITLE; +import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_THUMBNAIL_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_URL; +import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_USERNAME; + +public class HTMLUtils { + private HTMLUtils() {} + + public static SpannableStringBuilder getSpannableFromHtml(Activity activity, String html) { + CharSequence sequence; + if (Build.VERSION.SDK_INT >= 24) { + sequence = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + } else { + //noinspection deprecation + sequence = Html.fromHtml(html); + } + SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence); + URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class); + for (URLSpan span : urls) { + makeLinkClickable(activity, strBuilder, span); + } + return strBuilder; + } + + private static void makeLinkClickable(Activity activity, SpannableStringBuilder strBuilder, final URLSpan span) { + int start = strBuilder.getSpanStart(span); + int end = strBuilder.getSpanEnd(span); + int flags = strBuilder.getSpanFlags(span); + ClickableSpan clickable = new ClickableSpan() { + @Override + public void onClick(View view) { + ThmmyPage.PageCategory target = ThmmyPage.resolvePageCategory(Uri.parse(span.getURL())); + if (target.is(ThmmyPage.PageCategory.BOARD)) { + Intent intent = new Intent(activity.getApplicationContext(), BoardActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_BOARD_URL, span.getURL()); + extras.putString(BUNDLE_BOARD_TITLE, ""); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + activity.getApplicationContext().startActivity(intent); + } else if (target.is(ThmmyPage.PageCategory.PROFILE)) { + Intent intent = new Intent(activity.getApplicationContext(), ProfileActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_PROFILE_URL, span.getURL()); + extras.putString(BUNDLE_PROFILE_THUMBNAIL_URL, ""); + extras.putString(BUNDLE_PROFILE_USERNAME, ""); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + activity.getApplicationContext().startActivity(intent); + } else if (target.is(ThmmyPage.PageCategory.INDEX)) + activity.finish(); + } + }; + strBuilder.setSpan(clickable, start, end, flags); + strBuilder.removeSpan(span); + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java new file mode 100644 index 00000000..53ed84ef --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/BaseViewModel.java @@ -0,0 +1,18 @@ +package gr.thmmy.mthmmy.viewmodel; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.ViewModel; + +import gr.thmmy.mthmmy.model.Bookmark; + +public class BaseViewModel extends ViewModel { + protected MutableLiveData currentPageBookmark; + + public LiveData getCurrentPageBookmark() { + if (currentPageBookmark == null) { + currentPageBookmark = new MutableLiveData<>(); + } + return currentPageBookmark; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java new file mode 100644 index 00000000..8286b3fd --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java @@ -0,0 +1,40 @@ +package gr.thmmy.mthmmy.viewmodel; + +import android.app.NotificationManager; +import android.arch.lifecycle.MutableLiveData; +import android.content.Context; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import java.util.Objects; + +import gr.thmmy.mthmmy.activities.topic.TopicActivity; +import gr.thmmy.mthmmy.activities.topic.TopicTask; +import gr.thmmy.mthmmy.activities.topic.TopicTaskResult; +import gr.thmmy.mthmmy.model.Bookmark; +import timber.log.Timber; + +import static gr.thmmy.mthmmy.services.NotificationService.NEW_POST_TAG; + +public class TopicViewModel extends BaseViewModel implements TopicTask.OnTopicTaskCompleted { + private MutableLiveData topicTaskResultMutableLiveData; + + public MutableLiveData getTopicTaskResultMutableLiveData() { + if (topicTaskResultMutableLiveData == null) { + topicTaskResultMutableLiveData = new MutableLiveData<>(); + //load topic data + } + return topicTaskResultMutableLiveData; + } + + public void initialLoad(String pageUrl) { + new TopicTask().execute(pageUrl); + //load posts + } + + @Override + public void onTopicTaskCompleted(TopicTaskResult result) { + topicTaskResultMutableLiveData.setValue(result); + } +}