public class MainServiceHelper {
    private Context context;

    public MainServiceHelper(Context context) {
        this.context = context.getApplicationContext();
    }

    /**
     * Start a worker thread to load all universities from the server.
     *
     * @param publishedOnly - only published universities or all.
     */
    public void getUniversities(boolean publishedOnly) {
        int method = MainService.METHOD_GET_UNIVERSITIES;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_UNIVERSITY, method, requestId);
        intent.putExtra(MainService.PUBLISHED_ONLY, publishedOnly);
        context.startService(intent);
    }

    /**
     * Load all grades from the database.
     */
    public void getGradesFromDatabase() {
        int method = MainService.METHOD_GET_GRADES_FROM_DATABASE;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        context.startService(intent);
    }

    /**
     * Load all universities from the database.
     *
     * @param publishedOnly - only published universities or all.
     */
    public void getUniversitiesFromDatabase(boolean publishedOnly) {
        int method = MainService.METHOD_GET_UNIVERSITIES_FROM_DATABASE;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_UNIVERSITY, method, requestId);
        intent.putExtra(MainService.PUBLISHED_ONLY, publishedOnly);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to scrape for new grades.
     */
    public void scrapeForGrades(boolean initialScraping) {
        Intent intent = getIntentForScrapeForGrades(initialScraping, false);
        // start worker thread in background
        context.startService(intent);
    }

    public Intent getIntentForScrapeForGrades(boolean initialScraping, boolean automaticScraping) {
        int method = MainService.METHOD_SCRAPE_FOR_GRADES;

        // set request id
        // important: loginAndScrapeForGrades() must use the same requestId to avoid duplicate scraping
        long requestId = concatenateLong(method, 0);

        // prepare intent for intent service
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        intent.putExtra(MainService.INITIAL_SCRAPING, initialScraping);
        intent.putExtra(MainService.AUTOMATIC_SCRAPING, automaticScraping);

        return intent;
    }

    /**
     * Starts a worker thread to scrape for the overview of a specific subject.
     * @param gradeHash - Hash of Grade/Subject (unique identifier)
     */
    public void scrapeForOverview(String gradeHash) {
        int method = MainService.METHOD_SCRAPE_FOR_OVERVIEW;

        // set request id
        long requestId = concatenateLong(method, gradeHash.hashCode());

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        intent.putExtra(MainService.GRADE_HASH, gradeHash);
        context.startService(intent);
    }

    /**
     * Load all information for Grade Detail from Database.
     * @param gradeHash - Hash of Grade/Subject (unique identifier)
     */
    public void getGradeDetails(String gradeHash) {
        int method = MainService.METHOD_GET_GRADE_DETAILS;

        // set request id
        long requestId = concatenateLong(method, gradeHash.hashCode());

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        intent.putExtra(MainService.GRADE_HASH, gradeHash);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to save the username and password
     * and starts scraping for grades afterwards.
     *
     * @param username - username
     * @param password - password
     */
    public void loginAndScrapeForGrades(String username, String password, long universityId, long ruleId) {
        int method = MainService.METHOD_LOGIN_AND_SCRAPE_FOR_GRADES;

        // set request id
        // important: scrapeForGrades() must use the same requestId, to avoid duplicate scraping.
        long requestId = concatenateLong(MainService.METHOD_SCRAPE_FOR_GRADES, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_LOGIN, method, requestId);
        intent.putExtra(MainService.USERNAME, username);
        intent.putExtra(MainService.PASSWORD, password);
        intent.putExtra(MainService.UNIVERSITY_ID, universityId);
        intent.putExtra(MainService.RULE_ID, ruleId);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to get the username and selected university from database.
     */
    public void getLoginDataFromDatabase() {
        int method = MainService.METHOD_GET_LOGIN_DATA_FROM_DATABASE;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_LOGIN, method, requestId);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to delete all userdata and grades from the database.
     */
    public void logout() {
        int method = MainService.METHOD_LOGOUT;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_LOGIN, method, requestId);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to update a grade entry.
     *
     * @param gradeEntry grade entry to update.
     */
    public void updateGradeEntry(GradeEntry gradeEntry) {
        int method = MainService.METHOD_UPDATE_GRADE_ENTRY;

        // set request id
        long requestId = concatenateLong(method, gradeEntry.getHash().hashCode());

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        intent.putExtra(MainService.GRADE_ENTRY, gradeEntry);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to update the visibility of a grade entry.
     *
     * @param gradeEntryHash grade entry hash
     * @param hidden hidden or not
     */
    public void updateGradeEntryVisibility(String gradeEntryHash, boolean hidden) {
        int method = MainService.METHOD_UPDATE_GRADE_ENTRY_VISIBILITY;

        // set request id
        long requestId = concatenateLong(method, gradeEntryHash.hashCode() + (hidden ? 1 : 0));

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        intent.putExtra(MainService.GRADE_HASH, gradeEntryHash);
        intent.putExtra(MainService.GRADE_ENTRY_HIDDEN, hidden);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to delete a grade entry.
     *
     * @param gradeEntryHash grade entry hash
     */
    public void deleteGradeEntry(String gradeEntryHash) {
        int method = MainService.METHOD_DELETE_GRADE_ENTRY;

        // set request id
        long requestId = concatenateLong(method, gradeEntryHash.hashCode());

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_GRADES, method, requestId);
        intent.putExtra(MainService.GRADE_HASH, gradeEntryHash);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to post an error report to the server.
     *
     * @param name name
     * @param email email
     * @param errorMessage error message
     */
    public void postErrorReport(String name, String email, String errorMessage) {
        int method = MainService.METHOD_POST_ERROR;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_ERROR, method, requestId);
        intent.putExtra(MainService.NAME, name);
        intent.putExtra(MainService.EMAIL, email);
        intent.putExtra(MainService.ERROR_MESSAGE, errorMessage);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to post an university wish to the server.
     *
     * @param universityName university name
     * @param name name
     * @param email email
     * @param message message
     */
    public void postWish(String universityName, String name, String email, String message) {
        int method = MainService.METHOD_POST_WISH;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_WISH, method, requestId);
        intent.putExtra(MainService.UNIVERSITY_NAME, universityName);
        intent.putExtra(MainService.NAME, name);
        intent.putExtra(MainService.EMAIL, email);
        intent.putExtra(MainService.WISH_MESSAGE, message);
        context.startService(intent);
    }

    /**
     * Starts a worker thread to create and get statistics.
     */
    public void getStatistics() {
        int method = MainService.METHOD_GET_STATISTICS;

        // set request id
        long requestId = concatenateLong(method, 0);

        // start worker thread in background
        Intent intent = getBasicIntent(MainService.PROCESSOR_STATISTICS, method, requestId);
        context.startService(intent);
    }

    /**
     * Build a basic intent with required extra data for each request.
     *
     * @param processor - processor to create (declared in the MainService)
     * @param method - method to call (declared in the MainService)
     * @param requestId - request id
     * @return intent
     */
    private Intent getBasicIntent(int processor, int method, long requestId) {
        Intent intent = new Intent(context, MainService.class);
        intent.putExtra(MainService.PROCESSOR_KEY, processor);
        intent.putExtra(MainService.METHOD_KEY, method);
        intent.putExtra(MainService.REQUEST_ID, requestId);
        return intent;
    }

    /**
     * Concatenates two long values.
     * This can be used to generate unique request ids.
     *
     * @param a first long value
     * @param b second long value
     * @return ab as long
     */
    private long concatenateLong(long a, long b) {
        return Long.parseLong("" + Math.abs(a) + Math.abs(b));
    }
} public class AverageCalculator {
    private float average;
    private float creditPointsSum;
    private boolean simpleWeighting;

    public AverageCalculator(boolean simpleWeighting) {
        this.simpleWeighting = simpleWeighting;
    }

    /**
     * Calculates the average grade and credit points sum for a list of GradeItems.
     *
     * @param items - list of GradeAdapterItems
     */
    public void calculate(List<? extends GradesAdapterItem> items) {
        average = 0f;
        creditPointsSum = 0f;
        float creditPointsSumForAverage = 0f; // some grade_entries may have credit points, but no grade
        float passedGradesCounter = 0; // used, if simpleWeighting is true

        // iterate over items, count credit points and calculate average
        for(GradesAdapterItem item : items) {
            if (!(item instanceof GradeItem))
                continue;

            GradeItem gradeItem = (GradeItem) item;
            if (gradeItem.isHidden())
                continue;

            double weight = gradeItem.getWeight() == null ? 1 : gradeItem.getWeight();

            double actCreditPoints = (gradeItem.getCreditPoints() == null ? 0f : gradeItem.getCreditPoints());
            Double modifiedCreditPoints = gradeItem.getModifiedCreditPoints();
            actCreditPoints = modifiedCreditPoints == null ? actCreditPoints : modifiedCreditPoints;

            double grade = (gradeItem.getGrade() == null ? 0f : gradeItem.getGrade());
            Double modifiedGrade = gradeItem.getModifiedGrade();
            grade = modifiedGrade == null ? grade : modifiedGrade;

            if (grade >= 0 && grade < 5) {
                creditPointsSum += actCreditPoints;
                // only add cps with actual grade to the sum of credit points for average calculation
                if (grade > 0) {
                    creditPointsSumForAverage += (actCreditPoints * weight);
                }
            }

            if (simpleWeighting && isGradeRelevant(grade)) {
                passedGradesCounter += weight;
                average += (grade * weight);
            } else if (isGradeRelevant(grade)){
                average += grade * actCreditPoints * weight;
            }
        }

        if (simpleWeighting) {
            average = passedGradesCounter > 0 ? average / passedGradesCounter : 0f;
        } else {
            average = creditPointsSumForAverage > 0 ? average / creditPointsSumForAverage : 0f;
        }
    }

    /**
     * Converts grade entries to grade items and calculates the average and credit points sum.
     *
     * @param gradeEntries - list of grade entries
     */
    public void calculateFromGradeEntries(List<GradeEntry> gradeEntries) {
        List<GradeItem> gradeItems = new ArrayList<>();
        for(GradeEntry entry : gradeEntries) {
            gradeItems.add(new GradeItem(entry));
        }
        calculate(gradeItems);
    }

    /**
     * Checks if a grade is relevant for average calculation and credit point sum.
     *
     * @param grade grade to check
     * @return true, if grade is relevant
     */
    private boolean isGradeRelevant(double grade) {
        return grade > 0 && grade < 5;
    }

    public float getAverage() {
        return average;
    }

    public float getCreditPointsSum() {
        return creditPointsSum;
    }
} public class Constants {

    // secure preferences constants
    public static final String NOT_SO_SECURE_PREF_FILE = "userdata";
    public static final String PREF_KEY_USERNAME = "username";
    public static final String PREF_KEY_PASSWORD = "password";

    // normal preferences constants
    public static final String PREF_KEY_UNIVERSITY_ID = "university_id";
    public static final String PREF_KEY_RULE_ID = "rule_id";
    public static final String PREF_KEY_INITIAL_LOADING_DONE = "initial_loading_done";
    public static final String PREF_KEY_LAST_UPDATED_AT = "last_updated_at";
    public static final String PREF_KEY_SCRAPING_FAILS_INTERVAL = "scraping_fails_interval";
    public static final String PREF_KEY_ALARM_ERROR_SCRAPING_COUNT = "alarm_error_scraping_count";
    public static final String PREF_KEY_DISMISSED_NOTIFICATION_INFO = "dismissed_notification_info";
    public static final String PREF_KEY_DISMISSED_DONATION_INFO = "dismissed_donation_info";
    public static final String PREF_KEY_DISMISSED_RATING_INFO = "dismissed_rating_info";
    public static final String PREF_KEY_DISMISSED_EDIT_MODE_INFO = "dismissed_edit_mode_info";
    public static final String PREF_KEY_APPLICATION_LAUNCHES_COUNTER = "application_launches_counter";

    // Scraping status instance state
    public static final String INSTANCE_IS_SCRAPING_STATE = "is_scraping_state";
    public static final String INSTANCE_PROGRESS_STATE = "progress_state";

    // grade entry 'seen' state
    public static final int GRADE_ENTRY_SEEN = 0;
    public static final int GRADE_ENTRY_NEW = 1;
    public static final int GRADE_ENTRY_UPDATED = 2;
} public class LogoutHelper {

    private Context context;

    public LogoutHelper(Context context) {
        this.context = context;
    }

    /**
     * Shows the logout dialog.
     */
    public void showDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setMessage(R.string.dialog_logout_message);
        builder.setTitle(context.getString(R.string.dialog_logout_title));
        builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                logout();
            }
        });

        builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });

        AlertDialog dialog = builder.create();
        dialog.show();
    }

    /**
     * Logout user and goes to SelectUniversityActivity.
     * The user cannot return to the MainActivity by pressing the back-button.
     */
    private void logout() {
        MainServiceHelper mainServiceHelper = new MainServiceHelper(context);
        mainServiceHelper.logout();

        Intent intent = new Intent(context, SelectUniversityActivity.class);
        // set flags, so the user won't be able to go back to the main activity
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
                Intent.FLAG_ACTIVITY_CLEAR_TASK |
                Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
} public class UIHelper {

    /**
     * Shows a Snackbar with a given text and action on the given view.
     *
     * @param view view where the snackbar should be shown
     * @param text text to show
     * @param action OnClickListener
     * @param actionText text for the OnClickListener
     */
    public static void showSnackbar(View view, String text, View.OnClickListener action, String actionText) {
        if (view != null) {
            Snackbar snackbar = Snackbar
                    .make(view, text, Snackbar.LENGTH_LONG)
                    .setAction(actionText, action)
                    .setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.colorPrimary));

            // change text color
            View snackbarView = snackbar.getView();
            TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text);
            textView.setTextColor(Color.WHITE);

            snackbar.show();
        }
    }

    /**
     * Shows a Snackbar with a given text on the given view.
     *
     * @param view view where the snackbar should be shown
     * @param text text to show
     */
    public static void showSnackbar(View view, String text) {
        showSnackbar(view, text, null, null);
    }

    /**
     * Evaluates the Error from errorEvent and displays specific message.
     *
     * @param view view where the error should be shown
     * @param errorEvent Error Event which holds the information about the concrete Error
     * @param tryAgainListener Button
     * @param goToFaqListener Button
     */
    public static void displayErrorMessage(View view, ErrorEvent errorEvent, View.OnClickListener tryAgainListener, View.OnClickListener goToFaqListener) {
        switch (errorEvent.getType()) {
            case TIMEOUT:
                showSnackbar(view, "Zeitüberschreitung", tryAgainListener, "Nochmal");
                break;
            case NO_NETWORK:
                showSnackbar(view, "Keine Internetverbindung", tryAgainListener, "Nochmal");
                break;
            case GENERAL:
            default:
                showSnackbar(view, "Allgemeiner Fehler", goToFaqListener, "Was ist das?");
        }
    }


    public enum STATE {AN, BE, NB, EN}

    /**
     * Evaluates the state string
     * @param state string to evaluate
     * @return a {@link STATE} or null
     */
    public static @Nullable STATE evaluateStateString(@Nullable String state){
        if (state == null) return null;
        switch (state.toLowerCase()){
            case "an":
            case "angemeldet":
            case "registered":
                return STATE.AN;
            case "be":
            case "bestanden":
            case "passed":
            case "pass":
            case "ak":
            case "anerkannt":
            case "acknowledged":
                return STATE.BE;
            case "nb":
            case "nicht bestanden":
            case "fail":
            case "failed":
            case "ta":
            case "täuschung":
            case "cheating":
                return STATE.NB;
            case "en":
            case "endgültig nicht bestanden":
            case "failed in the final attempt":
                return STATE.EN;
            default:
                return null;
        }
    }

    /**
     * Returns short state string
     * @param state any state string
     * @return shortened state string
     */
    public static @Nullable String getShortState(@NonNull Context context, @Nullable String state){
        STATE s = evaluateStateString(state);
        if (s == null) return state;
        switch (s){
            case AN:
                return context.getString(R.string.state_an);
            case BE:
                return context.getString(R.string.state_be);
            case NB:
                return context.getString(R.string.state_nb);
            case EN:
                return context.getString(R.string.state_en);
        }
        return null;
    }

    /**
     * Returns the color (passed/failed/default) for the given grade and state
     *
     * @param context a context to get the color resource
     * @param grade grade to evaluate or null
     * @param state state to evaluate or null
     * @return color int
     */
    public static @ColorInt int getGradeColor(@NonNull Context context, @Nullable Double grade,
                                              @Nullable String state){
        Integer gradeColor = null;
        STATE s = evaluateStateString(state);
        if (s != null) {
            // evaluate state
            switch (s){
                case BE:
                    gradeColor = R.color.text_passed;
                    break;
                case NB:
                    gradeColor = R.color.text_failed;
                    break;
                case EN:
                    gradeColor = R.color.text_failed_permanently;
                    break;
            }

        }
        if (gradeColor == null && grade != null){
            // evaluate grade instead
            gradeColor = grade <= 4 ? R.color.text_passed : R.color.text_failed;
        }
        if (gradeColor == null){
            // default fallback
            gradeColor = R.color.text_default;
        }

        return context.getResources().getColor(gradeColor);
    }
} public class GradeDetailedActivity extends AppCompatActivity {

    // intent extra data
    public static final String EXTRA_GRADE_HASH = "grade_hash";
    public static final String EXTRA_ADD_NEW_GRADE_ENTRY = "add_new_grade_entry";

    // instance state
    private static final String IS_OVERVIEW_POSSIBLE_STATE = "is_overview_possible_state";
    private static final String GRADE_HASH_STATE = "grade_hash";
    private static final String EDIT_MODE_ENABLED_STATE = "edit_mode_enabled";
    private static final String ADD_NEW_GRADE_ENTRY = "add_new_grade_entry";
    private static final String EDIT_HELPER_STATE = "edit_helper_state";

    // views
    private LinearLayout llRootView; // used to show snackbar
    private PtrFrameLayout ptrFrame;
    private PtrHeader ptrHeader;

    // views for overview
    private LinearLayout llOverviewWrapper;
    private TextView tvOverviewParticipants;
    private TextView tvOverviewPassed;
    private TextView tvOverviewAverage;
    private TextView tvOverviewNotPossible;
    private TextView tvCustomGradeEntry;

    // bar chart
    private Overview overview;
    private BarChart barChart;
    private static final int COLOR_GRAY = Color.rgb(233, 233, 233); // light gray
    private static final int COLOR_HIGHLIGHT = Color.rgb(139, 195, 74); // primary color green

    private String gradeHash;
    private GradeEntry gradeEntry;
    private MainServiceHelper mainServiceHelper;
    private GradeDetailedActivityEditHelper editHelper;
    private boolean isOverviewPossible;
    private boolean receivedGradeEntryEvent;
    private boolean editModeEnabled;
    private boolean addNewGradeEntry;
    private Bundle editHelperState;

    // snackbar listener
    private View.OnClickListener tryAgainListener;
    private View.OnClickListener goToFaqListener;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_grade_detailed);

        // set toolbar
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        if (getSupportActionBar() != null) {
            getSupportActionBar().setTitle(R.string.toolbar_details);
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }

        mainServiceHelper = new MainServiceHelper(this);
        editHelper = new GradeDetailedActivityEditHelper(this);

        // get extra data
        Bundle extras = getIntent().getExtras();
        gradeHash = extras.getString(EXTRA_GRADE_HASH, "");
        addNewGradeEntry = extras.getBoolean(EXTRA_ADD_NEW_GRADE_ENTRY, false);

        initListener();
        initViews();

        // register event bus
        EventBus.getDefault().register(this);

        // start intent to get data for Grade Detail page
        mainServiceHelper.getGradeDetails(gradeHash);
    }

    /**
     * Initialize all needed views.
     */
    private void initViews() {
        // get views for overview
        llOverviewWrapper = (LinearLayout) findViewById(R.id.overview_wrapper);
        tvOverviewParticipants = (TextView) findViewById(R.id.tv_overview_participants);
        tvOverviewPassed = (TextView) findViewById(R.id.tv_overview_passed);
        tvOverviewAverage = (TextView) findViewById(R.id.tv_overview_average);
        barChart = (BarChart) findViewById(R.id.bar_chart);
        tvOverviewNotPossible = (TextView) findViewById(R.id.tv_overview_not_possible);
        tvOverviewNotPossible.setText(Html.fromHtml(getString(R.string.overview_not_possible, FaqDataProvider.GO_TO_WHY_NO_GRADING)));
        tvOverviewNotPossible.setMovementMethod(LinkMovementMethod.getInstance());
        llRootView = (LinearLayout) findViewById(R.id.ll_root_view);
        tvCustomGradeEntry = (TextView) findViewById(R.id.tv_custom_grade_entry);

        initPullToRefresh();
    }

    private void initListener() {
        // click listener to go to the FAQs and show the general-error question immediately
        goToFaqListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(GradeDetailedActivity.this, MainActivity.class);
                intent.putExtra(FragmentFaq.ARGUMENT_GO_TO_QUESTION, FaqDataProvider.GO_TO_GENERAL_ERROR);
                GradeDetailedActivity.this.startActivity(intent);
            }
        };

        // click listener to restart the scraping process
        tryAgainListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ptrFrame.autoRefresh();
                mainServiceHelper.scrapeForOverview(gradeHash);
            }
        };
    }

    /**
     * Initialize the pull to refresh layout.
     */
    private void initPullToRefresh() {
        ptrFrame = (PtrFrameLayout) findViewById(R.id.pull_to_refresh);
        ptrHeader = new PtrHeader(this, getString(R.string.ptr_header_refreshing_overview));
        ptrFrame.addPtrUIHandler(ptrHeader);
        ptrFrame.setHeaderView(ptrHeader);

        ptrFrame.setPtrHandler(new PtrHandler() {
            @Override
            public void onRefreshBegin(PtrFrameLayout frame) {
                if (!ptrFrame.isAutoRefresh()) {
                    mainServiceHelper.scrapeForOverview(gradeHash);
                }
            }

            @Override
            public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
                return PtrDefaultHandler.checkContentCanBePulledDown(frame, content, header);
            }
        });
    }

    /**
     * Shows the bar chart with data taken from an OverviewEvent.
     *
     * @param overviewEvent - overview event
     */
    private void showBarChart(OverviewEvent overviewEvent) {
        // general layout settings
        barChart.setPinchZoom(false);
        barChart.setDoubleTapToZoomEnabled(false);
        barChart.setScaleEnabled(false);
        barChart.setDescription("");
        barChart.setDrawBorders(false);
        barChart.setDrawBarShadow(false);
        barChart.setDrawGridBackground(false);
        barChart.getLegend().setEnabled(false);

        // set x-axis
        XAxis xAxis = barChart.getXAxis();
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setSpaceBetweenLabels(0);
        xAxis.setDrawAxisLine(false);
        xAxis.setDrawGridLines(false);
        xAxis.setTextColor(ContextCompat.getColor(this, R.color.text87));
        xAxis.setLabelsToSkip(0);

        // hide left y-axis
        barChart.getAxisLeft().setDrawGridLines(true);
        barChart.getAxisLeft().setDrawAxisLine(false);
        barChart.getAxisLeft().setDrawLabels(false);
        barChart.getAxisLeft().setGridColor(ContextCompat.getColor(this, R.color.divider));
        barChart.getAxisLeft().setLabelCount(6, true);

        // hide right y-axis
        barChart.getAxisRight().setDrawGridLines(false);
        barChart.getAxisRight().setDrawAxisLine(false);
        barChart.getAxisRight().setDrawLabels(false);

        Overview overview = overviewEvent.getOverview();

        // y-values
        ArrayList<BarEntry> yValues = new ArrayList<>();
        yValues.add(new BarEntry(overview.getSection1(), 0));
        yValues.add(new BarEntry(overview.getSection2(), 1));
        yValues.add(new BarEntry(overview.getSection3(), 2));
        yValues.add(new BarEntry(overview.getSection4(), 3));
        yValues.add(new BarEntry(overview.getSection5(), 4));

        // x-values
        ArrayList<String> xValues = new ArrayList<>();
        xValues.add("1,0 - 1,3");
        xValues.add("1,7 - 2,3");
        xValues.add("2,7 - 3,3");
        xValues.add("3,7 - 4,0");
        xValues.add("4,3 - 5,0");

        // set colors
        int[] BAR_CHART_COLORS = {COLOR_GRAY, COLOR_GRAY, COLOR_GRAY, COLOR_GRAY, COLOR_GRAY};
        if (overview.getUserSection() != null && overview.getUserSection() > 0) {
            BAR_CHART_COLORS[overview.getUserSection() - 1] = COLOR_HIGHLIGHT;
        }

        BarDataSet dataSet = new BarDataSet(yValues, "");
        dataSet.setValueTextSize(12);
        dataSet.setValueTextColor(ContextCompat.getColor(this, R.color.text87));
        dataSet.setColors(BAR_CHART_COLORS);
        dataSet.setDrawValues(true);
        dataSet.setBarSpacePercent(35);

        BarData barData = new BarData(xValues, dataSet);
        barData.setValueFormatter(new MyValueFormatter());
        barData.setHighlightEnabled(false);
        barChart.setData(barData);

        // add a nice and smooth animation
        barChart.animateY(1500);
        barChart.invalidate();
    }

    /**
     * Receive an event when the GradeEntry is retrieved from the DB.
     * Set TextViews to values from Grade and evaluate which one is shown.
     * @param gradeEntryEvent GradeEntryEvent
     */
    @SuppressWarnings("unused")
    public void onEventMainThread(GradeEntryEvent gradeEntryEvent) {
        gradeEntry = gradeEntryEvent.getGradeEntry();
        gradeHash = gradeEntry.getHash();

        editHelper.setGradeEntry(gradeEntry);
        editHelper.setSemesterToNumberMap(gradeEntryEvent.getSemesterToSemesterNumberMap());
        editHelper.init();
        editHelper.updateValues();
        if (editModeEnabled) {
            editHelper.restoreInstanceState(editHelperState);
        }

        if (addNewGradeEntry) {
            editHelper.enableEditMode(true);
            editModeEnabled = true;
        } else {
            editHelper.enableEditMode(editModeEnabled);
        }

        if (gradeEntry.isCustomGradeEntry()) {
            tvCustomGradeEntry.setVisibility(View.VISIBLE);
        }

        receivedGradeEntryEvent = true;
        invalidateOptionsMenu();
    }

    /**
     * Receive an event when the Overview is retrieved.
     * Set TextViews to values from Overview.
     * @param overviewEvent OverviewEvent
     */
    @SuppressWarnings("unused")
    public void onEventMainThread(OverviewEvent overviewEvent) {
        if (overview != null)
            return;

        overview = overviewEvent.getOverview();

        // only set overview if it belongs to current gradeEntry
        if (gradeEntry != null && overview.getGradeEntryHash().equals(gradeHash)) {
            // if it's a scraping result
            if (overviewEvent.isScrapingResult() && ptrFrame != null && ptrHeader != null) {
                ptrFrame.refreshComplete();
            }

            if (overview.getParticipants() != null) {
                llOverviewWrapper.setVisibility(View.VISIBLE);
                tvOverviewParticipants.setText(String.valueOf(overview.getParticipants()));

                // calculated passed participants
                int participants = overview.getParticipants();
                int section5 = overview.getSection5();
                if (participants > 0) {
                    int passedParticipants = participants - section5;
                    int passedParticipantsPercent = (int) Math.round(passedParticipants / (double) participants * 100);
                    tvOverviewPassed.setText(String.format("%d (%d%%)", passedParticipants, passedParticipantsPercent));
                }

                writeDoubleToTextView(tvOverviewAverage, overview.getAverage());

                showBarChart(overviewEvent);
            }
        }
    }

    /**
     * Receive an event when its determined whether it is possible to receive an overview.
     * Event is only sent if there is no overview for this grade entry in DB.
     * @param overviewPossibleEvent OverviewPossibleEvent
     */
    @SuppressWarnings("unused")
    public void onEventMainThread(OverviewPossibleEvent overviewPossibleEvent) {
        if (ptrHeader != null && !ptrHeader.isScraping()) {
            isOverviewPossible = overviewPossibleEvent.isOverviewPossible();

            if (isOverviewPossible) {
                ptrFrame.autoRefresh();
                mainServiceHelper.scrapeForOverview(gradeHash);
            } else {
                ptrFrame.setPtrHandler(new PtrHandler() {
                    @Override
                    public void onRefreshBegin(PtrFrameLayout frame) {

                    }

                    @Override
                    public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
                        return false;
                    }
                });
                // only show TextView if its possible in general for university to show a overview
                if (overviewPossibleEvent.isOverviewPossibleForUniversity() && !gradeEntry.isCustomGradeEntry()) {
                    tvOverviewNotPossible.setVisibility(View.VISIBLE);
                }
            }
        }
    }

    /**
     * Receive an event about the scraping progress and set the ProgressWheel accordingly.
     *
     * @param scrapeProgressEvent ScrapeProgressEvent
     */
    @SuppressWarnings("unused")
    public void onEventMainThread(ScrapeProgressEvent scrapeProgressEvent) {
        if (ptrHeader != null && scrapeProgressEvent.isScrapeForOverview()
                && gradeHash != null && gradeHash.equals(scrapeProgressEvent.getGradeHash())) {
            ptrHeader.increaseProgress(scrapeProgressEvent.getCurrentStep(), scrapeProgressEvent.getStepCount());
        }
    }

    /**
     * Receive error events.
     *
     * @param errorEvent ErrorEvent
     */
    @SuppressWarnings("unused")
    public void onEventMainThread(ErrorEvent errorEvent) {
        if (ptrFrame != null && ptrHeader != null) {
            ptrHeader.setIsError(true);
            ptrFrame.refreshComplete();
        }

        UIHelper.displayErrorMessage(llRootView, errorEvent, tryAgainListener, goToFaqListener);
    }

    private void writeDoubleToTextView(TextView textView, Double value) {
        String valueAsString = value == null ? "-" : String.format("%.1f", value);
        textView.setText(valueAsString);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // unregister EventBus
        EventBus.getDefault().unregister(this);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        ptrHeader.saveInstanceState(outState);
        outState.putBoolean(IS_OVERVIEW_POSSIBLE_STATE, isOverviewPossible);
        outState.putString(GRADE_HASH_STATE, gradeHash);
        outState.putBoolean(EDIT_MODE_ENABLED_STATE, editModeEnabled);
        outState.putBoolean(ADD_NEW_GRADE_ENTRY, addNewGradeEntry);
        outState.putBundle(EDIT_HELPER_STATE, editHelper.getInstanceState());
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);

        isOverviewPossible = savedInstanceState.getBoolean(IS_OVERVIEW_POSSIBLE_STATE);
        gradeHash = savedInstanceState.getString(GRADE_HASH_STATE, "");
        ptrHeader.restoreInstanceState(savedInstanceState, ptrFrame);
        editModeEnabled = savedInstanceState.getBoolean(EDIT_MODE_ENABLED_STATE);
        addNewGradeEntry = savedInstanceState.getBoolean(ADD_NEW_GRADE_ENTRY);
        editHelperState = savedInstanceState.getBundle(EDIT_HELPER_STATE);
    }

    private class MyValueFormatter implements ValueFormatter {

        private DecimalFormat mFormat;

        public MyValueFormatter() {
            mFormat = new DecimalFormat("#"); // use one decimal
        }

        @Override
        public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
            // write your logic here
            return mFormat.format(value);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.grade_detail, menu);

        MenuItem editItem = menu.findItem(R.id.grade_detail_edit);
        MenuItem saveItem = menu.findItem(R.id.grade_detail_save);
        MenuItem restoreItem = menu.findItem(R.id.grade_detail_restore);
        MenuItem deleteItem = menu.findItem(R.id.grade_detail_delete);
        editItem.setVisible(receivedGradeEntryEvent && !editHelper.isEditModeEnabled());
        saveItem.setVisible(receivedGradeEntryEvent && editHelper.isEditModeEnabled());
        restoreItem.setVisible(receivedGradeEntryEvent && editHelper.isEditModeEnabled() && !editHelper.isCustomGradeEntry());
        deleteItem.setVisible(receivedGradeEntryEvent && editHelper.isEditModeEnabled() && editHelper.isCustomGradeEntry());

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                super.onBackPressed();
                return true;
            case R.id.grade_detail_edit:
                editHelper.enableEditMode(true);
                editModeEnabled = true;
                invalidateOptionsMenu();
                return true;
            case R.id.grade_detail_save:
                editHelper.enableEditMode(false);
                editHelper.saveEdits();
                addNewGradeEntry = false;
                editModeEnabled = false;
                invalidateOptionsMenu();
                return true;
            case R.id.grade_detail_delete:
                showDeleteDialog();
                return true;
            case R.id.grade_detail_restore:
                showRestoreDialog();
                return true;
        }

        return false;
    }

    @Override
    public void onBackPressed() {
        if (addNewGradeEntry) {
            showDiscardDialog();
            return;
        }

        if (editModeEnabled) {
            editHelper.enableEditMode(false);
            editModeEnabled = false;
            invalidateOptionsMenu();
            mainServiceHelper.getGradeDetails(gradeHash);
        } else {
            super.onBackPressed();
        }
    }

    /**
     * Show dialog to ask the user whether he wants to restore the original GradeEntry.
     */
    private void showRestoreDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage(R.string.dialog_restore_grade_entry);
        builder.setTitle(getString(R.string.dialog_restore_grade_entry_title));
        builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                editHelper.restore();
                editModeEnabled = false;
                invalidateOptionsMenu();
            }
        });

        builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });

        AlertDialog dialog = builder.create();
        dialog.show();
    }

    /**
     * Show dialog to ask the user whether he wants to delete this GradeEntry.
     */
    private void showDeleteDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage(R.string.dialog_delete_grade_entry);
        builder.setTitle(getString(R.string.dialog_delete_grade_entry_title));
        builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                editHelper.delete();
                editModeEnabled = false;
                finish();
            }
        });

        builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });

        AlertDialog dialog = builder.create();
        dialog.show();
    }

    /**
     * Show dialog to ask the user whether he wants to discard this new GradeEntry.
     */
    private void showDiscardDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage(R.string.dialog_discard_grade_entry);
        builder.setTitle(getString(R.string.dialog_discard_grade_entry_title));
        builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                finish();
            }
        });

        builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });

        AlertDialog dialog = builder.create();
        dialog.show();
    }
} public class GradeItem implements GradesAdapterItem {
    private String name;
    private String modifiedName;
    private Double grade;
    private String state;
    private Double modifiedGrade;
    private String modifiedState;
    private Double creditPoints;
    private Double modifiedCreditPoints;
    private Double weight;
    private String hash;
    private String semester;
    private String modifiedSemester;
    private int seen;
    private boolean hidden;

    public GradeItem() {}

    /**
     * Create GradeItem from GradeEntry.
     *
     * @param gradeEntry - grade entry
     */
    public GradeItem(GradeEntry gradeEntry) {
        setName(gradeEntry.getName());
        setModifiedName(gradeEntry.getModifiedName());
        setHash(gradeEntry.getHash());

        setCreditPoints(gradeEntry.getCreditPoints());
        setModifiedCreditPoints(gradeEntry.getModifiedCreditPoints());

        setGrade(gradeEntry.getGrade());
        setModifiedGrade(gradeEntry.getModifiedGrade());

        setState(gradeEntry.getState());
        setModifiedState(gradeEntry.getModifiedState());

        setSemester(gradeEntry.getSemester());
        setModifiedSemester(gradeEntry.getModifiedSemester());

        setWeight(gradeEntry.getWeight());
        setHidden(gradeEntry.isHidden());

        if (gradeEntry.getSeen() != null) {
            setSeen(gradeEntry.getSeen());
        }
    }

    /**
     * Determines whether the weight should be shown in the overview.
     *
     * @return true, if weight should be shown
     */
    public boolean showWeight() {
        return weight != null && weight != 1.0;
    }

    /**
     * Determines whether the modified badge should be shown in the overview.
     *
     * @return true, if modified badge should be shown
     */
    public boolean showModifiedBadge() {
        return modifiedGrade != null || modifiedState != null || modifiedCreditPoints != null ||
                modifiedName != null || modifiedSemester != null;
    }

    /**
     * Get the name which should be shown to the user.
     *
     * @return name to show
     */
    public String getShownName() {
        return modifiedName == null ? name : modifiedName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GradeItem gradeItem = (GradeItem) o;

        if (seen != gradeItem.seen) return false;
        if (hidden != gradeItem.hidden) return false;
        if (name != null ? !name.equals(gradeItem.name) : gradeItem.name != null) return false;
        if (modifiedName != null ? !modifiedName.equals(gradeItem.modifiedName) : gradeItem.modifiedName != null)
            return false;
        if (grade != null ? !grade.equals(gradeItem.grade) : gradeItem.grade != null) return false;
        if (modifiedGrade != null ? !modifiedGrade.equals(gradeItem.modifiedGrade) : gradeItem.modifiedGrade != null)
            return false;
        if (state != null ? !state.equals(gradeItem.state) : gradeItem.state != null) return false;
        if (modifiedState != null ? !modifiedState.equals(gradeItem.modifiedState) : gradeItem.modifiedState != null)
            return false;
        if (creditPoints != null ? !creditPoints.equals(gradeItem.creditPoints) : gradeItem.creditPoints != null)
            return false;
        if (modifiedCreditPoints != null ? !modifiedCreditPoints.equals(gradeItem.modifiedCreditPoints) : gradeItem.modifiedCreditPoints != null)
            return false;
        if (weight != null ? !weight.equals(gradeItem.weight) : gradeItem.weight != null)
            return false;
        if (hash != null ? !hash.equals(gradeItem.hash) : gradeItem.hash != null) return false;
        if (semester != null ? !semester.equals(gradeItem.semester) : gradeItem.semester != null)
            return false;
        return modifiedSemester != null ? modifiedSemester.equals(gradeItem.modifiedSemester) : gradeItem.modifiedSemester == null;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (modifiedName != null ? modifiedName.hashCode() : 0);
        result = 31 * result + (grade != null ? grade.hashCode() : 0);
        result = 31 * result + (modifiedGrade != null ? modifiedGrade.hashCode() : 0);
        result = 31 * result + (creditPoints != null ? creditPoints.hashCode() : 0);
        result = 31 * result + (modifiedCreditPoints != null ? modifiedCreditPoints.hashCode() : 0);
        result = 31 * result + (weight != null ? weight.hashCode() : 0);
        result = 31 * result + (hash != null ? hash.hashCode() : 0);
        result = 31 * result + (semester != null ? semester.hashCode() : 0);
        result = 31 * result + (modifiedSemester != null ? modifiedSemester.hashCode() : 0);
        result = 31 * result + seen;
        result = 31 * result + (hidden ? 1 : 0);
        return result;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getGrade() {
        return grade;
    }

    public void setGrade(Double grade) {
        this.grade = grade;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public Double getCreditPoints() {
        return creditPoints;
    }

    public void setCreditPoints(Double creditPoints) {
        this.creditPoints = creditPoints;
    }

    public String getHash() {
        return hash;
    }

    public void setHash(String hash) {
        this.hash = hash;
    }

    public void setSeen(int seen) {
        this.seen = seen;
    }

    public int getSeen() {
        return seen;
    }

    public Double getWeight() {
        return weight;
    }

    public void setWeight(Double weight) {
        this.weight = weight;
    }

    public Double getModifiedGrade() {
        return modifiedGrade;
    }

    public void setModifiedGrade(Double modifiedGrade) {
        this.modifiedGrade = modifiedGrade;
    }

    public String getModifiedState(){
        return modifiedState;
    }

    public void setModifiedState(String modifiedState) {
        this.modifiedState = modifiedState;
    }

    public Double getModifiedCreditPoints() {
        return modifiedCreditPoints;
    }

    public void setModifiedCreditPoints(Double modifiedCreditPoints) {
        this.modifiedCreditPoints = modifiedCreditPoints;
    }

    public String getSemester() {
        return semester;
    }

    public void setSemester(String semester) {
        this.semester = semester;
    }

    public String getModifiedSemester() {
        return modifiedSemester;
    }

    public void setModifiedSemester(String modifiedSemester) {
        this.modifiedSemester = modifiedSemester;
    }

    public String getModifiedName() {
        return modifiedName;
    }

    public void setModifiedName(String modifiedName) {
        this.modifiedName = modifiedName;
    }

    public String getCurrentSemester() {
        return modifiedSemester == null ? semester : modifiedSemester;
    }

    public boolean isHidden() {
        return hidden;
    }

    public void setHidden(boolean hidden) {
        this.hidden = hidden;
    }
} public interface GradesAdapterItem {
} public class GradesFooterItem implements GradesAdapterItem {
    private boolean visible;

    public GradesFooterItem(boolean visible) {
        this.visible = visible;
    }

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }
} public class GradesSummaryItem implements GradesAdapterItem {
    private float average;
    private float creditPoints;
    private String lastUpdatedAt;
    private String infoBoxTitle;
    private String infoBoxMessage;
    private boolean infoBoxVisible;
    private String dismissPrefKey;
    private boolean noGradesInfoVisible;

    public GradesSummaryItem() {
    }

    public float getAverage() {
        return average;
    }

    public void setAverage(float average) {
        this.average = average;
    }

    public float getCreditPoints() {
        return creditPoints;
    }

    public void setCreditPoints(float creditPoints) {
        this.creditPoints = creditPoints;
    }

    public String getLastUpdatedAt() {
        return lastUpdatedAt;
    }

    public void setLastUpdatedAt(String lastUpdatedAt) {
        this.lastUpdatedAt = lastUpdatedAt;
    }

    public String getInfoBoxTitle() {
        return infoBoxTitle;
    }

    public void setInfoBoxTitle(String infoBoxTitle) {
        this.infoBoxTitle = infoBoxTitle;
    }

    public String getInfoBoxMessage() {
        return infoBoxMessage;
    }

    public void setInfoBoxMessage(String infoBoxMessage) {
        this.infoBoxMessage = infoBoxMessage;
    }

    public boolean isInfoBoxVisible() {
        return infoBoxVisible;
    }

    public void setInfoBoxVisible(boolean infoBoxVisible) {
        this.infoBoxVisible = infoBoxVisible;
    }

    public String getDismissPrefKey() {
        return dismissPrefKey;
    }

    public void setDismissPrefKey(String dismissPrefKey) {
        this.dismissPrefKey = dismissPrefKey;
    }

    public boolean isNoGradesInfoVisible() {
        return noGradesInfoVisible;
    }

    public void setNoGradesInfoVisible(boolean noGradesInfoVisible) {
        this.noGradesInfoVisible = noGradesInfoVisible;
    }
} public class SemesterItem implements GradesAdapterItem {
    private String semester;
    private float average;
    private float creditPoints;
    private List<GradeItem> grades;
    private boolean simpleWeighting;

    public SemesterItem(boolean simpleWeighting) {
        grades = new ArrayList<>();
        this.simpleWeighting = simpleWeighting;
    }

    public void addGrade(GradeItem gradeItem, boolean updateImmediately) {
        grades.add(gradeItem);

        if (updateImmediately) {
            update();
        }
    }

    /**
     * Updates the average grade and the sum of credit points.
     */
    public void update() {
        AverageCalculator calculator = new AverageCalculator(simpleWeighting);
        calculator.calculate(grades);

        this.average = calculator.getAverage();
        this.creditPoints = calculator.getCreditPointsSum();
    }

    public String getSemester() {
        return semester;
    }

    public void setSemester(String semester) {
        this.semester = semester;
    }

    public float getAverage() {
        return average;
    }

    public void setAverage(float average) {
        this.average = average;
    }

    public float getCreditPoints() {
        return creditPoints;
    }

    public void setCreditPoints(float creditPoints) {
        this.creditPoints = creditPoints;
    }

    public List<GradeItem> getGrades() {
        return grades;
    }

    public int getVisibleGradesCount() {
        int count = 0;
        for (GradeItem gradeItem : grades) {
            count += gradeItem.isHidden() ? 0 : 1;
        }
        return count;
    }

    public void setSimpleWeighting(boolean simpleWeighting) {
        this.simpleWeighting = simpleWeighting;
        update();
    }
} public class TriangleShapeView extends View {
    private Path path;
    private Paint paint;

    public TriangleShapeView(Context context) {
        super(context);
        create();
    }

    public TriangleShapeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        create();
    }

    public TriangleShapeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        create();
    }

    private void create() {
        paint = new Paint();
        paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
        paint.setStyle(Paint.Style.FILL);
        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int w = getWidth() / 2;

        path.rewind();
        path.moveTo(w, 0);
        path.lineTo(2 * w, 0);
        path.lineTo(2 * w, w);
        path.lineTo(w, 0);
        path.close();

        canvas.drawPath(path, paint);
    }
}
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.mygrades.R;
import de.mygrades.main.MainServiceHelper;
import de.mygrades.util.AverageCalculator;
import de.mygrades.util.Constants;
import de.mygrades.util.LogoutHelper;
import de.mygrades.view.UIHelper;
import de.mygrades.view.activity.GradeDetailedActivity;
import de.mygrades.view.adapter.model.GradeItem;
import de.mygrades.view.adapter.model.GradesAdapterItem;
import de.mygrades.view.adapter.model.GradesFooterItem;
import de.mygrades.view.adapter.model.GradesSummaryItem;
import de.mygrades.view.adapter.model.SemesterItem;
import de.mygrades.view.widget.TriangleShapeView;
package de.mygrades.view.adapter;




