Browse Source

Version 1.8.2

master v1.8.2
Ezerous 4 years ago
parent
commit
6cde8a58cd
No known key found for this signature in database GPG Key ID: 262B2954BBA319E3
  1. 1
      .gitignore
  2. 1
      README.md
  3. 27
      app/build.gradle
  4. 3
      app/src/main/AndroidManifest.xml
  5. 9
      app/src/main/assets/libraries_style.css
  6. 115
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java
  7. 22
      app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java
  8. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java
  9. 1
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java
  10. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java
  11. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java
  12. 116
      app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java
  13. 19
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java
  14. 5
      app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java
  15. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/SendShoutTask.java
  16. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxFragment.java
  17. 2
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxTask.java
  18. 7
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  19. 16
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  20. 15
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java
  21. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/DeleteTask.java
  22. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/RemoveVoteTask.java
  23. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/SubmitVoteTask.java
  24. 17
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java
  25. 91
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  26. 126
      app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java
  27. 13
      app/src/main/java/gr/thmmy/mthmmy/session/InvalidSessionException.java
  28. 86
      app/src/main/java/gr/thmmy/mthmmy/session/LogoutTask.java
  29. 65
      app/src/main/java/gr/thmmy/mthmmy/session/MarkAsReadTask.java
  30. 254
      app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java
  31. 18
      app/src/main/java/gr/thmmy/mthmmy/session/ValidateSessionTask.java
  32. 1
      app/src/main/java/gr/thmmy/mthmmy/utils/Parcel.java
  33. 13
      app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReporter.java
  34. 16
      app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReportingTree.java
  35. 2
      app/src/main/java/gr/thmmy/mthmmy/utils/networking/NetworkResultCodes.java
  36. 38
      app/src/main/java/gr/thmmy/mthmmy/utils/networking/NetworkTask.java
  37. 7
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/NewParseTask.java
  38. 17
      app/src/main/java/gr/thmmy/mthmmy/utils/ui/GlideUtils.java
  39. 2
      app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java
  40. 2
      app/src/main/res/drawable/ic_default_user_avatar.xml
  41. 2
      app/src/main/res/layout/activity_about.xml
  42. 9
      app/src/main/res/layout/activity_board_sub_board_row.xml
  43. 2
      app/src/main/res/layout/activity_settings.xml
  44. 6
      app/src/main/res/layout/activity_upload.xml
  45. 2
      app/src/main/res/layout/activity_upload_fields_builder.xml
  46. 2
      app/src/main/res/layout/activity_upload_filename_info_popup.xml
  47. 2
      app/src/main/res/layout/download_prompt_dialog.xml
  48. 2
      app/src/main/res/layout/fragment_bookmarks.xml
  49. 2
      app/src/main/res/layout/fragment_forum_board_row.xml
  50. 2
      app/src/main/res/layout/fragment_forum_category_row.xml
  51. 2
      app/src/main/res/layout/fragment_recent_row.xml
  52. 1
      app/src/main/res/layout/fragment_unread.xml
  53. 2
      app/src/main/res/layout/fragment_unread_row.xml
  54. 7
      app/src/main/res/values/colors.xml
  55. 8
      app/src/main/res/values/strings.xml
  56. 6
      app/src/main/res/values/styles.xml
  57. 7
      app/src/main/res/xml/network_security_config.xml
  58. 6
      build.gradle
  59. 9
      emojis/build.gradle
  60. 4
      gradle/wrapper/gradle-wrapper.properties

1
.gitignore

@ -14,6 +14,7 @@ bin/
gen/ gen/
out/ out/
output.json output.json
output-metadata.json
# Gradle files # Gradle files
.gradle/ .gradle/

1
README.md

