gov.wa.wsdot.android.wsdot.ui.tollrates.SR167TollRatesFragment.java Source code

Java tutorial

Introduction

Here is the source code for gov.wa.wsdot.android.wsdot.ui.tollrates.SR167TollRatesFragment.java

Source

/*
 * Copyright (c) 2017 Washington State Department of Transportation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

package gov.wa.wsdot.android.wsdot.ui.tollrates;

import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableString;
import android.text.style.UnderlineSpan;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

import javax.inject.Inject;

import gov.wa.wsdot.android.wsdot.R;
import gov.wa.wsdot.android.wsdot.database.tollrates.TollRateGroup;
import gov.wa.wsdot.android.wsdot.database.tollrates.TollRateSignEntity;
import gov.wa.wsdot.android.wsdot.database.tollrates.TollTripEntity;
import gov.wa.wsdot.android.wsdot.database.traveltimes.TravelTimeEntity;
import gov.wa.wsdot.android.wsdot.di.Injectable;
import gov.wa.wsdot.android.wsdot.ui.BaseFragment;
import gov.wa.wsdot.android.wsdot.ui.WsdotApplication;
import gov.wa.wsdot.android.wsdot.util.ParserUtils;
import gov.wa.wsdot.android.wsdot.util.decoration.SimpleDividerItemDecoration;
import gov.wa.wsdot.android.wsdot.util.sort.SortTollGroupByDirection;
import gov.wa.wsdot.android.wsdot.util.sort.SortTollGroupByLocation;
import gov.wa.wsdot.android.wsdot.util.sort.SortTollGroupByMilepost;
import gov.wa.wsdot.android.wsdot.util.sort.SortTollTripsByMilepost;

public class SR167TollRatesFragment extends BaseFragment
        implements SwipeRefreshLayout.OnRefreshListener, Injectable {

    private static final String TAG = SR167TollRatesFragment.class.getSimpleName();
    private static SR167TollRatesItemAdapter mAdapter;
    private View mEmptyView;
    private static SwipeRefreshLayout swipeRefreshLayout;

    protected RecyclerView mRecyclerView;
    protected LinearLayoutManager mLayoutManager;

    private Handler handler = new Handler();
    private Timer timer;

    private RadioGroup directionRadioGroup;
    private int radioGroupDirectionIndex = 0;

    private Tracker mTracker;

    private ArrayList<TollRateGroup> tollGroups = new ArrayList<>();
    private ArrayList<TravelTimeEntity> travelTimes = new ArrayList<>();

    @Inject
    ViewModelProvider.Factory viewModelFactory;
    TollRatesViewModel viewModel;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_dynamic_toll_rates, null);

        mRecyclerView = root.findViewById(R.id.my_recycler_view);
        mRecyclerView.setHasFixedSize(true);
        mLayoutManager = new LinearLayoutManager(getActivity());
        mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mAdapter = new SR167TollRatesItemAdapter(getActivity());
        mRecyclerView.setAdapter(mAdapter);

        mRecyclerView.addItemDecoration(new SimpleDividerItemDecoration(getActivity()));

        mRecyclerView.setPadding(0, 0, 0, 120);

        addDisclaimerView(root);

        directionRadioGroup = root.findViewById(R.id.segment_control);

        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getContext());
        radioGroupDirectionIndex = sharedPref.getInt(getString(R.string.toll_rates_167_travel_direction_key), 0);

        if (radioGroupDirectionIndex == 0) {
            RadioButton leftSegment = root.findViewById(R.id.radio_left);
            leftSegment.setChecked(true);
        } else {
            RadioButton rightSegment = root.findViewById(R.id.radio_right);
            rightSegment.setChecked(true);
        }

        directionRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {

            RadioButton selectedDirection = directionRadioGroup.findViewById(checkedId);

            mAdapter.setData(filterTollsForDirection(String.valueOf(selectedDirection.getText().charAt(0))));

            mLayoutManager.scrollToPositionWithOffset(0, 0);
            SharedPreferences sharedPref1 = PreferenceManager.getDefaultSharedPreferences(getContext());
            SharedPreferences.Editor editor = sharedPref1.edit();

            radioGroupDirectionIndex = directionRadioGroup.indexOfChild(selectedDirection);

            TextView travelTimeView = root.findViewById(R.id.travel_time_text);
            travelTimeView.setText(getTravelTimeStringForDirection(radioGroupDirectionIndex == 0 ? "N" : "S"));

            editor.putInt(getString(R.string.toll_rates_167_travel_direction_key), radioGroupDirectionIndex);

            editor.apply();

        });

        // For some reason, if we omit this, NoSaveStateFrameLayout thinks we are
        // FILL_PARENT / WRAP_CONTENT, making the progress bar stick to the top of the activity.
        root.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));

        swipeRefreshLayout = root.findViewById(R.id.swipe_container);
        swipeRefreshLayout.setOnRefreshListener(this);
        swipeRefreshLayout.setColorSchemeResources(R.color.holo_blue_bright, R.color.holo_green_light,
                R.color.holo_orange_light, R.color.holo_red_light);

        mEmptyView = root.findViewById(R.id.empty_list_view);

        TextView header_link = root.findViewById(R.id.header_text);

        // create spannable string for underline
        SpannableString content = new SpannableString(
                getActivity().getResources().getString(R.string.sr167_info_link));
        content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
        header_link.setText(content);

        header_link.setTextColor(getResources().getColor(R.color.primary_default));
        header_link.setOnClickListener(v -> {
            Intent intent = new Intent();
            // GA tracker
            mTracker = ((WsdotApplication) getActivity().getApplication()).getDefaultTracker();
            mTracker.setScreenName("/Toll Rates/Learn about SR-167");
            mTracker.send(new HitBuilders.ScreenViewBuilder().build());
            intent.setAction(Intent.ACTION_VIEW);
            intent.setData(Uri.parse("https://www.wsdot.wa.gov/Tolling/SR167HotLanes/HOTtollrates.htm"));
            startActivity(intent);

        });

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(TollRatesViewModel.class);

        viewModel.getResourceStatus().observe(this, resourceStatus -> {
            if (resourceStatus != null) {
                switch (resourceStatus.status) {
                case LOADING:
                    swipeRefreshLayout.setRefreshing(true);
                    break;
                case SUCCESS:
                    swipeRefreshLayout.setRefreshing(false);
                    break;
                case ERROR:
                    swipeRefreshLayout.setRefreshing(false);
                    Toast.makeText(this.getContext(), "connection error", Toast.LENGTH_LONG).show();
                }
            }
        });

        viewModel.getSR167TollRateItems().observe(this, tollRateGroups -> {
            if (tollRateGroups != null) {
                mEmptyView.setVisibility(View.GONE);

                Collections.sort(tollRateGroups, new SortTollGroupByLocation());
                Collections.sort(tollRateGroups, new SortTollGroupByDirection());

                tollGroups = new ArrayList<>(tollRateGroups);

                directionRadioGroup.getCheckedRadioButtonId();
                RadioButton selectedDirection = directionRadioGroup
                        .findViewById(directionRadioGroup.getCheckedRadioButtonId());

                mAdapter.setData(filterTollsForDirection(String.valueOf(selectedDirection.getText().charAt(0))));
            }
        });

        viewModel.getTravelTimesForETLFor("167").observe(this, travelTimes -> {

            TextView travelTimeView = root.findViewById(R.id.travel_time_text);

            if (travelTimes.size() > 0) {
                travelTimeView.setVisibility(View.VISIBLE);

                this.travelTimes = new ArrayList<>(travelTimes);

                travelTimeView.setText(getTravelTimeStringForDirection(radioGroupDirectionIndex == 0 ? "N" : "S"));
            } else {

                travelTimeView.setVisibility(View.GONE);
            }
        });

        timer = new Timer();
        timer.schedule(new SR167TollRatesFragment.RatesTimerTask(), 0, 60000); // Schedule rates to update every 60 seconds

        return root;
    }

    private ArrayList<TollRateGroup> filterTollsForDirection(String direction) {

        ArrayList<TollRateGroup> filteredTolls = new ArrayList<>();

        for (TollRateGroup group : tollGroups) {
            if (group.tollRateSign.getTravelDirection().equals(direction)) {

                if (direction.equals("S")) {
                    Collections.sort(group.trips,
                            new SortTollTripsByMilepost(SortTollTripsByMilepost.SortOrder.DESCENDING));
                } else if (direction.equals("N")) {
                    Collections.sort(group.trips,
                            new SortTollTripsByMilepost(SortTollTripsByMilepost.SortOrder.ASCENDING));
                }

                filteredTolls.add(group);
            }
        }

        if (direction.equals("S")) {
            Collections.sort(filteredTolls,
                    new SortTollGroupByMilepost(SortTollGroupByMilepost.SortOrder.DESCENDING));
        } else if (direction.equals("N")) {
            Collections.sort(filteredTolls,
                    new SortTollGroupByMilepost(SortTollGroupByMilepost.SortOrder.ASCENDING));
        }

        return filteredTolls;
    }

    private String getTravelTimeStringForDirection(String direction) {

        int[] timeIDs = new int[2];

        if (direction.equals("N")) {
            timeIDs[0] = 67; // GP
            timeIDs[1] = 68; // HOV
        } else if (direction.equals("S")) {
            timeIDs[0] = 70; // GP
            timeIDs[1] = 69; // HOV
        }

        // array holds indexes for travelTimes in north or southbound direction to build string with
        int[] timeIndexes = new int[2];

        for (TravelTimeEntity time : travelTimes) {
            if (time.getTravelTimeId() == timeIDs[0]) {
                timeIndexes[0] = travelTimes.indexOf(time);
            } else if (time.getTravelTimeId() == timeIDs[1]) {
                timeIndexes[1] = travelTimes.indexOf(time);
            }
        }

        return String.format("%s: %s min%s or %s min%s via HOT lane",
                travelTimes.get(timeIndexes[0]).getTripTitle(), travelTimes.get(timeIndexes[0]).getCurrent(),
                (travelTimes.get(timeIndexes[0]).getCurrent() > 1 ? "s" : ""),
                travelTimes.get(timeIndexes[1]).getCurrent(),
                (travelTimes.get(timeIndexes[1]).getCurrent() > 1 ? "s" : ""));
    }

    public class RatesTimerTask extends TimerTask {
        private Runnable runnable = new Runnable() {
            public void run() {
                viewModel.refresh();
            }
        };

        public void run() {
            handler.post(runnable);
        }
    }

    /**
     * Adds a toll rate accuracy disclaimer to the bottom of the view
     * @param root
     */
    private void addDisclaimerView(ViewGroup root) {
        FrameLayout frame = root.findViewById(R.id.list_container);
        TextView textView = new TextView(getContext());
        textView.setBackgroundColor(getResources().getColor(R.color.alerts));
        textView.setText(
                "Estimated toll rates provided as a courtesy. Youll always pay the toll you see on actual road signs when you enter.");
        textView.setPadding(15, 20, 15, 15);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
                FrameLayout.LayoutParams.WRAP_CONTENT);
        params.gravity = Gravity.BOTTOM;
        textView.setLayoutParams(params);
        frame.addView(textView);
    }

    /**
     * Custom adapter for items in recycler view.
     *
     * Binds the custom ViewHolder class to it's data.
     *
     * @see android.support.v7.widget.RecyclerView.Adapter
     */
    private class SR167TollRatesItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        private Typeface tf = Typeface.createFromAsset(getActivity().getAssets(), "fonts/Roboto-Regular.ttf");
        private Typeface tfb = Typeface.createFromAsset(getActivity().getAssets(), "fonts/Roboto-Bold.ttf");
        private Context context;

        private ArrayList<TollRateGroup> mData = new ArrayList<>();

        private List<RecyclerView.ViewHolder> mItems = new ArrayList<>();

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

        public void setData(ArrayList<TollRateGroup> data) {
            mData = data;
            this.notifyDataSetChanged();
        }

        @Override
        public SR167TollRatesItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(context).inflate(R.layout.list_item_travel_time_group, null);
            SR167TollRatesItemAdapter.ViewHolder viewholder = new SR167TollRatesItemAdapter.ViewHolder(view);
            view.setTag(viewholder);
            mItems.add(viewholder);
            return viewholder;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {

            SR167TollRatesItemAdapter.ViewHolder viewholder = (SR167TollRatesItemAdapter.ViewHolder) viewHolder;

            TollRateGroup tollRateGroup = mData.get(position);

            final String id = tollRateGroup.tollRateSign.getId();

            String title = tollRateGroup.tollRateSign.getLocationName();

            viewholder.title.setText(title);
            viewholder.title.setTypeface(tfb);

            viewholder.travel_times_layout.removeAllViews();

            // make a trip view with toll rate for each trip in the group
            for (TollTripEntity trip : tollRateGroup.trips) {

                View tripView = makeTripView(trip, tollRateGroup.tollRateSign, getContext());

                // remove the line from the last trip
                if (tollRateGroup.trips.indexOf(trip) == tollRateGroup.trips.size() - 1) {
                    tripView.findViewById(R.id.line).setVisibility(View.GONE);
                }

                viewholder.travel_times_layout.addView(tripView);
            }

            // Seems when Android recycles the views, the onCheckedChangeListener is still active
            // and the call to setChecked() causes that code within the listener to run repeatedly.
            // Assigning null to setOnCheckedChangeListener seems to fix it.
            viewholder.star_button.setOnCheckedChangeListener(null);
            viewholder.star_button.setChecked(tollRateGroup.tollRateSign.getIsStarred() != 0);

            viewholder.star_button.setOnCheckedChangeListener((buttonView, isChecked) -> {

                Snackbar added_snackbar = Snackbar.make(getView(), R.string.add_favorite, Snackbar.LENGTH_SHORT);

                Snackbar removed_snackbar = Snackbar.make(getView(), R.string.remove_favorite,
                        Snackbar.LENGTH_SHORT);

                if (isChecked) {
                    added_snackbar.show();
                } else {
                    removed_snackbar.show();
                }

                viewModel.setIsStarredFor(id, isChecked ? 1 : 0);
            });
        }

        @Override
        public int getItemCount() {
            return mData.size();
        }

        private class ViewHolder extends RecyclerView.ViewHolder {
            public LinearLayout travel_times_layout;
            public TextView title;
            public CheckBox star_button;

            public ViewHolder(View view) {
                super(view);
                travel_times_layout = view.findViewById(R.id.travel_times_linear_layout);
                title = view.findViewById(R.id.title);
                star_button = view.findViewById(R.id.star_button);
            }
        }
    }

    /**
     * Returns a view for the toll trip provided by the tripItem.
     * A tripItem holds information about the trip destination and toll rate
     *
     * @param tripItem
     * @param context
     * @return
     */
    public static View makeTripView(TollTripEntity tripItem, TollRateSignEntity sign, Context context) {

        Typeface tfb = Typeface.createFromAsset(context.getAssets(), "fonts/Roboto-Bold.ttf");
        LayoutInflater li = LayoutInflater.from(context);
        View cv = li.inflate(R.layout.trip_view, null);

        cv.findViewById(R.id.title).setVisibility(View.GONE);

        ((TextView) cv.findViewById(R.id.subtitle)).setText("Show on map");
        ((TextView) cv.findViewById(R.id.subtitle))
                .setTextColor(context.getResources().getColor(R.color.primary_default));
        cv.findViewById(R.id.subtitle).setOnClickListener(v -> {
            Bundle b = new Bundle();

            b.putDouble("startLat", sign.getStartLatitude());
            b.putDouble("startLong", sign.getStartLongitude());

            b.putDouble("endLat", tripItem.getEndLatitude());
            b.putDouble("endLong", tripItem.getEndLongitude());

            b.putString("title", sign.getLocationName());
            b.putString("text", String.format("Travel as far as %s.", tripItem.getEndLocationName()));

            Intent intent = new Intent(context, TollRatesRouteActivity.class);
            intent.putExtras(b);
            context.startActivity(intent);
        });

        ((TextView) cv.findViewById(R.id.content)).setText("Carpools and motorcycles free");

        // set updated label
        ((TextView) cv.findViewById(R.id.updated))
                .setText(ParserUtils.relativeTime(tripItem.getUpdated(), "MMMM d, yyyy h:mm a", false));

        // set toll
        TextView currentTimeTextView = cv.findViewById(R.id.current_value);
        currentTimeTextView.setTypeface(tfb);
        currentTimeTextView.setText(String.format(Locale.US, "$%.2f", tripItem.getTollRate() / 100));

        // set message if there is one
        if (!tripItem.getMessage().equals("null")) {
            currentTimeTextView.setText(tripItem.getMessage());
        }

        return cv;
    }

    public void onRefresh() {
        swipeRefreshLayout.setRefreshing(true);
        viewModel.refresh();
    }
}