From c8e68f4351401ea5eaddbfbf5fc861155155f004 Mon Sep 17 00:00:00 2001 From: Apostolof Date: Thu, 19 Jan 2017 22:18:17 +0200 Subject: [PATCH] Downloads init, profile and download fixes. --- app/src/main/AndroidManifest.xml | 2 + .../activities/board/BoardActivity.java | 4 + .../downloads/DownloadsActivity.java | 274 ++++++++++++++++++ .../downloads/DownloadsAdapter.java | 181 ++++++++++++ .../profile/stats/StatsFragment.java | 36 ++- .../profile/summary/SummaryFragment.java | 11 +- .../activities/topic/TopicActivity.java | 19 +- .../mthmmy/activities/topic/TopicAdapter.java | 2 +- .../gr/thmmy/mthmmy/base/BaseActivity.java | 121 ++++---- .../java/gr/thmmy/mthmmy/model/Download.java | 58 ++++ .../gr/thmmy/mthmmy/model/LinkTarget.java | 34 ++- .../mthmmy/utils/FileManager/ThmmyFile.java | 1 - app/src/main/res/drawable/ic_file_upload.xml | 11 + .../main/res/layout/activity_downloads.xml | 62 ++++ .../res/layout/activity_downloads_row.xml | 65 +++++ app/src/main/res/values/strings.xml | 1 + 16 files changed, 776 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java create mode 100644 app/src/main/java/gr/thmmy/mthmmy/model/Download.java create mode 100644 app/src/main/res/drawable/ic_file_upload.xml create mode 100644 app/src/main/res/layout/activity_downloads.xml create mode 100644 app/src/main/res/layout/activity_downloads_row.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f8ce186c..b1e421d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,8 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activities.main.MainActivity"/> + + \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java index 80a607a5..734eafff 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/board/BoardActivity.java @@ -186,6 +186,7 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo @SuppressWarnings("unused") private static final String TAG = "BoardTask"; //Separate tag for AsyncTask + @Override protected void onPreExecute() { if (!isLoadingMore) progressBar.setVisibility(ProgressBar.VISIBLE); if (newTopicFAB.getVisibility() != View.GONE) newTopicFAB.setEnabled(false); @@ -207,6 +208,7 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo return false; } + @Override protected void onPostExecute(Boolean result) { if (!result) { //Parse failed! Report.d(TAG, "Parse failed!"); @@ -214,6 +216,8 @@ public class BoardActivity extends BaseActivity implements BoardAdapter.OnLoadMo , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); finish(); } + if (boardTitle == null || Objects.equals(boardTitle, "")) toolbar.setTitle(boardTitle); + //Parse was successful ++pagesLoaded; if (newTopicFAB.getVisibility() != View.GONE) newTopicFAB.setEnabled(true); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java new file mode 100644 index 00000000..60d83b44 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsActivity.java @@ -0,0 +1,274 @@ +package gr.thmmy.mthmmy.activities.downloads; + +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.Objects; + +import javax.net.ssl.SSLHandshakeException; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.base.BaseActivity; +import gr.thmmy.mthmmy.model.Download; +import gr.thmmy.mthmmy.model.LinkTarget; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +public class DownloadsActivity extends BaseActivity implements DownloadsAdapter.OnLoadMoreListener { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "DownloadsActivity"; + /** + * The key to use when putting download's url String to {@link DownloadsActivity}'s Bundle. + */ + public static final String BUNDLE_DOWNLOADS_URL = "DOWNLOADS_URL"; + /** + * The key to use when putting download's title String to {@link DownloadsActivity}'s Bundle. + */ + public static final String BUNDLE_DOWNLOADS_TITLE = "DOWNLOADS_TITLE"; + private static final String downloadsIndexUrl = "https://www.thmmy.gr/smf/index.php?action=tpmod;dl;"; + private String downloadsUrl; + String downloadsTitle; + private ArrayList parsedDownloads = new ArrayList<>(); + + private MaterialProgressBar progressBar; + private RecyclerView recyclerView; + private DownloadsAdapter downloadsAdapter; + private FloatingActionButton uploadFAB; + + private ParseDownloadPageTask parseDownloadPageTask; + private int numberOfPages = -1; + private int pagesLoaded = 0; + private boolean isLoadingMore; + private static final int visibleThreshold = 5; + private int lastVisibleItem, totalItemCount; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_downloads); + + Bundle extras = getIntent().getExtras(); + downloadsTitle = extras.getString(BUNDLE_DOWNLOADS_TITLE); + if (downloadsTitle == null || Objects.equals(downloadsTitle, "")) + downloadsTitle = "Downloads"; + downloadsUrl = extras.getString(BUNDLE_DOWNLOADS_URL); + if (downloadsUrl != null && !Objects.equals(downloadsUrl, "")) { + LinkTarget.Target target = LinkTarget.resolveLinkTarget(Uri.parse(downloadsUrl)); + if (!target.is(LinkTarget.Target.DOWNLOADS)) { + Report.e(TAG, "Bundle came with a non board url!\nUrl:\n" + downloadsUrl); + Toast.makeText(this, "An error has occurred\nAborting.", Toast.LENGTH_SHORT).show(); + finish(); + } + } else downloadsUrl = downloadsIndexUrl; + + toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(downloadsTitle); + /*setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + }*/ + + createDrawer(); + + progressBar = (MaterialProgressBar) findViewById(R.id.progressBar); + + recyclerView = (RecyclerView) findViewById(R.id.downloads_recycler_view); + recyclerView.setHasFixedSize(true); + final LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); + recyclerView.setLayoutManager(layoutManager); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + downloadsAdapter = new DownloadsAdapter(getApplicationContext(), parsedDownloads); + recyclerView.setAdapter(downloadsAdapter); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + totalItemCount = layoutManager.getItemCount(); + lastVisibleItem = layoutManager.findLastVisibleItemPosition(); + + if (!isLoadingMore && totalItemCount <= (lastVisibleItem + visibleThreshold)) { + isLoadingMore = true; + onLoadMore(); + } + } + }); + + uploadFAB = (FloatingActionButton) findViewById(R.id.download_fab); + uploadFAB.setEnabled(false); + + parseDownloadPageTask = new ParseDownloadPageTask(); + parseDownloadPageTask.execute(downloadsUrl); + } + + @Override + public void onLoadMore() { + if (pagesLoaded < numberOfPages) { + parsedDownloads.add(null); + downloadsAdapter.notifyItemInserted(parsedDownloads.size()); + + //Load data + parseDownloadPageTask = new ParseDownloadPageTask(); + if (downloadsUrl.contains("tpstart")) + parseDownloadPageTask.execute(downloadsUrl.substring(0 + , downloadsUrl.lastIndexOf(";tpstart=")) + ";tpstart=" + pagesLoaded * 10); + else parseDownloadPageTask.execute(downloadsUrl + ";tpstart=" + pagesLoaded * 10); + } + } + + @Override + public void onBackPressed() { + if (drawer.isDrawerOpen()) { + drawer.closeDrawer(); + return; + } + super.onBackPressed(); + } + + @Override + protected void onResume() { + drawer.setSelection(-1); + super.onResume(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + recyclerView.setAdapter(null); + if (parseDownloadPageTask != null && parseDownloadPageTask.getStatus() != AsyncTask.Status.RUNNING) + parseDownloadPageTask.cancel(true); + } + + /** + * An {@link AsyncTask} that handles asynchronous fetching of a downloads page and parsing it's + * data. {@link AsyncTask#onPostExecute(Object) OnPostExecute} method calls {@link RecyclerView#swapAdapter} + * to build graphics. + *

