From 9e9b79a6d3204e95bc237ddf11ef47dfdb0f0b1f Mon Sep 17 00:00:00 2001 From: Apostolof Date: Thu, 5 Jan 2017 23:40:18 +0200 Subject: [PATCH] StatsFragment completion and minor fixes --- .../activities/profile/ProfileActivity.java | 6 +- .../latestPosts/LatestPostsFragment.java | 5 +- .../profile/stats/StatsFragment.java | 355 +++++++++++++++++- .../profile/summary/SummaryFragment.java | 19 +- .../main/res/drawable/line_chart_gradient.xml | 10 + .../main/res/layout-v21/activity_profile.xml | 7 +- app/src/main/res/layout/activity_main.xml | 4 +- app/src/main/res/layout/activity_profile.xml | 6 +- .../res/layout/profile_fragment_stats.xml | 149 ++++---- app/src/main/res/values/strings.xml | 1 + 10 files changed, 475 insertions(+), 87 deletions(-) create mode 100644 app/src/main/res/drawable/line_chart_gradient.xml diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java index beec6b08..900fbb32 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/ProfileActivity.java @@ -27,6 +27,7 @@ import org.jsoup.nodes.Element; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javax.net.ssl.SSLHandshakeException; @@ -34,6 +35,7 @@ import gr.thmmy.mthmmy.R; import gr.thmmy.mthmmy.activities.LoginActivity; import gr.thmmy.mthmmy.activities.base.BaseActivity; import gr.thmmy.mthmmy.activities.profile.latestPosts.LatestPostsFragment; +import gr.thmmy.mthmmy.activities.profile.stats.StatsFragment; import gr.thmmy.mthmmy.activities.profile.summary.SummaryFragment; import gr.thmmy.mthmmy.activities.topic.TopicActivity; import gr.thmmy.mthmmy.data.TopicSummary; @@ -105,7 +107,7 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment progressBar = (MaterialProgressBar) findViewById(R.id.progressBar); ImageView thumbnailView = (ImageView) findViewById(R.id.user_thumbnail); - if (thumbnailUrl != null) + if (thumbnailUrl != null && !Objects.equals(thumbnailUrl, "")) //noinspection ConstantConditions Picasso.with(this) .load(thumbnailUrl) @@ -252,7 +254,7 @@ public class ProfileActivity extends BaseActivity implements LatestPostsFragment ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager()); adapter.addFrag(SummaryFragment.newInstance(profilePage), "SUMMARY"); adapter.addFrag(LatestPostsFragment.newInstance(profileUrl), "LATEST POSTS"); - //adapter.addFrag(new ); + adapter.addFrag(StatsFragment.newInstance(profileUrl), "STATS"); viewPager.setAdapter(adapter); } diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java index 336ab0c9..b0417809 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/profile/latestPosts/LatestPostsFragment.java @@ -5,7 +5,6 @@ import android.os.Bundle; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -175,10 +174,8 @@ public class LatestPostsFragment extends BaseFragment { } protected Boolean doInBackground(String... profileUrl) { - String pageUrl = profileUrl[0]; - Request request = new Request.Builder() - .url(pageUrl) + .url(profileUrl[0]) .build(); try { Response response = BaseActivity.getClient().newCall(request).execute(); 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 b16139c4..33dc692d 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 @@ -1,8 +1,355 @@ package gr.thmmy.mthmmy.activities.profile.stats; -/** - * Created by apostolof on 1/1/17. - */ +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; -public class StatsFragment { +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.formatter.PercentFormatter; + +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.List; + +import javax.net.ssl.SSLHandshakeException; + +import gr.thmmy.mthmmy.R; +import gr.thmmy.mthmmy.activities.base.BaseActivity; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; +import mthmmy.utils.Report; +import okhttp3.Request; +import okhttp3.Response; + +public class StatsFragment extends Fragment { + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "StatsFragment"; + /** + * The key to use when putting profile's url String to {@link StatsFragment}'s Bundle. + */ + static final String PROFILE_URL = "PROFILE_DOCUMENT"; + private String profileUrl; + private ProfileStatsTask profileStatsTask; + private LinearLayout mainContent; + private MaterialProgressBar progressBar; + private boolean haveParsed = false; + + private String generalStatisticsTitle = "", generalStatistics = "", postingActivityByTimeTitle = "", mostPopularBoardsByPostsTitle = "", mostPopularBoardsByActivityTitle = ""; + private List postingActivityByTime = new ArrayList<>(); + private List mostPopularBoardsByPosts = new ArrayList<>(), mostPopularBoardsByActivity = new ArrayList<>(); + private ArrayList mostPopularBoardsByPostsLabels = new ArrayList<>(), mostPopularBoardsByActivityLabels = new ArrayList<>(); + + public StatsFragment() { + // Required empty public constructor + } + + /** + * Use ONLY this factory method to create a new instance of this fragment using the provided + * parameters. + * + * @param profileUrl String containing this profile's url + * @return A new instance of fragment Stats. + */ + public static StatsFragment newInstance(String profileUrl) { + StatsFragment fragment = new StatsFragment(); + Bundle args = new Bundle(); + args.putString(PROFILE_URL, profileUrl); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + profileUrl = getArguments().getString(PROFILE_URL); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.profile_fragment_stats, container, false); + mainContent = (LinearLayout) rootView.findViewById(R.id.main_content); + progressBar = (MaterialProgressBar) rootView.findViewById(R.id.progressBar); + if (haveParsed) + populateLayout(); + return rootView; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (!haveParsed) { + profileStatsTask = new ProfileStatsTask(); + profileStatsTask.execute(profileUrl + ";sa=statPanel"); + } + Report.d(TAG, "onActivityCreated"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (profileStatsTask != null && profileStatsTask.getStatus() != AsyncTask.Status.RUNNING) + profileStatsTask.cancel(true); + } + + /** + * An {@link AsyncTask} that handles asynchronous parsing of a profile page's data. + * {@link AsyncTask#onPostExecute(Object) OnPostExecute} method calls {@link #()} + * to build graphics. + *

