Browse Source

Version 2.0.0

master v2.0.0
Ezerous 3 years ago
parent
commit
91094de6f8
  1. 4
      README.md
  2. 57
      app/build.gradle
  3. 10
      app/src/main/assets/apache_libraries.html
  4. 2
      app/src/main/assets/mit_libraries.html
  5. 2
      app/src/main/assets/other_libraries.html
  6. 10
      app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java
  7. 11
      app/src/main/java/gr/thmmy/mthmmy/activities/bookmarks/BookmarksFragment.java
  8. 20
      app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java
  9. 9
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java
  10. 5
      app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java
  11. 4
      app/src/main/java/gr/thmmy/mthmmy/activities/shoutbox/ShoutAdapter.java
  12. 9
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java
  13. 10
      app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java
  14. 57
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadActivity.java
  15. 85
      app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadsCourse.java
  16. 112
      app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java
  17. 72
      app/src/main/java/gr/thmmy/mthmmy/base/BaseApplication.java
  18. 4
      app/src/main/java/gr/thmmy/mthmmy/services/NotificationService.java
  19. 85
      app/src/main/java/gr/thmmy/mthmmy/services/UploadsReceiver.java
  20. 46
      app/src/main/java/gr/thmmy/mthmmy/utils/io/ResourceUtils.java
  21. 24
      app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java
  22. 124
      app/src/main/res/layout-v21/activity_profile.xml
  23. 262
      app/src/main/res/layout-v21/activity_topic_post_row.xml
  24. 1
      app/src/main/res/layout/activity_profile.xml
  25. 4
      app/src/main/res/layout/activity_topic_post_row.xml
  26. 1702
      app/src/main/res/raw/uploads_courses.json
  27. 11
      app/src/main/res/values-v21/styles.xml
  28. 10
      app/src/main/res/values/styles.xml
  29. 158
      app/src/main/res/values/uploads_courses.xml
  30. 42
      app/src/test/java/gr/thmmy/mthmmy/utils/UploadsCoursesJSONReadingTest.java
  31. 5
      app/src/test/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParserTest.java
  32. 6
      build.gradle

4
README.md