@ -1,5 +1,6 @@
# mTHMMY # mTHMMY
[![GitHub release](https://img.shields.io/github/release/ThmmyNoLife/mTHMMY.svg)](https://github.com/ThmmyNoLife/mTHMMY/releases)
[![API](https://img.shields.io/badge/API-19%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=19) [![API](https://img.shields.io/badge/API-19%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=19)
[![Discord Channel](https://img.shields.io/badge/discord-public@mTHMMY-738bd7.svg?style=flat)][discord-server] [![Discord Channel](https://img.shields.io/badge/discord-public@mTHMMY-738bd7.svg?style=flat)][discord-server]
![Last Commit](https://img.shields.io/github/last-commit/ThmmyNoLife/mTHMMY/develop.svg?style=flat) ![Last Commit](https://img.shields.io/github/last-commit/ThmmyNoLife/mTHMMY/develop.svg?style=flat)

27
app/build.gradle

@ -3,19 +3,20 @@ import groovy.json.JsonSlurper
apply from: 'gradle/grgit.gradle' apply from: 'gradle/grgit.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'io.fabric' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
android { android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion = '29.0.3' buildToolsVersion = '29.0.3'
defaultConfig { defaultConfig {
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true //TODO: Remove when minSdkVersion >= 21
applicationId "gr.thmmy.mthmmy" applicationId "gr.thmmy.mthmmy"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
versionCode 24 versionCode 25
versionName "1.8.1" versionName "1.8.2"
archivesBaseName = "mTHMMY-v$versionName" archivesBaseName = "mTHMMY-v$versionName"
buildConfigField "String", "CURRENT_BRANCH", "\"" + getCurrentBranch() + "\"" buildConfigField "String", "CURRENT_BRANCH", "\"" + getCurrentBranch() + "\""
buildConfigField "String", "COMMIT_HASH", "\"" + getCommitHash() + "\"" buildConfigField "String", "COMMIT_HASH", "\"" + getCommitHash() + "\""
@ -24,17 +25,19 @@ android {
buildTypes { buildTypes {
release { release {
multiDexEnabled true //TODO: Remove when minSdkVersion >= 21
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
multiDexKeepProguard file('proguard-rules.pro') //TODO: Remove when minSdkVersion >= 21 multiDexKeepProguard file('proguard-rules.pro') //TODO: Remove when minSdkVersion >= 21
} }
debug { debug {
multiDexEnabled true multiDexEnabled true //TODO: Remove when minSdkVersion >= 21
def date = new Date().format('ddMMyy_HHmmss') def date = new Date().format('ddMMyy_HHmmss')
archivesBaseName = archivesBaseName + "-$date" archivesBaseName = archivesBaseName + "-$date"
// Disable fabric build ID generation for debug builds firebaseCrashlytics {
ext.enableCrashlytics = false mappingFileUploadEnabled false // Disable mapping file uploading for debug builds
}
} }
} }
@ -75,7 +78,7 @@ tasks.whenTaskAdded { task ->
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":emojis") implementation project(":emojis")
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@ -86,9 +89,9 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.2.0' implementation 'androidx.exifinterface:exifinterface:1.2.0'
implementation 'androidx.multidex:multidex:2.0.1' //TODO: Remove when minSdkVersion >= 21 implementation 'androidx.multidex:multidex:2.0.1' //TODO: Remove when minSdkVersion >= 21
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.firebase:firebase-analytics:17.4.3' implementation 'com.google.firebase:firebase-analytics:17.4.4'
implementation 'com.google.firebase:firebase-messaging:20.2.0' implementation 'com.google.firebase:firebase-crashlytics:17.1.1'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.google.firebase:firebase-messaging:20.2.3'
implementation 'com.snatik:storage:2.1.0' implementation 'com.snatik:storage:2.1.0'
implementation ('com.squareup.okhttp3:okhttp:3.12.12') { //TODO: Warning: OkHttp has dropped support for Android 19 since OkHttp 3.13! implementation ('com.squareup.okhttp3:okhttp:3.12.12') { //TODO: Warning: OkHttp has dropped support for Android 19 since OkHttp 3.13!
force = true //TODO: Remove when minSdkVersion >= 21 force = true //TODO: Remove when minSdkVersion >= 21
@ -116,5 +119,3 @@ dependencies {
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2' testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
} }
apply plugin: 'com.google.gms.google-services'

3
app/src/main/AndroidManifest.xml

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -19,7 +20,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config" android:usesCleartextTraffic="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<meta-data <meta-data

9
app/src/main/assets/libraries_style.css

@ -15,7 +15,6 @@ pre {
h4, h5 { h4, h5 {
display: inline; display: inline;
padding: 1em;
} }
a, h4, h5 { a, h4, h5 {
@ -23,6 +22,14 @@ a, h4, h5 {
word-wrap: break-word; word-wrap: break-word;
} }
h4 {
padding: 1em;
}
li { li {
color: #26A69A; color: #26A69A;
}
ul a {
margin-left: 1em;
} }

115
app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java

@ -80,12 +80,11 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
Toast.makeText(this, "An error has occurred\nAborting.", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "An error has occurred\nAborting.", Toast.LENGTH_SHORT).show();
finish(); finish();
} }
//Fixes url //Fixes url
{ String tmpUrlSbstr = boardUrl.replaceAll("(.+)(board=)([0-9]*)(\\.*[0-9]*).*", "$1$2$3");
String tmpUrlSbstr = boardUrl.replaceAll("(.+)(board=)([0-9]*)(\\.*[0-9]*).*", "$1$2$3"); if (!tmpUrlSbstr.substring(tmpUrlSbstr.indexOf("board=")).contains("."))
if (!tmpUrlSbstr.substring(tmpUrlSbstr.indexOf("board=")).contains(".")) boardUrl = tmpUrlSbstr + ".0";
boardUrl = tmpUrlSbstr + ".0";
}
//Initializes graphics //Initializes graphics
toolbar = findViewById(R.id.toolbar); toolbar = findViewById(R.id.toolbar);
@ -230,6 +229,9 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
if (newTopicButton == null) if (newTopicButton == null)
newTopicButton = boardPage.select("a:has(img[alt=Νέο θέμα])").first(); newTopicButton = boardPage.select("a:has(img[alt=Νέο θέμα])").first();
if (newTopicButton != null) newTopicUrl = newTopicButton.attr("href"); if (newTopicButton != null) newTopicUrl = newTopicButton.attr("href");
final Pattern pLastPostPattern = Pattern.compile("((?:(?!(?:by|από)).)*)\\s(?:by|από)\\s(.*)");
if(pagesLoaded == 0) { //Finds sub boards if(pagesLoaded == 0) { //Finds sub boards
Elements subBoardRows = boardPage.select("div.tborder>table>tbody>tr"); Elements subBoardRows = boardPage.select("div.tborder>table>tbody>tr");
if (subBoardRows != null && !subBoardRows.isEmpty()) { if (subBoardRows != null && !subBoardRows.isEmpty()) {
@ -237,39 +239,61 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
if (!Objects.equals(subBoardRow.className(), "titlebg")) { if (!Objects.equals(subBoardRow.className(), "titlebg")) {
String pUrl = "", pTitle = "", pMods = "", pStats = "", String pUrl = "", pTitle = "", pMods = "", pStats = "",
pLastPost = "No posts yet", pLastPostUrl = ""; pLastPost = "No posts yet", pLastPostUrl = "";
boolean parsingFailed = false;
Elements subBoardColumns = subBoardRow.select(">td"); Elements subBoardColumns = subBoardRow.select(">td");
for (Element subBoardCol : subBoardColumns) { for (Element subBoardCol : subBoardColumns) {
if (Objects.equals(subBoardCol.className(), "windowbg")) if (Objects.equals(subBoardCol.className(), "windowbg")){
pStats = subBoardCol.text(); pStats = subBoardCol.text();
if(pStats.equals("--"))
pStats = "";
}
else if (Objects.equals(subBoardCol.className(), "smalltext")) { else if (Objects.equals(subBoardCol.className(), "smalltext")) {
pLastPost = subBoardCol.text(); pLastPost = subBoardCol.text();
if (pLastPost.contains(" in ")) { if (pLastPost.contains(" in ") || pLastPost.contains(" σε ")) {
pLastPost = pLastPost.substring(0, pLastPost.indexOf(" in ")) + Pattern pattern = Pattern.compile("(?:Last post on |Τελευταίο μήνυμα στις )((?:(?!(?:in|σε)).)*)\\s(?:in|σε)\\s.*");
"\n" + Matcher matcher = pattern.matcher(pLastPost);
pLastPost.substring(pLastPost.indexOf(" in ") + 1, pLastPost.indexOf(" by ")) + if (matcher.find()){
"\n" + String pLastPostDateTime = matcher.group(1);
pLastPost.substring(pLastPost.lastIndexOf(" by ") + 1); String pSubject = subBoardCol.select("a").first().attr("title");
pLastPostUrl = subBoardCol.select("a").first().attr("href");
} else if (pLastPost.contains(" σε ")) { // Purification for extreme edge cases
pLastPost = pLastPost.substring(0, pLastPost.indexOf(" σε ")) + String pSubjectConcat = subBoardCol.select("a").first().text();
"\n" + pLastPost = pLastPost.replace(pSubjectConcat, "");
pLastPost.substring(pLastPost.indexOf(" σε ") + 1, pLastPost.indexOf(" από ")) +
"\n" + String pLastUser;
pLastPost.substring(pLastPost.lastIndexOf(" από ") + 1); matcher = pLastPostPattern.matcher(pLastPost); //Don't even try simply grabbing <a>, user might be guest
pLastPostUrl = subBoardCol.select("a").first().attr("href"); if (matcher.find())
} else { pLastUser = matcher.group(2);
pLastPost = "No posts yet."; else {
pLastPostUrl = ""; parsingFailed = true;
} break;
}
pLastPost = "Last post on: " + pLastPostDateTime + "\nin: " + pSubject + "\nby " +pLastUser;
pLastPostUrl = subBoardCol.select("a").first().attr("href");
}
else {
parsingFailed = true;
break;
}
} else if (pLastPost.contains("redirected clicks")||pLastPost.contains("N/A"))
pLastPost = "";
else
pLastPost = "No posts yet";
} else { } else {
pUrl = subBoardCol.select("a").first().attr("href"); pUrl = subBoardCol.select("a").first().attr("href");
pTitle = subBoardCol.select("a").first().text(); pTitle = subBoardCol.select("a").first().text();
if (subBoardCol.select("div.smalltext").first() != null) { if (subBoardCol.select("div.smalltext").first() != null)
pMods = subBoardCol.select("div.smalltext").first().text(); pMods = subBoardCol.select("div.smalltext").first().text();
}
} }
} }
tempSubboards.add(new Board(pUrl, pTitle, pMods, pStats, pLastPost, pLastPostUrl)); if(!parsingFailed)
tempSubboards.add(new Board(pUrl, pTitle, pMods, pStats, pLastPost, pLastPostUrl));
else
Timber.e("Parsing failed (pLastPost came with: \"%s\", subBoardColumns html was \"%s\")", pLastPost, subBoardColumns);
} }
} }
} }
@ -282,30 +306,31 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
String pTopicUrl, pSubject, pStarter, pLastUser="", pLastPostDateTime="00:00:00", pLastPost, pLastPostUrl, pStats; String pTopicUrl, pSubject, pStarter, pLastUser="", pLastPostDateTime="00:00:00", pLastPost, pLastPostUrl, pStats;
boolean pLocked = false, pSticky = false, pUnread = false; boolean pLocked = false, pSticky = false, pUnread = false;
Elements topicColumns = topicRow.select(">td"); Elements topicColumns = topicRow.select(">td");
{
Element column = topicColumns.get(2); Element column = topicColumns.get(2);
Element tmp = column.select("span[id^=msg_] a").first(); Element tmp = column.select("span[id^=msg_] a").first();
pTopicUrl = tmp.attr("href"); pTopicUrl = tmp.attr("href");
pSubject = tmp.text(); pSubject = tmp.text();
if (column.select("img[id^=stickyicon]").first() != null) if (column.select("img[id^=stickyicon]").first() != null)
pSticky = true; pSticky = true;
if (column.select("img[id^=lockicon]").first() != null) if (column.select("img[id^=lockicon]").first() != null)
pLocked = true; pLocked = true;
if (column.select("a[id^=newicon]").first() != null) if (column.select("a[id^=newicon]").first() != null)
pUnread = true; pUnread = true;
}
pStarter = topicColumns.get(3).text(); pStarter = topicColumns.get(3).text();
pStats = "Replies: " + topicColumns.get(4).text() + ", Views: " + topicColumns.get(5).text(); pStats = "Replies: " + topicColumns.get(4).text() + ", Views: " + topicColumns.get(5).text();
pLastPost = topicColumns.last().text(); pLastPost = topicColumns.last().text();
Pattern pattern = Pattern.compile("(.+)\\s(by|από)\\s(.+)$"); Matcher matcher = pLastPostPattern.matcher(pLastPost);
Matcher matcher = pattern.matcher(pLastPost);
if (matcher.find()){ if (matcher.find()){
pLastPostDateTime = matcher.group(1); pLastPostDateTime = matcher.group(1);
pLastUser = matcher.group(3); pLastUser = matcher.group(2);
}
else{
Timber.e("Parsing failed (pLastPost came with: \"%s\", topicColumns html was \"%s\")", pLastPost, topicColumns);
continue;
} }
else
throw new ParseException("Parsing failed (pLastPost came with: \"" + pLastPost + "\")");
pLastPostUrl = topicColumns.last().select("a:has(img)").first().attr("href"); pLastPostUrl = topicColumns.last().select("a:has(img)").first().attr("href");
tempTopics.add(new Topic(pTopicUrl, pSubject, pStarter, pLastUser, pLastPostDateTime, pLastPostUrl, tempTopics.add(new Topic(pTopicUrl, pSubject, pStarter, pLastUser, pLastPostDateTime, pLastPostUrl,
@ -323,7 +348,7 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo
|| !Objects.equals(boardTitle, parsedTitle)) { || !Objects.equals(boardTitle, parsedTitle)) {
boardTitle = parsedTitle; boardTitle = parsedTitle;
toolbar.setTitle(boardTitle); toolbar.setTitle(boardTitle);
thisPageBookmark = new Bookmark(boardTitle, ThmmyPage.getBoardId(boardUrl), true); thisPageBookmark = new Bookmark(boardTitle, thisPageBookmark.getId(), thisPageBookmark.isNotificationsEnabled());
setBoardBookmark(findViewById(R.id.bookmark)); setBoardBookmark(findViewById(R.id.bookmark));
} }

22
app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardAdapter.java

@ -161,9 +161,25 @@ class BoardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
boardExpandableVisibility.set(subBoardViewHolder.getAdapterPosition() - 1, !visible); boardExpandableVisibility.set(subBoardViewHolder.getAdapterPosition() - 1, !visible);
}); });
subBoardViewHolder.boardTitle.setText(subBoard.getTitle()); subBoardViewHolder.boardTitle.setText(subBoard.getTitle());
subBoardViewHolder.boardMods.setText(subBoard.getMods()); String mods = subBoard.getMods();
subBoardViewHolder.boardStats.setText(subBoard.getStats()); String stats = subBoard.getStats();
subBoardViewHolder.boardLastPost.setText(subBoard.getLastPost()); String lastPost = subBoard.getLastPost();
if(!mods.isEmpty()){
subBoardViewHolder.boardMods.setText(mods);
subBoardViewHolder.boardMods.setVisibility(View.VISIBLE);
}
if(!stats.isEmpty()){
subBoardViewHolder.boardStats.setText(stats);
subBoardViewHolder.boardStats.setVisibility(View.VISIBLE);
}
if(!lastPost.isEmpty()){
subBoardViewHolder.boardLastPost.setText(lastPost);
subBoardViewHolder.boardLastPost.setVisibility(View.VISIBLE);
}
if (!Objects.equals(subBoard.getLastPostUrl(), "")) { if (!Objects.equals(subBoard.getLastPostUrl(), "")) {
subBoardViewHolder.boardLastPost.setOnClickListener(view -> { subBoardViewHolder.boardLastPost.setOnClickListener(view -> {
Intent intent = new Intent(context, TopicActivity.class); Intent intent = new Intent(context, TopicActivity.class);

2
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksActivity.java

@ -26,8 +26,6 @@ import static gr.thmmy.mthmmy.activities.board.BoardActivity.BUNDLE_BOARD_URL;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL;
//TODO proper handling with adapter etc.
//TODO after clicking bookmark and then back button should return to this activity
public class BookmarksActivity extends BaseActivity { public class BookmarksActivity extends BaseActivity {
private static final String TOPIC_URL = "https://www.thmmy.gr/smf/index.php?topic="; private static final String TOPIC_URL = "https://www.thmmy.gr/smf/index.php?topic=";
private static final String BOARD_URL = "https://www.thmmy.gr/smf/index.php?board="; private static final String BOARD_URL = "https://www.thmmy.gr/smf/index.php?board=";

1
app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java

@ -20,6 +20,7 @@ import java.util.ArrayList;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.model.Bookmark; import gr.thmmy.mthmmy.model.Bookmark;
//TODO refactor using RecyclerView
public class BookmarksFragment extends Fragment { public class BookmarksFragment extends Fragment {
enum Type {TOPIC, BOARD} enum Type {TOPIC, BOARD}
private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER"; private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER";

2
app/src/main/java/gr/thmmy/mthmmy/activities/main/forum/ForumFragment.java

@ -30,7 +30,7 @@ import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.Board; import gr.thmmy.mthmmy.model.Board;
import gr.thmmy.mthmmy.model.Category; import gr.thmmy.mthmmy.model.Category;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView; import gr.thmmy.mthmmy.views.CustomRecyclerView;

2
app/src/main/java/gr/thmmy/mthmmy/activities/main/recent/RecentFragment.java

@ -26,7 +26,7 @@ import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView; import gr.thmmy.mthmmy.views.CustomRecyclerView;

116
app/src/main/java/gr/thmmy/mthmmy/activities/main/unread/UnreadFragment.java

@ -26,18 +26,17 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.base.BaseFragment; import gr.thmmy.mthmmy.base.BaseFragment;
import gr.thmmy.mthmmy.model.TopicSummary; import gr.thmmy.mthmmy.model.TopicSummary;
import gr.thmmy.mthmmy.session.InvalidSessionException;
import gr.thmmy.mthmmy.session.MarkAsReadTask;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.session.ValidateSessionTask; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.views.CustomRecyclerView; import gr.thmmy.mthmmy.views.CustomRecyclerView;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber;
/** /**
* A {@link BaseFragment} subclass. * A {@link BaseFragment} subclass.
@ -59,13 +58,11 @@ public class UnreadFragment extends BaseFragment {
private UnreadAdapter unreadAdapter; private UnreadAdapter unreadAdapter;
private List<TopicSummary> topicSummaries; private List<TopicSummary> topicSummaries;
private String markAsReadUrl;
private int numberOfPages = 0; private int numberOfPages = 0;
private int loadedPages = 0; private int loadedPages = 0;
private UnreadTask unreadTask; private UnreadTask unreadTask;
private MarkReadTask markReadTask; private MarkAsReadTask markAsReadTask;
private ValidateSessionTask validateSessionTask;
// Required empty public constructor // Required empty public constructor
public UnreadFragment() {} public UnreadFragment() {}
@ -89,22 +86,20 @@ public class UnreadFragment extends BaseFragment {
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
topicSummaries = new ArrayList<>(); topicSummaries = new ArrayList<>();
markAsReadUrl = BaseApplication.getInstance().getSessionManager().getMarkAllAsReadLink();
if(markAsReadUrl==null){
Timber.i("MarkAsRead URL is null.");
startValidateSessionTask();
}
} }
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
if (topicSummaries.isEmpty()){ if (topicSummaries.isEmpty()){
hideMarkAsReadFAB();
unreadTask = new UnreadTask(this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, this::onUnreadTaskFinished); unreadTask = new UnreadTask(this::onUnreadTaskStarted, UnreadFragment.this::onUnreadTaskCancelled, this::onUnreadTaskFinished);
assert SessionManager.unreadUrl != null; assert SessionManager.unreadUrl != null;
unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString()); unreadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, SessionManager.unreadUrl.toString());
} }
markReadTask = new MarkReadTask(this::onMarkReadTaskStarted, this::onMarkReadTaskFinished); else
showMarkAsReadFAB();
markAsReadTask = new MarkAsReadTask(UnreadFragment.this::onMarkAsReadTaskStarted, UnreadFragment.this::onMarkAsReadTaskFinished);
} }
@ -119,16 +114,7 @@ public class UnreadFragment extends BaseFragment {
progressBar = rootView.findViewById(R.id.progressBar); progressBar = rootView.findViewById(R.id.progressBar);
noUnreadTopicsTextView = rootView.findViewById(R.id.no_unread_topics); noUnreadTopicsTextView = rootView.findViewById(R.id.no_unread_topics);
markAsReadFAB = rootView.findViewById(R.id.unread_fab); markAsReadFAB = rootView.findViewById(R.id.unread_fab);
if(topicSummaries.isEmpty()){
hideMarkAsReadFAB();
noUnreadTopicsTextView.setVisibility(View.VISIBLE);
}
else{
noUnreadTopicsTextView.setVisibility(View.INVISIBLE);
showMarkAsReadFAB();
}
unreadAdapter = new UnreadAdapter(topicSummaries, fragmentInteractionListener); unreadAdapter = new UnreadAdapter(topicSummaries, fragmentInteractionListener);
CustomRecyclerView recyclerView = rootView.findViewById(R.id.list); CustomRecyclerView recyclerView = rootView.findViewById(R.id.list);
@ -146,7 +132,6 @@ public class UnreadFragment extends BaseFragment {
this::startUnreadTask this::startUnreadTask
); );
} }
return rootView; return rootView;
} }
@ -154,17 +139,10 @@ public class UnreadFragment extends BaseFragment {
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
cancelUnreadTaskIfRunning(); cancelUnreadTaskIfRunning();
if (markReadTask!=null){ if (markAsReadTask !=null){
try{
if(markReadTask.isRunning())
markReadTask.cancel(true);
} // Yes, it happens even though we checked
catch (NullPointerException ignored){ }
}
if (validateSessionTask!=null){
try{ try{
if(validateSessionTask.isRunning()) if(markAsReadTask.isRunning())
validateSessionTask.cancel(true); markAsReadTask.cancel(true);
} // Yes, it happens even though we checked } // Yes, it happens even though we checked
catch (NullPointerException ignored){ } catch (NullPointerException ignored){ }
} }
@ -187,11 +165,6 @@ public class UnreadFragment extends BaseFragment {
} }
} }
private void startValidateSessionTask(){
validateSessionTask = new ValidateSessionTask();
validateSessionTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void cancelUnreadTaskIfRunning(){ private void cancelUnreadTaskIfRunning(){
if (unreadTask!=null){ if (unreadTask!=null){
try{ try{
@ -223,9 +196,9 @@ public class UnreadFragment extends BaseFragment {
builder.setTitle("Mark all as read"); builder.setTitle("Mark all as read");
builder.setMessage("Are you sure that you want to mark ALL topics as read?"); builder.setMessage("Are you sure that you want to mark ALL topics as read?");
builder.setPositiveButton("Yep", (dialogInterface, i) -> { builder.setPositiveButton("Yep", (dialogInterface, i) -> {
if (!markReadTask.isRunning() && markAsReadUrl!=null){ if (!markAsReadTask.isRunning()){
markReadTask = new MarkReadTask(this::onMarkReadTaskStarted, this::onMarkReadTaskFinished); markAsReadTask = new MarkAsReadTask(this::onMarkAsReadTaskStarted, this::onMarkAsReadTaskFinished);
markReadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, markAsReadUrl); markAsReadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
}); });
builder.setNegativeButton("Nope", (dialogInterface, i) -> {}); builder.setNegativeButton("Nope", (dialogInterface, i) -> {});
@ -275,6 +248,8 @@ public class UnreadFragment extends BaseFragment {
hideProgressUI(); hideProgressUI();
if (resultCode == NetworkResultCodes.NETWORK_ERROR) if (resultCode == NetworkResultCodes.NETWORK_ERROR)
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
else if (resultCode == SessionManager.INVALID_SESSION)
Toast.makeText(getContext(), "Session verification failed. Please try logging in again.", Toast.LENGTH_LONG).show();
else else
Toast.makeText(getContext(), "Unexpected error," + Toast.makeText(getContext(), "Unexpected error," +
" please contact the developers with the details", Toast.LENGTH_LONG).show(); " please contact the developers with the details", Toast.LENGTH_LONG).show();
@ -287,7 +262,10 @@ public class UnreadFragment extends BaseFragment {
} }
@Override @Override
protected ArrayList<TopicSummary> parse(Document document, Response response) throws ParseException { protected ArrayList<TopicSummary> parse(Document document, Response response) throws ParseException, InvalidSessionException {
if(!document.select("td:containsOwn(Only registered members are allowed to access this section.)").isEmpty())
throw new InvalidSessionException();
Elements unread = document.select("table.bordercolor[cellspacing=1] tr:not(.titlebg)"); Elements unread = document.select("table.bordercolor[cellspacing=1] tr:not(.titlebg)");
ArrayList<TopicSummary> fetchedTopicSummaries = new ArrayList<>(); ArrayList<TopicSummary> fetchedTopicSummaries = new ArrayList<>();
if (!unread.isEmpty()) { if (!unread.isEmpty()) {
@ -307,12 +285,10 @@ public class UnreadFragment extends BaseFragment {
} }
Element topBar = document.select("table:not(.bordercolor):not(#bodyarea):has(td.middletext)").first(); Element topBar = document.select("table:not(.bordercolor):not(#bodyarea):has(td.middletext)").first();
Element pagesElement = null, markRead = null; Element pagesElement = null;
if (topBar != null) { if (topBar != null)
pagesElement = topBar.select("td.middletext").first(); pagesElement = topBar.select("td.middletext").first();
markRead = document.select("table:not(.bordercolor):not([width])").select("a")
.first();
}
if (numberOfPages == 0 && pagesElement != null) { if (numberOfPages == 0 && pagesElement != null) {
Elements pages = pagesElement.select("a"); Elements pages = pagesElement.select("a");
@ -322,14 +298,6 @@ public class UnreadFragment extends BaseFragment {
numberOfPages = 1; numberOfPages = 1;
} }
if (markRead != null && loadedPages == numberOfPages - 1){
String retrievedMarkAsReadUrl = markRead.attr("href");
if(!retrievedMarkAsReadUrl.equals(markAsReadUrl)) {
markAsReadUrl = retrievedMarkAsReadUrl;
BaseApplication.getInstance().getSessionManager().refreshSescFromUrl(retrievedMarkAsReadUrl);
}
}
return fetchedTopicSummaries; return fetchedTopicSummaries;
} }
return new ArrayList<>(); return new ArrayList<>();
@ -341,49 +309,25 @@ public class UnreadFragment extends BaseFragment {
} }
} }
//---------------------------------------MARKREAD TASK------------------------------------------ //---------------------------------------MARK AS READ TASK------------------------------------------
private void onMarkReadTaskStarted() { private void onMarkAsReadTaskStarted() {
cancelUnreadTaskIfRunning(); cancelUnreadTaskIfRunning();
progressBar.setVisibility(ProgressBar.VISIBLE); progressBar.setVisibility(ProgressBar.VISIBLE);
} }
private void onMarkReadTaskFinished(int resultCode, Boolean isSessionVerified) { private void onMarkAsReadTaskFinished(int resultCode, Void v) {
hideProgressUI(); hideProgressUI();
if (resultCode == NetworkResultCodes.SUCCESSFUL) { if (resultCode == NetworkResultCodes.SUCCESSFUL)
if (!isSessionVerified){
Toast.makeText(getContext(), "Session verification failed", Toast.LENGTH_SHORT).show();
startValidateSessionTask();
}
else
startUnreadTask(); startUnreadTask();
}
else{ else{
hideProgressUI(); hideProgressUI();
if (resultCode == NetworkResultCodes.NETWORK_ERROR) if (resultCode == NetworkResultCodes.NETWORK_ERROR)
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
else if (resultCode == SessionManager.INVALID_SESSION)
Toast.makeText(getContext(), "Session verification failed. Please try logging out and back in again", Toast.LENGTH_LONG).show();
else else
Toast.makeText(getContext(), "Unexpected error," + Toast.makeText(getContext(), "Unexpected error," +
" please contact the developers with the details", Toast.LENGTH_LONG).show(); " please contact the developers with the details", Toast.LENGTH_LONG).show();
} }
} }
private class MarkReadTask extends NewParseTask<Boolean> {
MarkReadTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<Boolean> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
}
@Override
protected Boolean parse(Document document, Response response) throws ParseException {
Elements sessionVerificationFailed = document.select("td:containsOwn(Session " +
"verification failed. Please try logging out and back in again, and then try " +
"again.), td:containsOwn(Η επαλήθευση συνόδου απέτυχε. Παρακαλούμε κάντε " +
"αποσύνδεση, επανασύνδεση και ξαναδοκιμάστε.)");
return sessionVerificationFailed.isEmpty();
}
@Override
protected int getResultCode(Response response, Boolean isSessionVerified) {
return NetworkResultCodes.SUCCESSFUL;
}
}
} }

19
app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java

@ -45,8 +45,8 @@ import gr.thmmy.mthmmy.activities.topic.TopicActivity;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.model.PostSummary; import gr.thmmy.mthmmy.model.PostSummary;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.utils.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.Parcel; import gr.thmmy.mthmmy.utils.Parcel;
import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.ui.CenterVerticalSpan; import gr.thmmy.mthmmy.utils.ui.CenterVerticalSpan;
@ -57,6 +57,7 @@ import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE; import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_TITLE;
import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL; import static gr.thmmy.mthmmy.activities.topic.TopicActivity.BUNDLE_TOPIC_URL;
import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.emojiTagToHtml; import static gr.thmmy.mthmmy.utils.parsing.ParseHelpers.emojiTagToHtml;
import static gr.thmmy.mthmmy.utils.ui.GlideUtils.isValidContextForGlide;
import static gr.thmmy.mthmmy.utils.ui.PhotoViewUtils.displayPhotoViewImage; import static gr.thmmy.mthmmy.utils.ui.PhotoViewUtils.displayPhotoViewImage;
/** /**
@ -222,12 +223,16 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
avatarView.setOnClickListener(v -> displayPhotoViewImage(ProfileActivity.this, avatarUrl)); avatarView.setOnClickListener(v -> displayPhotoViewImage(ProfileActivity.this, avatarUrl));
} }
Glide.with(this) if(isValidContextForGlide(this)){
.load(avatarUri) Glide.with(this)
.circleCrop() .load(avatarUri)
.error(R.drawable.ic_default_user_avatar) .circleCrop()
.placeholder(R.drawable.ic_default_user_avatar) .error(R.drawable.ic_default_user_avatar)
.into(avatarView); .placeholder(R.drawable.ic_default_user_avatar)
.into(avatarView);
}
else
Timber.d("Will not load Glide image (invalid context)");
} }
/** /**

5
app/src/main/java/gr/thmmy/mthmmy/activities/settings/SettingsFragment.java

@ -200,14 +200,15 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared
if (key.equals(getString(R.string.pref_privacy_crashlytics_enable_key))) { if (key.equals(getString(R.string.pref_privacy_crashlytics_enable_key))) {
enabled = sharedPreferences.getBoolean(key, false); enabled = sharedPreferences.getBoolean(key, false);
if(enabled) if(enabled)
BaseApplication.getInstance().startFirebaseCrashlyticsCollection(); BaseApplication.getInstance().setFirebaseCrashlyticsEnabled(true);
else { else {
Timber.i("Crashlytics collection will be disabled after restarting."); Timber.i("Crashlytics collection will be disabled after restarting.");
BaseApplication.getInstance().setFirebaseCrashlyticsEnabled(false);
displayRestartAppToTakeEffectToast(); displayRestartAppToTakeEffectToast();
} }
} else if (key.equals(getString(R.string.pref_privacy_analytics_enable_key))) { } else if (key.equals(getString(R.string.pref_privacy_analytics_enable_key))) {
enabled = sharedPreferences.getBoolean(key, false); enabled = sharedPreferences.getBoolean(key, false);
BaseApplication.getInstance().setFirebaseAnalyticsCollection(enabled); BaseApplication.getInstance().setFirebaseAnalyticsEnabled(enabled);
if(enabled) if(enabled)
Timber.i("Analytics collection enabled."); Timber.i("Analytics collection enabled.");
else else

4
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/SendShoutTask.java

@ -4,8 +4,8 @@ import org.jsoup.nodes.Document;
import java.io.IOException; import java.io.IOException;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.NetworkTask; import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;

2
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxFragment.java

@ -21,7 +21,7 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.Shout; import gr.thmmy.mthmmy.model.Shout;
import gr.thmmy.mthmmy.model.Shoutbox; import gr.thmmy.mthmmy.model.Shoutbox;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.viewmodel.ShoutboxViewModel; import gr.thmmy.mthmmy.viewmodel.ShoutboxViewModel;
import gr.thmmy.mthmmy.views.CustomRecyclerView; import gr.thmmy.mthmmy.views.CustomRecyclerView;
import gr.thmmy.mthmmy.views.editorview.EditorView; import gr.thmmy.mthmmy.views.editorview.EditorView;

2
app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutboxTask.java

@ -7,7 +7,7 @@ import java.util.ArrayList;
import gr.thmmy.mthmmy.model.Shout; import gr.thmmy.mthmmy.model.Shout;
import gr.thmmy.mthmmy.model.Shoutbox; import gr.thmmy.mthmmy.model.Shoutbox;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.NewParseTask; import gr.thmmy.mthmmy.utils.parsing.NewParseTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;

7
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java

@ -54,7 +54,7 @@ import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyPage; import gr.thmmy.mthmmy.model.ThmmyPage;
import gr.thmmy.mthmmy.model.TopicItem; import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.HTMLUtils; import gr.thmmy.mthmmy.utils.HTMLUtils;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import gr.thmmy.mthmmy.viewmodel.TopicViewModel; import gr.thmmy.mthmmy.viewmodel.TopicViewModel;
import gr.thmmy.mthmmy.views.CustomLinearLayoutManager; import gr.thmmy.mthmmy.views.CustomLinearLayoutManager;
@ -155,6 +155,7 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
} }
topicPageUrl = ThmmyPage.sanitizeTopicUrl(topicPageUrl); topicPageUrl = ThmmyPage.sanitizeTopicUrl(topicPageUrl);
//TODO if topicTitle provided is null make bookmark button unclickable until title is fetched (also for BoardActivity)
thisPageBookmark = new Bookmark(topicTitle, ThmmyPage.getTopicId(topicPageUrl), true); thisPageBookmark = new Bookmark(topicTitle, ThmmyPage.getTopicId(topicPageUrl), true);
//Initializes graphics //Initializes graphics
@ -671,8 +672,10 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
}); });
viewModel.getTopicTitle().observe(this, newTopicTitle -> { viewModel.getTopicTitle().observe(this, newTopicTitle -> {
if (newTopicTitle == null) return; if (newTopicTitle == null) return;
if (!TextUtils.equals(toolbarTitle.getText(), newTopicTitle)) if (!TextUtils.equals(toolbarTitle.getText(), newTopicTitle)) {
thisPageBookmark = new Bookmark(newTopicTitle, thisPageBookmark.getId(), thisPageBookmark.isNotificationsEnabled());
toolbarTitle.setText(newTopicTitle); toolbarTitle.setText(newTopicTitle);
}
}); });
viewModel.getPageTopicId().observe(this, pageTopicId -> { viewModel.getPageTopicId().observe(this, pageTopicId -> {
if (pageTopicId == null) return; if (pageTopicId == null) return;

16
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java

@ -88,6 +88,7 @@ import static gr.thmmy.mthmmy.activities.topic.TopicParser.USER_COLOR_WHITE;
import static gr.thmmy.mthmmy.activities.topic.TopicParser.USER_COLOR_YELLOW; import static gr.thmmy.mthmmy.activities.topic.TopicParser.USER_COLOR_YELLOW;
import static gr.thmmy.mthmmy.base.BaseActivity.getSessionManager; import static gr.thmmy.mthmmy.base.BaseActivity.getSessionManager;
import static gr.thmmy.mthmmy.utils.FileUtils.faIconFromFilename; import static gr.thmmy.mthmmy.utils.FileUtils.faIconFromFilename;
import static gr.thmmy.mthmmy.utils.ui.GlideUtils.isValidContextForGlide;
/** /**
* Custom {@link RecyclerView.Adapter} used for topics. * Custom {@link RecyclerView.Adapter} used for topics.
@ -810,12 +811,14 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
if(imageUrl!=null) if(imageUrl!=null)
imageUrl = imageUrl.trim(); imageUrl = imageUrl.trim();
Glide.with(context) if(isValidContextForGlide(context)) {
.load(imageUrl) Glide.with(context)
.circleCrop() .load(imageUrl)
.error(R.drawable.ic_default_user_avatar_darker) .circleCrop()
.placeholder(R.drawable.ic_default_user_avatar_darker) .error(R.drawable.ic_default_user_avatar_darker)
.into(imageView); .placeholder(R.drawable.ic_default_user_avatar_darker)
.into(imageView);
}
} }
@Override @Override
@ -979,7 +982,6 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
for (int i = 0; i < topicItems.size(); i++) { for (int i = 0; i < topicItems.size(); i++) {
if (topicItems.get(i) instanceof Post && ((Post) topicItems.get(i)).getPostIndex() == testAgainst) { if (topicItems.get(i) instanceof Post && ((Post) topicItems.get(i)).getPostIndex() == testAgainst) {
//same page //same page
Timber.d(Integer.toString(i));
postFocusListener.onPostFocusChange(i); postFocusListener.onPostFocusChange(i);
return true; return true;
} }

15
app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicParser.java

@ -18,7 +18,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import gr.thmmy.mthmmy.base.BaseActivity; import gr.thmmy.mthmmy.base.BaseActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.Poll; import gr.thmmy.mthmmy.model.Poll;
import gr.thmmy.mthmmy.model.Post; import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyFile;
@ -26,8 +25,6 @@ import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import timber.log.Timber; import timber.log.Timber;
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp;
/** /**
* Singleton used for parsing a topic. * Singleton used for parsing a topic.
@ -178,7 +175,6 @@ public class TopicParser {
p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate, p_specialRank, p_gender, p_personalText, p_numberOfPosts, p_postLastEditDate,
p_postURL, p_deletePostURL, p_editPostURL; p_postURL, p_deletePostURL, p_editPostURL;
int p_postNum, p_postIndex, p_numberOfStars, p_userColor; int p_postNum, p_postIndex, p_numberOfStars, p_userColor;
long p_timestamp;
boolean p_isDeleted = false, p_isUserMentionedInPost = false; boolean p_isDeleted = false, p_isUserMentionedInPost = false;
ArrayList<ThmmyFile> p_attachedFiles; ArrayList<ThmmyFile> p_attachedFiles;
@ -195,7 +191,6 @@ public class TopicParser {
p_postLastEditDate = null; p_postLastEditDate = null;
p_deletePostURL = null; p_deletePostURL = null;
p_editPostURL = null; p_editPostURL = null;
p_timestamp = 0;
//Language independent parsing //Language independent parsing
//Finds thumbnail url //Finds thumbnail url
@ -272,12 +267,6 @@ public class TopicParser {
p_postDate = p_postDate.substring(p_postDate.indexOf("στις:") + 6 p_postDate = p_postDate.substring(p_postDate.indexOf("στις:") + 6
, p_postDate.indexOf(" »")); , p_postDate.indexOf(" »"));
if (BaseApplication.getInstance().isDisplayRelativeTimeEnabled()) {
String timestamp = convertToTimestamp(p_postDate);
if(timestamp!=null)
p_timestamp = Long.valueOf(timestamp);
}
//Finds post's reply index number //Finds post's reply index number
Element postNum = thisRow.select("div.smalltext:matches(Απάντηση #)").first(); Element postNum = thisRow.select("div.smalltext:matches(Απάντηση #)").first();
if (postNum == null) { //Topic starter if (postNum == null) { //Topic starter
@ -305,7 +294,7 @@ public class TopicParser {
Timber.e(e, "Attached file malformed url"); Timber.e(e, "Attached file malformed url");
break; break;
} }
String attachedFileName = tmpAttachedFileUrlAndName.text().substring(1); String attachedFileName = tmpAttachedFileUrlAndName.wholeText().substring(1);
//Gets file's info (size and download count) //Gets file's info (size and download count)
String postAttachmentsTextSbstr = postAttachmentsText.substring( String postAttachmentsTextSbstr = postAttachmentsText.substring(
@ -377,7 +366,7 @@ public class TopicParser {
Timber.e(e, "Attached file malformed url"); Timber.e(e, "Attached file malformed url");
break; break;
} }
String attachedFileName = tmpAttachedFileUrlAndName.text().substring(1); String attachedFileName = tmpAttachedFileUrlAndName.wholeText().substring(1);
//Gets file's info (size and download count) //Gets file's info (size and download count)
String postAttachmentsTextSbstr = postAttachmentsText.substring( String postAttachmentsTextSbstr = postAttachmentsText.substring(

4
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/DeleteTask.java

@ -4,8 +4,8 @@ import org.jsoup.nodes.Document;
import java.io.IOException; import java.io.IOException;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.NetworkTask; import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;

4
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/RemoveVoteTask.java

@ -2,8 +2,8 @@ package gr.thmmy.mthmmy.activities.topic.tasks;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.NetworkTask; import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import okhttp3.Response; import okhttp3.Response;
public class RemoveVoteTask extends NetworkTask<Void> { public class RemoveVoteTask extends NetworkTask<Void> {

4
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/SubmitVoteTask.java

@ -4,8 +4,8 @@ import org.jsoup.nodes.Document;
import java.io.IOException; import java.io.IOException;
import gr.thmmy.mthmmy.utils.NetworkResultCodes; import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.NetworkTask; import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;

17
app/src/main/java/gr/thmmy/mthmmy/activities/topic/tasks/TopicTask.java

@ -11,6 +11,9 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.activities.topic.TopicParser; import gr.thmmy.mthmmy.activities.topic.TopicParser;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
@ -29,6 +32,8 @@ import timber.log.Timber;
* parameter.</p> * parameter.</p>
*/ */
public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> { public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
private static final Pattern msgPattern = Pattern.compile("msg(\\d+)");
private TopicTaskObserver topicTaskObserver; private TopicTaskObserver topicTaskObserver;
private OnTopicTaskCompleted finishListener; private OnTopicTaskCompleted finishListener;
@ -58,15 +63,9 @@ public class TopicTask extends AsyncTask<String, Void, TopicTaskResult> {
//Finds the index of message focus if present //Finds the index of message focus if present
int postFocus = 0; int postFocus = 0;
Matcher matcher = msgPattern.matcher(newPageUrl);
//TODO: Better parseInt handling - may rarely fail if (matcher.find())
if (newPageUrl.contains("msg")) { postFocus = Integer.parseInt(Objects.requireNonNull(matcher.group(1)));
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('#')));
}
Request request = new Request.Builder() Request request = new Request.Builder()
.url(newPageUrl) .url(newPageUrl)

91
app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java

@ -62,9 +62,11 @@ import gr.thmmy.mthmmy.model.Bookmark;
import gr.thmmy.mthmmy.model.ThmmyFile; import gr.thmmy.mthmmy.model.ThmmyFile;
import gr.thmmy.mthmmy.services.DownloadHelper; import gr.thmmy.mthmmy.services.DownloadHelper;
import gr.thmmy.mthmmy.services.UploadsReceiver; import gr.thmmy.mthmmy.services.UploadsReceiver;
import gr.thmmy.mthmmy.session.LogoutTask;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.FileUtils; import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.utils.io.AssetUtils; import gr.thmmy.mthmmy.utils.io.AssetUtils;
import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.viewmodel.BaseViewModel; import gr.thmmy.mthmmy.viewmodel.BaseViewModel;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar; import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -82,7 +84,6 @@ import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_
import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DEFAULT_HOME_TAB; import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DEFAULT_HOME_TAB;
import static gr.thmmy.mthmmy.services.DownloadHelper.SAVE_DIR; import static gr.thmmy.mthmmy.services.DownloadHelper.SAVE_DIR;
import static gr.thmmy.mthmmy.services.UploadsReceiver.UPLOAD_ID_KEY; import static gr.thmmy.mthmmy.services.UploadsReceiver.UPLOAD_ID_KEY;
import static gr.thmmy.mthmmy.session.SessionManager.SUCCESS;
import static gr.thmmy.mthmmy.utils.FileUtils.getMimeType; import static gr.thmmy.mthmmy.utils.FileUtils.getMimeType;
public abstract class BaseActivity extends AppCompatActivity { public abstract class BaseActivity extends AppCompatActivity {
@ -205,7 +206,7 @@ public abstract class BaseActivity extends AppCompatActivity {
*/ */
protected void createDrawer() { protected void createDrawer() {
final int primaryColor = ContextCompat.getColor(this, R.color.iron); final int primaryColor = ContextCompat.getColor(this, R.color.iron);
final int selectedPrimaryColor = ContextCompat.getColor(this, R.color.primary_dark); final int selectedPrimaryColor = ContextCompat.getColor(this, R.color.primary_light);
final int selectedSecondaryColor = ContextCompat.getColor(this, R.color.accent); final int selectedSecondaryColor = ContextCompat.getColor(this, R.color.accent);
PrimaryDrawerItem homeItem, bookmarksItem, settingsItem, aboutItem, shoutboxItem; PrimaryDrawerItem homeItem, bookmarksItem, settingsItem, aboutItem, shoutboxItem;
@ -360,7 +361,7 @@ public abstract class BaseActivity extends AppCompatActivity {
.withActivity(this) .withActivity(this)
.withCompactStyle(true) .withCompactStyle(true)
.withSelectionListEnabledForSingleProfile(false) .withSelectionListEnabledForSingleProfile(false)
.withHeaderBackground(R.color.primary) .withHeaderBackground(R.color.primary_dark)
.withTextColor(getResources().getColor(R.color.iron)) .withTextColor(getResources().getColor(R.color.iron))
.addProfiles(profileDrawerItem) .addProfiles(profileDrawerItem)
.withOnAccountHeaderListener((view, profile, currentProfile) -> { .withOnAccountHeaderListener((view, profile, currentProfile) -> {
@ -389,7 +390,7 @@ public abstract class BaseActivity extends AppCompatActivity {
.withActivity(this) .withActivity(this)
.withToolbar(toolbar) .withToolbar(toolbar)
.withDrawerWidthDp((int) BaseApplication.getInstance().getWidthInDp() / 2) .withDrawerWidthDp((int) BaseApplication.getInstance().getWidthInDp() / 2)
.withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_light)) .withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_lighter))
.withAccountHeader(accountHeader) .withAccountHeader(accountHeader)
.withOnDrawerItemClickListener((view, position, drawerItem) -> { .withOnDrawerItemClickListener((view, position, drawerItem) -> {
if (drawerItem.equals(HOME_ID)) { if (drawerItem.equals(HOME_ID)) {
@ -463,8 +464,7 @@ public abstract class BaseActivity extends AppCompatActivity {
private void updateDrawer() { private void updateDrawer() {
if (drawer != null) { if (drawer != null) {
if (!sessionManager.isLoggedIn()) //When logged out or if user is guest if (!sessionManager.isLoggedIn()){ //When logged out or if user is guest
{
drawer.removeItem(DOWNLOADS_ID); drawer.removeItem(DOWNLOADS_ID);
drawer.removeItem(UPLOAD_ID); drawer.removeItem(UPLOAD_ID);
loginLogoutItem.withName(R.string.login).withIcon(loginIcon); //Swap logout with login loginLogoutItem.withName(R.string.login).withIcon(loginIcon); //Swap logout with login
@ -486,60 +486,43 @@ public abstract class BaseActivity extends AppCompatActivity {
} }
accountHeader.updateProfile(profileDrawerItem); accountHeader.updateProfile(profileDrawerItem);
drawer.updateItem(loginLogoutItem); drawer.updateItem(loginLogoutItem);
} }
} }
private void setDefaultAvatar() { private void setDefaultAvatar() {
profileDrawerItem.withIcon(new IconicsDrawable(this) profileDrawerItem.withIcon(R.drawable.ic_default_user_avatar);
.icon(FontAwesome.Icon.faw_user)
.paddingDp(10)
.color(ContextCompat.getColor(this, R.color.iron))
.backgroundColor(ContextCompat.getColor(this, R.color.primary_light)));
} }
//-------------------------------------------LOGOUT------------------------------------------------- //-------------------------------------------LOGOUT-------------------------------------------------
private ProgressDialog progressDialog;
/** private void onLogoutTaskStarted() {
* Result toast will always display a success, because when user chooses logout all data are progressDialog = new ProgressDialog(BaseActivity.this,
* cleared regardless of the actual outcome R.style.AppTheme_Dark_Dialog);
*/ progressDialog.setCancelable(false);
private class LogoutTask extends AsyncTask<Void, Void, Integer> { //Attempt logout progressDialog.setIndeterminate(true);
ProgressDialog progressDialog; progressDialog.setMessage("Logging out...");
progressDialog.show();
protected Integer doInBackground(Void... voids) { }
return sessionManager.logout();
} private void onLogoutTaskFinished(int resultCode, Void v) {
if (resultCode == NetworkResultCodes.SUCCESSFUL) {
protected void onPreExecute() { //Show a progress dialog until done SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
progressDialog = new ProgressDialog(BaseActivity.this, if (sharedPrefs.getString(DEFAULT_HOME_TAB, "0").equals("2")) {
R.style.AppTheme_Dark_Dialog); SharedPreferences.Editor editor = sharedPrefs.edit();
progressDialog.setCancelable(false); editor.putString(DEFAULT_HOME_TAB, "0").apply();
progressDialog.setIndeterminate(true);
progressDialog.setMessage("Logging out...");
progressDialog.show();
}
protected void onPostExecute(Integer result) {
if (result == SUCCESS) {
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (sharedPrefs.getString(DEFAULT_HOME_TAB, "0").equals("2")) {
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(DEFAULT_HOME_TAB, "0").apply();
}
} }
updateDrawer();
if (mainActivity != null)
mainActivity.updateTabs();
progressDialog.dismiss();
//TODO: Redirect to Main only for some Activities (e.g. Topic, Board, Downloads)
//if (BaseActivity.this instanceof TopicActivity){
Intent intent = new Intent(BaseActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
//}
} }
updateDrawer();
if (mainActivity != null)
mainActivity.updateTabs();
progressDialog.dismiss();
//TODO: Redirect to Main only for some Activities (e.g. Topic, Board, Downloads)
//if (BaseActivity.this instanceof TopicActivity){
Intent intent = new Intent(BaseActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
//}
} }
private void showLogoutDialog() { private void showLogoutDialog() {
@ -547,7 +530,7 @@ public abstract class BaseActivity extends AppCompatActivity {
builder.setTitle("Logout"); builder.setTitle("Logout");
builder.setMessage("Are you sure that you want to logout?"); builder.setMessage("Are you sure that you want to logout?");
builder.setPositiveButton("Yep", (dialogInterface, i) -> { builder.setPositiveButton("Yep", (dialogInterface, i) -> {
new LogoutTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Avoid delays between onPreExecute() and doInBackground() new LogoutTask(this::onLogoutTaskStarted, this::onLogoutTaskFinished).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); //Avoid delays between onPreExecute() and doInBackground()
}); });
builder.setNegativeButton("Nope", (dialogInterface, i) -> {}); builder.setNegativeButton("Nope", (dialogInterface, i) -> {});
builder.create().show(); builder.create().show();
@ -807,8 +790,8 @@ public abstract class BaseActivity extends AppCompatActivity {
builder.setPositiveButton("Yes, I want to help", (dialogInterface, i) -> { builder.setPositiveButton("Yes, I want to help", (dialogInterface, i) -> {
addUserConsent(); addUserConsent();
FirebaseMessaging.getInstance().setAutoInitEnabled(true); FirebaseMessaging.getInstance().setAutoInitEnabled(true);
BaseApplication.getInstance().startFirebaseCrashlyticsCollection(); BaseApplication.getInstance().setFirebaseCrashlyticsEnabled(true);
BaseApplication.getInstance().setFirebaseAnalyticsCollection(true); BaseApplication.getInstance().setFirebaseAnalyticsEnabled(true);
setUserDataShareEnabled(true); setUserDataShareEnabled(true);
}); });
builder.setNegativeButton("Nope, leave me alone", (dialogInterface, i) -> { builder.setNegativeButton("Nope, leave me alone", (dialogInterface, i) -> {

126
app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java

@ -14,13 +14,12 @@ import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.crashlytics.android.Crashlytics;
import com.crashlytics.android.core.CrashlyticsCore;
import com.franmontiel.persistentcookiejar.PersistentCookieJar; import com.franmontiel.persistentcookiejar.PersistentCookieJar;
import com.franmontiel.persistentcookiejar.cache.SetCookieCache; import com.franmontiel.persistentcookiejar.cache.SetCookieCache;
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor; import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor;
import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseApp;
import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor; import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor;
import com.mikepenz.fontawesome_typeface_library.FontAwesome; import com.mikepenz.fontawesome_typeface_library.FontAwesome;
import com.mikepenz.iconics.IconicsDrawable; import com.mikepenz.iconics.IconicsDrawable;
@ -40,7 +39,6 @@ import gr.thmmy.mthmmy.BuildConfig;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.crashreporting.CrashReportingTree; import gr.thmmy.mthmmy.utils.crashreporting.CrashReportingTree;
import io.fabric.sdk.android.Fabric;
import okhttp3.CipherSuite; import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec; import okhttp3.ConnectionSpec;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
@ -50,9 +48,12 @@ import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DISPLAY_RELATIVE_TIME; import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DISPLAY_RELATIVE_TIME;
// TODO: Replace MultiDexApplication with Application after KitKat support is dropped
public class BaseApplication extends MultiDexApplication { public class BaseApplication extends MultiDexApplication {
private static BaseApplication baseApplication; //BaseApplication singleton private static BaseApplication baseApplication; //BaseApplication singleton
private CrashReportingTree crashReportingTree;
//Firebase //Firebase
private static String firebaseProjectId; private static String firebaseProjectId;
private FirebaseAnalytics firebaseAnalytics; private FirebaseAnalytics firebaseAnalytics;
@ -63,9 +64,6 @@ public class BaseApplication extends MultiDexApplication {
private boolean displayRelativeTime; private boolean displayRelativeTime;
//TODO: maybe use PreferenceManager.getDefaultSharedPreferences here as well?
private static final String SHARED_PREFS = "ThmmySharedPrefs";
//Display Metrics //Display Metrics
private static float widthDp; private static float widthDp;
private static int widthPxl, heightPxl; private static int widthPxl, heightPxl;
@ -84,26 +82,52 @@ public class BaseApplication extends MultiDexApplication {
Timber.plant(new Timber.DebugTree()); Timber.plant(new Timber.DebugTree());
//Shared Preferences //Shared Preferences
SharedPreferences sharedPrefs = getSharedPreferences(SHARED_PREFS, MODE_PRIVATE); SharedPreferences sessionSharedPrefs = getSharedPreferences(getString(R.string.session_shared_prefs), MODE_PRIVATE);
SharedPreferences settingsSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences settingsSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences draftsPrefs = getSharedPreferences(getString(R.string.pref_topic_drafts_key), MODE_PRIVATE); SharedPreferences draftsPrefs = getSharedPreferences(getString(R.string.pref_topic_drafts_key), MODE_PRIVATE);
if (settingsSharedPrefs.getBoolean(getString(R.string.pref_privacy_crashlytics_enable_key), false)) initFirebase(settingsSharedPrefs);
startFirebaseCrashlyticsCollection();
else SharedPrefsCookiePersistor sharedPrefsCookiePersistor = new SharedPrefsCookiePersistor(getApplicationContext());
Timber.i("Starting app with Crashlytics disabled."); PersistentCookieJar cookieJar = new PersistentCookieJar(new SetCookieCache(), sharedPrefsCookiePersistor);
initOkHttp(cookieJar);
sessionManager = new SessionManager(client, cookieJar, sharedPrefsCookiePersistor, sessionSharedPrefs, draftsPrefs);
//Sets up upload service
UploadService.NAMESPACE = BuildConfig.APPLICATION_ID;
UploadService.HTTP_STACK = new OkHttpStack(client);
//Initialize and create the image loader logic for the drawer
initDrawerImageLoader();
setDisplayMetrics();
displayRelativeTime = settingsSharedPrefs.getBoolean(DISPLAY_RELATIVE_TIME, true);
}
private void initFirebase(SharedPreferences settingsSharedPrefs){
if (settingsSharedPrefs.getBoolean(getString(R.string.pref_privacy_crashlytics_enable_key), false)){
Timber.i("Starting app with Firebase Crashlytics enabled.");
setFirebaseCrashlyticsEnabled(true);
}
else {
Timber.i("Starting app with Firebase Crashlytics disabled.");
setFirebaseCrashlyticsEnabled(false);
}
firebaseProjectId = FirebaseApp.getInstance().getOptions().getProjectId(); firebaseProjectId = FirebaseApp.getInstance().getOptions().getProjectId();
firebaseAnalytics = FirebaseAnalytics.getInstance(this); firebaseAnalytics = FirebaseAnalytics.getInstance(this);
boolean enableAnalytics = settingsSharedPrefs.getBoolean(getString(R.string.pref_privacy_analytics_enable_key), false); boolean enableAnalytics = settingsSharedPrefs.getBoolean(getString(R.string.pref_privacy_analytics_enable_key), false);
firebaseAnalytics.setAnalyticsCollectionEnabled(enableAnalytics); firebaseAnalytics.setAnalyticsCollectionEnabled(enableAnalytics);
if (enableAnalytics) if (enableAnalytics)
Timber.i("Starting app with Analytics enabled."); Timber.i("Starting app with Firebase Analytics enabled.");
else else
Timber.i("Starting app with Analytics disabled."); Timber.i("Starting app with Firebase Analytics disabled.");
}
SharedPrefsCookiePersistor sharedPrefsCookiePersistor = new SharedPrefsCookiePersistor(getApplicationContext()); private void initOkHttp(PersistentCookieJar cookieJar){
PersistentCookieJar cookieJar = new PersistentCookieJar(new SetCookieCache(), sharedPrefsCookiePersistor);
OkHttpClient.Builder builder = new OkHttpClient.Builder() OkHttpClient.Builder builder = new OkHttpClient.Builder()
.cookieJar(cookieJar) .cookieJar(cookieJar)
.addInterceptor(chain -> { .addInterceptor(chain -> {
@ -111,9 +135,9 @@ public class BaseApplication extends MultiDexApplication {
HttpUrl oldUrl = chain.request().url(); HttpUrl oldUrl = chain.request().url();
if (Objects.equals(chain.request().url().host(), "www.thmmy.gr") if (Objects.equals(chain.request().url().host(), "www.thmmy.gr")
&& !oldUrl.toString().contains("theme=4")) { && !oldUrl.toString().contains("theme=4")) {
//Probably works but needs more testing: //Probably works but needs more testing:
HttpUrl newUrl = oldUrl.newBuilder().addQueryParameter("theme", "4").build(); HttpUrl newUrl = oldUrl.newBuilder().addQueryParameter("theme", "4").build();
request = request.newBuilder().url(newUrl).build(); request = request.newBuilder().url(newUrl).build();
} }
return chain.proceed(request); return chain.proceed(request);
}) })
@ -139,14 +163,9 @@ public class BaseApplication extends MultiDexApplication {
builder.addInterceptor(new OkHttpProfilerInterceptor()); builder.addInterceptor(new OkHttpProfilerInterceptor());
client = builder.build(); client = builder.build();
}
sessionManager = new SessionManager(client, cookieJar, sharedPrefsCookiePersistor, sharedPrefs, draftsPrefs); private void initDrawerImageLoader(){
//Sets up upload service
UploadService.NAMESPACE = BuildConfig.APPLICATION_ID;
UploadService.HTTP_STACK = new OkHttpStack(client);
//Initialize and create the image loader logic
DrawerImageLoader.init(new AbstractDrawerImageLoader() { DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override @Override
public void set(ImageView imageView, Uri uri, Drawable placeholder, String tag) { public void set(ImageView imageView, Uri uri, Drawable placeholder, String tag) {
@ -160,27 +179,32 @@ public class BaseApplication extends MultiDexApplication {
@Override @Override
public Drawable placeholder(Context ctx, String tag) { public Drawable placeholder(Context ctx, String tag) {
if (DrawerImageLoader.Tags.PROFILE.name().equals(tag)) { if (DrawerImageLoader.Tags.PROFILE.name().equals(tag)){
return new IconicsDrawable(ctx).icon(FontAwesome.Icon.faw_user) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
.paddingDp(10) return ContextCompat.getDrawable(BaseApplication.getInstance(), R.drawable.ic_default_user_avatar);
.color(ContextCompat.getColor(ctx, R.color.primary_light)) else { // Just for KitKats
.backgroundColor(ContextCompat.getColor(ctx, R.color.primary)); return new IconicsDrawable(ctx).icon(FontAwesome.Icon.faw_user)
.paddingDp(10)
.color(ContextCompat.getColor(ctx, R.color.iron))
.backgroundColor(ContextCompat.getColor(ctx, R.color.primary_lighter));
}
} }
return super.placeholder(ctx, tag); return super.placeholder(ctx, tag);
} }
}); });
}
private void setDisplayMetrics(){
DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics(); DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
widthPxl = displayMetrics.widthPixels; widthPxl = displayMetrics.widthPixels;
widthDp = widthPxl / displayMetrics.density; widthDp = widthPxl / displayMetrics.density;
heightPxl = displayMetrics.heightPixels; heightPxl = displayMetrics.heightPixels;
displayRelativeTime = settingsSharedPrefs.getBoolean(DISPLAY_RELATIVE_TIME, true);
} }
//Getters
//-------------------- Getters --------------------
public Context getContext() { public Context getContext() {
return getApplicationContext(); return getApplicationContext();
} }
@ -209,30 +233,38 @@ public class BaseApplication extends MultiDexApplication {
return displayRelativeTime; return displayRelativeTime;
} }
//--------------------Firebase-------------------- //-------------------- Firebase --------------------
public void logFirebaseAnalyticsEvent(String event, Bundle params) { public void logFirebaseAnalyticsEvent(String event, Bundle params) {
firebaseAnalytics.logEvent(event, params); firebaseAnalytics.logEvent(event, params);
} }
public void setFirebaseAnalyticsCollection(boolean enabled) { public void setFirebaseAnalyticsEnabled(boolean enabled) {
firebaseAnalytics.setAnalyticsCollectionEnabled(enabled); firebaseAnalytics.setAnalyticsCollectionEnabled(enabled);
if (!enabled) if (!enabled)
firebaseAnalytics.resetAnalyticsData(); firebaseAnalytics.resetAnalyticsData();
if(enabled)
Timber.i("Firebase Analytics enabled.");
else
Timber.i("Firebase Analytics disabled.");
} }
// Set up Crashlytics, disabled for debug builds public void setFirebaseCrashlyticsEnabled(boolean enable) {
public void startFirebaseCrashlyticsCollection() { FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enable);
if (!Fabric.isInitialized()) { if(enable){
Crashlytics crashlyticsKit = new Crashlytics.Builder() crashReportingTree = new CrashReportingTree();
.core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) Timber.plant(crashReportingTree);
.build(); Timber.i("CrashReporting tree planted.");
// Initialize Fabric with the debug-disabled Crashlytics. Timber.i("Firebase Crashlytics enabled.");
Fabric.with(this, crashlyticsKit); }
Timber.plant(new CrashReportingTree()); else{
Timber.i("Crashlytics enabled."); if(crashReportingTree!=null) {
} else Timber.uproot(crashReportingTree);
Timber.i("Crashlytics were already initialized for this app session."); Timber.i("CrashReporting tree uprooted.");
}
Timber.i("Firebase Crashlytics disabled.");
}
} }
public static String getFirebaseProjectId(){ public static String getFirebaseProjectId(){

13
app/src/main/java/gr/thmmy/mthmmy/session/InvalidSessionException.java

@ -0,0 +1,13 @@
package gr.thmmy.mthmmy.session;
public class InvalidSessionException extends RuntimeException {
public InvalidSessionException() {}
public InvalidSessionException(String message) {
super(message);
}
public InvalidSessionException(String message, Throwable cause) {
super(message, cause);
}
}

86
app/src/main/java/gr/thmmy/mthmmy/session/LogoutTask.java

@ -0,0 +1,86 @@
package gr.thmmy.mthmmy.session;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.utils.Parcel;
import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import okhttp3.Response;
import timber.log.Timber;
import static gr.thmmy.mthmmy.session.SessionManager.baseLogoutLink;
import static gr.thmmy.mthmmy.session.SessionManager.indexUrl;
public class LogoutTask extends NetworkTask<Void> {
private String logoutLink;
public LogoutTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<Void> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
}
@Override
protected Parcel<Void> doInBackground(String... input) {
/* Firstly we will find the logout link
Keep in mind, server changes sesc at will over time for a given session!
*/
Parcel<Void> parcel = executeInBackground(indexUrl.toString());
if(parcel.getResultCode() == NetworkResultCodes.SUCCESSFUL)
return executeInBackground(logoutLink); // Now we will attempt to logout
else return parcel;
}
@Override
protected Void performTask(Document document, Response response) {
try {
if(logoutLink==null)
logoutLink = extractLogoutLink(document);
else { // Just for logging purposes
Elements sessionVerificationFailed = document.select("td:containsOwn(Session " +
"verification failed. Please try logging out and back in again, and then try " +
"again.), td:containsOwn(Η επαλήθευση συνόδου απέτυχε. Παρακαλούμε κάντε " +
"αποσύνδεση, επανασύνδεση και ξαναδοκιμάστε.)");
if(!sessionVerificationFailed.isEmpty()){
Timber.i("Logout failed (invalid session)");
throw new InvalidSessionException();
}
Elements loginButton = document.select("[value=Login]"); //Attempt to find login button
if (!loginButton.isEmpty()) //If login button exists, logout was successful
Timber.i("Logout successful!");
else
Timber.i("Logout failed");
}
} catch (InvalidSessionException ise) {
throw ise;
} catch (Exception e) {
throw new ParseException("Parsing failed", e);
}
return null;
}
@Override
protected void onPostExecute(Parcel<Void> voidParcel) {
super.onPostExecute(voidParcel);
//All data should always be cleared from device regardless the result of logout
BaseApplication.getInstance().getSessionManager().logoutCleanup();
}
@Override
protected int getResultCode(Response response, Void v) {
return NetworkResultCodes.SUCCESSFUL;
}
private String extractLogoutLink(Document document){
Elements logoutLink = document.select("a[href^=" + baseLogoutLink + "]");
if (!logoutLink.isEmpty()) {
String link = logoutLink.first().attr("href");
if (link != null && !link.isEmpty())
return link;
}
throw new ParseException("Parsing failed (logoutLink extraction)");
}
}

65
app/src/main/java/gr/thmmy/mthmmy/session/MarkAsReadTask.java

@ -0,0 +1,65 @@
package gr.thmmy.mthmmy.session;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import gr.thmmy.mthmmy.utils.Parcel;
import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import gr.thmmy.mthmmy.utils.parsing.ParseException;
import okhttp3.Response;
import static gr.thmmy.mthmmy.session.SessionManager.baseMarkAllAsReadLink;
import static gr.thmmy.mthmmy.session.SessionManager.unreadUrl;
public class MarkAsReadTask extends NetworkTask<Void> {
private String markAsReadLink;
public MarkAsReadTask(OnTaskStartedListener onTaskStartedListener, OnNetworkTaskFinishedListener<Void> onParseTaskFinishedListener) {
super(onTaskStartedListener, onParseTaskFinishedListener);
}
@Override
protected Parcel<Void> doInBackground(String... input) {
Parcel<Void> parcel = executeInBackground(unreadUrl.toString());
if(parcel.getResultCode() == NetworkResultCodes.SUCCESSFUL)
return executeInBackground(markAsReadLink);
else return parcel;
}
@Override
protected Void performTask(Document document, Response response) {
try {
Elements sessionVerificationFailed = document.select("td:containsOwn(Session " +
"verification failed. Please try logging out and back in again, and then try " +
"again.), td:containsOwn(Η επαλήθευση συνόδου απέτυχε. Παρακαλούμε κάντε " +
"αποσύνδεση, επανασύνδεση και ξαναδοκιμάστε.)");
if(!sessionVerificationFailed.isEmpty())
throw new InvalidSessionException();
if(markAsReadLink==null)
markAsReadLink = extractMarkAsReadLink(document);
} catch (InvalidSessionException ise) {
throw ise;
} catch (Exception e) {
throw new ParseException("Parsing failed", e);
}
return null;
}
@Override
protected int getResultCode(Response response, Void v) {
return NetworkResultCodes.SUCCESSFUL;
}
private String extractMarkAsReadLink(Document document){
Elements markAllAsReadLink = document.select("a[href^=" + baseMarkAllAsReadLink + "]");
if (!markAllAsReadLink.isEmpty()) {
String link = markAllAsReadLink.first().attr("href");
if (link != null && !link.isEmpty())
return link;
}
throw new ParseException("Parsing failed (markAllAsReadLink extraction)");
}
}

254
app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java

@ -39,19 +39,20 @@ public class SessionManager {
private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2"); private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2");
public static final HttpUrl unreadUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=unread;all;start=0;theme=4"); public static final HttpUrl unreadUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=unread;all;start=0;theme=4");
public static final HttpUrl shoutboxUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=tpmod;sa=shoutbox;theme=4"); public static final HttpUrl shoutboxUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=tpmod;sa=shoutbox;theme=4");
private static final String baseLogoutLink = "https://www.thmmy.gr/smf/index.php?action=logout;sesc="; static final String baseLogoutLink = "https://www.thmmy.gr/smf/index.php?action=logout;sesc=";
private static final String baseMarkAllAsReadLink = "https://www.thmmy.gr/smf/index.php?action=markasread;sa=all;sesc="; static final String baseMarkAllAsReadLink = "https://www.thmmy.gr/smf/index.php?action=markasread;sa=all;sesc=";
private static final String guestName = "Guest"; private static final String guestName = "Guest";
//Response Codes //Response Codes - make sure they do not overlap with NetworkResultCodes, just in case
public static final int SUCCESS = 0; public static final int SUCCESS = 20;
public static final int FAILURE = 1; //Generic Error public static final int FAILURE = 21; //Generic Error
public static final int WRONG_USER = 2; public static final int WRONG_USER = 22;
public static final int WRONG_PASSWORD = 3; public static final int WRONG_PASSWORD = 23;
public static final int CANCELLED = 4; public static final int CANCELLED = 24;
public static final int CONNECTION_ERROR = 5; public static final int CONNECTION_ERROR = 25;
public static final int EXCEPTION = 6; public static final int EXCEPTION = 26;
public static final int BANNED_USER = 7; public static final int BANNED_USER = 27;
public static final int INVALID_SESSION = 28;
// Client & Cookies // Client & Cookies
private final OkHttpClient client; private final OkHttpClient client;
@ -59,59 +60,48 @@ public class SessionManager {
private final SharedPrefsCookiePersistor cookiePersistor; //Used to explicitly edit cookies in cookieJar private final SharedPrefsCookiePersistor cookiePersistor; //Used to explicitly edit cookies in cookieJar
//Shared Preferences & its keys //Shared Preferences & its keys
private final SharedPreferences sharedPrefs; private final SharedPreferences sessionSharedPrefs;
private final SharedPreferences draftsPrefs; private final SharedPreferences draftsPrefs;
private static final String USERNAME = "Username"; private static final String USERNAME = "Username";
private static final String USER_ID = "UserID"; private static final String USER_ID = "UserID";
private static final String AVATAR_LINK = "AvatarLink"; private static final String AVATAR_LINK = "AvatarLink";
private static final String HAS_AVATAR = "HasAvatar"; private static final String HAS_AVATAR = "HasAvatar";
private static final String SESC = "Sesc";
private static final String LOGOUT_LINK = "LogoutLink";
private static final String MARK_ALL_AS_READ_LINK = "MarkAllAsReadLink";
private static final String LOGGED_IN = "LoggedIn"; private static final String LOGGED_IN = "LoggedIn";
private static final String LOGIN_SCREEN_AS_DEFAULT = "LoginScreenAsDefault"; private static final String LOGIN_SCREEN_AS_DEFAULT = "LoginScreenAsDefault";
//Constructor //Constructor
public SessionManager(OkHttpClient client, PersistentCookieJar cookieJar, public SessionManager(OkHttpClient client, PersistentCookieJar cookieJar,
SharedPrefsCookiePersistor cookiePersistor, SharedPreferences sharedPrefs, SharedPreferences draftsPrefs) { SharedPrefsCookiePersistor cookiePersistor, SharedPreferences sessionSharedPrefs, SharedPreferences draftsPrefs) {
this.client = client; this.client = client;
this.cookiePersistor = cookiePersistor; this.cookiePersistor = cookiePersistor;
this.cookieJar = cookieJar; this.cookieJar = cookieJar;
this.sharedPrefs = sharedPrefs; this.sessionSharedPrefs = sessionSharedPrefs;
this.draftsPrefs = draftsPrefs; this.draftsPrefs = draftsPrefs;
} }
//------------------------------------AUTH BEGINS---------------------------------------------- //------------------------------------ AUTH ----------------------------------------------
/** /**
* Login function with two options: (username, password) or nothing (using saved cookies). * Login function with two options: (username, password) or nothing (using saved cookies).
* Always call it in a separate thread. * Always call it in a separate thread.
*/ */
public int login(String... strings) { public int login(String username, String password) {
Timber.d("Logging in..."); Timber.d("Logging in...");
//Build the login request for each case //Build the login request for each case
Request request; Request request;
if (strings.length == 2) { clearSessionData();
clearSessionData();
RequestBody formBody = new FormBody.Builder()
String loginName = strings[0]; .add("user", username)
String password = strings[1]; .add("passwrd", password)
.add("cookielength", "-1") //-1 is forever
RequestBody formBody = new FormBody.Builder() .build();
.add("user", loginName) request = new Request.Builder()
.add("passwrd", password) .url(loginUrl)
.add("cookielength", "-1") //-1 is forever .post(formBody)
.build(); .build();
request = new Request.Builder()
.url(loginUrl)
.post(formBody)
.build();
} else {
request = new Request.Builder()
.url(loginUrl)
.build();
}
try { try {
//Make request & handle response //Make request & handle response
@ -123,7 +113,7 @@ public class SessionManager {
setPersistentCookieSession(); //Store cookies setPersistentCookieSession(); //Store cookies
//Edit SharedPreferences, save session's data //Edit SharedPreferences, save session's data
SharedPreferences.Editor editor = sharedPrefs.edit(); SharedPreferences.Editor editor = sessionSharedPrefs.edit();
setLoginScreenAsDefault(false); setLoginScreenAsDefault(false);
editor.putBoolean(LOGGED_IN, true); editor.putBoolean(LOGGED_IN, true);
editor.putString(USERNAME, extractUserName(document)); editor.putString(USERNAME, extractUserName(document));
@ -132,10 +122,6 @@ public class SessionManager {
if (avatar != null) if (avatar != null)
editor.putString(AVATAR_LINK, avatar); editor.putString(AVATAR_LINK, avatar);
editor.putBoolean(HAS_AVATAR, avatar != null); editor.putBoolean(HAS_AVATAR, avatar != null);
String sesc = extractSesc(document);
editor.putString(SESC, sesc);
editor.putString(LOGOUT_LINK, generateLogoutLink(sesc));
editor.putString(MARK_ALL_AS_READ_LINK, generateMarkAllAsReadLink(sesc));
editor.apply(); editor.apply();
return SUCCESS; return SUCCESS;
@ -178,29 +164,6 @@ public class SessionManager {
} }
} }
/**
* A function that checks the validity of the current saved session (if it exists).
* If isLoggedIn() is true, it will call login() with cookies. On failure, this can only return
* the code FAILURE. CANCELLED, CONNECTION_ERROR and EXCEPTION are simply considered a SUCCESS
* (e.g. no internet connection), at least until a more thorough handling of different
* exceptions is implemented (if considered mandatory).
* Always call it in a separate thread in a way that won't hinder performance (e.g. after
* fragments' data are retrieved).
*/
void validateSession() {
Timber.i("Validating session...");
if (isLoggedIn()) {
Timber.i("Refreshing session...");
int loginResult = login();
if (loginResult != FAILURE)
return;
} else if (isLoginScreenDefault())
return;
setLoginScreenAsDefault(true);
clearSessionData();
}
/** /**
* Call this function when user explicitly chooses to continue as a guest (UI thread). * Call this function when user explicitly chooses to continue as a guest (UI thread).
*/ */
@ -210,62 +173,32 @@ public class SessionManager {
setLoginScreenAsDefault(false); setLoginScreenAsDefault(false);
} }
/** void logoutCleanup() {
* Logout function. Always call it in a separate thread. clearSessionData();
*/ guestLogin();
public int logout() {
Timber.i("Logging out...");
try {
Request request = new Request.Builder()
.url(getLogoutLink())
.build();
//Make request & handle response
Response response = client.newCall(request).execute();
Document document = Jsoup.parse(response.body().string());
Elements loginButton = document.select("[value=Login]"); //Attempt to find login button
if (!loginButton.isEmpty()) //If login button exists, logout was successful
{
Timber.i("Logout successful!");
return SUCCESS;
} else {
Timber.i("Logout failed.");
return FAILURE;
}
} catch (IOException e) {
Timber.w(e, "Logout IOException");
return CONNECTION_ERROR;
} catch (Exception e) {
Timber.e(e, "Logout Exception");
return EXCEPTION;
} finally {
//All data should always be cleared from device regardless the result of logout
clearSessionData();
guestLogin();
}
} }
public void refreshSescFromUrl(String url){ private void clearSessionData() {
String sesc = extractSescFromLink(url); cookieJar.clear();
if(sesc!=null){ sessionSharedPrefs.edit().clear().apply(); //Clear session data
setSesc(sesc); sessionSharedPrefs.edit().putString(USERNAME, guestName).apply();
setLogoutLink(generateLogoutLink(sesc)); sessionSharedPrefs.edit().putInt(USER_ID, -1).apply();
setMarkAsReadLink(sesc); sessionSharedPrefs.edit().putBoolean(LOGGED_IN, false).apply(); //User logs out
} draftsPrefs.edit().clear().apply(); //Clear saved drafts
Timber.i("Session data cleared.");
} }
//--------------------------------------AUTH ENDS-----------------------------------------------
//---------------------------------------GETTERS------------------------------------------------ //--------------------------------------- GETTERS ------------------------------------------------
public String getUsername() { public String getUsername() {
return sharedPrefs.getString(USERNAME, USERNAME); return sessionSharedPrefs.getString(USERNAME, USERNAME);
} }
public int getUserId() { public int getUserId() {
return sharedPrefs.getInt(USER_ID, -1); return sessionSharedPrefs.getInt(USER_ID, -1);
} }
public String getAvatarLink() { public String getAvatarLink() {
return sharedPrefs.getString(AVATAR_LINK, AVATAR_LINK); return sessionSharedPrefs.getString(AVATAR_LINK, AVATAR_LINK);
} }
public Cookie getThmmyCookie() { public Cookie getThmmyCookie() {
@ -277,64 +210,22 @@ public class SessionManager {
return null; return null;
} }
public String getMarkAllAsReadLink() {
String markAsReadLink = sharedPrefs.getString(MARK_ALL_AS_READ_LINK, null);
if(markAsReadLink == null){ //For older versions, extract it from logout link (otherwise user would have to login again)
String sesc = extractSescFromLink(getLogoutLink());
if(sesc!=null) {
setSesc(sesc);
markAsReadLink = generateMarkAllAsReadLink(sesc);
setMarkAsReadLink(markAsReadLink);
return markAsReadLink;
}
}
return markAsReadLink; // Warning: it can be null
}
private String getLogoutLink() {
return sharedPrefs.getString(LOGOUT_LINK, null);
}
public boolean hasAvatar() { public boolean hasAvatar() {
return sharedPrefs.getBoolean(HAS_AVATAR, false); return sessionSharedPrefs.getBoolean(HAS_AVATAR, false);
} }
public boolean isLoggedIn() { public boolean isLoggedIn() {
return sharedPrefs.getBoolean(LOGGED_IN, false); return sessionSharedPrefs.getBoolean(LOGGED_IN, false);
} }
public boolean isLoginScreenDefault() { public boolean isLoginScreenDefault() {
return sharedPrefs.getBoolean(LOGIN_SCREEN_AS_DEFAULT, true); return sessionSharedPrefs.getBoolean(LOGIN_SCREEN_AS_DEFAULT, true);
} }
//--------------------------------------GETTERS END--------------------------------------------- //------------------------------------ OTHER -------------------------------------------
//---------------------------------------SETTERS------------------------------------------------
private void setSesc(String sesc){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(SESC, sesc);
editor.apply();
}
private void setMarkAsReadLink(String markAllAsReadLink){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(MARK_ALL_AS_READ_LINK, markAllAsReadLink);
editor.apply();
}
private void setLogoutLink(String logoutLink){
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(LOGOUT_LINK, logoutLink);
editor.apply();
}
//--------------------------------------SETTERS END---------------------------------------------
//------------------------------------OTHER FUNCTIONS-------------------------------------------
private boolean validateRetrievedCookies() { private boolean validateRetrievedCookies() {
List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl); List<Cookie> cookieList = cookieJar.loadForRequest(indexUrl);
for(Cookie cookie: cookieList) for(Cookie cookie: cookieList) {
{
if(cookie.name().equals("THMMYgrC00ki3")) if(cookie.name().equals("THMMYgrC00ki3"))
return true; return true;
} }
@ -353,21 +244,10 @@ public class SessionManager {
cookieList.add(builder.build()); cookieList.add(builder.build());
cookiePersistor.clear(); cookiePersistor.clear();
cookiePersistor.saveAll(cookieList); cookiePersistor.saveAll(cookieList);
}
private void clearSessionData() {
cookieJar.clear();
sharedPrefs.edit().clear().apply(); //Clear session data
sharedPrefs.edit().putString(USERNAME, guestName).apply();
sharedPrefs.edit().putInt(USER_ID, -1).apply();
sharedPrefs.edit().putBoolean(LOGGED_IN, false).apply(); //User logs out
draftsPrefs.edit().clear().apply(); //Clear saved drafts
Timber.i("Session data cleared.");
} }
private void setLoginScreenAsDefault(boolean b){ private void setLoginScreenAsDefault(boolean b){
sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, b).apply(); sessionSharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, b).apply();
} }
@NonNull @NonNull
@ -421,7 +301,6 @@ public class SessionManager {
return -1; return -1;
} }
@Nullable @Nullable
private String extractAvatarLink(@NonNull Document doc) { private String extractAvatarLink(@NonNull Document doc) {
Elements avatar = doc.getElementsByClass("avatar"); Elements avatar = doc.getElementsByClass("avatar");
@ -431,35 +310,4 @@ public class SessionManager {
Timber.i("Extracting avatar's link failed!"); Timber.i("Extracting avatar's link failed!");
return null; return null;
} }
private String extractSesc(@NonNull Document doc) {
Elements logoutLink = doc.select("a[href^=https://www.thmmy.gr/smf/index.php?action=logout;sesc=]");
if (!logoutLink.isEmpty()) {
String link = logoutLink.first().attr("href");
return extractSescFromLink(link);
}
Timber.e(new ParseException("Parsing failed(extractSesc)"),"ParseException");
return null;
}
private String extractSescFromLink(String link){
if (link != null){
Pattern pattern = Pattern.compile(".+;sesc=(\\w+)");
Matcher matcher = pattern.matcher(link);
if (matcher.find())
return matcher.group(1);
}
Timber.e(new ParseException("Parsing failed(extractSescFromLogoutLink)"),"ParseException");
return null;
}
private String generateLogoutLink(String sesc){
return baseLogoutLink + sesc;
}
private String generateMarkAllAsReadLink(String sesc){
return baseMarkAllAsReadLink + sesc;
}
//----------------------------------OTHER FUNCTIONS END-----------------------------------------
} }

18
app/src/main/java/gr/thmmy/mthmmy/session/ValidateSessionTask.java

@ -1,18 +0,0 @@
package gr.thmmy.mthmmy.session;
import android.os.AsyncTask;
import gr.thmmy.mthmmy.base.BaseApplication;
public class ValidateSessionTask extends AsyncTask<String, Void, Void> {
@Override
protected Void doInBackground(String... params) {
BaseApplication.getInstance().getSessionManager().validateSession();
return null;
}
public boolean isRunning(){
return getStatus() == AsyncTask.Status.RUNNING;
}
}

1
app/src/main/java/gr/thmmy/mthmmy/utils/Parcel.java

@ -1,7 +1,6 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils;
public class Parcel<T> { public class Parcel<T> {
private int resultCode; private int resultCode;
private T data; private T data;

13
app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReporter.java

@ -1,6 +1,7 @@
package gr.thmmy.mthmmy.utils.crashreporting; package gr.thmmy.mthmmy.utils.crashreporting;
import com.crashlytics.android.Crashlytics;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
@ -43,12 +44,14 @@ public class CrashReporter {
else else
languageValue = "Unknown"; languageValue = "Unknown";
Crashlytics.setString(themeKey, themeValue); FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
Crashlytics.setString(languageKey, languageValue); crashlytics.setCustomKey(themeKey, themeValue);
Crashlytics.setBool("isLoggedIn", BaseApplication.getInstance().getSessionManager().isLoggedIn()); crashlytics.setCustomKey(languageKey, languageValue);
crashlytics.setCustomKey("isLoggedIn", BaseApplication.getInstance().getSessionManager().isLoggedIn());
} }
public static void reportDocument(Document document, String key) { public static void reportDocument(Document document, String key) {
FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
String documentString = document.toString(); String documentString = document.toString();
ParseHelpers.Language language = ParseHelpers.Language.getLanguage(document); ParseHelpers.Language language = ParseHelpers.Language.getLanguage(document);
@ -71,7 +74,7 @@ public class CrashReporter {
batch = documentString.substring(i * STRING_BATCH_LENGTH, (i + 1) * STRING_BATCH_LENGTH); batch = documentString.substring(i * STRING_BATCH_LENGTH, (i + 1) * STRING_BATCH_LENGTH);
else else
batch = documentString.substring(i * STRING_BATCH_LENGTH); batch = documentString.substring(i * STRING_BATCH_LENGTH);
Crashlytics.setString(key + "_" + i + 1, batch); crashlytics.setCustomKey(key + "_" + i + 1, batch);
} }
} }
} }

16
app/src/main/java/gr/thmmy/mthmmy/utils/crashreporting/CrashReportingTree.java

@ -2,12 +2,17 @@ package gr.thmmy.mthmmy.utils.crashreporting;
import android.util.Log; import android.util.Log;
import com.crashlytics.android.Crashlytics; import com.google.firebase.crashlytics.FirebaseCrashlytics;
import timber.log.Timber.DebugTree; import timber.log.Timber.DebugTree;
public class CrashReportingTree extends DebugTree { public class CrashReportingTree extends DebugTree {
private FirebaseCrashlytics firebaseCrashlytics;
public CrashReportingTree() {
super();
firebaseCrashlytics = FirebaseCrashlytics.getInstance();
}
@Override @Override
protected void log(int priority, String tag, String message, Throwable t) { protected void log(int priority, String tag, String message, Throwable t) {
if (priority == Log.VERBOSE || priority == Log.DEBUG) { if (priority == Log.VERBOSE || priority == Log.DEBUG) {
@ -25,14 +30,13 @@ public class CrashReportingTree extends DebugTree {
else else
level = 'A'; level = 'A';
Crashlytics.log(level + "/" + tag + ": " + message); firebaseCrashlytics.log(level + "/" + tag + ": " + message);
if(priority == Log.ERROR) { if(priority == Log.ERROR) {
if (t!=null) if (t!=null)
Crashlytics.logException(t); firebaseCrashlytics.recordException(t);
else else
Crashlytics.logException(new Exception(message)); firebaseCrashlytics.recordException(new Exception(message));
} }
} }
} }

2
app/src/main/java/gr/thmmy/mthmmy/utils/NetworkResultCodes.java → app/src/main/java/gr/thmmy/mthmmy/utils/networking/NetworkResultCodes.java

@ -1,4 +1,4 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.networking;
public class NetworkResultCodes { public class NetworkResultCodes {
/** /**

38
app/src/main/java/gr/thmmy/mthmmy/utils/NetworkTask.java → app/src/main/java/gr/thmmy/mthmmy/utils/networking/NetworkTask.java

@ -1,7 +1,8 @@
package gr.thmmy.mthmmy.utils; package gr.thmmy.mthmmy.utils.networking;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.preference.PreferenceManager;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
@ -10,6 +11,10 @@ import java.io.IOException;
import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.base.BaseApplication; import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.session.InvalidSessionException;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.ExternalAsyncTask;
import gr.thmmy.mthmmy.utils.Parcel;
import gr.thmmy.mthmmy.utils.crashreporting.CrashReporter; import gr.thmmy.mthmmy.utils.crashreporting.CrashReporter;
import gr.thmmy.mthmmy.utils.parsing.ParseException; import gr.thmmy.mthmmy.utils.parsing.ParseException;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -34,7 +39,19 @@ public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>
public NetworkTask() {} public NetworkTask() {}
@Override @Override
protected final Parcel<T> doInBackground(String... input) { protected Parcel<T> doInBackground(String... input) {
return executeInBackground(input);
}
@Override
protected void onPostExecute(Parcel<T> tParcel) {
if (onNetworkTaskFinishedListener != null)
onNetworkTaskFinishedListener.onNetworkTaskFinished(tParcel.getResultCode(), tParcel.getData());
else
super.onPostExecute(tParcel);
}
protected Parcel<T> executeInBackground(String... input) {
Response response; Response response;
try { try {
response = sendRequest(BaseApplication.getInstance().getClient(), input); response = sendRequest(BaseApplication.getInstance().getClient(), input);
@ -63,20 +80,17 @@ public abstract class NetworkTask<T> extends ExternalAsyncTask<String, Parcel<T>
.getString(R.string.pref_privacy_crashlytics_enable_key), false)) .getString(R.string.pref_privacy_crashlytics_enable_key), false))
CrashReporter.reportForumInfo(Jsoup.parse(responseBodyString)); CrashReporter.reportForumInfo(Jsoup.parse(responseBodyString));
return new Parcel<>(NetworkResultCodes.PARSE_ERROR, null); return new Parcel<>(NetworkResultCodes.PARSE_ERROR, null);
} catch (Exception e) { } catch (InvalidSessionException ise) {
//TODO: Uncomment the lines below when UI is ready to auto-adjust to changes in session data
// BaseApplication.getInstance().getSessionManager().clearSessionData();
// BaseApplication.getInstance().getSessionManager().guestLogin();
return new Parcel<>(SessionManager.INVALID_SESSION, null);
}catch (Exception e) {
Timber.e(e); Timber.e(e);
return new Parcel<>(NetworkResultCodes.PERFORM_TASK_ERROR, null); return new Parcel<>(NetworkResultCodes.PERFORM_TASK_ERROR, null);
} }
} }
@Override
protected void onPostExecute(Parcel<T> tParcel) {
if (onNetworkTaskFinishedListener != null)
onNetworkTaskFinishedListener.onNetworkTaskFinished(tParcel.getResultCode(), tParcel.getData());
else
super.onPostExecute(tParcel);
}
protected Response sendRequest(OkHttpClient client, String... input) throws IOException { protected Response sendRequest(OkHttpClient client, String... input) throws IOException {
String url = input[0]; String url = input[0];
Request request = new Request.Builder() Request request = new Request.Builder()

7
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/NewParseTask.java

@ -2,7 +2,8 @@ package gr.thmmy.mthmmy.utils.parsing;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import gr.thmmy.mthmmy.utils.NetworkTask; import gr.thmmy.mthmmy.session.InvalidSessionException;
import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import okhttp3.Response; import okhttp3.Response;
public abstract class NewParseTask<T> extends NetworkTask<T> { public abstract class NewParseTask<T> extends NetworkTask<T> {
@ -22,8 +23,10 @@ public abstract class NewParseTask<T> extends NetworkTask<T> {
protected final T performTask(Document document, Response response) { protected final T performTask(Document document, Response response) {
try { try {
return parse(document, response); return parse(document, response);
} catch (InvalidSessionException ise) {
throw ise;
} catch (Exception e) { } catch (Exception e) {
throw new ParseException("Parse failed.", e); throw new ParseException("Parsing failed", e);
} }
} }

17
app/src/main/java/gr/thmmy/mthmmy/utils/ui/GlideUtils.java

@ -0,0 +1,17 @@
package gr.thmmy.mthmmy.utils.ui;
import android.app.Activity;
import android.content.Context;
public class GlideUtils {
public static boolean isValidContextForGlide(final Context context) {
if (context == null)
return false;
if (context instanceof Activity) {
final Activity activity = (Activity) context;
return !activity.isDestroyed() && !activity.isFinishing();
}
return true;
}
}

2
app/src/main/java/gr/thmmy/mthmmy/viewmodel/TopicViewModel.java

@ -30,7 +30,7 @@ import gr.thmmy.mthmmy.model.Post;
import gr.thmmy.mthmmy.model.TopicItem; import gr.thmmy.mthmmy.model.TopicItem;
import gr.thmmy.mthmmy.session.SessionManager; import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.ExternalAsyncTask; import gr.thmmy.mthmmy.utils.ExternalAsyncTask;
import gr.thmmy.mthmmy.utils.NetworkTask; import gr.thmmy.mthmmy.utils.networking.NetworkTask;
import gr.thmmy.mthmmy.utils.parsing.ParseHelpers; import gr.thmmy.mthmmy.utils.parsing.ParseHelpers;
import timber.log.Timber; import timber.log.Timber;

2
app/src/main/res/drawable/ic_default_user_avatar.xml

@ -1,5 +1,5 @@
<vector android:height="24dp" android:viewportHeight="512" <vector android:height="24dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:pathData="M256,60c-108.27,0 -196,87.73 -196,196c0,108.27 87.73,196 196,196c108.27,0 196,-87.73 196,-196c0,-108.27 -87.73,-196 -196,-196z"/> <path android:fillColor="#ffffff" android:pathData="M256,60c-108.27,0 -196,87.73 -196,196c0,108.27 87.73,196 196,196c108.27,0 196,-87.73 196,-196c0,-108.27 -87.73,-196 -196,-196z"/>
<path android:fillColor="#3c3c3c" android:pathData="M504,256c0,137 -111,248 -248,248c-137,0 -248,-111 -248,-248c0,-137 111,-248 248,-248c137,0 248,111 248,248zM168,192c0,48.6 39.4,88 88,88c48.6,0 88,-39.4 88,-88c0,-48.6 -39.4,-88 -88,-88c-48.6,0 -88,39.4 -88,88zM402.5,379.8c-18.8,-35.4 -55.6,-59.8 -98.5,-59.8c-2.4,0 -4.8,0.4 -7.1,1.1c-12.9,4.2 -26.6,6.9 -40.9,6.9c-14.3,0 -27.9,-2.7 -40.9,-6.9c-2.3,-0.7 -4.7,-1.1 -7.1,-1.1c-42.9,0 -79.7,24.4 -98.5,59.8c35.2,41.6 87.8,68.2 146.5,68.2c58.7,0 111.3,-26.6 146.5,-68.2z"/> <path android:fillColor="#494949" android:pathData="M504,256c0,137 -111,248 -248,248c-137,0 -248,-111 -248,-248c0,-137 111,-248 248,-248c137,0 248,111 248,248zM168,192c0,48.6 39.4,88 88,88c48.6,0 88,-39.4 88,-88c0,-48.6 -39.4,-88 -88,-88c-48.6,0 -88,39.4 -88,88zM402.5,379.8c-18.8,-35.4 -55.6,-59.8 -98.5,-59.8c-2.4,0 -4.8,0.4 -7.1,1.1c-12.9,4.2 -26.6,6.9 -40.9,6.9c-14.3,0 -27.9,-2.7 -40.9,-6.9c-2.3,-0.7 -4.7,-1.1 -7.1,-1.1c-42.9,0 -79.7,24.4 -98.5,59.8c35.2,41.6 87.8,68.2 146.5,68.2c58.7,0 111.3,-26.6 146.5,-68.2z"/>
</vector> </vector>

2
app/src/main/res/layout/activity_about.xml

@ -28,7 +28,7 @@
android:id="@+id/scrollview" android:id="@+id/scrollview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/primary_light" android:background="@color/primary_lighter"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

9
app/src/main/res/layout/activity_board_sub_board_row.xml

@ -62,7 +62,8 @@
android:text="@string/child_board_mods" android:text="@string/child_board_mods"
android:textColor="@color/secondary_text" android:textColor="@color/secondary_text"
android:textSize="12sp" android:textSize="12sp"
android:textStyle="italic" /> android:textStyle="italic"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/child_board_stats" android:id="@+id/child_board_stats"
@ -72,7 +73,8 @@
android:layout_marginBottom="1dp" android:layout_marginBottom="1dp"
android:text="@string/child_board_stats" android:text="@string/child_board_stats"
android:textColor="@color/secondary_text" android:textColor="@color/secondary_text"
android:textSize="12sp" /> android:textSize="12sp"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/child_board_last_post" android:id="@+id/child_board_last_post"
@ -84,7 +86,8 @@
android:focusable="true" android:focusable="true"
android:text="@string/child_board_last_post" android:text="@string/child_board_last_post"
android:textColor="@color/primary_text" android:textColor="@color/primary_text"
android:textSize="12sp" /> android:textSize="12sp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

2
app/src/main/res/layout/activity_settings.xml

@ -28,7 +28,7 @@
android:id="@+id/pref_container" android:id="@+id/pref_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/primary_light" android:background="@color/primary_lighter"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

6
app/src/main/res/layout/activity_upload.xml

@ -28,7 +28,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="top|start" android:layout_gravity="top|start"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:paddingStart="@dimen/activity_horizontal_margin" android:paddingStart="@dimen/activity_horizontal_margin"
android:paddingEnd="@dimen/activity_horizontal_margin" android:paddingEnd="@dimen/activity_horizontal_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
@ -37,7 +37,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:orientation="vertical"> android:orientation="vertical">
@ -46,7 +46,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:orientation="vertical"> android:orientation="vertical">
<gr.thmmy.mthmmy.views.AppCompatSpinnerWithoutDefault <gr.thmmy.mthmmy.views.AppCompatSpinnerWithoutDefault

2
app/src/main/res/layout/activity_upload_fields_builder.xml

@ -27,7 +27,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="top|start" android:layout_gravity="top|start"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:paddingEnd="@dimen/activity_horizontal_margin" android:paddingEnd="@dimen/activity_horizontal_margin"
android:paddingStart="@dimen/activity_horizontal_margin" android:paddingStart="@dimen/activity_horizontal_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"

2
app/src/main/res/layout/activity_upload_filename_info_popup.xml

@ -3,7 +3,7 @@
android:id="@+id/upload_filename_info_text" android:id="@+id/upload_filename_info_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:linksClickable="true" android:linksClickable="true"
android:padding="8dp" android:padding="8dp"
android:text="@string/upload_filename_info" android:text="@string/upload_filename_info"

2
app/src/main/res/layout/download_prompt_dialog.xml

@ -3,7 +3,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:padding="4dp"> android:padding="4dp">

2
app/src/main/res/layout/fragment_bookmarks.xml

@ -9,7 +9,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="top|start" android:layout_gravity="top|start"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:scrollbars="none" android:scrollbars="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">

2
app/src/main/res/layout/fragment_forum_board_row.xml

@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/primary_lighter" android:background="@color/primary_lighter_2"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView

2
app/src/main/res/layout/fragment_forum_category_row.xml

@ -3,7 +3,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@color/primary_light"> android:background="@color/primary_lighter">
<TextView <TextView
android:id="@+id/category" android:id="@+id/category"

2
app/src/main/res/layout/fragment_recent_row.xml

@ -8,7 +8,7 @@
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:paddingLeft="10dp" android:paddingLeft="10dp"

1
app/src/main/res/layout/fragment_unread.xml

@ -61,6 +61,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margins" android:layout_marginEnd="@dimen/fab_margins"
android:layout_marginBottom="@dimen/fab_margins" android:layout_marginBottom="@dimen/fab_margins"
android:visibility="gone"
app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior" app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_mark_as_read" /> app:srcCompat="@drawable/ic_mark_as_read" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

2
app/src/main/res/layout/fragment_unread_row.xml

@ -8,7 +8,7 @@
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/primary_light" android:background="@color/primary_lighter"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:paddingLeft="10dp" android:paddingLeft="10dp"

7
app/src/main/res/values/colors.xml

@ -7,9 +7,10 @@
<!--Dark--> <!--Dark-->
<color name="primary">#2B2B2B</color> <color name="primary">#2B2B2B</color>
<color name="primary_dark">#333333</color> <color name="primary_light">#333333</color>
<color name="primary_light">#3C3C3C</color> <color name="primary_lighter">#3C3C3C</color>
<color name="primary_lighter">#494949</color> <color name="primary_lighter_2">#494949</color>
<color name="primary_dark">#222222</color>
<color name="accent">#26A69A</color> <color name="accent">#26A69A</color>
<color name="primary_text">#E7E7E7</color> <color name="primary_text">#E7E7E7</color>
<color name="secondary_text">#757575</color> <color name="secondary_text">#757575</color>

8
app/src/main/res/values/strings.xml

@ -209,7 +209,7 @@
<string name="pref_title_privacy_analytics_enable">Analytics data reports</string> <string name="pref_title_privacy_analytics_enable">Analytics data reports</string>
<string name="pref_summary_privacy_analytics_enable">Automatically send us anonymized data for analytical purposes</string> <string name="pref_summary_privacy_analytics_enable">Automatically send us anonymized data for analytical purposes</string>
<!-- EditorView --> <!--EditorView -->
<string name="black">Black</string> <string name="black">Black</string>
<string name="red">Red</string> <string name="red">Red</string>
<string name="yellow">Yellow</string> <string name="yellow">Yellow</string>
@ -229,8 +229,12 @@
<string name="dialog_link_text_hint">Link text</string> <string name="dialog_link_text_hint">Link text</string>
<string name="input_field_required">Required</string> <string name="input_field_required">Required</string>
<!-- New topic activity --> <!--New topic activity-->
<string name="new_topic_toolbar">New topic</string> <string name="new_topic_toolbar">New topic</string>
<string name="create_topic">Create topic</string> <string name="create_topic">Create topic</string>
<string name="link_copied_msg">Link copied</string> <string name="link_copied_msg">Link copied</string>
<!--SessionManager-->
<string name="session_shared_prefs">SessionSharedPrefs</string>
</resources> </resources>

6
app/src/main/res/values/styles.xml

@ -2,7 +2,7 @@
<!-- Base Theme to also be inherited in v21 --> <!-- Base Theme to also be inherited in v21 -->
<style name="BaseAppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="BaseAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item> <item name="colorPrimaryDark">@color/primary_light</item>
<item name="colorAccent">@color/accent</item> <item name="colorAccent">@color/accent</item>
<item name="android:textColorPrimary">@color/primary_text</item> <item name="android:textColorPrimary">@color/primary_text</item>
<item name="android:textColorSecondary">@color/secondary_text</item> <item name="android:textColorSecondary">@color/secondary_text</item>
@ -36,7 +36,7 @@
</style> </style>
<style name="PopupMenuStyle"> <style name="PopupMenuStyle">
<item name="android:popupBackground">@color/primary_light</item> <item name="android:popupBackground">@color/primary_lighter</item>
<item name="android:textAppearanceLargePopupMenu">@color/accent</item> <item name="android:textAppearanceLargePopupMenu">@color/accent</item>
<item name="android:textAppearanceSmallPopupMenu">@color/accent</item> <item name="android:textAppearanceSmallPopupMenu">@color/accent</item>
</style> </style>
@ -86,6 +86,6 @@
</style> </style>
<style name="LightBackgroundColoredButton" parent="Widget.AppCompat.ImageButton"> <style name="LightBackgroundColoredButton" parent="Widget.AppCompat.ImageButton">
<item name="colorButtonNormal">@color/primary_light</item> <item name="colorButtonNormal">@color/primary_lighter</item>
</style> </style>
</resources> </resources>

7
app/src/main/res/xml/network_security_config.xml

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- This is needed because some sections of thmmy.gr are responding with cleartext traffic -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">www.thmmy.gr</domain>
</domain-config>
</network-security-config>

6
build.gradle

@ -3,15 +3,14 @@ apply plugin: "com.github.ben-manes.versions"
buildscript { buildscript {
repositories { repositories {
maven { url "https://maven.fabric.io/public" }
google() google()
jcenter() jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.google.gms:google-services:4.3.3' classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.31.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1'
classpath 'org.ajoberstar.grgit:grgit-core:3.1.1' // Also change in app/gradle/grgit.gradle classpath 'org.ajoberstar.grgit:grgit-core:3.1.1' // Also change in app/gradle/grgit.gradle
classpath "com.github.ben-manes:gradle-versions-plugin:0.21.0" classpath "com.github.ben-manes:gradle-versions-plugin:0.21.0"
} }
@ -20,6 +19,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
maven { url "https://maven.google.com" } maven { url "https://maven.google.com" }
google()
jcenter() jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }

9
emojis/build.gradle

@ -4,7 +4,6 @@ android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion "29.0.3" buildToolsVersion "29.0.3"
defaultConfig { defaultConfig {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
@ -20,10 +19,4 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
}

4
gradle/wrapper/gradle-wrapper.properties

@ -1,6 +1,6 @@
#Sat May 09 21:06:12 EEST 2020 #Mon Jul 20 13:59:13 EEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

Loading…
Cancel
Save