/**
 * Custom RecyclerView adapter for the overview of grades with semester headers.
 */
public class GradesRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final int VIEW_TYPE_SEMESTER = 0;
    private final int VIEW_TYPE_GRADE = 1;
    private final int VIEW_TYPE_SUMMARY = 2;
    private final int VIEW_TYPE_FOOTER = 3;

    private Context context;
    private SharedPreferences prefs;
    private SharedPreferences.OnSharedPreferenceChangeListener prefsListener;
    private boolean simpleWeighting;

    private List<GradesAdapterItem> items;
    private Map<String, Integer> semesterNumberMap;
    private String actualFirstSemester;

    private boolean editModeEnabled;
    private boolean hideCreditPoints;
    private boolean coloredGrades;

    public GradesRecyclerViewAdapter(Context context) {
        super();
        items = new ArrayList<>();
        semesterNumberMap = new HashMap<>();

        GradesSummaryItem summary = new GradesSummaryItem();
        GradesFooterItem footer = new GradesFooterItem(false);
        items.add(0, summary);
        items.add(1, footer);
        notifyItemRangeInserted(0, 1);

        this.context = context;
        prefs = PreferenceManager.getDefaultSharedPreferences(this.context);
        hideCreditPoints = prefs.getBoolean(context.getString(R.string.pref_key_hide_credit_points), false);
        coloredGrades = prefs.getBoolean(context.getString(R.string.pref_key_colored_grades), true);

        // register for preference changes
        prefsListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                if (key.equals(GradesRecyclerViewAdapter.this.context.getString(R.string.pref_key_max_credit_points))) {
                    notifyItemChanged(0);
                } else if (key.equals(GradesRecyclerViewAdapter.this.context.getString(R.string.pref_key_simple_weighting))) {
                    updateSimpleWeighting();
                    updateSummary();
                    updateSemesterSummaries();
                } else if (key.equals(GradesRecyclerViewAdapter.this.context.getString(R.string.pref_key_hide_credit_points))) {
                    hideCreditPoints(prefs.getBoolean(key, false));
                } else if (key.equals(GradesRecyclerViewAdapter.this.context.getString(R.string.pref_key_colored_grades))) {
                    coloredGrades = prefs.getBoolean(key, true);
                    notifyItemRangeChanged(0, items.size() - 1);
                }
            }
        };

        prefs.registerOnSharedPreferenceChangeListener(prefsListener);

        // set simple weighting
        updateSimpleWeighting();
    }

    /**
     * Add a new grade to the given semester by its termCount.
     *
     * @param newGrade - new grade
     */
    public void addGrade(GradeItem newGrade) {
        int semesterNumber = semesterNumberMap.get(newGrade.getCurrentSemester());
        String semester = newGrade.getCurrentSemester();

        // check if grade already exists in different semester and delete it
        checkForDeletion(newGrade);

        // find semester index, where the grade should be added
        int semesterIndex = getIndexForSemester(semesterNumber, semester);

        // update grade if necessary
        if (!updateGrade(newGrade, semesterIndex)) {
            addGrade(newGrade, semesterIndex);
        }

        updateFooter();
    }

    /**
     * Show footer if grade items exist.
     */
    private void updateFooter() {
        if (items.size() > 1) {
            ((GradesFooterItem)items.get(items.size() - 1)).setVisible(items.size() > 2);
            notifyItemChanged(items.size() - 1);
        }
    }

    /**
     * Deletes a grade item and its semester if necessary.
     *
     * @param newGrade grade item to delete
     */
    public void deleteGrade(GradeItem newGrade) {
        int semesterNumber = semesterNumberMap.get(newGrade.getCurrentSemester());
        String semester = newGrade.getCurrentSemester();

        // find semester index, where the grade should be added
        int semesterIndex = getIndexForSemester(semesterNumber, semester);
        SemesterItem semesterItem = (SemesterItem) items.get(semesterIndex);