Java tutorial
/* * Copyright 2017 Gurupad Mamadapur * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.github.protino.codewatch.ui; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; import android.util.SparseBooleanArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import com.github.mikephil.charting.animation.Easing; import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.charts.PieChart; 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.PieEntry; import com.github.mikephil.charting.formatter.IAxisValueFormatter; import com.github.mikephil.charting.highlight.Highlight; import com.github.mikephil.charting.listener.OnChartValueSelectedListener; import com.google.firebase.crash.FirebaseCrash; import org.joda.time.DateTime; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import butterknife.BindArray; import butterknife.BindColor; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.Unbinder; import icepick.Icepick; import icepick.State; import io.github.protino.codewatch.R; import io.github.protino.codewatch.bundler.ProjectBundler; import io.github.protino.codewatch.model.firebase.Project; import io.github.protino.codewatch.model.project.summary.Editor; import io.github.protino.codewatch.model.project.summary.Language; import io.github.protino.codewatch.model.project.summary.OperatingSystem; import io.github.protino.codewatch.model.project.summary.ProjectSummaryData; import io.github.protino.codewatch.remote.FetchWakatimeData; import io.github.protino.codewatch.ui.widget.CustomMarkerView; import io.github.protino.codewatch.utils.FileProviderUtils; import io.github.protino.codewatch.utils.FormatUtils; import io.github.protino.codewatch.utils.UiUtils; import timber.log.Timber; /** * Display details of the project * * @author Gurupad Mamadapur */ public class ProjectDetailsFragment extends ChartFragment { //@formatter:off //languages @BindView(R.id.piechart_languages) public PieChart pieChartLanguages; @BindView(R.id.expand_piechart_language) public ImageView expandLanguages; @BindView(R.id.list_languages) public RecyclerView languagesListview; @State public Float languageHighlightedEntryX = null; //editors @BindView(R.id.piechart_editors) public PieChart pieChartEditors; @BindView(R.id.expand_piechart_editors) public ImageView expandEditors; @BindView(R.id.list_editors) public RecyclerView editorsListView; @State public Float editorHighlightedEntryX = null; //os @BindView(R.id.piechart_os) public PieChart pieChartOs; @BindView(R.id.expand_piechart_os) public ImageView expandOs; @BindView(R.id.list_os) public RecyclerView osListview; @BindArray(R.array.chart_colors) public int[] chartColors; @BindColor(R.color.blue_400) public int blue400; @State public Float osHighlightedEntryX = null; @State(ProjectBundler.class) public Project project; @State public String projectName; @State public Integer scrollPosition; @BindView(R.id.content) View content; @BindView(R.id.scroll_view) ScrollView scrollView; @BindView(R.id.progressBarLayout) View progressBarLayout; @BindView(R.id.barchart) BarChart barChart; @BindView(R.id.activity_total) TextView activityTotal; //@formatter:on private SparseBooleanArray isExpandedMap = new SparseBooleanArray(); private Context context; private long referenceTime; private Unbinder unbinder; private View rootView; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); projectName = getArguments().getString(Intent.EXTRA_TEXT); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { Icepick.restoreInstanceState(this, savedInstanceState); rootView = inflater.inflate(R.layout.fragment_project_detail, container, false); unbinder = ButterKnife.bind(this, rootView); context = getActivity(); hideProgressBar(false); setChartColors(chartColors); setContext(context); if (project == null) { new FetchProjectDetails().execute(projectName); } else { hideProgressBar(true); displayData(); } if (scrollPosition != null && scrollView != null) { scrollView.post(new Runnable() { @Override public void run() { scrollView.scrollTo(0, scrollPosition); } }); } return rootView; } @Override public void onDestroyView() { unbinder.unbind(); super.onDestroyView(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (scrollView != null) { scrollPosition = scrollView.getScrollY(); } Icepick.saveInstanceState(this, outState); } @OnClick({ R.id.expand_piechart_editors, R.id.expand_piechart_language, R.id.expand_piechart_os }) public void onExpand(View view) { boolean isExpanded = isExpandedMap.get(view.getId()); Drawable drawable = ContextCompat.getDrawable(context, isExpanded ? R.drawable.ic_expand_more_white_24dp : R.drawable.ic_expand_less_white_24dp); isExpandedMap.put(view.getId(), !isExpanded); switch (view.getId()) { case R.id.expand_piechart_language: expandLanguages.setImageDrawable(drawable); handleHighlights(pieChartLanguages, isExpanded); toggleListViewVisibility(!isExpanded, languagesListview, LANGUAGE_CHART_ID); break; case R.id.expand_piechart_editors: expandEditors.setImageDrawable(drawable); handleHighlights(pieChartEditors, isExpanded); toggleListViewVisibility(!isExpanded, editorsListView, EDITORS_CHART_ID); break; case R.id.expand_piechart_os: expandOs.setImageDrawable(drawable); handleHighlights(pieChartEditors, isExpanded); toggleListViewVisibility(!isExpanded, osListview, OS_CHART_ID); break; default://ignore break; } } @OnClick({ R.id.share_activity_chart, R.id.share_editors_chart, R.id.share_language_chart, R.id.share_os_chart }) public void onShareClick(View view) { Bitmap bitmap = null; switch (view.getId()) { case R.id.share_activity_chart: bitmap = barChart.getChartBitmap(); break; case R.id.share_editors_chart: bitmap = pieChartEditors.getChartBitmap(); break; case R.id.share_language_chart: bitmap = pieChartLanguages.getChartBitmap(); break; case R.id.share_os_chart: bitmap = pieChartOs.getChartBitmap(); break; default: break; } if (bitmap != null) { //add watermark bitmap = UiUtils.addWaterMark(bitmap, context); try { FileProviderUtils.shareBitmap(context, bitmap); } catch (IOException e) { Timber.d(e, "IO Error while saving image"); Toast.makeText(context, R.string.share_error, Toast.LENGTH_SHORT).show(); } } } private void hideProgressBar(boolean hide) { progressBarLayout.setVisibility(hide ? View.GONE : View.VISIBLE); content.setVisibility(hide ? View.VISIBLE : View.GONE); } private void displayData() { setUpBarChart(); setUpPieChart(pieChartLanguages, project.getLanguageList(), LANGUAGE_CHART_ID); setUpPieChart(pieChartEditors, project.getEditorPaiList(), EDITORS_CHART_ID); setUpPieChart(pieChartOs, project.getOsPairList(), OS_CHART_ID); setUpOnExpandRecyclerView(languagesListview, LANGUAGE_CHART_ID); setUpOnExpandRecyclerView(editorsListView, EDITORS_CHART_ID); setUpOnExpandRecyclerView(osListview, OS_CHART_ID); pieChartLanguages.animateXY(1500, 1500); pieChartEditors.animateXY(1500, 1500); pieChartOs.animateXY(1500, 1500); setListeners(); isExpandedMap.clear(); if (languageHighlightedEntryX != null) { pieChartLanguages.highlightValue(new Highlight(languageHighlightedEntryX, Float.NaN, 0), true); } if (editorHighlightedEntryX != null) { pieChartEditors.highlightValue(new Highlight(editorHighlightedEntryX, Float.NaN, 0), true); } if (osHighlightedEntryX != null) { pieChartOs.highlightValue(new Highlight(osHighlightedEntryX, Float.NaN, 0), true); } } private void setUpBarChart() { BarData barData = generateBarData(project.getTimeSpent()); XAxis xAxis = barChart.getXAxis(); xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); xAxis.setAxisLineColor(Color.WHITE); xAxis.setTextColor(Color.WHITE); xAxis.setDrawGridLines(false); xAxis.setGranularity(1f); xAxis.setValueFormatter(new FormatUtils().getBarXAxisValueFormatterInstance(referenceTime)); xAxis.setAxisLineWidth(2f); YAxis leftAxis = barChart.getAxisLeft(); leftAxis.removeAllLimitLines(); leftAxis.setDrawGridLines(false); leftAxis.setAxisLineColor(Color.WHITE); leftAxis.setTextColor(Color.WHITE); leftAxis.setGranularity(60 * 60); leftAxis.setValueFormatter(new IAxisValueFormatter() { @Override public String getFormattedValue(float value, AxisBase axis) { int hours = (int) TimeUnit.SECONDS.toHours((long) Math.ceil(value)); return (hours == 0) ? "" : context.getString(R.string.hours, hours); } }); leftAxis.setAxisLineWidth(2f); int maxYData = (int) (Math.ceil(barData.getYMax()) + 3600); leftAxis.setAxisMaximum(maxYData); leftAxis.setAxisMinimum(0f); CustomMarkerView customMarkerView = new CustomMarkerView(context, R.layout.marker_view, referenceTime); barChart.setMarker(customMarkerView); barChart.getAxisRight().setEnabled(false); barChart.getLegend().setEnabled(false); barChart.getDescription().setEnabled(false); barChart.setBackground(context.getResources().getDrawable(R.color.colorPrimaryDark)); barChart.setDrawGridBackground(false); barChart.setDragEnabled(false); barChart.setScaleEnabled(false); barChart.setDragDecelerationEnabled(false); barChart.setPinchZoom(false); barChart.setDoubleTapToZoomEnabled(false); barChart.setDrawBorders(false); barChart.setData(barData); customMarkerView.setChartView(barChart); barChart.setVisibility(View.VISIBLE); barChart.animateY(1500, Easing.EasingOption.Linear); } private BarData generateBarData(List<Integer> progressSoFar) { ArrayList<BarEntry> barEntries = new ArrayList<>(progressSoFar.size()); DateTime dateTime = new DateTime(); referenceTime = dateTime.minusDays(7).getMillis(); int totalSeconds = 0; for (int i = 0; i < progressSoFar.size(); i++) { int progress = progressSoFar.get(i); barEntries.add(new BarEntry(i, progress)); totalSeconds += progress; } activityTotal.setText(context.getString(R.string.total_project_time, FormatUtils.getFormattedTime(context, totalSeconds))); BarDataSet barDataSet = new BarDataSet(barEntries, ""); barDataSet.setColors(blue400); BarData barData = new BarData(barDataSet); barData.setBarWidth(0.85f); barData.setDrawValues(false); return barData; } private void setListeners() { pieChartLanguages.setOnChartValueSelectedListener(new CustomOnValueSelectedListener(LANGUAGE_CHART_ID)); pieChartEditors.setOnChartValueSelectedListener(new CustomOnValueSelectedListener(EDITORS_CHART_ID)); pieChartOs.setOnChartValueSelectedListener(new CustomOnValueSelectedListener(OS_CHART_ID)); } /** * Fetch project details dynamically from the API endpoint. * Note:// Actually the API endpoint does not expose any such functionality * todo:// request api developers to add such functionality */ private class FetchProjectDetails extends AsyncTask<String, Void, Project> { /* IMPORTANT! For some reason the api endpoint of wakatime requires project name instead of project id. */ private String projectName; @Override protected void onPreExecute() { super.onPreExecute(); hideProgressBar(false); } @Override protected Project doInBackground(String... params) { long start = System.currentTimeMillis(); Project project = new Project(); projectName = params[0]; List<Integer> timeSpent = new ArrayList<>(7); Map<String, Integer> languageStats = new HashMap<>(); Map<String, Integer> editorStats = new HashMap<>(); Map<String, Integer> osStats = new HashMap<>(); FetchWakatimeData fetchWakatimeData = new FetchWakatimeData(context); try { List<ProjectSummaryData> dataList = fetchWakatimeData.fetchProjectSummary(projectName).getData(); Timber.d("Download time - " + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (ProjectSummaryData summaryData : dataList) { timeSpent.add(summaryData.getGrandTotal().getTotalSeconds()); List<Language> languageList = summaryData.getLanguages(); List<Editor> editorList = summaryData.getEditors(); List<OperatingSystem> osList = summaryData.getOperatingSystems(); Map<String, Integer> map = new HashMap<>(); for (Language language : languageList) { map.put(language.getName(), language.getTotalSeconds()); } sumMaps(languageStats, map); map.clear(); for (Editor editor : editorList) { map.put(editor.getName(), editor.getTotalSeconds()); } sumMaps(editorStats, map); map.clear(); for (OperatingSystem operatingSystem : osList) { map.put(operatingSystem.getName(), operatingSystem.getTotalSeconds()); } sumMaps(osStats, map); } project.setName(projectName); project.setTimeSpent(timeSpent); project.setLanguageList(languageStats); project.setEditorPaiList(editorStats); project.setOsPairList(osStats); Timber.d("Parse time " + String.valueOf(System.currentTimeMillis() - start)); } catch (IOException e) { FirebaseCrash.report(e); return null; } catch (Exception e) { FirebaseCrash.report(e); return null; } return project; } private void sumMaps(Map<String, Integer> original, Map<String, Integer> newMap) { if (newMap != null) { for (Map.Entry<String, Integer> entry : newMap.entrySet()) { Integer previousValue = original.get(entry.getKey()); if (previousValue == null) { previousValue = 0; } original.put(entry.getKey(), entry.getValue() + previousValue); } } } @Override protected void onPostExecute(Project result) { hideProgressBar(true); if (result != null) { project = result; displayData(); } else { Snackbar.make(rootView, R.string.internet_error_message, Snackbar.LENGTH_LONG).show(); } } } /** * Handles expanding the recylcer view when a particular item is clicked on the chart */ private class CustomOnValueSelectedListener implements OnChartValueSelectedListener { private int CHART_ID; private CustomOnValueSelectedListener() { } private CustomOnValueSelectedListener(int chart_id) { CHART_ID = chart_id; } @Override public void onValueSelected(Entry e, Highlight h) { Timber.d(e.toString()); switch (CHART_ID) { case LANGUAGE_CHART_ID: if (!isExpandedMap.get(expandLanguages.getId())) { onExpand(expandLanguages); } languageHighlightedEntryX = h.getX(); resetAdapter(languagesListview, languageListAdapter, ((PieEntry) e).getLabel()); break; case OS_CHART_ID: if (!isExpandedMap.get(expandOs.getId())) { onExpand(expandOs); } osHighlightedEntryX = h.getX(); resetAdapter(osListview, osListAdapter, ((PieEntry) e).getLabel()); break; case EDITORS_CHART_ID: if (!isExpandedMap.get(expandEditors.getId())) { onExpand(expandEditors); } editorHighlightedEntryX = h.getX(); resetAdapter(editorsListView, editorsListAdapter, ((PieEntry) e).getLabel()); break; default: //common code break; } } @Override public void onNothingSelected() { switch (CHART_ID) { case LANGUAGE_CHART_ID: languageHighlightedEntryX = null; onExpand(expandLanguages); break; case OS_CHART_ID: osHighlightedEntryX = null; onExpand(expandOs); break; case EDITORS_CHART_ID: editorHighlightedEntryX = null; onExpand(expandEditors); break; default: break; } } } }