com.ofalvai.bpinfo.ui.alertlist.AlertListFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.ofalvai.bpinfo.ui.alertlist.AlertListFragment.java

Source

/*
 * Copyright 2016 Olivr Falvai
 *
 *    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 com.ofalvai.bpinfo.ui.alertlist;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.android.volley.VolleyError;
import com.ofalvai.bpinfo.BpInfoApplication;
import com.ofalvai.bpinfo.R;
import com.ofalvai.bpinfo.model.Alert;
import com.ofalvai.bpinfo.model.RouteType;
import com.ofalvai.bpinfo.ui.alert.AlertDetailFragment;
import com.ofalvai.bpinfo.ui.settings.SettingsActivity;
import com.ofalvai.bpinfo.util.EmptyRecyclerView;
import com.ofalvai.bpinfo.util.FabricUtils;
import com.ofalvai.bpinfo.util.Utils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import butterknife.BindView;
import butterknife.ButterKnife;

public class AlertListFragment extends Fragment
        implements AlertListContract.View, AlertFilterFragment.AlertFilterListener {

    private static final String TAG = "AlertListFragment";

    private static final String KEY_ACTIVE_FILTER = "active_filter";

    private static final String KEY_ALERT_LIST_TYPE = "alert_list_type";

    private static final String FILTER_DIALOG_TAG = "filter_dialog";

    private static final String NOTICE_DIALOG_TAG = "notice_dialog";

    private AlertListContract.Presenter mPresenter;

    @Nullable
    private AlertAdapter mAlertAdapter;

    private AlertListType mAlertListType;

    @Nullable
    private AlertFilterFragment mFilterFragment;

    @BindView(R.id.alerts_recycler_view)
    EmptyRecyclerView mAlertRecyclerView;

    @BindView(R.id.alerts_swipe_refresh_layout)
    SwipeRefreshLayout mSwipeRefreshLayout;

    @BindView(R.id.error_with_action)
    LinearLayout mErrorLayout;

    @BindView(R.id.alert_list_filter_active_message)
    TextView mFilterWarningView;

    @BindView(R.id.empty_view)
    TextView mEmptyView;

    @BindView(R.id.alert_list_notice)
    TextView mNoticeView;

    public static AlertListFragment newInstance(@NonNull AlertListType type) {
        AlertListFragment fragment = new AlertListFragment();
        fragment.mAlertListType = type;
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Set<RouteType> restoredFilter = null;

        if (savedInstanceState != null) {
            mAlertListType = (AlertListType) savedInstanceState.getSerializable(KEY_ALERT_LIST_TYPE);
            restoredFilter = (HashSet<RouteType>) savedInstanceState.getSerializable(KEY_ACTIVE_FILTER);
        }

        mPresenter = new AlertListPresenter(mAlertListType);
        mPresenter.attachView(this);

        if (restoredFilter != null) {
            mPresenter.setFilter(restoredFilter);
        }
    }

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

        View view = inflater.inflate(R.layout.fragment_alert_list, container, false);
        ButterKnife.bind(this, view);

        setupRecyclerView();

        mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                initRefresh();
                FabricUtils.logManualRefresh();
            }
        });

        // If this fragment got recreated while the filter dialog was open, we need to update
        // the listener reference
        if (savedInstanceState != null) {
            mFilterFragment = (AlertFilterFragment) getFragmentManager().findFragmentByTag(FILTER_DIALOG_TAG);

            // Only attach to the filter fragment if it filters our type of list
            if (mFilterFragment != null && mAlertListType == mFilterFragment.getAlertListType()) {
                mFilterFragment.setFilterListener(this);
                mFilterFragment.setFilter(mPresenter.getFilter());
            }
        }

        return view;
    }

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);

        initRefresh();
        updateFilterWarning();
    }

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

        outState.putSerializable(KEY_ALERT_LIST_TYPE, mAlertListType);

        // Casting to HashSet, because Set is not serializable :(
        //noinspection CollectionDeclaredAsConcreteClass
        HashSet<RouteType> filter = (HashSet<RouteType>) mPresenter.getFilter();

        outState.putSerializable(KEY_ACTIVE_FILTER, filter);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.menu_main, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_item_filter_alerts:
            displayFilterDialog();
            break;
        case R.id.menu_item_settings:
            startActivity(SettingsActivity.newIntent(getContext()));
            break;
        }
        return true;
    }

    @Override
    public void onStart() {
        super.onStart();

        boolean updating = mPresenter.updateIfNeeded();
        if (updating && Utils.hasNetworkConnection(getContext())) {
            setUpdating(true);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (mPresenter != null) {
            mPresenter.detachView();
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        BpInfoApplication.getRefWatcher(getContext()).watch(this);
    }

    /**
     * Updates the Toolbar's subtitle to the number of current items in the RecyclerView's Adapter
     */
    @Override
    public void updateSubtitle() {
        // Update subtitle only if the fragment is attached and visible to the user (not preloaded
        // by ViewPager)
        if (mAlertAdapter != null && isAdded()) {
            int count = mAlertAdapter.getItemCount();
            String subtitle = getResources().getQuantityString(R.plurals.actionbar_subtitle_alert_count, count,
                    count);
            AppCompatActivity activity = (AppCompatActivity) getActivity();
            if (activity.getSupportActionBar() != null) {
                activity.getSupportActionBar().setSubtitle(subtitle);
            }
        }
    }

    @Override
    public void onFilterChanged(@NonNull Set<RouteType> selectedTypes) {
        // Prefent leaking the fragment
        mFilterFragment = null;

        mPresenter.setFilter(selectedTypes);
        mPresenter.getAlertList();

        updateFilterWarning();
    }

    @Override
    public void onFilterDismissed() {
        // Prevents leaking the fragment
        mFilterFragment = null;
    }

    @Override
    public void displayAlerts(@NonNull List<Alert> alerts) {
        // It's possible that the network response callback thread executes this faster than
        // the UI thread attaching the fragment to the activity. In that case getResources() or
        // getString() would throw an exception.
        if (isAdded()) {
            setErrorView(false, null);

            if (mAlertAdapter != null) {
                mAlertAdapter.updateAlertData(alerts, new AlertListUpdateCallback());
            }

            setUpdating(false);
        }
    }

    @Override
    public void displayNetworkError(@NonNull VolleyError error) {
        // It's possible that the network response callback thread executes this faster than
        // the UI thread attaching the fragment to the activity. In that case getResources() would
        // throw an exception.
        if (isAdded()) {
            int errorMessageId = Utils.volleyErrorTypeHandler(error);
            String errorMessage = getResources().getString(errorMessageId);

            setErrorView(true, errorMessage);
        }
    }

    @Override
    public void displayDataError() {
        if (isAdded()) {
            setErrorView(true, getString(R.string.error_list_display));
        }
    }

    @Override
    public void displayGeneralError() {
        if (isAdded()) {
            setErrorView(true, getString(R.string.error_list_display));
        }
    }

    @Override
    public void displayNoNetworkWarning() {
        if (isAdded()) {
            setUpdating(false);

            Snackbar snackbar = Snackbar.make(mSwipeRefreshLayout, R.string.error_no_connection,
                    Snackbar.LENGTH_LONG);

            snackbar.setAction(R.string.label_retry, new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    initRefresh();
                }
            });
            snackbar.show();
        }
    }

    @Override
    public void displayNotice(final String noticeText) {
        mNoticeView.setVisibility(View.VISIBLE);
        mNoticeView.setText(Html.fromHtml(noticeText));
        mNoticeView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NoticeFragment fragment = NoticeFragment.newInstance(noticeText);
                FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
                fragment.show(transaction, NOTICE_DIALOG_TAG);

                FabricUtils.logNoticeDialogView();
            }
        });
    }

    @Override
    public void removeNotice() {
        mNoticeView.setVisibility(View.GONE);
    }

    public void launchAlertDetail(@NonNull Alert alert) {
        displayAlertDetail(alert);

        mPresenter.fetchAlert(alert.getId());
    }

    @Override
    public void displayAlertDetail(@NonNull Alert alert) {
        AlertDetailFragment alertDetailFragment = AlertDetailFragment.newInstance(alert, mPresenter);
        alertDetailFragment.show(getActivity().getSupportFragmentManager(), AlertDetailFragment.FRAGMENT_TAG);
    }

    @Override
    public void updateAlertDetail(@NonNull Alert alert) {
        FragmentManager manager = getActivity().getSupportFragmentManager();
        AlertDetailFragment fragment = (AlertDetailFragment) manager
                .findFragmentByTag(AlertDetailFragment.FRAGMENT_TAG);

        // It's possible that the presenter calls this method instantly, when the fragment is not
        // yet attached.
        if (fragment != null) {
            fragment.updateAlert(alert);
        }
    }

    @Override
    public void displayAlertDetailError() {
        FragmentManager manager = getActivity().getSupportFragmentManager();
        AlertDetailFragment fragment = (AlertDetailFragment) manager
                .findFragmentByTag(AlertDetailFragment.FRAGMENT_TAG);

        // It's possible that the presenter calls this method instantly, when the fragment is not
        // yet attached.
        if (fragment != null) {
            fragment.onAlertUpdateFailed();
        }
    }

    @Override
    public AlertListType getAlertListType() {
        return mAlertListType;
    }

    private void setupRecyclerView() {
        mAlertAdapter = new AlertAdapter(new ArrayList<Alert>(), getActivity(), this);
        mAlertRecyclerView.setAdapter(mAlertAdapter);

        final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        mAlertRecyclerView.setLayoutManager(layoutManager);

        final DividerItemDecoration decoration = new DividerItemDecoration(getContext(),
                layoutManager.getOrientation());
        mAlertRecyclerView.addItemDecoration(decoration);

        mAlertRecyclerView.setEmptyView(mEmptyView);

        // Fixing overscroll effect at the bottom of the list. If a SwipeRefreshLayout is the parent
        // of the RecyclerView, we need to disable that when the user scrolls down.
        mAlertRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                try {
                    int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition();
                    mSwipeRefreshLayout.setEnabled(firstVisiblePosition == 0);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void initRefresh() {
        setUpdating(true);

        mPresenter.fetchAlertList();
        mPresenter.fetchNotice();

        mPresenter.setLastUpdate();
    }

    private void setUpdating(boolean updating) {
        mSwipeRefreshLayout.setRefreshing(updating);
    }

    private void displayFilterDialog() {
        mFilterFragment = AlertFilterFragment.newInstance(this, mPresenter.getFilter(), mAlertListType);
        FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
        mFilterFragment.show(transaction, FILTER_DIALOG_TAG);

        FabricUtils.logFilterDialogOpened();
    }

    /**
     * Displays or hides the error view. If displaying, it also sets the retry button's event listener
     * and the error message.
     *
     * @param state true to display, false to hide
     */
    private void setErrorView(boolean state, String errorMessage) {
        if (state) {
            setUpdating(false);

            mAlertRecyclerView.setVisibility(View.GONE);

            TextView errorMessageView = (TextView) mErrorLayout.findViewById(R.id.error_message);
            Button refreshButton = (Button) mErrorLayout.findViewById(R.id.error_action_button);

            if (!refreshButton.hasOnClickListeners()) {
                refreshButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        initRefresh();
                    }
                });
            }
            refreshButton.setText(getString(R.string.label_retry));

            mErrorLayout.setVisibility(View.VISIBLE);
            errorMessageView.setText(errorMessage);
        } else {
            mAlertRecyclerView.setVisibility(View.VISIBLE);
            mErrorLayout.setVisibility(View.GONE);
        }
    }

    /**
     * Updates the filter warning bar above the list based on the currently selected RouteTypes.
     * Hides the bar if nothing is selected as filter.
     */
    private void updateFilterWarning() {
        // Might be null, because it gets called by onCreate() too
        if (mFilterWarningView != null && mPresenter != null) {
            Set<RouteType> selectedTypes = mPresenter.getFilter();

            if (selectedTypes == null) {
                return;
            }

            if (selectedTypes.isEmpty()) {
                mFilterWarningView.setVisibility(View.GONE);
            } else {
                StringBuilder typeList = new StringBuilder();
                final String separator = ", ";
                for (RouteType type : selectedTypes) {
                    typeList.append(Utils.routeTypeToString(getContext(), type));
                    typeList.append(separator);
                }

                // Removing the last separator at the end of the list
                String completeString = getString(R.string.filter_message, typeList.toString());
                if (completeString.endsWith(separator)) {
                    completeString = completeString.substring(0, completeString.length() - separator.length());
                }

                mFilterWarningView.setText(completeString);
                mFilterWarningView.setVisibility(View.VISIBLE);
            }
        }
    }

    /**
     * Scrolls to top and calls updateSubtitle() after the Alert list changed visually.
     */
    private class AlertListUpdateCallback implements ListUpdateCallback {
        @Override
        public void onInserted(int position, int count) {
            // Only update the toolbar if this fragment is currently selected in the ViewPager
            if (getUserVisibleHint()) {
                updateSubtitle();
            }
            mAlertRecyclerView.smoothScrollToPosition(0);
        }

        @Override
        public void onRemoved(int position, int count) {
            if (getUserVisibleHint()) {
                updateSubtitle();
            }

            // For some reason, the usual RecyclerView.smoothScrollToPosition(0) doesn't work here,
            // the list scrolls to the bottom, instead of the top.
            if (mAlertRecyclerView.getLayoutManager() instanceof LinearLayoutManager) {
                LinearLayoutManager manager = (LinearLayoutManager) mAlertRecyclerView.getLayoutManager();
                manager.scrollToPosition(0);
            }
        }

        @Override
        public void onMoved(int fromPosition, int toPosition) {
            if (getUserVisibleHint()) {
                updateSubtitle();
            }
            mAlertRecyclerView.smoothScrollToPosition(0);
        }

        @Override
        public void onChanged(int position, int count, Object payload) {
            if (getUserVisibleHint()) {
                updateSubtitle();
            }
            mAlertRecyclerView.smoothScrollToPosition(0);
        }
    }
}