+ *

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

+ */ + public class ProfileStatsTask extends AsyncTask { + //Class variables + /** + * Debug Tag for logging debug output to LogCat + */ + @SuppressWarnings("unused") + private static final String TAG = "ProfileStatsTask"; //Separate tag for AsyncTask + + @Override + protected void onPreExecute() { + progressBar.setVisibility(ProgressBar.VISIBLE); + haveParsed = true; + } + + @Override + protected Boolean doInBackground(String... profileUrl) { + Request request = new Request.Builder() + .url(profileUrl[0]) + .build(); + try { + Response response = BaseActivity.getClient().newCall(request).execute(); + return parseStats(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 false; + } + + @Override + protected void onPostExecute(Boolean result) { + if (!result) { //Parse failed! + Toast.makeText(getContext() + , "Fatal error!\n Aborting...", Toast.LENGTH_LONG).show(); + getActivity().finish(); + } + //Parse was successful + progressBar.setVisibility(ProgressBar.INVISIBLE); + populateLayout(); + } + + private boolean parseStats(Document statsPage) { + if (statsPage.select("table.bordercolor>tbody>tr").size() != 6) + return false; //It's my profile + { + Elements titleRows = statsPage.select("table.bordercolor>tbody>tr.titlebg"); + generalStatisticsTitle = titleRows.first().text(); + postingActivityByTimeTitle = titleRows.get(1).text(); + mostPopularBoardsByPostsTitle = titleRows.last().select("td").first().text(); + mostPopularBoardsByActivityTitle = titleRows.last().select("td").last().text(); + } + { + Elements statsRows = statsPage.select("table.bordercolor>tbody>tr:not(.titlebg)"); + { + Elements generalStatisticsRows = statsRows.first().select("tbody>tr"); + for (Element generalStatisticsRow : generalStatisticsRows) + generalStatistics += generalStatisticsRow.text() + "\n"; + generalStatistics = generalStatistics.trim(); + } + { + Elements postingActivityByTimeCols = statsRows.get(1).select(">td").last() + .select("tr").first().select("td[width=4%]"); + int i = -1; + for (Element postingActivityByTimeColumn : postingActivityByTimeCols) { + postingActivityByTime.add(new Entry(++i, Float.parseFloat(postingActivityByTimeColumn + .select("img").first().attr("height")))); + } + } + { + Elements mostPopularBoardsByPostsRows = statsRows.last().select(">td").get(1) + .select(">table>tbody>tr"); + int i = mostPopularBoardsByPostsRows.size(); + for (Element mostPopularBoardsByPostsRow : mostPopularBoardsByPostsRows) { + Elements dataCols = mostPopularBoardsByPostsRow.select("td"); + mostPopularBoardsByPosts.add(new BarEntry(--i, + Integer.parseInt(dataCols.last().text()))); + mostPopularBoardsByPostsLabels.add(dataCols.first().text()); + } + } + { + Elements mostPopularBoardsByActivityRows = statsRows.last().select(">td").last() + .select(">table>tbody>tr"); + Log.d(TAG, String.valueOf(mostPopularBoardsByActivityRows.size())); + int i = mostPopularBoardsByActivityRows.size(); + for (Element mostPopularBoardsByActivityRow : mostPopularBoardsByActivityRows) { + Elements dataCols = mostPopularBoardsByActivityRow.select("td"); + String tmp = dataCols.last().text(); + mostPopularBoardsByActivity.add(new BarEntry(--i, + Float.parseFloat(tmp.substring(0, tmp.indexOf("%"))))); + mostPopularBoardsByActivityLabels.add(dataCols.first().text()); + } + } + } + return true; + } + } + + private void populateLayout() { + ((TextView) mainContent.findViewById(R.id.general_statistics_title)) + .setText(generalStatisticsTitle); + ((TextView) mainContent.findViewById(R.id.general_statistics)) + .setText(generalStatistics); + ((TextView) mainContent.findViewById(R.id.posting_activity_by_time_title)) + .setText(postingActivityByTimeTitle); + + LineChart postingActivityByTimeChart = (LineChart) mainContent + .findViewById(R.id.posting_activity_by_time_chart); + postingActivityByTimeChart.setDescription(null); + postingActivityByTimeChart.getLegend().setEnabled(false); + postingActivityByTimeChart.setScaleYEnabled(false); + postingActivityByTimeChart.setDrawBorders(true); + postingActivityByTimeChart.getAxisLeft().setEnabled(false); + postingActivityByTimeChart.getAxisRight().setEnabled(false); + XAxis postingActivityByTimeChartXAxis = postingActivityByTimeChart.getXAxis(); + postingActivityByTimeChartXAxis.setTextColor(Color.WHITE); + postingActivityByTimeChartXAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + postingActivityByTimeChartXAxis.setDrawLabels(true); + postingActivityByTimeChartXAxis.setLabelCount(24, false); + 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)); + postingActivityByTimeDataSet.setDrawFilled(true); + postingActivityByTimeDataSet.setDrawCircles(false); + postingActivityByTimeDataSet.setDrawValues(false); + LineData postingActivityByTimeData = new LineData(postingActivityByTimeDataSet); + postingActivityByTimeChart.setData(postingActivityByTimeData); + postingActivityByTimeChart.invalidate(); + + ((TextView) mainContent.findViewById(R.id.most_popular_boards_by_posts_title)) + .setText(mostPopularBoardsByPostsTitle); + + HorizontalBarChart mostPopularBoardsByPostsChart = (HorizontalBarChart) mainContent. + findViewById(R.id.most_popular_boards_by_posts_chart); + mostPopularBoardsByPostsChart.setDescription(null); + mostPopularBoardsByPostsChart.getLegend().setEnabled(false); + mostPopularBoardsByPostsChart.setScaleEnabled(false); + mostPopularBoardsByPostsChart.setDrawBorders(true); + mostPopularBoardsByPostsChart.getAxisLeft().setEnabled(false); + + XAxis mostPopularBoardsByPostsChartXAxis = mostPopularBoardsByPostsChart.getXAxis(); + mostPopularBoardsByPostsChartXAxis.setPosition(XAxis.XAxisPosition.TOP_INSIDE); + mostPopularBoardsByPostsChartXAxis.setTextColor(Color.WHITE); + mostPopularBoardsByPostsChartXAxis.setLabelCount(mostPopularBoardsByPostsLabels.size()); + mostPopularBoardsByPostsChartXAxis.setValueFormatter(new MyXAxisValueFormatter(mostPopularBoardsByPostsLabels)); + + YAxis mostPopularBoardsByPostsChartYAxis = mostPopularBoardsByPostsChart.getAxisRight(); + mostPopularBoardsByPostsChartYAxis.setTextColor(Color.WHITE); + mostPopularBoardsByPostsChartYAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART); + mostPopularBoardsByPostsChartYAxis.setDrawLabels(true); + mostPopularBoardsByPostsChartYAxis.setLabelCount(10, false); + 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)); + mostPopularBoardsByPostsDataSet.setDrawValues(false); + mostPopularBoardsByPostsDataSet.setValueTextColor(Color.WHITE); + + BarData mostPopularBoardsByPostsData = new BarData(mostPopularBoardsByPostsDataSet); + mostPopularBoardsByPostsData.setDrawValues(false); + mostPopularBoardsByPostsData.setValueTextColor(Color.WHITE); + mostPopularBoardsByPostsChart.setData(mostPopularBoardsByPostsData); + mostPopularBoardsByPostsChart.invalidate(); + + ((TextView) mainContent.findViewById(R.id.most_popular_boards_by_activity_title)) + .setText(mostPopularBoardsByActivityTitle); + + HorizontalBarChart mostPopularBoardsByActivityChart = (HorizontalBarChart) mainContent. + findViewById(R.id.most_popular_boards_by_activity_chart); + mostPopularBoardsByActivityChart.setDescription(null); + mostPopularBoardsByActivityChart.getLegend().setEnabled(false); + mostPopularBoardsByActivityChart.setScaleEnabled(false); + mostPopularBoardsByActivityChart.setDrawBorders(true); + mostPopularBoardsByActivityChart.getAxisLeft().setEnabled(false); + + XAxis mostPopularBoardsByActivityChartXAxis = mostPopularBoardsByActivityChart.getXAxis(); + mostPopularBoardsByActivityChartXAxis.setPosition(XAxis.XAxisPosition.TOP_INSIDE); + mostPopularBoardsByActivityChartXAxis.setTextColor(Color.WHITE); + mostPopularBoardsByActivityChartXAxis.setLabelCount(mostPopularBoardsByActivity.size()); + mostPopularBoardsByActivityChartXAxis.setValueFormatter(new MyXAxisValueFormatter(mostPopularBoardsByActivityLabels)); + + YAxis mostPopularBoardsByActivityChartYAxis = mostPopularBoardsByActivityChart.getAxisRight(); + mostPopularBoardsByActivityChartYAxis.setValueFormatter(new PercentFormatter()); + mostPopularBoardsByActivityChartYAxis.setTextColor(Color.WHITE); + mostPopularBoardsByActivityChartYAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART); + mostPopularBoardsByActivityChartYAxis.setDrawLabels(true); + 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)); + mostPopularBoardsByActivityDataSet.setDrawValues(false); + mostPopularBoardsByActivityDataSet.setValueTextColor(Color.WHITE); + + BarData mostPopularBoardsByActivityData = new BarData(mostPopularBoardsByActivityDataSet); + mostPopularBoardsByActivityData.setDrawValues(false); + mostPopularBoardsByActivityData.setValueTextColor(Color.WHITE); + mostPopularBoardsByActivityChart.setData(mostPopularBoardsByActivityData); + mostPopularBoardsByActivityChart.invalidate(); + } + + class MyXAxisValueFormatter implements IAxisValueFormatter { + private ArrayList mValues; + + MyXAxisValueFormatter(ArrayList values) { + this.mValues = values; + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + return mValues.get((int) value); + } + } } 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 be76d2b0..9951d457 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 @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Objects; import gr.thmmy.mthmmy.R; -import gr.thmmy.mthmmy.activities.profile.ProfileActivity; import mthmmy.utils.Report; @@ -77,6 +76,16 @@ public class SummaryFragment extends Fragment { parsedProfileSummaryData = new ArrayList<>(); } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.profile_fragment_summary, container, false); + mainContent = (LinearLayout) rootView.findViewById(R.id.profile_activity_content); + if (!parsedProfileSummaryData.isEmpty()) + populateLayout(); + return rootView; + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -94,14 +103,6 @@ public class SummaryFragment extends Fragment { profileSummaryTask.cancel(true); } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.profile_fragment_summary, container, false); - mainContent = (LinearLayout) rootView.findViewById(R.id.profile_activity_content); - return rootView; - } - /** * An {@link AsyncTask} that handles asynchronous parsing of a profile page's data. * {@link AsyncTask#onPostExecute(Object) OnPostExecute} method calls {@link #populateLayout()} diff --git a/app/src/main/res/drawable/line_chart_gradient.xml b/app/src/main/res/drawable/line_chart_gradient.xml new file mode 100644 index 00000000..37f994c0 --- /dev/null +++ b/app/src/main/res/drawable/line_chart_gradient.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v21/activity_profile.xml b/app/src/main/res/layout-v21/activity_profile.xml index 7601b753..adb8ad6b 100644 --- a/app/src/main/res/layout-v21/activity_profile.xml +++ b/app/src/main/res/layout-v21/activity_profile.xml @@ -75,7 +75,12 @@ + android:layout_height="wrap_content" + android:gravity="center" + app:tabGravity="fill" + app:tabMode="fixed" + app:tabSelectedTextColor="@color/accent" + app:tabTextColor="@color/white"/> + android:layout_height="wrap_content" + app:tabSelectedTextColor="@color/accent" + app:tabTextColor="@color/white"/> + app:tabMode="fixed" + app:tabSelectedTextColor="@color/accent" + app:tabTextColor="@color/white"/> - + android:layout_height="match_parent"> - + android:layout_marginBottom="4dp" + android:layout_marginEnd="16dp" + android:layout_marginStart="16dp" + android:layout_marginTop="4dp" + android:background="@color/background"> - + android:layout_height="match_parent" + android:orientation="vertical"> - + - + - + - + - + - + - - - \ No newline at end of file + + + + + + + + \ 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 4c3f904b..ad1504df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ + #%1$d