@ -1,7 +1,7 @@
# mTHMMY
[![GitHub release](https://img.shields.io/github/release/ThmmyNoLife/mTHMMY.svg?color=orange)](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-21%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=21)
[![Discord Channel](https://img.shields.io/discord/252539000571559947?style=flat&color=738bd7&label=discord)][discord-server]
![Last Commit](https://img.shields.io/github/last-commit/ThmmyNoLife/mTHMMY/develop.svg?style=flat)
@ -11,7 +11,7 @@ A mobile app for [thmmy.gr](https://www.thmmy.gr).
## Requirements
mTHMMY can be installed on any smartphone with Android 4.4 KitKat or newer.
mTHMMY can be installed on any smartphone with Android 5.0 Lollipop or newer.
## Download

57
app/build.gradle

@ -7,16 +7,15 @@ apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
android {
compileSdkVersion 29
buildToolsVersion = '29.0.3'
compileSdkVersion 30
buildToolsVersion = '30.0.2'
defaultConfig {
vectorDrawables.useSupportLibrary = true //TODO: Remove when minSdkVersion >= 21
applicationId "gr.thmmy.mthmmy"
minSdkVersion 19
targetSdkVersion 29
versionCode 29
versionName "1.9.0"
minSdkVersion 21
targetSdkVersion 30
versionCode 30
versionName "2.0.0"
archivesBaseName = "mTHMMY-v$versionName"
buildConfigField "String", "CURRENT_BRANCH", "\"" + getCurrentBranch() + "\""
buildConfigField "String", "COMMIT_HASH", "\"" + getCommitHash() + "\""
@ -25,14 +24,11 @@ android {
buildTypes {
release {
multiDexEnabled true //TODO: Remove when minSdkVersion >= 21
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
multiDexKeepProguard file('proguard-rules.pro') //TODO: Remove when minSdkVersion >= 21
}
debug {
multiDexEnabled true //TODO: Remove when minSdkVersion >= 21
def date = new Date().format('ddMMyy_HHmmss')
archivesBaseName = archivesBaseName + "-$date"
firebaseCrashlytics {
@ -41,6 +37,10 @@ android {
}
}
sourceSets {
test.resources.srcDirs += 'src/main/res/raw'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -76,27 +76,26 @@ tasks.whenTaskAdded { task ->
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":emojis")
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
implementation 'androidx.appcompat:appcompat:1.4.0-alpha03'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.exifinterface:exifinterface:1.2.0'
implementation 'androidx.multidex:multidex:2.0.1' //TODO: Remove when minSdkVersion >= 21
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.firebase:firebase-analytics:17.4.4'
implementation 'com.google.firebase:firebase-crashlytics:17.3.0'
implementation 'com.google.firebase:firebase-messaging:21.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'com.google.android.material:material:1.4.0'
implementation platform('com.google.firebase:firebase-bom:28.4.0')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-config'
implementation 'com.google.firebase:firebase-crashlytics'
implementation 'com.google.firebase:firebase-messaging'
implementation 'com.google.code.gson:gson:2.8.8'
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!
force = true //TODO: Remove when minSdkVersion >= 21
}
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'joda-time:joda-time:2.10.4'
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'org.jsoup:jsoup:1.14.2'
implementation 'joda-time:joda-time:2.10.10'
implementation 'com.github.franmontiel:PersistentCookieJar:1.0.1'
implementation 'com.github.PhilJay:MPAndroidChart:3.0.3'
implementation 'com.mikepenz:materialdrawer:6.1.1'
@ -108,15 +107,15 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'ru.noties:markwon:2.0.2'
implementation 'net.gotev:uploadservice:3.5.2'
implementation 'net.gotev:uploadservice-okhttp:3.4.2'
//TODO: Warning: v.3.5 depends on okhttp 3.13!
implementation 'com.itkacher.okhttpprofiler:okhttpprofiler:1.0.7'
implementation 'net.gotev:uploadservice-okhttp:3.5.2'
implementation 'com.localebro:okhttpprofiler:1.0.8'
//Plugin: https://plugins.jetbrains.com/plugin/11249-okhttp-profiler
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.powermock:powermock-core:2.0.2'
testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
testImplementation 'org.json:json:20210307'
}

10
app/src/main/assets/apache_libraries.html

@ -6,7 +6,7 @@
<body>
<ul>
<li>
<h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.12.12 (Copyright ©2019
<h5><a href="https://square.github.io/okhttp/">OkHttp</a>&nbsp;v3.14.9 (Copyright ©2019
Square, Inc.)</h5>
</li>
<li>
@ -40,7 +40,7 @@
</li>
<li>
<h5><a href="https://github.com/gotev/android-upload-service">Android Upload Service</a>&nbsp;v3.5.2
(Copyright ©2013-2019 Aleksandar Gotev)</h5>
(Copyright ©2013-2021 Aleksandar Gotev)</h5>
</li>
<li>
<h5><a href="https://github.com/noties/Markwon">Markwon</a>&nbsp;v2.0.2 (Copyright ©2017
@ -51,8 +51,8 @@
Andrew Oberstar)</h5>
</li>
<li>
<h5><a href="https://github.com/JodaOrg/joda-time">Joda-Time</a>&nbsp;v2.10.4 (Copyright
©2002-2019 Joda.org)</h5>
<h5><a href="https://github.com/JodaOrg/joda-time">Joda-Time</a>&nbsp;v2.10.10 (Copyright
©2002-2021 Joda.org)</h5>
</li>
<li>
<h5><a href="https://github.com/powermock/powermock">PowerMock</a>&nbsp;v2.0.2</h5>
@ -61,7 +61,7 @@
<h5><a href="https://github.com/sromku/android-storage">android-storage</a>&nbsp;v2.1.0</h5>
</li>
<li>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.7</h5>
<h5><a href=https://github.com/itkacher/OkHttpProfiler">OkHttpProfiler</a>&nbsp;v1.0.8</h5>
</li>
</ul>

2
app/src/main/assets/mit_libraries.html

@ -6,7 +6,7 @@
<body>
<ul>
<li>
<h5><a href="https://jsoup.org">jsoup</a>&nbsp;v1.13.1 (Copyright ©2009-2020, Jonathan
<h5><a href="https://jsoup.org">jsoup</a>&nbsp;v1.14.2 (Copyright ©2009-2021, Jonathan
Hedley &lt;jonathan@hedley.net&gt;)</h5>
</li>
<li>

2
app/src/main/assets/other_libraries.html

@ -6,7 +6,7 @@
<body>
<ul>
<li>
<h5><a href="https://github.com/bumptech/glide">Glide</a>&nbsp;v4.11.0</h5>
<h5><a href="https://github.com/bumptech/glide">Glide</a>&nbsp;v4.12.0</h5>
</li>
</ul>

10
app/src/main/java/gr/thmmy/mthmmy/activities/AboutActivity.java

@ -133,7 +133,7 @@ public class AboutActivity extends BaseActivity {
@Override
public void onBackPressed() {
if (easterEggImage.getVisibility() == View.INVISIBLE)
if (easterEggImage.getVisibility() == View.GONE)
super.onBackPressed();
else
hideEasterEgg();
@ -184,9 +184,9 @@ public class AboutActivity extends BaseActivity {
@SuppressLint("SourceLockedOrientationActivity")
private void showEasterEgg() {
if (getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //TODO: why?
appBar.setVisibility(View.INVISIBLE);
mainContent.setVisibility(View.INVISIBLE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
appBar.setVisibility(View.GONE);
mainContent.setVisibility(View.GONE);
easterEggImage.setVisibility(View.VISIBLE);
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
@ -195,7 +195,7 @@ public class AboutActivity extends BaseActivity {
private void hideEasterEgg() {
appBar.setVisibility(View.VISIBLE);
mainContent.setVisibility(View.VISIBLE);
easterEggImage.setVisibility(View.INVISIBLE);
easterEggImage.setVisibility(View.GONE);
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}

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

@ -2,7 +2,6 @@ package gr.thmmy.mthmmy.activities.bookmarks;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -13,7 +12,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import java.util.ArrayList;
@ -85,15 +83,10 @@ public class BookmarksFragment extends Fragment {
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
notificationsEnabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_on, null);
else
notificationsEnabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_on, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
notificationsEnabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_on, null);
notificationsDisabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_off, null);
else
notificationsDisabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_off, null);
}
@Override

20
app/src/main/java/gr/thmmy/mthmmy/activities/main/MainActivity.java

@ -3,13 +3,11 @@ package gr.thmmy.mthmmy.activities.main;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
@ -59,13 +57,6 @@ public class MainActivity extends BaseActivity implements RecentFragment.RecentF
private TabLayout tabLayout;
private boolean displayCompactTabs;
//Fix for vector drawables on android <21
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -263,6 +254,7 @@ public class MainActivity extends BaseActivity implements RecentFragment.RecentF
private void redirectToActivityFromIntent(Intent intent) {
if (intent != null) {
Uri uri = intent.getData();
Bundle extras = intent.getExtras();
if (uri != null) {
ThmmyPage.PageCategory page = ThmmyPage.resolvePageCategory(uri);
if (!page.is(ThmmyPage.PageCategory.NOT_THMMY)) {
@ -299,6 +291,16 @@ public class MainActivity extends BaseActivity implements RecentFragment.RecentF
Toast.makeText(BaseApplication.getInstance().getApplicationContext(), "This is not thmmy.", Toast.LENGTH_LONG).show();
}
}
else if(extras!=null){
String topicTitle = extras.getString(BUNDLE_TOPIC_TITLE);
String topicPageUrl = extras.getString(BUNDLE_TOPIC_URL);
if(topicTitle!=null && topicPageUrl!=null){
Intent redirectIntent = new Intent(MainActivity.this, TopicActivity.class);
redirectIntent.putExtra(BUNDLE_TOPIC_URL, topicPageUrl);
redirectIntent.putExtra(BUNDLE_TOPIC_TITLE, topicTitle);
startActivity(redirectIntent);
}
}
}
}
}

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

@ -5,7 +5,6 @@ import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
@ -19,7 +18,6 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
@ -98,13 +96,6 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment
private String username;
private int tabSelect;
//Fix for vector drawables on android <21
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

5
app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java

@ -260,13 +260,8 @@ public class StatsFragment extends Fragment {
LineDataSet postingActivityByTimeDataSet = new LineDataSet(postingActivityByTime, null);
if (isAdded()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
postingActivityByTimeDataSet.setFillDrawable(getResources().getDrawable(R.drawable.line_chart_gradient, null));
}
else
//noinspection deprecation
postingActivityByTimeDataSet.setFillDrawable(getResources().getDrawable(R.drawable.line_chart_gradient));
}
postingActivityByTimeDataSet.setDrawFilled(true);
postingActivityByTimeDataSet.setDrawCircles(false);
postingActivityByTimeDataSet.setDrawValues(false);

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

@ -1,11 +1,9 @@
package gr.thmmy.mthmmy.activities.shoutbox;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -99,8 +97,6 @@ public class ShoutAdapter extends CustomRecyclerView.Adapter<ShoutAdapter.ShoutV
}
class LinkLauncher extends WebViewClient {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
final Uri uri = request.getUrl();

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

@ -9,7 +9,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
@ -31,7 +30,6 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
@ -128,13 +126,6 @@ public class TopicActivity extends BaseActivity implements TopicAdapter.OnPostFo
private EmojiKeyboard emojiKeyboard;
private AlertDialog topicInfoDialog;
//Fix for vector drawables on android <21
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

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

@ -509,24 +509,14 @@ class TopicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
holder.stars.setVisibility(View.GONE);
if (currentPost.isUserMentionedInPost()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
holder.cardChildLinear.setBackground(context.getResources().
getDrawable(R.drawable.mention_card, null));
}
else
holder.cardChildLinear.setBackground(context.getResources().
getDrawable(R.drawable.mention_card));
}
else if (mUserColor == TopicParser.USER_COLOR_PINK) {
//Special card for special member of the month!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
holder.cardChildLinear.setBackground(context.getResources().
getDrawable(R.drawable.member_of_the_month_card, null));
}
else
holder.cardChildLinear.setBackground(context.getResources().
getDrawable(R.drawable.member_of_the_month_card));
}
else holder.cardChildLinear.setBackground(null);
//Avoid's view's visibility recycling

57
app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadActivity.java

@ -6,12 +6,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
@ -39,10 +37,13 @@ import androidx.core.content.FileProvider;
import androidx.preference.PreferenceManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import net.gotev.uploadservice.UploadNotificationAction;
import net.gotev.uploadservice.UploadNotificationConfig;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
@ -87,6 +88,8 @@ public class UploadActivity extends BaseActivity {
* The key to use when putting upload's category String to {@link UploadActivity}'s Bundle.
*/
public static final String BUNDLE_UPLOAD_CATEGORY = "UPLOAD_CATEGORY";
public static final String firebaseConfigUploadsCoursesKey = "uploads_courses";
private static final String uploadIndexUrl = "https://www.thmmy.gr/smf/index.php?action=tpmod;dl=upload";
private static final String uploadedFromTHMMYPromptHtml = "<br /><div style=\"text-align: right;\"><span style=\"font-style: italic;\">uploaded from <a href=\"https://play.google.com/store/apps/details?id=gr.thmmy.mthmmy\">mTHMMY</a></span>";
/**
@ -104,7 +107,7 @@ public class UploadActivity extends BaseActivity {
private static final int MAX_FILE_SIZE_SUPPORTED = 45000000;
private HashMap<String, UploadsCourse> uploadsCourses;
private HashMap<Integer, UploadsCourse> uploadsCourses;
private ArrayList<UploadCategory> uploadRootCategories = new ArrayList<>();
private ParseUploadPageTask parseUploadPageTask;
@ -417,10 +420,16 @@ public class UploadActivity extends BaseActivity {
updateUIElements();
generateFieldsButton.setEnabled(true);
}
Resources res = getResources();
uploadsCourses = new HashMap<>(UploadsCourse
.generateUploadsCourses(res.getStringArray(R.array.string_array_uploads_courses)));
FirebaseRemoteConfig firebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
String uploadsCoursesString = firebaseRemoteConfig.getValue(firebaseConfigUploadsCoursesKey).asString();
JSONObject uploadsCoursesJSON;
try {
uploadsCoursesJSON = new JSONObject(uploadsCoursesString);
uploadsCourses = UploadsCourse.generateCoursesFromJSON(uploadsCoursesJSON);
} catch (JSONException e) {
uploadsCourses = new HashMap<>();
Timber.e(e, "JSONException in uploads courses.");
}
}
@Override
@ -603,6 +612,8 @@ public class UploadActivity extends BaseActivity {
Toast.makeText(BaseApplication.getInstance().getApplicationContext(), "Please retry uploading.", Toast.LENGTH_SHORT).show();
}
break;
default:
super.onRequestPermissionsResult(permsRequestCode, permissions, grantResults);
}
}
@ -697,29 +708,10 @@ public class UploadActivity extends BaseActivity {
uploadNotificationConfig.getCompleted().iconResourceID = android.R.drawable.stat_sys_upload_done;
uploadNotificationConfig.getError().iconResourceID = android.R.drawable.stat_sys_upload_done;
uploadNotificationConfig.getError().iconColorResourceID = R.color.error_red;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
uploadNotificationConfig.getError().message = "Error during upload. Click for options";
}
uploadNotificationConfig.getCancelled().iconColorResourceID = android.R.drawable.stat_sys_upload_done;
uploadNotificationConfig.getCancelled().autoClear = true;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Intent combinedActionsIntent = new Intent(UploadsReceiver.ACTION_COMBINED_UPLOAD);
combinedActionsIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_ID_KEY, uploadID);
/*combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_FILENAME, filename);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_CATEGORY, retryCategory);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_TITLE, retryTitleText);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_DESCRIPTION, retryDescription);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_ICON, retryIcon);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_UPLOADER, retryUploaderProfile);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_RETRY_FILE_URI, retryFileUri);*/
uploadNotificationConfig.setClickIntentForAllStatuses(PendingIntent.getBroadcast(context,
1, combinedActionsIntent, PendingIntent.FLAG_UPDATE_CURRENT));
}
else {
Intent retryIntent = new Intent(context, UploadsReceiver.class);
retryIntent.setAction(UploadsReceiver.ACTION_RETRY_UPLOAD);
retryIntent.putExtra(UploadsReceiver.UPLOAD_ID_KEY, uploadID);
@ -740,7 +732,6 @@ public class UploadActivity extends BaseActivity {
PendingIntent.getBroadcast(context, 0, retryIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
));
}
return uploadNotificationConfig;
}
@ -958,8 +949,10 @@ public class UploadActivity extends BaseActivity {
.trim();
if (!retrievedCourse.isEmpty()) {
UploadsCourse foundUploadsCourse = UploadsCourse.findCourse(retrievedCourse, uploadsCourses);
try {
int categoryValue = Integer.parseInt(categorySelected);
if(uploadsCourses.containsKey(categoryValue)){
UploadsCourse foundUploadsCourse = uploadsCourses.get(categoryValue);
if (foundUploadsCourse != null) {
uploadsCourse = foundUploadsCourse;
semester = maybeSemester.replaceAll("-", "").trim().substring(0, 1);
@ -967,6 +960,10 @@ public class UploadActivity extends BaseActivity {
generateFieldsButton.setEnabled(true);
}
}
} catch (final NumberFormatException e) {
Timber.e(e, "Invalid category value!");
}
}
}
}

85
app/src/main/java/gr/thmmy/mthmmy/activities/upload/UploadsCourse.java

@ -1,79 +1,64 @@
package gr.thmmy.mthmmy.activities.upload;
import android.os.Bundle;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.base.BaseApplication;
import timber.log.Timber;
class UploadsCourse {
private String name;
private String minifiedName;
private String greeklishName;
public class UploadsCourse {
private final int id;
private final String name, minifiedName, greeklishName;
private UploadsCourse(String fullName, String minifiedName, String greeklishName) {
this.name = fullName;
private UploadsCourse(int id, String name, String minifiedName, String greeklishName) {
this.id = id;
this.name = name;
this.minifiedName = minifiedName;
this.greeklishName = greeklishName;
}
String getName() {
public int getId() {
return id;
}
public String getName() {
return name;
}
String getMinifiedName() {
public String getMinifiedName() {
return minifiedName;
}
String getGreeklishName() {
public String getGreeklishName() {
return greeklishName;
}
static Map<String, UploadsCourse> generateUploadsCourses(String[] uploadsCoursesRes) {
Map<String, UploadsCourse> uploadsCourses = new HashMap<>();
for (String uploadsCourseStr : uploadsCoursesRes) {
String[] split = uploadsCourseStr.split(":");
UploadsCourse uploadsCourse = new UploadsCourse(split[0], split[1], split[2]);
uploadsCourses.put(uploadsCourse.getName(), uploadsCourse);
}
return uploadsCourses;
public static HashMap<Integer, UploadsCourse> generateCoursesFromJSON(JSONObject json) throws JSONException {
HashMap<Integer, UploadsCourse> coursesHashMap = new HashMap<>();
if(json.has("courses")){
JSONArray coursesArray = json.getJSONArray("courses");
for(int i=0, size = coursesArray.length(); i<size; i++) {
JSONObject course = coursesArray.getJSONObject(i);
int id = course.getInt("id");
String name = course.getString("name");
String minifiedName = course.getString("minified");
String greeklisName = course.getString("greeklish");
if(coursesHashMap.containsKey(id))
Timber.w("Added a duplicate id (%d) in uploads courses!", id);
coursesHashMap.put(id, new UploadsCourse(id, name, minifiedName, greeklisName));
}
static UploadsCourse findCourse(String retrievedCourse,
Map<String, UploadsCourse> uploadsCourses) {
retrievedCourse = normalizeGreekNumbers(retrievedCourse);
UploadsCourse uploadsCourse = uploadsCourses.get(retrievedCourse);
if (uploadsCourse != null) return uploadsCourse;
String foundKey = null;
for (Map.Entry<String, UploadsCourse> entry : uploadsCourses.entrySet()) {
String key = entry.getKey();
if ((key.contains(retrievedCourse) || retrievedCourse.contains(key))
&& (foundKey == null || key.length() > foundKey.length()))
foundKey = key;
}
if (foundKey == null) {
Timber.w("Couldn't find course that matches %s", retrievedCourse);
Bundle bundle = new Bundle();
bundle.putString("course_name", retrievedCourse);
BaseApplication.getInstance().logFirebaseAnalyticsEvent("unsupported_uploads_course", bundle);
return null;
if(json.has("categories")){
JSONArray categoriesArray = json.getJSONArray("categories");
for(int i=0, size = categoriesArray.length(); i<size; i++) {
JSONObject category = categoriesArray.getJSONObject(i);
coursesHashMap.putAll(generateCoursesFromJSON(category));
}
return uploadsCourses.get(foundKey);
}
private static String normalizeGreekNumbers(String stringWithGreekNumbers) {
StringBuilder normalizedStrBuilder = new StringBuilder(stringWithGreekNumbers);
Pattern pattern = Pattern.compile("(Ι+)(?:\\s|\\(|\\)|$)");
Matcher matcher = pattern.matcher(stringWithGreekNumbers);
while (matcher.find())
normalizedStrBuilder.replace(matcher.start(1), matcher.end(1), matcher.group(1).replaceAll("Ι", "I"));
return normalizedStrBuilder.toString();
return coursesHashMap;
}
}

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

@ -2,22 +2,18 @@ package gr.thmmy.mthmmy.base;
import android.Manifest;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
@ -43,8 +39,6 @@ import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
import com.snatik.storage.Storage;
import net.gotev.uploadservice.UploadService;
import java.io.File;
import java.util.ArrayList;
@ -61,14 +55,12 @@ import gr.thmmy.mthmmy.activities.upload.UploadActivity;
import gr.thmmy.mthmmy.model.Bookmark;
import gr.thmmy.mthmmy.model.ThmmyFile;
import gr.thmmy.mthmmy.services.DownloadHelper;
import gr.thmmy.mthmmy.services.UploadsReceiver;
import gr.thmmy.mthmmy.session.LogoutTask;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.FileUtils;
import gr.thmmy.mthmmy.utils.io.AssetUtils;
import gr.thmmy.mthmmy.utils.networking.NetworkResultCodes;
import gr.thmmy.mthmmy.viewmodel.BaseViewModel;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import okhttp3.OkHttpClient;
import ru.noties.markwon.LinkResolverDef;
import ru.noties.markwon.Markwon;
@ -83,7 +75,6 @@ import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_
import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_USERNAME;
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.UploadsReceiver.UPLOAD_ID_KEY;
import static gr.thmmy.mthmmy.utils.FileUtils.getMimeType;
public abstract class BaseActivity extends AppCompatActivity {
@ -110,9 +101,6 @@ public abstract class BaseActivity extends AppCompatActivity {
//Common UI elements
protected Toolbar toolbar;
protected Drawer drawer;
//Uploads progress dialog
UploadsShowDialogReceiver uploadsShowDialogReceiver;
AlertDialog uploadsProgressDialog;
private MainActivity mainActivity;
private boolean isMainActivity;
@ -152,13 +140,6 @@ public abstract class BaseActivity extends AppCompatActivity {
isUserConsentDialogShown = true;
showUserConsentDialog();
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
if (uploadsShowDialogReceiver == null) {
uploadsShowDialogReceiver = new UploadsShowDialogReceiver(this);
}
this.registerReceiver(uploadsShowDialogReceiver, new IntentFilter(UploadsReceiver.ACTION_COMBINED_UPLOAD));
}
}
@Override
@ -166,10 +147,6 @@ public abstract class BaseActivity extends AppCompatActivity {
super.onPause();
if (drawer != null) //close drawer animation after returning to activity
drawer.closeDrawer();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && uploadsShowDialogReceiver != null) {
this.unregisterReceiver(uploadsShowDialogReceiver);
}
}
@ -855,95 +832,6 @@ public abstract class BaseActivity extends AppCompatActivity {
editor.putBoolean(getString(R.string.pref_privacy_analytics_enable_key), enabled).apply();
}
//------------------------------------------ UPLOADS -------------------------------------------
private class UploadsShowDialogReceiver extends BroadcastReceiver {
private final Context activityContext;
UploadsShowDialogReceiver(Context activityContext) {
this.activityContext = activityContext;
}
@Override
public void onReceive(Context context, Intent intent) {
Bundle intentBundle = intent.getExtras();
if (intentBundle == null) {
return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
String dialogUploadID = intentBundle.getString(UPLOAD_ID_KEY);
/*String retryFilename = intentBundle.getString(UPLOAD_RETRY_FILENAME);
String retryCategory = intentBundle.getString(UPLOAD_RETRY_CATEGORY);
String retryTitleText = intentBundle.getString(UPLOAD_RETRY_TITLE);
String retryDescription = intentBundle.getString(UPLOAD_RETRY_DESCRIPTION);
String retryIcon = intentBundle.getString(UPLOAD_RETRY_ICON);
String retryUploaderProfile = intentBundle.getString(UPLOAD_RETRY_UPLOADER);
Uri retryFileUri = (Uri) intentBundle.get(UPLOAD_RETRY_FILE_URI);
Intent retryIntent = new Intent(context, UploadsReceiver.class);
retryIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
retryIntent.setAction(UploadsReceiver.ACTION_RETRY_UPLOAD);
retryIntent.putExtra(UPLOAD_RETRY_FILENAME, retryFilename);
retryIntent.putExtra(UPLOAD_RETRY_CATEGORY, retryCategory);
retryIntent.putExtra(UPLOAD_RETRY_TITLE, retryTitleText);
retryIntent.putExtra(UPLOAD_RETRY_DESCRIPTION, retryDescription);
retryIntent.putExtra(UPLOAD_RETRY_ICON, retryIcon);
retryIntent.putExtra(UPLOAD_RETRY_UPLOADER, retryUploaderProfile);
retryIntent.putExtra(UPLOAD_RETRY_FILE_URI, retryFileUri);*/
if (uploadsProgressDialog == null) {
AlertDialog.Builder progressDialogBuilder = new AlertDialog.Builder(activityContext);
LayoutInflater inflater = LayoutInflater.from(activityContext);
LinearLayout progressDialogLayout = (LinearLayout) inflater.inflate(R.layout.dialog_upload_progress, null);
MaterialProgressBar dialogProgressBar = progressDialogLayout.findViewById(R.id.dialogProgressBar);
dialogProgressBar.setMax(100);
progressDialogBuilder.setView(progressDialogLayout);
uploadsProgressDialog = progressDialogBuilder.create();
if (!UploadService.getTaskList().contains(dialogUploadID)) {
//Upload probably failed at this point
uploadsProgressDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "", (progressDialog, progressWhich) -> {
/*LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext());
localBroadcastManager.sendBroadcast(multipartUploadRetryIntent);*/
//uploadsProgressDialog.dismiss();
//context.sendBroadcast(retryIntent);
});
uploadsProgressDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel), (progressDialog, progressWhich) -> {
uploadsProgressDialog.dismiss();
});
TextView dialogProgressText = progressDialogLayout.findViewById(R.id.dialog_upload_progress_text);
dialogProgressBar.setVisibility(View.GONE);
dialogProgressText.setText(getString(R.string.upload_failed));
uploadsProgressDialog.show();
}
else {
//Empty buttons are needed, they are updated with correct values in the receiver
uploadsProgressDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "placeholder", (progressDialog, progressWhich) -> {
});
uploadsProgressDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "placeholder", (progressDialog, progressWhich) -> {
});
UploadsReceiver.setDialogDisplay(uploadsProgressDialog, dialogUploadID, null);
//UploadsReceiver.setDialogDisplay(uploadsProgressDialog, dialogUploadID, retryIntent);
uploadsProgressDialog.show();
}
}
else {
UploadsReceiver.setDialogDisplay(uploadsProgressDialog, dialogUploadID, null);
//UploadsReceiver.setDialogDisplay(uploadsProgressDialog, dialogUploadID, retryIntent);
uploadsProgressDialog.show();
}
}
}
}
//----------------------------------MISC----------------------
protected void setMainActivity(MainActivity mainActivity) {
this.mainActivity = mainActivity;

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

@ -1,16 +1,15 @@
package gr.thmmy.mthmmy.base;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.widget.ImageView;
import androidx.core.content.ContextCompat;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide;
@ -20,27 +19,26 @@ import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersisto
import com.google.firebase.FirebaseApp;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor;
import com.mikepenz.fontawesome_typeface_library.FontAwesome;
import com.mikepenz.iconics.IconicsDrawable;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
import com.localebro.okhttpprofiler.OkHttpProfilerInterceptor;
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
import com.mikepenz.materialdrawer.util.DrawerImageLoader;
import net.gotev.uploadservice.UploadService;
import net.gotev.uploadservice.okhttp.OkHttpStack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import gr.thmmy.mthmmy.BuildConfig;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.session.SessionManager;
import gr.thmmy.mthmmy.utils.crashreporting.CrashReportingTree;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@ -48,9 +46,10 @@ import timber.log.Timber;
import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DISPLAY_COMPACT_TABS;
import static gr.thmmy.mthmmy.activities.settings.SettingsActivity.DISPLAY_RELATIVE_TIME;
import static gr.thmmy.mthmmy.activities.upload.UploadActivity.firebaseConfigUploadsCoursesKey;
import static gr.thmmy.mthmmy.utils.io.ResourceUtils.readJSONResourceToString;
// TODO: Replace MultiDexApplication with Application after KitKat support is dropped
public class BaseApplication extends MultiDexApplication {
public class BaseApplication extends Application implements Executor{
private static BaseApplication baseApplication; //BaseApplication singleton
private CrashReportingTree crashReportingTree;
@ -58,6 +57,7 @@ public class BaseApplication extends MultiDexApplication {
//Firebase
private static String firebaseProjectId;
private FirebaseAnalytics firebaseAnalytics;
private FirebaseRemoteConfig firebaseRemoteConfig;
//Client & SessionManager
private OkHttpClient client;
@ -128,6 +128,27 @@ public class BaseApplication extends MultiDexApplication {
Timber.i("Starting app with Firebase Analytics enabled.");
else
Timber.i("Starting app with Firebase Analytics disabled.");
// Firebase Remote Config (uploads courses)
InputStream inputStream = getApplicationContext().getResources().openRawResource(R.raw.uploads_courses);
String uploadsCourses = readJSONResourceToString(inputStream);
Map<String, Object> uploadsCoursesMap = new HashMap<>();
uploadsCoursesMap.put(firebaseConfigUploadsCoursesKey, uploadsCourses);
firebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(3600)
.build();
firebaseRemoteConfig.setConfigSettingsAsync(configSettings);
firebaseRemoteConfig.setDefaultsAsync(uploadsCoursesMap);
firebaseRemoteConfig.fetchAndActivate()
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
boolean updated = task.getResult();
Timber.i("Firebase remote config params updated: %s", updated);
} else
Timber.e("Fetching Firebase remote config params failed!");
});
}
private void initOkHttp(PersistentCookieJar cookieJar) {
@ -148,20 +169,6 @@ public class BaseApplication extends MultiDexApplication {
.writeTimeout(40, TimeUnit.SECONDS)
.readTimeout(40, TimeUnit.SECONDS);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // Just for KitKats
// Necessary because our servers don't have the right cipher suites.
// https://github.com/square/okhttp/issues/4053
List<CipherSuite> cipherSuites = new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites());
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
ConnectionSpec legacyTls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
.build();
builder.connectionSpecs(Arrays.asList(legacyTls, ConnectionSpec.CLEARTEXT));
}
if (BuildConfig.DEBUG)
builder.addInterceptor(new OkHttpProfilerInterceptor());
@ -183,14 +190,7 @@ public class BaseApplication extends MultiDexApplication {
@Override
public Drawable placeholder(Context ctx, String tag) {
if (DrawerImageLoader.Tags.PROFILE.name().equals(tag)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
return ContextCompat.getDrawable(BaseApplication.getInstance(), R.drawable.ic_default_user_avatar);
else { // Just for KitKats
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);
}
@ -277,4 +277,10 @@ public class BaseApplication extends MultiDexApplication {
public static String getFirebaseProjectId() {
return firebaseProjectId;
}
// Implement Executor (for Firebase remote config)
@Override
public void execute(Runnable runnable) {
runnable.run();
}
}

4
app/src/main/java/gr/thmmy/mthmmy/services/NotificationService.java

@ -28,7 +28,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.topic.TopicActivity;
import gr.thmmy.mthmmy.activities.main.MainActivity;
import gr.thmmy.mthmmy.base.BaseApplication;
import gr.thmmy.mthmmy.model.Bookmark;
import gr.thmmy.mthmmy.model.PostNotification;
@ -172,7 +172,7 @@ public class NotificationService extends FirebaseMessagingService {
//Builds notification
String topicUrl = "https://www.thmmy.gr/smf/index.php?topic=" + postNotification.getTopicId() + "." + postNotification.getPostId();
Intent intent = new Intent(this, TopicActivity.class);
Intent intent = new Intent(this, MainActivity.class);
Bundle extras = new Bundle();
extras.putString(BUNDLE_TOPIC_URL, topicUrl);
extras.putString(BUNDLE_TOPIC_TITLE, postNotification.getTopicTitle());

85
app/src/main/java/gr/thmmy/mthmmy/services/UploadsReceiver.java

@ -3,12 +3,7 @@ package gr.thmmy.mthmmy.services;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
@ -24,7 +19,6 @@ import gr.thmmy.mthmmy.R;
import gr.thmmy.mthmmy.activities.upload.UploadsHelper;
import gr.thmmy.mthmmy.activities.upload.multipart.MultipartUploadException;
import gr.thmmy.mthmmy.base.BaseApplication;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import timber.log.Timber;
public class UploadsReceiver extends UploadServiceBroadcastReceiver {
@ -88,87 +82,17 @@ public class UploadsReceiver extends UploadServiceBroadcastReceiver {
@Override
public void onProgress(Context context, UploadInfo uploadInfo) {
Timber.i("Upload in progress (id: %s)", uploadInfo.getUploadId());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP &&
uploadInfo.getUploadId().equals(dialogUploadID) &&
uploadProgressDialog != null) {
Button alertDialogNeutral = uploadProgressDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
alertDialogNeutral.setText(R.string.upload_resume_in_background);
alertDialogNeutral.setOnClickListener(v -> uploadProgressDialog.dismiss());
Button alertDialogNegative = uploadProgressDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
alertDialogNegative.setText(R.string.cancel);
alertDialogNegative.setOnClickListener(v -> {
Timber.d("Cancelling upload (id: %s)", dialogUploadID);
UploadService.stopUpload(dialogUploadID);
uploadProgressDialog.dismiss();
});
if (uploadProgressDialog.isShowing()) {
Window progressWindow = uploadProgressDialog.getWindow();
if (progressWindow != null) {
MaterialProgressBar dialogProgressBar = progressWindow.findViewById(R.id.dialogProgressBar);
TextView dialogProgressText = progressWindow.findViewById(R.id.dialog_upload_progress_text);
dialogProgressBar.setProgress(uploadInfo.getProgressPercent());
dialogProgressText.setText(context.getResources().getString(
R.string.upload_progress_dialog_bytes_uploaded,
(float) uploadInfo.getUploadRate(),
(int) uploadInfo.getUploadedBytes() / 1000,
(int) uploadInfo.getTotalBytes() / 1000));
}
if (uploadInfo.getUploadedBytes() == uploadInfo.getTotalBytes()) {
uploadProgressDialog.dismiss();
}
}
}
}
@Override
public void onError(Context context, UploadInfo uploadInfo, ServerResponse serverResponse,
Exception exception) {
Timber.i("Error while uploading (id: %s)", uploadInfo.getUploadId());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP &&
uploadInfo.getUploadId().equals(dialogUploadID) &&
uploadProgressDialog != null) {
/*Button alertDialogNeutral = uploadProgressDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
alertDialogNeutral.setText("Retry");
alertDialogNeutral.setOnClickListener(v -> {
if (multipartUploadRetryIntent != null) {
context.sendBroadcast(multipartUploadRetryIntent);
}
uploadProgressDialog.dismiss();
});*/
Button alertDialogNegative = uploadProgressDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
alertDialogNegative.setText(R.string.cancel);
alertDialogNegative.setOnClickListener(v -> {
cancelNotification(context, uploadInfo.getNotificationID());
UploadsHelper.deleteTempFiles(storage);
uploadProgressDialog.dismiss();
});
if (uploadProgressDialog.isShowing()) {
Window progressWindow = uploadProgressDialog.getWindow();
if (progressWindow != null) {
MaterialProgressBar dialogProgressBar = progressWindow.findViewById(R.id.dialogProgressBar);
TextView dialogProgressText = progressWindow.findViewById(R.id.dialog_upload_progress_text);
dialogProgressBar.setVisibility(View.GONE);
dialogProgressText.setText(R.string.upload_failed);
}
if (uploadInfo.getUploadedBytes() == uploadInfo.getTotalBytes()) {
uploadProgressDialog.dismiss();
}
}
}
else {
cancelNotification(context, uploadInfo.getNotificationID());
Intent combinedActionsIntent = new Intent(UploadsReceiver.ACTION_COMBINED_UPLOAD);
combinedActionsIntent.putExtra(UploadsReceiver.UPLOAD_ID_KEY, uploadInfo.getUploadId());
context.sendBroadcast(combinedActionsIntent);
}
Toast.makeText(context.getApplicationContext(), R.string.upload_failed, Toast.LENGTH_SHORT).show();
if (storage == null) {
@ -178,11 +102,6 @@ public class UploadsReceiver extends UploadServiceBroadcastReceiver {
@Override
public void onCompleted(Context context, UploadInfo uploadInfo, ServerResponse serverResponse) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
uploadProgressDialog = null;
dialogUploadID = null;
}
String response = serverResponse.getBodyAsString();
if (response.contains("Η προσθήκη του αρχείου ήταν επιτυχημένη.") || response.contains("The upload was successful.")) {
Timber.i("Upload completed successfully (id: %s)", uploadInfo.getUploadId());
@ -205,10 +124,6 @@ public class UploadsReceiver extends UploadServiceBroadcastReceiver {
@Override
public void onCancelled(Context context, UploadInfo uploadInfo) {
Timber.i("Upload cancelled (id: %s)", uploadInfo.getUploadId());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
uploadProgressDialog = null;
dialogUploadID = null;
}
Toast.makeText(context.getApplicationContext(), R.string.upload_cancelled, Toast.LENGTH_SHORT).show();
if (storage == null)

46
app/src/main/java/gr/thmmy/mthmmy/utils/io/ResourceUtils.java

@ -0,0 +1,46 @@
package gr.thmmy.mthmmy.utils.io;
import android.content.res.Resources;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import timber.log.Timber;
public class ResourceUtils {
public static String readJSONResourceToString(Resources resources, int id) {
InputStream inputStream = resources.openRawResource(id);
return readStream(inputStream);
}
public static String readJSONResourceToString(InputStream inputStream) {
return readStream(inputStream);
}
private static String readStream(InputStream inputStream) {
Writer writer = new StringWriter();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = reader.readLine();
while (line != null) {
writer.write(line);
line = reader.readLine();
}
} catch (Exception e) {
Timber.e(e, "Unhandled exception while using readJSONFromResource");
} finally {
try {
inputStream.close();
} catch (Exception e) {
Timber.e(e, "Unhandled exception while using readJSONFromResource");
}
}
return writer.toString();
}
}

24
app/src/main/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParser.java

@ -5,6 +5,8 @@ import androidx.annotation.VisibleForTesting;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.IllegalInstantException;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
@ -33,9 +35,8 @@ public class ThmmyDateTimeParser {
.append(null, parsers)
.toFormatter();
//TODO: Replace with Locale.forLanguageTag() (with "el-GR","en-US") when KitKat support is dropped
private static final Locale greekLocale = new Locale("el", "GR");
private static final Locale englishLocale = new Locale("en", "US");
private static final Locale greekLocale = Locale.forLanguageTag("el-GR");
private static final Locale englishLocale = Locale.forLanguageTag("en-US");
private static final Pattern pattern = Pattern.compile("\\s((1[3-9]|2[0-3]):)");
@ -64,8 +65,9 @@ public class ThmmyDateTimeParser {
thmmyDateTime = thmmyDateTime.replaceAll("\\spm", "");
DateTime dateTime;
LocalDateTime localDateTime;
try {
dateTime = formatter.withZone(dtz).withLocale(englishLocale).parseDateTime(thmmyDateTime);
localDateTime = formatter.withLocale(englishLocale).parseLocalDateTime(thmmyDateTime);
} catch (IllegalArgumentException e1) {
Timber.v("Parsing DateTime %s using English Locale failed.", thmmyDateTime);
try {
@ -73,13 +75,23 @@ public class ThmmyDateTimeParser {
thmmyDateTime = thmmyDateTime.replace("am", dfs.getAmPmStrings()[0]);
thmmyDateTime = thmmyDateTime.replace("pm", dfs.getAmPmStrings()[1]);
Timber.v("Attempting to parse DateTime %s using Greek Locale...", thmmyDateTime);
dateTime = formatter.withZone(dtz).withLocale(greekLocale).parseDateTime(thmmyDateTime);
localDateTime = formatter.withLocale(greekLocale).parseLocalDateTime(thmmyDateTime);
} catch (IllegalArgumentException e2) {
Timber.d("Parsing DateTime %s using Greek Locale failed too.", thmmyDateTime);
Timber.v("Parsing DateTime %s using Greek Locale failed too.", thmmyDateTime);
Timber.e("Couldn't convert DateTime %s to timestamp!", originalDateTime);
return null;
}
}
// Ensure DST time overlaps/ gaps are handled properly
try{
// For autumn overlaps
dateTime = localDateTime.toDateTime(dtz).withEarlierOffsetAtOverlap();
} catch (IllegalInstantException e2) {
// For spring gaps
dateTime = localDateTime.plusHours(1).toDateTime(dtz);
}
String timestamp = Long.toString(dateTime.getMillis());
Timber.v("DateTime %s was converted to %s, or %s", originalDateTime, timestamp, dateTime.toString());

124
app/src/main/res/layout-v21/activity_profile.xml

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activities.profile.ProfileActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ToolbarTheme">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/main_collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/user_thumbnail"
android:layout_width="@dimen/profile_activity_avatar_size"
android:layout_height="@dimen/profile_activity_avatar_size"
android:layout_marginBottom="6dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:fitsSystemWindows="true"
android:transitionName="user_thumbnail"
app:layout_collapseMode="parallax" />
<TextView
android:id="@+id/profile_activity_personal_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="6dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@color/primary_text"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:gravity="center"
app:popupTheme="@style/ToolbarTheme">
<TextView
android:id="@+id/profile_activity_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="2dp"
android:text="@string/username"
android:textColor="@color/accent"
android:textSize="25sp" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/profile_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:tabGravity="fill"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/accent"
app:tabTextColor="@color/white" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/profile_tab_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top|start"
android:background="@color/background"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|center"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progressBar"
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
android:layout_width="match_parent"
android:layout_height="@dimen/progress_bar_height"
android:indeterminate="true"
android:visibility="invisible"
app:layout_anchor="@id/profile_tab_container"
app:layout_anchorGravity="top|center"
app:mpb_indeterminateTint="@color/accent"
app:mpb_progressStyle="horizontal" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/profile_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margins"
app:layout_behavior="gr.thmmy.mthmmy.utils.ui.ScrollAwareFABBehavior"
app:srcCompat="@drawable/ic_pm_fab" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

262
app/src/main/res/layout-v21/activity_topic_post_row.xml

@ -1,262 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingEnd="4dp"
android:paddingStart="4dp"
tools:ignore="SmallSp">
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardBackgroundColor="@color/background_light"
card_view:cardCornerRadius="5dp"
card_view:cardElevation="2dp"
card_view:cardPreventCornerOverlap="false"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:id="@+id/card_child_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<FrameLayout
android:id="@+id/thumbnail_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="@dimen/thumbnail_size"
android:layout_height="@dimen/thumbnail_size"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:transitionName="user_thumbnail"
app:srcCompat="@drawable/ic_default_user_avatar_darker" />
</FrameLayout>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/post_author"
android:textColor="@color/primary_text"
android:textStyle="bold" />
<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/username"
android:layout_toEndOf="@+id/thumbnail_holder"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/post_subject" />
</RelativeLayout>
<ImageButton
android:id="@+id/toggle_quote_button"
android:layout_width="@dimen/post_image_button"
android:layout_height="@dimen/post_image_button"
android:layout_marginTop="9dp"
android:background="@color/background_light"
android:clickable="true"
android:contentDescription="@string/post_quote_button"
android:focusable="true"
app:srcCompat="@drawable/ic_format_quote_unchecked_24dp" />
<!--<ImageButton
android:id="@+id/post_share_button"
android:layout_width="@dimen/post_image_button"
android:layout_height="@dimen/post_image_button"
android:layout_marginEnd="9dp"
android:layout_marginTop="9dp"
android:background="@color/card_background"
android:clickable="true"
android:contentDescription="@string/post_share_button"
android:focusable="true"
android:src="@drawable/ic_share" />-->
<ImageButton
android:id="@+id/post_overflow_menu"
android:layout_width="@dimen/post_image_button"
android:layout_height="@dimen/post_image_button"
android:layout_marginTop="9dp"
android:layout_marginEnd="9dp"
android:background="@color/background_light"
android:clickable="true"
android:contentDescription="@string/post_overflow_menu_button"
android:focusable="true"
app:srcCompat="@drawable/ic_more_vert_white_24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/user_extra_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="3dp"
android:visibility="gone"
android:weightSum="1.0">
<TextView
android:id="@+id/special_rank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/rank"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/stars"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/number_of_posts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:visibility="gone" />
<TextView
android:id="@+id/personal_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/card_expand_text_color"
android:textSize="10sp"
android:textStyle="italic"
android:visibility="gone" />
</LinearLayout>
<FrameLayout
android:id="@+id/post_date_and_number_exp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/post_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:text=""
android:textColor="@color/accent"
android:textSize="11sp" />
<TextView
android:id="@+id/post_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text=""
android:textColor="@color/accent"
android:textSize="11sp" />
</FrameLayout>
<View
android:id="@+id/header_body_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="9dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="5dp"
android:background="@color/divider" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
android:descendantFocusability="blocksDescendants">
<gr.thmmy.mthmmy.views.ReactiveWebView
android:id="@+id/post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:background="@color/background_light"
android:text="@string/post" />
</FrameLayout>
<View
android:id="@+id/body_footer_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="9dp"
android:background="@color/divider"
android:visibility="gone" />
<LinearLayout
android:id="@+id/post_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="9dp"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

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

@ -41,6 +41,7 @@
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:fitsSystemWindows="true"
android:transitionName="user_thumbnail"
app:layout_collapseMode="parallax" />
<TextView

4
app/src/main/res/layout/activity_topic_post_row.xml

@ -58,6 +58,7 @@
android:layout_gravity="center"
android:adjustViewBounds="true"
android:contentDescription="@string/post_thumbnail"
android:transitionName="user_thumbnail"
app:srcCompat="@drawable/ic_default_user_avatar_darker" />
</FrameLayout>
@ -197,8 +198,7 @@
android:gravity="start"
android:text=""
android:textColor="@color/accent"
android:textSize="11sp"
tools:text="date" />
android:textSize="11sp" />
<TextView
android:id="@+id/post_number"

1702
app/src/main/res/raw/uploads_courses.json

File diff suppressed because it is too large

11
app/src/main/res/values-v21/styles.xml

@ -1,11 +0,0 @@
<resources>
<style name="AppTheme" parent="BaseAppTheme">
<item name="android:windowContentTransitions">true</item>
</style>
<style name="AppTheme.NoActionBar" parent="BaseAppTheme.NoActionBar">
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

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

@ -1,5 +1,4 @@
<resources>
<!-- Base Theme to also be inherited in v21 -->
<style name="BaseAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_light</item>
@ -21,14 +20,19 @@
</style>
<!-- Dark application theme. -->
<style name="AppTheme" parent="BaseAppTheme" />
<style name="AppTheme" parent="BaseAppTheme">
<item name="android:windowContentTransitions">true</item>
</style>
<style name="BaseAppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.NoActionBar" parent="BaseAppTheme.NoActionBar" />
<style name="AppTheme.NoActionBar" parent="BaseAppTheme.NoActionBar">
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
<style name="AppTheme.PreferenceTheme" parent="AppTheme.NoActionBar">
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>

158
app/src/main/res/values/uploads_courses.xml

@ -1,158 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Format: Original:Minified:Greeklish-->
<string-array name="string_array_uploads_courses">
<item>Ακουστική I:Ακουστική 1:Akoustiki_I</item>
<item>Ακουστική II:Ακουστική 2:Akoustiki_II</item>
<item>Ανάλυση Ηλεκτρικών Κυκλωμάτων με Υπολογιστή:Ανάλυση Ηλεκτρικ. Κυκλ. με Υπολογιστή:Analysi_Ilektr_Kykl</item>
<item>Ανάλυση Συστημάτων Ηλεκτρικής Ενέργειας:ΑΣΗΕ:ASHE</item>
<item>Ανάλυση Χρονοσειρών:Χρονοσειρές:Xronoseires</item>
<item>Ανάλυση και Σχεδίαση Αλγορίθμων:Αλγόριθμοι:Algorithms</item>
<item>Αναγνώριση Προτύπων:Αναγνώριση Προτύπων:protipa</item>
<item>Αναλογικές Τηλεπικοινωνίες (πρώην Τηλεπικοινωνιακά Συστήματα I):Αναλογικές Τηλεπ.:Anal_Tilep</item>
<item>Αντικειμενοστραφής Προγραμματισμός:Αντικειμενοστραφής:OOP</item>
<item>Αξιοπιστία Συστημάτων:Αξιοπιστία Συστημάτων:Aksiopistia_Systimaton</item>
<item>Αριθμητική Ανάλυση:Αριθμ. Ανάλυση:Arith_Anal</item>
<item>Αρχές Οικονομίας:Αρχές Οικονομίας:Arx_Oikonomias</item>
<item>Αρχές Παράλληλης Επεξεργασίας:Αρχές Παράλληλης Επεξεργασίας:Arxes_Parall_Epeksergasias</item>
<item>Αρχιτεκτονική Υπολογιστών:Αρχ. Υπολογιστών:Arx_Ypologiston</item>
<item>Ασαφή Συστήματα:Ασαφή:Asafi</item>
<item>Ασφάλεια Πληροφοριακών Συστημάτων:Ασφάλεια:Asfaleia</item>
<item>Ασύρματος Τηλεπικοινωνία I:Ασύρματος 1:Asyrmatos_I</item>
<item>Ασύρματος Τηλεπικοινωνία II:Ασύρματος 2:Asyrmatos_II</item>
<item>Βάσεις Δεδομένων:Βάσεις:Vaseis</item>
<item>Βιομηχανικά Ηλεκτρονικά:Βιομηχανικά Ηλεκτρονικά:Viomix_Ilektronika</item>
<item>Βιομηχανική Πληροφορική:Βιομηχανική Πληρ:Viomix_Plir</item>
<item>Βιοϊατρική Τεχνολογία:Βιοιατρική:Vioiatriki</item>
<item>Γεωηλεκτρομαγνητισμός:Γεωηλεκτρομαγνητισμός:Geoilektromagnitismos</item>
<item>Γραμμική Άλγεβρα:Γραμμ. Άλγεβρ.:Grammiki_Algevra</item>
<item>Γραφική με Υπολογιστές:Γραφική:Grafiki</item>
<item>Δίκτυα Τηλεπικοινωνιών:Δίκτυα Τηλέπ.:Diktya_Tilep</item>
<item>Δίκτυα Υπολογιστών I:Δίκτυα 1:Diktya_I</item>
<item>Δίκτυα Υπολογιστών II:Δίκτυα 2:Diktya_II</item>
<item>Διάδοση Η/Μ Κύματος II:Διάδοση 2:Diadosi_II</item>
<item>Διάδοση Ηλεκτρομαγνητικού Κύματος I (πρώην Πεδίο III):Διάδοση 1:Diadosi_I</item>
<item>Διακριτά Μαθηματικά:Διακριτά Μαθηματικά:Diakrita</item>
<item>Διακριτά μαθηματικά:Διακριτά Μαθηματικά:Diakrita</item>
<item>Διανεμημένη Παραγωγή:Διανεμημένη Παραγωγή:Dian_Paragogi</item>
<item>Διατάξεις Υψηλών Συχνοτήτων:ΔΥΣ:DYS</item>
<item>Διαφορικές Εξισώσεις:Διαφορικές:Diaforikes</item>
<item>Διαχείριση Συστημάτων Ηλεκτρικής Ενέργειας:ΔΣΗΕ:DSHE</item>
<item>Δομές Δεδομένων:Δομ. Δεδομ.:Domes_Dedomenon</item>
<item>Δομημένος Προγραμματισμός:Δομ. Προγραμμ.:C</item>
<item>Ειδικά Κεφάλαια Διαφορικών Εξισώσεων:Ειδικά Κεφάλαια Διαφορικών Εξισώσεων:Eidika_Kef_Diaf_Eksis</item>
<item>Ειδικά Κεφάλαια Ηλεκτρομαγνητικού Πεδίου I:Ειδικά Κεφάλαια Ηλεκτρομαγνητικού Πεδίου I:Eidika_Kef_HM_Pediou_I</item>
<item>Ειδικά Κεφάλαια Συστημάτων Ηλεκτρικής Ενέργειας:ΕΚΣΗΕ:EKSHE</item>
<item>Ειδικές Αρχιτεκτονικές Υπολογιστών:Ειδικές Αρχιτεκτονικές Υπολογιστών:Eidikes_Arx_Ypolog</item>
<item>Ειδικές Κεραίες, Σύνθεση Κεραιών:Ειδικές Κεραίες, Σύνθεση Κεραιών:Eidikes_Keraies</item>
<item>Εισαγωγή στην Ενεργειακή Τεχνολογία I:ΕΕΤ 1:EET_I</item>
<item>Εισαγωγή στην Ενεργειακή Τεχνολογία II:ΕΕΤ2:EET_II</item>
<item>Εισαγωγή στην Πολιτική Οικονομία:Πολιτική Οικονομία:Polit_Oik</item>
<item>Εισαγωγή στις εφαρμογές Πυρηνικής Τεχνολογίας:Εισ. Πυρηνικη Τεχν.:Intro_Pyriniki_Texn</item>
<item>Ενσωματωμένα Συστήματα Πραγματικού Χρόνου:Ενσωματωμένα:Enswmatwmena</item>
<item>Επιχειρησιακή Έρευνα:Επιχειρησιακή Έρευνα:Epixeirisiaki</item>
<item>Ευρυζωνικά Δίκτυα:Ευρυζωνικά:Evryzonika</item>
<item>Ευφυή Συστήματα Ρομπότ:Ευφυή:eufuh</item>
<item>Εφαρμογές Τηλεπικοινωνιακών Διατάξεων:Εφαρμογές Τηλεπ. Διατάξεων:Efarm_Tilep_Diatakseon</item>
<item>Εφαρμοσμένα Μαθηματικά I:Εφαρμοσμένα 1:Efarmosmena_Math_I</item>
<item>Εφαρμοσμένα Μαθηματικά II:Εφαρμοσμένα 2:Efarmosmena_Math_II</item>
<item>Εφαρμοσμένη Θερμοδυναμική:Θερμοδυναμική:Thermodynamiki</item>
<item>Ηλεκτρακουστική I:Ηλεκτρακουστική 1:Ilektrakoustiki_I</item>
<item>Ηλεκτρακουστική II:Ηλεκτρακουστική 2:Ilektrakoustiki_II</item>
<item>Ηλεκτρικά Κυκλώματα I:Κυκλώματα 1:Kyklomata_I</item>
<item>Ηλεκτρικά Κυκλώματα II:Κυκλώματα 2:Kyklomata_II</item>
<item>Ηλεκτρικά Κυκλώματα III:Κυκλώματα 3:Kyklomata_I</item>
<item>Ηλεκτρικές Μετρήσεις I:Μετρήσεις 1:Metriseis_I</item>
<item>Ηλεκτρικές Μετρήσεις II:Μετρήσεις 2:Metriseis_II</item>
<item>Ηλεκτρικές Μηχανές I:Μηχανές I:Mixanes_I</item>
<item>Ηλεκτρικές Μηχανές Α\':Μηχανές Α:Mixanes_A</item>
<item>Ηλεκτρικές Μηχανές Β\':Μηχανές Β:Mixanes_B</item>
<item>Ηλεκτρικές Μηχανές Γ\':Μηχανές Γ:Mixanes_C</item>
<item>Ηλεκτρική Οικονομία:Ηλεκτρική Οικονομία:Ilektr_Oikonomia</item>
<item>Ηλεκτρολογικά Υλικά:Ηλεκτρ. Υλικά:Ylika</item>
<item>Ηλεκτρομαγνητική Συμβατότητα:H/M Συμβατότητα:HM_Symvatotita</item>
<item>Ηλεκτρομαγνητικό Πεδίο I:Πεδίο 1:Pedio_I</item>
<item>Ηλεκτρομαγνητικό Πεδίο II:Πεδίο 2:Pedio_II</item>
<item>Ηλεκτρονικά Ισχύος I:Ισχύος 1:Isxyos_I</item>
<item>Ηλεκτρονικά Ισχύος II:Ισχύος 2:Isxyos_II</item>
<item>Ηλεκτρονικές Διατάξεις και Μετρήσεις:Ηλεκτρονικές Διατάξεις και Μετρήσεις:Ilektron_Diatakseis_Metriseis</item>
<item>Ηλεκτρονική I:Ηλεκτρονική 1:Ilektroniki_I</item>
<item>Ηλεκτρονική II:Ηλεκτρονική 2:Ilektroniki_II</item>
<item>Ηλεκτρονική III:Ηλεκτρονική 3:Ilektroniki_III</item>
<item>Ημιαγωγά Υλικά: Θεωρία-Διατάξεις:Ημιαγωγά Υλικά:Imiagoga_Ylika</item>
<item>Θεωρία Πιθανοτήτων και Στατιστική:Πιθανότητες:Pithanotites</item>
<item>Θεωρία Πληροφοριών:Θεωρία Πληρ.:Theoria_Plir</item>
<item>Θεωρία Σημάτων και Γραμμικών Συστημάτων:Σήματα &amp; Συστήματα:Analog_Sima</item>
<item>Θεωρία Σκέδασης:Σκέδαση:Skedasi</item>
<item>Θεωρία Υπολογισμών και Αλγορίθμων:ΘΥΑ:THYA</item>
<item>Θεωρία και Τεχνολογία Πυρηνικών Αντιδραστήρων:Τεχνολογία Αντιδραστήρων:Texn_Antidrasthron</item>
<item>Κβαντική Φυσική:Κβαντική:Kvantiki</item>
<item>Κινητές και Δορυφορικές Επικοινωνίες:Κινητές &amp; Δορυφορικές Επικοινωνίες:Kinites_Doryforikes_Epik</item>
<item>Λειτουργικά Συστήματα:Λειτουργικά:OS</item>
<item>Λογική Σχεδίαση:Λογική Σχεδίαση:Logiki_Sxediasi</item>
<item>Λογισμός I:Λογισμός 1:Logismos_I</item>
<item>Λογισμός II:Λογισμός 2:Logismos_II</item>
<item>Μετάδοση Θερμότητας:Μετάδοση Θερμ.:Metadosi_Therm</item>
<item>Μικροεπεξεργαστές και Περιφερειακά:Μίκρο 2:Mikro_II</item>
<item>Μικροκυματική Τηλεπισκόπηση:Τηλεπισκόπηση:Tilepiskopisi</item>
<item>Μικροκύματα I:Μικροκύματα 1:Mikrokymata_I</item>
<item>Μικροκύματα II:Μικροκύματα 2:Mikrokymata_II</item>
<item>Οπτικές Επικοινωνίες:Οπτικές Τηλεπ.:Optikes_Tilep</item>
<item>Οπτική I:Οπτική 1:Optiki_I</item>
<item>Οπτική II:Οπτική 2:Optiki_II</item>
<item>Οργάνωση Υπολογιστών:Οργάνωση Υπολ.:Org_Ypol</item>
<item>Οργάνωση και Διοίκηση Εργοστασίων:Οργάνωση και Διοίκηση Εργοστασίων:Organ_Dioik_Ergostasion</item>
<item>Παράλληλα και Κατανεμημένα Συστήματα:Παράλληλα:Parallila</item>
<item>Προγραμματιζόμενα Κυκλώματα ASIC:ASIC:ASIC</item>
<item>Προγραμματιστικές Τεχνικές:Προγραμματ. Τεχν.:CPP</item>
<item>Προηγμένες Τεχνικές Επεξεργασίας Σήματος:ΠΤΕΣ:PTES</item>
<item>Προσομοίωση και Μοντελοποίηση Συστημάτων:Μοντελοποίηση:Montelopoiisi</item>
<item>Ρομποτική:Ρομποτική:Robotiki</item>
<item>Σήματα και Συστήματα:Σήματα &amp; Συστήματα:Analog_Sima</item>
<item>Σερβοκινητήρια Συστήματα:Σέρβο:Servo</item>
<item>Σταθμοί Παραγωγής Ηλεκτρικής Ενέργειας:ΣΠΗΕ:SPHE</item>
<item>Στοχαστικά Σήματα και Διαδικασίες:Στοχαστικό:Stochastic</item>
<item>Στοχαστικό Σήμα:Στοχαστικό:Stochastic</item>
<item>Συστήματα Αυτομάτου Ελέγχου I:ΣΑΕ 1:SAE_I</item>
<item>Συστήματα Αυτομάτου Ελέγχου II:ΣΑΕ 2:SAE_II</item>
<item>Συστήματα Αυτομάτου Ελέγχου III:ΣΑΕ 3:SAE_III</item>
<item>Συστήματα Ηλεκτρικής Ενέργειας I:ΣΗΕ 1:SHE_I</item>
<item>Συστήματα Ηλεκτρικής Ενέργειας II:ΣΗΕ 2:SHE_II</item>
<item>Συστήματα Ηλεκτρικής Ενέργειας III:ΣΗΕ 3:SHE_III</item>
<item>Συστήματα Ηλεκτροκίνησης:Ηλεκτροκίνηση:Ilektrokinisi</item>
<item>Συστήματα Μικροϋπολογιστών:Μίκρο 1:Mikro_I</item>
<item>Συστήματα Πολυμέσων και Εικονική Πραγματικότητα:Πολυμέσα:Polymesa</item>
<item>Συστήματα Υπολογιστών (Υπολογιστικά Συστήματα):Συσ. Υπολογιστών:Sys_Ypologiston</item>
<item>Σχεδίαση Συστημάτων VLSI:VLSI:VLSI</item>
<item>Σύνθεση Ενεργών και Παθητικών Κυκλωμάτων:Σύνθεση:Synthesi</item>
<item>Σύνθεση Τηλεπικοινωνιακών Διατάξεων:Σύνθεση Τηλεπ. Διατάξεων:Synth_Tilep_Diatakseon</item>
<item>Τεχνικές Βελτιστοποίησης:Βελτιστοποίηση:Veltistopoiisi</item>
<item>Τεχνικές Κωδικοποίησης:Τεχνικές Κωδικοποίησης:Texn_Kodikopoiisis</item>
<item>Τεχνικές Σχεδίασης με Η/Υ:Σχέδιο:sxedio</item>
<item>Τεχνικές μη Καταστρεπτικών Δοκιμών:Μη Καταστρεπτικές Δοκιμές:Non_Destructive_Tests</item>
<item>Τεχνική Μηχανική:Τεχν. Μηχαν.:Texn_Mixan</item>
<item>Τεχνολογία Ήχου και Εικόνας:Τεχνολογία Ήχου και Εικόνας:Texn_Ixou_Eikonas</item>
<item>Τεχνολογία Ηλεκτροτεχνικών Υλικών:Ηλεκτροτεχνικά Υλικά:Ilektrotexnika_Ylika</item>
<item>Τεχνολογία Λογισμικού:Τεχνολογία Λογισμικού:SE</item>
<item>Τηλεοπτικά Συστήματα:Τηλεοπτικά:Tileoptika</item>
<item>Τηλεπικοινωνιακά Συστήματα I:Τηλεπικοινωνιακά I:Tilepikoinoniaka_I</item>
<item>Τηλεπικοινωνιακά Συστήματα II:Τηλεπικοινωνιακά II:Tilepikoinoniaka_II</item>
<item>Τηλεπικοινωνιακή Ηλεκτρονική:Τηλεπ. Ηλεκτρ.:Tilep_Ilektr</item>
<item>Υπολογιστικές Μέθοδοι στα Ενεργειακά Συστήματα:ΥΜΕΣ:YMES</item>
<item>Υπολογιστικός Ηλεκτρομαγνητισμός:Υπολογιστικός Η/Μ:Ypologistikos_HM</item>
<item>Υψηλές Τάσεις I:Υψηλές 1:Ypsiles_I</item>
<item>Υψηλές Τάσεις II:Υψηλές 2:Ypsiles_II</item>
<item>Υψηλές Τάσεις III:Υψηλές 3:Ypsiles_III</item>
<item>Υψηλές Τάσεις 4:Υψηλές 4:Ypsiles_IV</item>
<item>Φυσική I:Φυσική 1:Fysiki_I</item>
<item>Φωτονική Τεχνολογία:Φωτονική:Fotoniki</item>
<item>Ψηφιακά Συστήματα I:Ψηφιακά 1:Psifiaka_I</item>
<item>Ψηφιακά Συστήματα II:Ψηφιακά 2:Psifiaka_II</item>
<item>Ψηφιακά Συστήματα III:Ψηφιακά 3:Psifiaka_III</item>
<item>Ψηφιακά Φίλτρα:Φίλτρα:Filtra</item>
<item>Ψηφιακές Τηλεπικοινωνίες I:Ψηφιακές Τηλεπ. 1:Psif_Tilep_I</item>
<item>Ψηφιακές Τηλεπικοινωνίες II:Ψηφιακές Τηλεπ. 2:Psif_Tilep_II</item>
<item>Ψηφιακή Επεξεργασία Εικόνας:ΨΕΕ:PSEE</item>
<item>Ψηφιακή Επεξεργασία Σήματος:ΨΕΣ:PSES</item>
</string-array>
</resources>

42
app/src/test/java/gr/thmmy/mthmmy/utils/UploadsCoursesJSONReadingTest.java

@ -0,0 +1,42 @@
package gr.thmmy.mthmmy.utils;
import net.lachlanmckee.timberjunit.TimberTestRule;
import org.json.JSONObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.InputStream;
import java.util.HashMap;
import gr.thmmy.mthmmy.activities.upload.UploadsCourse;
import gr.thmmy.mthmmy.utils.io.ResourceUtils;
import static gr.thmmy.mthmmy.activities.upload.UploadsCourse.generateCoursesFromJSON;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(PowerMockRunner.class)
@PrepareForTest(JSONObject.class)
public class UploadsCoursesJSONReadingTest {
private final String filePath = "uploads_courses.json";
@Rule
public TimberTestRule logAllAlwaysRule = TimberTestRule.logAllAlways();
@Test
public void uploadsCoursesRetrievedCorrectly() throws Exception {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(filePath);
assertNotNull(is);
String uploadsCoursesJSON = ResourceUtils.readJSONResourceToString(is);
assertNotNull(uploadsCoursesJSON);;
JSONObject jsonObject = new JSONObject(uploadsCoursesJSON);
assertTrue(jsonObject.has("categories"));
HashMap<Integer, UploadsCourse> coursesHashMap = generateCoursesFromJSON(jsonObject);
assertEquals(coursesHashMap.size(), 216);
}
}

5
app/src/test/java/gr/thmmy/mthmmy/utils/parsing/ThmmyDateTimeParserTest.java

@ -25,7 +25,7 @@ public class ThmmyDateTimeParserTest {
private static final String TIME_ZONE = "Europe/Athens";
private static final String GET_DTZ = "getDtz";
private final String[] expTimestamps = {"1569245936000", "1569191627000", "1570050809000"};
private final String[] expTimestamps = {"1569245936000", "1569191627000", "1570050809000", "1553995543000"};
private final String[][] dateTimes = {
{
"Σεπτεμβρίου 23, 2019, 16:38:56",
@ -45,6 +45,9 @@ public class ThmmyDateTimeParserTest {
{
"Οκτωβρίου 03, 2019, 12:13:29 am",
"Οκτωβρίου 03, 2019, 00:13:29 am"
},
{
"Μαρτίου 31, 2019, 03:25:43 am"
}
};

6
build.gradle

@ -8,9 +8,9 @@ buildscript {
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'com.google.gms:google-services:4.3.4'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
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"
}

Loading…
Cancel
Save