+ *

Calling TopicTask's {@link AsyncTask#execute execute} method needs to have profile's url + * as String parameter!

+ */ + class ParseDownloadPageTask extends AsyncTask { + /** + * Debug Tag for logging debug output to LogCat + */ + private static final String TAG = "ParseDownloadPageTask"; //Separate tag for AsyncTask + private String thisPageUrl; + + @Override + protected void onPreExecute() { + if (!isLoadingMore) progressBar.setVisibility(ProgressBar.VISIBLE); + if (uploadFAB.getVisibility() != View.GONE) uploadFAB.setEnabled(false); + } + + @Override + protected Void doInBackground(String... downloadsUrl) { + thisPageUrl = downloadsUrl[0]; + Request request = new Request.Builder() + .url(downloadsUrl[0]) + .build(); + try { + Response response = BaseActivity.getClient().newCall(request).execute(); + parseDownloads(Jsoup.parse(response.body().string())); + } catch (SSLHandshakeException e) { + Report.w(TAG, "Certificate problem (please switch to unsafe connection)."); + } catch (Exception e) { + Report.e("TAG", "ERROR", e); + } + return null; + } + + @Override + protected void onPostExecute(Void voids) { + if (downloadsTitle == null || Objects.equals(downloadsTitle, "")) + toolbar.setTitle(downloadsTitle); + + ++pagesLoaded; + if (uploadFAB.getVisibility() != View.GONE) uploadFAB.setEnabled(true); + progressBar.setVisibility(ProgressBar.INVISIBLE); + downloadsAdapter.notifyDataSetChanged(); + isLoadingMore = false; + } + + private void parseDownloads(Document downloadPage) { + if (downloadsTitle == null || Objects.equals(downloadsTitle, "")) + downloadsTitle = downloadPage.select("div.nav>b>a.nav").last().text(); + + //Removes loading item + if (isLoadingMore) { + if (parsedDownloads.size() > 0) parsedDownloads.remove(parsedDownloads.size() - 1); + } + + Download.DownloadItemType type; + if (LinkTarget.resolveLinkTarget(Uri.parse(thisPageUrl)).is(LinkTarget. + Target.DOWNLOADS_CATEGORY)) type = Download.DownloadItemType.DOWNLOADS_CATEGORY; + else type = Download.DownloadItemType.DOWNLOADS_FILE; + + Elements pages = downloadPage.select("a.navPages"); + if (pages != null) { + for (Element page : pages) { + int pageNumber = Integer.parseInt(page.text()); + if (pageNumber > numberOfPages) numberOfPages = pageNumber; + } + } else numberOfPages = 1; + + Elements rows = downloadPage.select("table.tborder>tbody>tr"); + if (type == Download.DownloadItemType.DOWNLOADS_CATEGORY) { + Elements navigationLinks = downloadPage.select("div.nav>b"); + for (Element row : rows) { + if (row.select("td").size() == 1) continue; + + String url = row.select("b>a").first().attr("href"), + title = row.select("b>a").first().text(), + subtitle = row.select("div.smalltext:not(:has(a))").text(); + if (!row.select("td").last().hasClass("windowbg2")) { + if (navigationLinks.size() < 4) { + + parsedDownloads.add(new Download(type, url, title, subtitle, null, + true, null)); + } else { + String stats = row.text(); + stats = stats.replace(title, "").replace(subtitle, "").trim(); + parsedDownloads.add(new Download(type, url, title, subtitle, stats, + false, null)); + } + } else { + String stats = row.text(); + stats = stats.replace(title, "").replace(subtitle, "").trim(); + parsedDownloads.add(new Download(type, url, title, subtitle, stats, + false, null)); + } + } + } else { + parsedDownloads.add(new Download(type, + rows.select("b>a").first().attr("href"), + rows.select("b>a").first().text(), + rows.select("div.smalltext:not(:has(a))").text(), + rows.select("span:not(:has(a))").first().text(), + false, + rows.select("span:has(a)").first().html())); + } + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java new file mode 100644 index 00000000..8abfdf55 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/downloads/DownloadsAdapter.java @@ -0,0 +1,181 @@ +package gr.thmmy.mthmmy.activities.downloads; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Objects; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.model.Download; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.downloads.DownloadsActivity.BUNDLE_DOWNLOADS_TITLE; +import static gr.thmmy.mthmmy.activities.downloads.DownloadsActivity.BUNDLE_DOWNLOADS_URL; + +class DownloadsAdapter extends RecyclerView.Adapter { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "DownloadsAdapter"; + private final int VIEW_TYPE_DOWNLOAD = 0; + private final int VIEW_TYPE_LOADING = 1; + + private final Context context; + private ArrayList parsedDownloads = new ArrayList<>(); + private final ArrayList downloadExpandableVisibility = new ArrayList<>(); + + DownloadsAdapter(Context context, ArrayList parsedDownloads) { + this.context = context; + this.parsedDownloads = parsedDownloads; + } + + interface OnLoadMoreListener { + void onLoadMore(); + } + + @Override + public int getItemViewType(int position) { + return (parsedDownloads.get(position) == null) ? VIEW_TYPE_LOADING : VIEW_TYPE_DOWNLOAD; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_DOWNLOAD) { + View download = LayoutInflater.from(parent.getContext()). + inflate(R.layout.activity_downloads_row, parent, false); + return new DownloadViewHolder(download); + } else if (viewType == VIEW_TYPE_LOADING) { + View loading = LayoutInflater.from(parent.getContext()). + inflate(R.layout.recycler_loading_item, parent, false); + return new LoadingViewHolder(loading); + } + return null; + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { + if (holder instanceof DownloadViewHolder) { + final Download download = parsedDownloads.get(position); + final DownloadViewHolder downloadViewHolder = (DownloadViewHolder) holder; + + if (downloadExpandableVisibility.size() != parsedDownloads.size()) { + for (int i = downloadExpandableVisibility.size(); i < parsedDownloads.size(); ++i) + downloadExpandableVisibility.add(false); + } + + if (download.getType() == Download.DownloadItemType.DOWNLOADS_CATEGORY) { + downloadViewHolder.downloadRow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(context, DownloadsActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_DOWNLOADS_URL, download.getUrl()); + extras.putString(BUNDLE_DOWNLOADS_TITLE, download.getTitle()); + intent.putExtras(extras); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + + if (downloadExpandableVisibility.get(downloadViewHolder.getAdapterPosition())) { + downloadViewHolder.informationExpandable.setVisibility(View.VISIBLE); + downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_up); + } else { + downloadViewHolder.informationExpandable.setVisibility(View.GONE); + downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_down); + } + downloadViewHolder.informationExpandableBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final boolean visible = downloadExpandableVisibility.get(downloadViewHolder. + getAdapterPosition()); + if (visible) { + downloadViewHolder.informationExpandable.setVisibility(View.GONE); + downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_down); + } else { + downloadViewHolder.informationExpandable.setVisibility(View.VISIBLE); + downloadViewHolder.informationExpandableBtn.setImageResource(R.drawable.ic_arrow_drop_up); + } + downloadExpandableVisibility.set(downloadViewHolder.getAdapterPosition(), !visible); + } + }); + downloadViewHolder.title.setTypeface(Typeface.createFromAsset(context.getAssets() + , "fonts/fontawesome-webfont.ttf")); + if (download.hasSubCategory()) { + String tmp = context.getResources().getString(R.string.fa_folder) + " " + + download.getTitle(); + downloadViewHolder.title.setText(tmp); + } else { + String tmp = context.getResources().getString(R.string.fa_file) + " " + + download.getTitle(); + downloadViewHolder.title.setText(tmp); + } + } else { + //TODO implement download on click + + downloadViewHolder.upperLinear.setBackgroundColor(context.getResources().getColor(R.color.background)); + downloadViewHolder.informationExpandable.setVisibility(View.VISIBLE); + downloadViewHolder.informationExpandableBtn.setVisibility(View.GONE); + downloadViewHolder.informationExpandableBtn.setEnabled(false); + downloadViewHolder.title.setText(download.getTitle()); + } + + downloadViewHolder.subTitle.setText(download.getSubTitle()); + String tmp = download.getExtraInfo(); + if (tmp != null && !Objects.equals(tmp, "")) + downloadViewHolder.extraInfo.setText(tmp); + else downloadViewHolder.extraInfo.setVisibility(View.GONE); + tmp = download.getStatNumbers(); + if (tmp != null && !Objects.equals(tmp, "")) + downloadViewHolder.uploaderDate.setText(tmp); + else downloadViewHolder.uploaderDate.setVisibility(View.GONE); + } else if (holder instanceof LoadingViewHolder) { + LoadingViewHolder loadingViewHolder = (LoadingViewHolder) holder; + loadingViewHolder.progressBar.setIndeterminate(true); + } + } + + @Override + public int getItemCount() { + return parsedDownloads.size(); + } + + private static class DownloadViewHolder extends RecyclerView.ViewHolder { + final LinearLayout upperLinear, downloadRow, informationExpandable; + final TextView title, subTitle, extraInfo, uploaderDate; + final ImageButton informationExpandableBtn; + + DownloadViewHolder(View download) { + super(download); + upperLinear = (LinearLayout) download.findViewById(R.id.upper_linear); + downloadRow = (LinearLayout) download.findViewById(R.id.download_row); + informationExpandable = (LinearLayout) download.findViewById(R.id.child_board_expandable); + title = (TextView) download.findViewById(R.id.download_title); + subTitle = (TextView) download.findViewById(R.id.download_sub_title); + extraInfo = (TextView) download.findViewById(R.id.download_extra_info); + uploaderDate = (TextView) download.findViewById(R.id.download_uploader_date); + informationExpandableBtn = (ImageButton) download.findViewById(R.id.download_information_button); + } + } + + private static class LoadingViewHolder extends RecyclerView.ViewHolder { + final MaterialProgressBar progressBar; + + LoadingViewHolder(View itemView) { + super(itemView); + progressBar = (MaterialProgressBar) itemView.findViewById(R.id.recycler_progress_bar); + } + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java index 789bcfe4..d99f7d65 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/stats/StatsFragment.java @@ -248,11 +248,13 @@ public class StatsFragment extends Fragment { postingActivityByTimeChartXAxis.setGranularity(1f); LineDataSet postingActivityByTimeDataSet = new LineDataSet(postingActivityByTime, null); - 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)); + 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); @@ -285,11 +287,13 @@ public class StatsFragment extends Fragment { mostPopularBoardsByPostsChartYAxis.setGranularity(1f); BarDataSet mostPopularBoardsByPostsDataSet = new BarDataSet(mostPopularBoardsByPosts, null); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - mostPopularBoardsByPostsDataSet.setColors(getResources().getColor(R.color.accent, null)); - } else - //noinspection deprecation - mostPopularBoardsByPostsDataSet.setColors(getResources().getColor(R.color.accent)); + if (isAdded()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mostPopularBoardsByPostsDataSet.setColors(getResources().getColor(R.color.accent, null)); + } else + //noinspection deprecation + mostPopularBoardsByPostsDataSet.setColors(getResources().getColor(R.color.accent)); + } mostPopularBoardsByPostsDataSet.setDrawValues(false); mostPopularBoardsByPostsDataSet.setValueTextColor(Color.WHITE); @@ -324,11 +328,13 @@ public class StatsFragment extends Fragment { mostPopularBoardsByActivityChartYAxis.setLabelCount(10, false); BarDataSet mostPopularBoardsByActivityDataSet = new BarDataSet(mostPopularBoardsByActivity, null); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - mostPopularBoardsByActivityDataSet.setColors(getResources().getColor(R.color.accent, null)); - } else - //noinspection deprecation - mostPopularBoardsByActivityDataSet.setColors(getResources().getColor(R.color.accent)); + if (isAdded()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mostPopularBoardsByActivityDataSet.setColors(getResources().getColor(R.color.accent, null)); + } else + //noinspection deprecation + mostPopularBoardsByActivityDataSet.setColors(getResources().getColor(R.color.accent)); + } mostPopularBoardsByActivityDataSet.setDrawValues(false); mostPopularBoardsByActivityDataSet.setValueTextColor(Color.WHITE); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java index c621f295..97d7f1e8 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/summary/SummaryFragment.java @@ -187,11 +187,12 @@ public class SummaryFragment extends Fragment { } TextView entry = new TextView(this.getContext()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - entry.setTextColor(getResources().getColor(R.color.primary_text, null)); - } else { - //noinspection deprecation - entry.setTextColor(getResources().getColor(R.color.primary_text)); + if (isAdded()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + entry.setTextColor(getResources().getColor(R.color.primary_text, null)); + else + //noinspection deprecation + entry.setTextColor(getResources().getColor(R.color.primary_text)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { entry.setText(Html.fromHtml(profileSummaryRow, Html.FROM_HTML_MODE_LEGACY)); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java index 3da819cb..b940cda7 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicActivity.java @@ -9,7 +9,6 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; @@ -63,7 +62,6 @@ public class TopicActivity extends BaseActivity { */ public static final String BUNDLE_TOPIC_TITLE = "TOPIC_TITLE"; private static final int PERMISSIONS_REQUEST_CODE = 69; - static boolean readWriteAccepted; private static TopicTask topicTask; //About posts private TopicAdapter topicAdapter; @@ -213,7 +211,7 @@ public class TopicActivity extends BaseActivity { topicTask.cancel(true); } - @Override + /*@Override public void onRequestPermissionsResult(int permsRequestCode, @NonNull String[] permissions , @NonNull int[] grantResults) { switch (permsRequestCode) { @@ -221,18 +219,20 @@ public class TopicActivity extends BaseActivity { readWriteAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED; break; } - } + }*/ - private void requestPerms() { //Runtime permissions for devices with API >= 23 + boolean requestPerms() { //Runtime permissions request for devices with API >= 23 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { String[] PERMISSIONS_STORAGE = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; - checkSelfPermission(PERMISSIONS_STORAGE[0]); - checkSelfPermission(PERMISSIONS_STORAGE[1]); - requestPermissions(PERMISSIONS_STORAGE, PERMISSIONS_REQUEST_CODE); - } else readWriteAccepted = true; + if (checkSelfPermission(PERMISSIONS_STORAGE[0]) == PackageManager.PERMISSION_DENIED || + checkSelfPermission(PERMISSIONS_STORAGE[1]) == PackageManager.PERMISSION_DENIED) { + requestPermissions(PERMISSIONS_STORAGE, PERMISSIONS_REQUEST_CODE); + return false; + } else return true; + } else return true; } //--------------------------------------BOTTOM NAV BAR METHODS---------------------------------- @@ -365,6 +365,7 @@ public class TopicActivity extends BaseActivity { private static final int OTHER_ERROR = 2; private static final int SAME_PAGE = 3; + @Override protected void onPreExecute() { progressBar.setVisibility(ProgressBar.VISIBLE); paginationEnabled(false); diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java index 05a2779f..ad96703d 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java @@ -243,7 +243,7 @@ class TopicAdapter extends RecyclerView.Adapter { attached.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - if (TopicActivity.readWriteAccepted) { + if (((TopicActivity) context).requestPerms()) { downloadTask = new DownloadTask(); downloadTask.execute(attachedFile); } else diff --git a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java index 8760198d..1f893856 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/base/BaseActivity.java @@ -24,18 +24,20 @@ import com.mikepenz.materialdrawer.model.interfaces.IProfile; import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.AboutActivity; import gr.thmmy.mthmmy.activities.LoginActivity; +import gr.thmmy.mthmmy.activities.downloads.DownloadsActivity; import gr.thmmy.mthmmy.activities.main.MainActivity; import gr.thmmy.mthmmy.activities.profile.ProfileActivity; import gr.thmmy.mthmmy.session.SessionManager; import okhttp3.OkHttpClient; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static gr.thmmy.mthmmy.activities.downloads.DownloadsActivity.BUNDLE_DOWNLOADS_TITLE; +import static gr.thmmy.mthmmy.activities.downloads.DownloadsActivity.BUNDLE_DOWNLOADS_URL; import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_PROFILE_URL; import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_THUMBNAIL_URL; import static gr.thmmy.mthmmy.activities.profile.ProfileActivity.BUNDLE_USERNAME; -public abstract class BaseActivity extends AppCompatActivity -{ +public abstract class BaseActivity extends AppCompatActivity { // Client & Cookies protected static OkHttpClient client; @@ -49,10 +51,10 @@ public abstract class BaseActivity extends AppCompatActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if(client==null) + if (client == null) client = BaseApplication.getInstance().getClient(); //must check every time - e.g. // they become null when app restarts after crash - if(sessionManager==null) + if (sessionManager == null) sessionManager = BaseApplication.getInstance().getSessionManager(); } @@ -65,27 +67,25 @@ public abstract class BaseActivity extends AppCompatActivity @Override protected void onPause() { super.onPause(); - if(drawer!=null) //close drawer animation after returning to activity + if (drawer != null) //close drawer animation after returning to activity drawer.closeDrawer(); } - public static OkHttpClient getClient() - { + public static OkHttpClient getClient() { return client; } - public static SessionManager getSessionManager() - { + public static SessionManager getSessionManager() { return sessionManager; } //TODO: move stuff below (?) //------------------------------------------DRAWER STUFF---------------------------------------- - protected static final int HOME_ID=0; - protected static final int DOWNLOADS_ID=1; - protected static final int LOG_ID =2; - protected static final int ABOUT_ID=3; + protected static final int HOME_ID = 0; + protected static final int DOWNLOADS_ID = 1; + protected static final int LOG_ID = 2; + protected static final int ABOUT_ID = 3; private AccountHeader accountHeader; private ProfileDrawerItem profileDrawerItem; @@ -96,42 +96,41 @@ public abstract class BaseActivity extends AppCompatActivity /** * Call only after initializing Toolbar */ - protected void createDrawer() - { + protected void createDrawer() { final int primaryColor = ContextCompat.getColor(this, R.color.iron); final int selectedPrimaryColor = ContextCompat.getColor(this, R.color.primary_dark); final int selectedSecondaryColor = ContextCompat.getColor(this, R.color.accent); //Drawer Icons - homeIcon =new IconicsDrawable(this) + homeIcon = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_home) .color(primaryColor); - homeIconSelected =new IconicsDrawable(this) + homeIconSelected = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_home) .color(selectedSecondaryColor); - downloadsIcon =new IconicsDrawable(this) + downloadsIcon = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_download) .color(primaryColor); - downloadsIconSelected =new IconicsDrawable(this) + downloadsIconSelected = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_download) .color(selectedSecondaryColor); - loginIcon =new IconicsDrawable(this) + loginIcon = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_sign_in) .color(primaryColor); - logoutIcon =new IconicsDrawable(this) + logoutIcon = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_sign_out) .color(primaryColor); - aboutIcon =new IconicsDrawable(this) + aboutIcon = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_info_circle) .color(primaryColor); - aboutIconSelected =new IconicsDrawable(this) + aboutIconSelected = new IconicsDrawable(this) .icon(FontAwesome.Icon.faw_info_circle) .color(selectedSecondaryColor); @@ -146,7 +145,6 @@ public abstract class BaseActivity extends AppCompatActivity .withSelectedIcon(homeIconSelected); - if (sessionManager.isLoggedIn()) //When logged in { loginLogoutItem = new PrimaryDrawerItem() @@ -164,8 +162,7 @@ public abstract class BaseActivity extends AppCompatActivity .withName(R.string.downloads) .withIcon(downloadsIcon) .withSelectedIcon(downloadsIconSelected); - } - else + } else loginLogoutItem = new PrimaryDrawerItem() .withTextColor(primaryColor) .withSelectedColor(selectedSecondaryColor) @@ -195,12 +192,11 @@ public abstract class BaseActivity extends AppCompatActivity .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { @Override public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { - if(sessionManager.isLoggedIn()) - { + if (sessionManager.isLoggedIn()) { Intent intent = new Intent(BaseActivity.this, ProfileActivity.class); Bundle extras = new Bundle(); extras.putString(BUNDLE_PROFILE_URL, "https://www.thmmy.gr/smf/index.php?action=profile"); - if(!sessionManager.hasAvatar()) + if (!sessionManager.hasAvatar()) extras.putString(BUNDLE_THUMBNAIL_URL, ""); else extras.putString(BUNDLE_THUMBNAIL_URL, sessionManager.getAvatarLink()); @@ -220,44 +216,38 @@ public abstract class BaseActivity extends AppCompatActivity DrawerBuilder drawerBuilder = new DrawerBuilder() .withActivity(this) .withToolbar(toolbar) - .withDrawerWidthDp((int)BaseApplication.getInstance().getDpWidth()/2) + .withDrawerWidthDp((int) BaseApplication.getInstance().getDpWidth() / 2) .withSliderBackgroundColor(ContextCompat.getColor(this, R.color.primary_light)) .withAccountHeader(accountHeader) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override public boolean onItemClick(View view, int position, IDrawerItem drawerItem) { - if(drawerItem.equals(HOME_ID)) - { - if(!(BaseActivity.this instanceof MainActivity)) - { + if (drawerItem.equals(HOME_ID)) { + if (!(BaseActivity.this instanceof MainActivity)) { Intent i = new Intent(BaseActivity.this, MainActivity.class); startActivity(i); } - } -// else if(drawerItem.equals(DOWNLOADS_ID)) -// { -// if (sessionManager.isLoggedIn()) //When logged out or if user is guest -// { -// Intent i = new Intent(BaseActivity.this, DownloadsActivity.class); -// startActivity(i); -// } -// } - else if(drawerItem.equals(LOG_ID)) - { + } else if (drawerItem.equals(DOWNLOADS_ID)) { + if (sessionManager.isLoggedIn()) //When logged out or if user is guest + { + Intent i = new Intent(BaseActivity.this, DownloadsActivity.class); + Bundle extras = new Bundle(); + extras.putString(BUNDLE_DOWNLOADS_URL, ""); + extras.putString(BUNDLE_DOWNLOADS_TITLE, null); + i.putExtras(extras); + startActivity(i); + } + } else if (drawerItem.equals(LOG_ID)) { if (!sessionManager.isLoggedIn()) //When logged out or if user is guest { Intent intent = new Intent(BaseActivity.this, LoginActivity.class); startActivity(intent); finish(); overridePendingTransition(R.anim.push_right_in, R.anim.push_right_out); - } - else + } else new LogoutTask().execute(); - } - else if(drawerItem.equals(ABOUT_ID)) - { - if(!(BaseActivity.this instanceof AboutActivity)) - { + } else if (drawerItem.equals(ABOUT_ID)) { + if (!(BaseActivity.this instanceof AboutActivity)) { Intent i = new Intent(BaseActivity.this, AboutActivity.class); startActivity(i); } @@ -269,10 +259,10 @@ public abstract class BaseActivity extends AppCompatActivity } }); - if(sessionManager.isLoggedIn()) - drawerBuilder.addDrawerItems(homeItem,downloadsItem,loginLogoutItem,aboutItem); + if (sessionManager.isLoggedIn()) + drawerBuilder.addDrawerItems(homeItem, downloadsItem, loginLogoutItem, aboutItem); else - drawerBuilder.addDrawerItems(homeItem,loginLogoutItem,aboutItem); + drawerBuilder.addDrawerItems(homeItem, loginLogoutItem, aboutItem); drawer = drawerBuilder.build(); @@ -286,10 +276,8 @@ public abstract class BaseActivity extends AppCompatActivity }); } - protected void updateDrawer() - { - if(drawer!=null) - { + protected void updateDrawer() { + if (drawer != null) { if (!sessionManager.isLoggedIn()) //When logged out or if user is guest { drawer.removeItem(DOWNLOADS_ID); @@ -299,9 +287,7 @@ public abstract class BaseActivity extends AppCompatActivity .paddingDp(10) .color(ContextCompat.getColor(this, R.color.primary_light)) .backgroundColor(ContextCompat.getColor(this, R.color.primary))); - } - else - { + } else { loginLogoutItem.withName(R.string.logout).withIcon(logoutIcon); //Swap login with logout profileDrawerItem.withName(sessionManager.getUsername()).withIcon(sessionManager.getAvatarLink()); } @@ -313,9 +299,10 @@ public abstract class BaseActivity extends AppCompatActivity //-------------------------------------------LOGOUT------------------------------------------------- + /** - * Result toast will always display a success, because when user chooses logout all data are - * cleared regardless of the actual outcome + * Result toast will always display a success, because when user chooses logout all data are + * cleared regardless of the actual outcome */ protected class LogoutTask extends AsyncTask { //Attempt logout ProgressDialog progressDialog; @@ -324,8 +311,7 @@ public abstract class BaseActivity extends AppCompatActivity return sessionManager.logout(); } - protected void onPreExecute() - { //Show a progress dialog until done + protected void onPreExecute() { //Show a progress dialog until done progressDialog = new ProgressDialog(BaseActivity.this, R.style.AppTheme_Dark_Dialog); progressDialog.setCancelable(false); @@ -334,8 +320,7 @@ public abstract class BaseActivity extends AppCompatActivity progressDialog.show(); } - protected void onPostExecute(Integer result) - { + protected void onPostExecute(Integer result) { Toast.makeText(getBaseContext(), "Logged out successfully!", Toast.LENGTH_LONG).show(); updateDrawer(); progressDialog.dismiss(); diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/Download.java b/app/src/main/java/gr/thmmy/mthmmy/model/Download.java new file mode 100644 index 00000000..5216fea4 --- /dev/null +++ b/app/src/main/java/gr/thmmy/mthmmy/model/Download.java @@ -0,0 +1,58 @@ +package gr.thmmy.mthmmy.model; + +public class Download { + public enum DownloadItemType {DOWNLOADS_CATEGORY, DOWNLOADS_FILE} + + private final String url, title, subTitle, statNumbers, extraInfo; + private final boolean hasSubCategory; + private final DownloadItemType type; + + public Download() { + type = null; + url = null; + title = null; + subTitle = null; + statNumbers = null; + hasSubCategory = false; + extraInfo = null; + } + + public Download(DownloadItemType type, String url, String title, String subTitle, + String statNumbers, boolean hasSubCategory, String extraInfo) { + this.type = type; + this.url = url; + this.title = title; + this.subTitle = subTitle; + this.statNumbers = statNumbers; + this.hasSubCategory = hasSubCategory; + this.extraInfo = extraInfo; + } + + public DownloadItemType getType() { + return type; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public String getSubTitle() { + return subTitle; + } + + public String getStatNumbers() { + return statNumbers; + } + + public String getExtraInfo() { + return extraInfo; + } + + public boolean hasSubCategory() { + return hasSubCategory; + } +} diff --git a/app/src/main/java/gr/thmmy/mthmmy/model/LinkTarget.java b/app/src/main/java/gr/thmmy/mthmmy/model/LinkTarget.java index 4a112d2a..991c79bb 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/model/LinkTarget.java +++ b/app/src/main/java/gr/thmmy/mthmmy/model/LinkTarget.java @@ -72,7 +72,19 @@ public class LinkTarget { /** * Link points to a profile. */ - PROFILE; + PROFILE, + /** + * Link points to a download. + */ + DOWNLOADS_CATEGORY, + /** + * Link points to a download category. + */ + DOWNLOADS_FILE, + /** + * Link points to downloads. + */ + DOWNLOADS; /** * This method defines a custom equality check for {@link Target} enums. It does not check @@ -81,19 +93,23 @@ public class LinkTarget { * cases described below, false otherwise.

    *
  • (Everything but {@link #NOT_THMMY}).is({@link #THMMY}) returns true
  • *
  • {@link #PROFILE_SUMMARY}.is({@link #PROFILE}) returns true
  • - *
  • {@link #PROFILE_LATEST_POSTS}.is({@link #PROFILE}) returns true
  • - *
  • {@link #PROFILE_STATS}.is({@link #PROFILE}) returns true
  • *
  • {@link #PROFILE}.is({@link #PROFILE_SUMMARY}) returns false
  • + *
  • {@link #PROFILE_LATEST_POSTS}.is({@link #PROFILE}) returns true
  • *
  • {@link #PROFILE}.is({@link #PROFILE_LATEST_POSTS}) returns false
  • - *
  • {@link #PROFILE}.is({@link #PROFILE_STATS}) returns false
+ *
  • {@link #PROFILE_STATS}.is({@link #PROFILE}) returns true
  • + *
  • {@link #PROFILE}.is({@link #PROFILE_STATS}) returns false
  • + *
  • {@link #DOWNLOADS_CATEGORY}.is({@link #DOWNLOADS}) returns true
  • + *
  • {@link #DOWNLOADS}.is({@link #DOWNLOADS_CATEGORY}) returns false
  • + *
  • {@link #DOWNLOADS_FILE}.is({@link #DOWNLOADS}) returns true
  • + *
  • {@link #DOWNLOADS}.is({@link #DOWNLOADS_FILE}) returns false
  • * * @param other another Target * @return true if enums are equal, false otherwise */ public boolean is(Target other) { - return (this == PROFILE_LATEST_POSTS || - this == PROFILE_STATS || - this == PROFILE_SUMMARY) && other == PROFILE + return ((this == PROFILE_LATEST_POSTS || this == PROFILE_STATS || this == PROFILE_SUMMARY) + && other == PROFILE) + || ((this == DOWNLOADS_FILE || this == DOWNLOADS_CATEGORY) && other == DOWNLOADS) || (this != NOT_THMMY && other == THMMY) || this == other; } @@ -130,6 +146,10 @@ public class LinkTarget { else return Target.PROFILE_SUMMARY; } else if (uriString.contains("action=unread")) return Target.UNREAD_POSTS; + else if (uriString.contains("action=tpmod;dl=item")) + return Target.DOWNLOADS_FILE; + else if (uriString.contains("action=tpmod;dl")) + return Target.DOWNLOADS_CATEGORY; Report.v(TAG, "Unknown thmmy link found, link: " + uriString); return Target.UNKNOWN_THMMY; } diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java b/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java index 5de8cbc8..eed84689 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java @@ -164,7 +164,6 @@ public class ThmmyFile { request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); } catch (IllegalStateException e) { Report.d(TAG, "External directory not available!", e); - Log.d(TAG, "External directory not available!", e); throw e; } diff --git a/app/src/main/res/drawable/ic_file_upload.xml b/app/src/main/res/drawable/ic_file_upload.xml new file mode 100644 index 00000000..74f3b4ca --- /dev/null +++ b/app/src/main/res/drawable/ic_file_upload.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/activity_downloads.xml b/app/src/main/res/layout/activity_downloads.xml new file mode 100644 index 00000000..3547d5ea --- /dev/null +++ b/app/src/main/res/layout/activity_downloads.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_downloads_row.xml b/app/src/main/res/layout/activity_downloads_row.xml new file mode 100644 index 00000000..f4fd93b5 --- /dev/null +++ b/app/src/main/res/layout/activity_downloads_row.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b469a93..dccbf7e3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,4 +81,5 @@ +