org.onebusaway.android.ui.ArrivalsListHeader.java Source code

Java tutorial

Introduction

Here is the source code for org.onebusaway.android.ui.ArrivalsListHeader.java

Source

/*
 * Copyright (C) 2011-2014 Paul Watts (paulcwatts@gmail.com),
 * University of South Florida (sjbarbeau@gmail.com)
 *
 * 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 org.onebusaway.android.ui;

import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.ObaAnalytics;
import org.onebusaway.android.io.elements.ObaArrivalInfo;
import org.onebusaway.android.io.elements.ObaRegion;
import org.onebusaway.android.provider.ObaContract;
import org.onebusaway.android.util.UIUtils;

import android.annotation.TargetApi;
import android.content.ContentQueryMap;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.GradientDrawable;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.FragmentManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

//
// A helper class that gets most of the header interaction
// out of the Fragment itself.
//
class ArrivalsListHeader {

    interface Controller {

        Location getStopLocation();

        String getStopName();

        String getStopDirection();

        String getUserStopName();

        String getStopId();

        void setUserStopName(String userName);

        long getLastGoodResponseTime();

        // Returns a sorted list (by ETA) of arrival times for the current stop
        ArrayList<ArrivalInfo> getArrivalInfo();

        ArrayList<String> getRoutesFilter();

        void setRoutesFilter(ArrayList<String> filter);

        int getNumRoutes();

        boolean isFavoriteStop();

        boolean setFavoriteStop(boolean favorite);

        AlertList getAlertList();

        List<String> getRouteDisplayNames();

        /**
         * Gets the range of arrival info (i.e., arrival info for the next "minutesAfter" minutes),
         * or -1 if this information isn't available
         *
         * @return minutesAfter the range of arrival info (i.e., arrival info for the next
         * "minutesAfter" minutes), or -1 if this information isn't available
         */
        int getMinutesAfter();

        /**
         * Shows the list item menu for the given view and arrival info
         *
         * @param v    view for the container of the arrival info
         * @param stop arrival info to show a list for
         */
        void showListItemMenu(View v, final ArrivalInfo stop);

        /**
         * Triggers a local refresh of the controller content (i.e., does not trigger another
         * call to the server)
         */
        void refreshLocal();
    }

    private static final String TAG = "ArrivalsListHeader";

    private Controller mController;

    private Context mContext;

    private Resources mResources;

    private FragmentManager mFragmentManager;

    //
    // Cached views
    //
    private View mView; // Entire header parent view

    private View mMainContainerView; // Holds everything except for the mFilterGroup

    private View mNameContainerView;

    private View mEditNameContainerView;

    private TextView mNameView;

    private EditText mEditNameView;

    private ImageButton mStopFavorite;

    private View mFilterGroup;

    private TextView mShowAllView;

    private ClickableSpan mShowAllClick;

    private boolean mInNameEdit = false;

    // Default state for the header is inside a sliding fragment
    private boolean mIsSlidingPanelCollapsed = true;

    private int mShortAnimationDuration;

    private ProgressBar mProgressBar;

    private ImageButton mStopInfo;

    private ImageView mExpandCollapse;

    /**
     * Variables used to show/hide alerts in the header
     */
    private ImageView mAlertView;

    boolean mHasWarning = false;

    boolean mHasError = false;

    // All arrival info returned by the adapter
    private ArrayList<ArrivalInfo> mArrivalInfo;

    // Arrival info for the two rows of arrival info in the header
    private ArrayList<ArrivalInfo> mHeaderArrivalInfo = new ArrayList<>(2);

    // Trip (e.g., reminder) content for this stop
    protected ContentQueryMap mTripsForStop;

    /**
     * Views to show ETA information in header
     */
    // Number of arrival current shown in the header - needed for various header view sizing.
    int mNumHeaderArrivals = -1; // -1 if mArrivalInfo hasn't been refreshed

    private TextView mNoArrivals;

    private final float ETA_TEXT_SIZE_SP = 30;

    private final float ETA_TEXT_NOW_SIZE_SP = 28;

    // Row 1
    private View mEtaContainer1;

    private ImageButton mEtaRouteFavorite1;

    private ImageButton mEtaReminder1;

    private TextView mEtaRouteName1;

    private TextView mEtaRouteDirection1;

    private RelativeLayout mEtaAndMin1;

    private TextView mEtaArrivalInfo1;

    private TextView mEtaMin1;

    private ViewGroup mEtaRealtime1;

    private ImageButton mEtaMoreVert1;

    private PopupWindow mPopup1;

    private View mEtaSeparator;

    // Row 2
    private View mEtaContainer2;

    private ImageButton mEtaRouteFavorite2;

    private ImageButton mEtaReminder2;

    private TextView mEtaRouteName2;

    private TextView mEtaRouteDirection2;

    private RelativeLayout mEtaAndMin2;

    private TextView mEtaArrivalInfo2;

    private TextView mEtaMin2;

    private ViewGroup mEtaRealtime2;

    private ImageButton mEtaMoreVert2;

    private PopupWindow mPopup2;

    // Animations
    private static final float ANIM_PIVOT_VALUE = 0.5f; // 50%

    private static final float ANIM_STATE_NORMAL = 0.0f; // 0 degrees (no rotation)

    private static final float ANIM_STATE_INVERTED = 180.0f; // 180 degrees

    private static final long ANIM_DURATION = 300; // milliseconds

    // Manages header size in "stop name edit mode"
    private int cachedExpandCollapseViewVisibility;

    private static float HEADER_HEIGHT_ONE_ARRIVAL_DP;

    private static float HEADER_HEIGHT_TWO_ARRIVALS_DP;

    private static float HEADER_HEIGHT_EDIT_NAME_DP;

    private static float HEADER_OFFSET_FILTER_ROUTES_DP;

    // Controller to change parent sliding panel
    HomeActivity.SlidingPanelController mSlidingPanelController;

    ArrivalsListHeader(Context context, Controller controller, FragmentManager fm) {
        mController = controller;
        mContext = context;
        mResources = context.getResources();
        mFragmentManager = fm;

        // Retrieve and cache the system's default "short" animation time.
        mShortAnimationDuration = mResources.getInteger(android.R.integer.config_shortAnimTime);
    }

    void initView(View view) {
        // Clear any existing arrival info
        mArrivalInfo = null;
        mHeaderArrivalInfo.clear();
        mNumHeaderArrivals = -1;

        // Cache the ArrivalsListHeader height values
        HEADER_HEIGHT_ONE_ARRIVAL_DP = view.getResources().getDimension(R.dimen.arrival_header_height_one_arrival)
                / view.getResources().getDisplayMetrics().density;
        HEADER_HEIGHT_TWO_ARRIVALS_DP = view.getResources().getDimension(R.dimen.arrival_header_height_two_arrivals)
                / view.getResources().getDisplayMetrics().density;
        HEADER_OFFSET_FILTER_ROUTES_DP = view.getResources()
                .getDimension(R.dimen.arrival_header_height_offset_filter_routes)
                / view.getResources().getDisplayMetrics().density;
        HEADER_HEIGHT_EDIT_NAME_DP = view.getResources().getDimension(R.dimen.arrival_header_height_edit_name)
                / view.getResources().getDisplayMetrics().density;

        // Init views
        mView = view;
        mMainContainerView = mView.findViewById(R.id.main_header_content);
        mNameContainerView = mView.findViewById(R.id.stop_name_and_info_container);
        mEditNameContainerView = mView.findViewById(R.id.edit_name_container);
        mNameView = (TextView) mView.findViewById(R.id.stop_name);
        mEditNameView = (EditText) mView.findViewById(R.id.edit_name);
        mStopFavorite = (ImageButton) mView.findViewById(R.id.stop_favorite);
        mStopFavorite.setColorFilter(mView.getResources().getColor(R.color.header_text_color));
        mFilterGroup = mView.findViewById(R.id.filter_group);

        mShowAllView = (TextView) mView.findViewById(R.id.show_all);
        // Remove any previous clickable spans - we're recycling views between fragments for efficiency
        UIUtils.removeAllClickableSpans(mShowAllView);
        mShowAllClick = new ClickableSpan() {
            public void onClick(View v) {
                mController.setRoutesFilter(new ArrayList<String>());
            }
        };
        UIUtils.setClickableSpan(mShowAllView, mShowAllClick);

        mNoArrivals = (TextView) mView.findViewById(R.id.no_arrivals);

        // First ETA row
        mEtaContainer1 = mView.findViewById(R.id.eta_container1);
        mEtaRouteFavorite1 = (ImageButton) mEtaContainer1.findViewById(R.id.eta_route_favorite);
        mEtaRouteFavorite1.setColorFilter(mView.getResources().getColor(R.color.header_text_color));
        mEtaReminder1 = (ImageButton) mEtaContainer1.findViewById(R.id.reminder);
        mEtaReminder1.setColorFilter(mView.getResources().getColor(R.color.header_text_color));
        mEtaRouteName1 = (TextView) mEtaContainer1.findViewById(R.id.eta_route_name);
        mEtaRouteDirection1 = (TextView) mEtaContainer1.findViewById(R.id.eta_route_direction);
        mEtaAndMin1 = (RelativeLayout) mEtaContainer1.findViewById(R.id.eta_and_min);
        mEtaArrivalInfo1 = (TextView) mEtaContainer1.findViewById(R.id.eta);
        mEtaMin1 = (TextView) mEtaContainer1.findViewById(R.id.eta_min);
        mEtaRealtime1 = (ViewGroup) mEtaContainer1.findViewById(R.id.eta_realtime_indicator);
        mEtaMoreVert1 = (ImageButton) mEtaContainer1.findViewById(R.id.eta_more_vert);
        mEtaMoreVert1.setColorFilter(mView.getResources().getColor(R.color.header_text_color));

        mEtaSeparator = mView.findViewById(R.id.eta_separator);

        // Second ETA row
        mEtaContainer2 = mView.findViewById(R.id.eta_container2);
        mEtaRouteFavorite2 = (ImageButton) mEtaContainer2.findViewById(R.id.eta_route_favorite);
        mEtaRouteFavorite2.setColorFilter(mView.getResources().getColor(R.color.header_text_color));
        mEtaReminder2 = (ImageButton) mEtaContainer2.findViewById(R.id.reminder);
        mEtaReminder2.setColorFilter(mView.getResources().getColor(R.color.header_text_color));
        mEtaRouteName2 = (TextView) mEtaContainer2.findViewById(R.id.eta_route_name);
        mEtaAndMin2 = (RelativeLayout) mEtaContainer2.findViewById(R.id.eta_and_min);
        mEtaRouteDirection2 = (TextView) mEtaContainer2.findViewById(R.id.eta_route_direction);
        mEtaArrivalInfo2 = (TextView) mEtaContainer2.findViewById(R.id.eta);
        mEtaMin2 = (TextView) mEtaContainer2.findViewById(R.id.eta_min);
        mEtaRealtime2 = (ViewGroup) mEtaContainer2.findViewById(R.id.eta_realtime_indicator);
        mEtaMoreVert2 = (ImageButton) mEtaContainer2.findViewById(R.id.eta_more_vert);
        mEtaMoreVert2.setColorFilter(mView.getResources().getColor(R.color.header_text_color));

        mProgressBar = (ProgressBar) mView.findViewById(R.id.header_loading_spinner);
        mStopInfo = (ImageButton) mView.findViewById(R.id.stop_info_button);
        mExpandCollapse = (ImageView) mView.findViewById(R.id.expand_collapse);
        mAlertView = (ImageView) mView.findViewById(R.id.alert);
        mAlertView.setColorFilter(mView.getResources().getColor(R.color.header_text_color));
        mAlertView.setVisibility(View.GONE);

        resetExpandCollapseAnimation();

        // Initialize right margin view visibilities
        UIUtils.showViewWithAnimation(mProgressBar, mShortAnimationDuration);

        UIUtils.hideViewWithAnimation(mEtaContainer1, mShortAnimationDuration);
        UIUtils.hideViewWithAnimation(mEtaSeparator, mShortAnimationDuration);
        UIUtils.hideViewWithAnimation(mEtaContainer2, mShortAnimationDuration);

        // Initialize stop info view
        final ObaRegion obaRegion = Application.get().getCurrentRegion();

        if (obaRegion == null || TextUtils.isEmpty(obaRegion.getStopInfoUrl())) {
            // This region doesn't support StopInfo - hide the info icon
            mStopInfo.setVisibility(View.GONE);
        } else {
            mStopInfo.setVisibility(View.VISIBLE);

            mStopInfo.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Assemble StopInfo URL for the current stop
                    Uri stopInfoUri = Uri.parse(obaRegion.getStopInfoUrl());
                    Uri.Builder stopInfoBuilder = stopInfoUri.buildUpon();
                    stopInfoBuilder.appendPath(mContext.getString(R.string.stop_info_url_path));
                    stopInfoBuilder.appendPath(mController.getStopId());

                    Log.d(TAG, "StopInfoUrl - " + stopInfoBuilder.build());

                    Intent i = new Intent(Intent.ACTION_VIEW);
                    i.setData(stopInfoBuilder.build());
                    mContext.startActivity(i);
                    //Analytics
                    if (obaRegion != null && obaRegion.getName() != null)
                        ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                                mContext.getString(R.string.analytics_action_button_press),
                                mContext.getString(R.string.analytics_label_button_press_stopinfo)
                                        + obaRegion.getName());
                }
            });
        }

        mStopFavorite.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mController.setFavoriteStop(!mController.isFavoriteStop());
                refreshStopFavorite();
            }
        });

        mNameView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                beginNameEdit(null);
            }
        });

        // Implement the "Save" and "Clear" buttons
        View save = mView.findViewById(R.id.edit_name_save);
        save.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mController.setUserStopName(mEditNameView.getText().toString());
                endNameEdit();
            }
        });

        mEditNameView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_DONE) {
                    mController.setUserStopName(mEditNameView.getText().toString());
                    endNameEdit();
                    return true;
                }
                return false;
            }
        });

        // "Cancel"
        View cancel = mView.findViewById(R.id.edit_name_cancel);
        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                endNameEdit();
            }
        });

        View clear = mView.findViewById(R.id.edit_name_revert);
        clear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mController.setUserStopName(null);
                endNameEdit();
            }
        });
    }

    /**
     * Used to give the header control over the containing sliding panel, primarily to change the
     * panel header height
     */
    public void setSlidingPanelController(HomeActivity.SlidingPanelController controller) {
        mSlidingPanelController = controller;
    }

    /**
     * Should be called from onPause() of context hosting this header
     */
    public void onPause() {
        // If we're editing a stop name, close out the editing session
        if (mInNameEdit) {
            mController.setUserStopName(null);
            endNameEdit();
        }
    }

    synchronized public void setSlidingPanelCollapsed(boolean collapsed) {
        // If the state has changed and image is initialized, then rotate the expand/collapse image
        if (mExpandCollapse != null && collapsed != mIsSlidingPanelCollapsed) {
            // Update value
            mIsSlidingPanelCollapsed = collapsed;

            doExpandCollapseRotation(collapsed);
        }
    }

    /**
     * Sets the trip details (e.g., reminders) for this stop
     */
    public void setTripsForStop(ContentQueryMap tripsForStop) {
        mTripsForStop = tripsForStop;
        refreshArrivalInfoVisibilityAndListeners();
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
    private void doExpandCollapseRotation(boolean collapsed) {
        if (!UIUtils.canAnimateViewModern()) {
            rotateExpandCollapseImageViewLegacy(collapsed);
            return;
        }
        if (!collapsed) {
            // Rotate clockwise
            mExpandCollapse.animate().setDuration(ANIM_DURATION).rotationBy(ANIM_STATE_INVERTED);
        } else {
            // Rotate counter-clockwise
            mExpandCollapse.animate().setDuration(ANIM_DURATION).rotationBy(-ANIM_STATE_INVERTED);
        }
    }

    private void rotateExpandCollapseImageViewLegacy(boolean isSlidingPanelCollapsed) {
        RotateAnimation rotate;

        if (!isSlidingPanelCollapsed) {
            // Rotate clockwise
            rotate = getRotation(ANIM_STATE_NORMAL, ANIM_STATE_INVERTED);
        } else {
            // Rotate counter-clockwise
            rotate = getRotation(ANIM_STATE_INVERTED, ANIM_STATE_NORMAL);
        }

        mExpandCollapse.setAnimation(rotate);
    }

    /**
     * Resets the animation that has been applied to the expand/collapse indicator image
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
    private synchronized void resetExpandCollapseAnimation() {
        if (mExpandCollapse == null) {
            return;
        }
        if (UIUtils.canAnimateViewModern()) {
            if (mExpandCollapse.getRotation() != 0) {
                mExpandCollapse.setRotation(0);
            }
        } else {
            mExpandCollapse.clearAnimation();
        }
    }

    /**
     * Allows external classes to set whether or not the expand/collapse indicator should be
     * shown (e.g., if the header is not in a sliding window, we don't want to show it)
     *
     * @param value true if the expand/collapse indicator should be shown, false if it should not
     */
    public void showExpandCollapseIndicator(boolean value) {
        if (mExpandCollapse != null) {
            if (value) {
                mExpandCollapse.setVisibility(View.VISIBLE);
            } else {
                mExpandCollapse.setVisibility(View.GONE);
            }
        }
    }

    /**
     * Creates and returns a new rotation animation for the expand/collapse image based on the
     * provided startState and endState.
     *
     * @param startState beginning state of the image, either ANIM_STATE_NORMAL or
     *                   ANIM_STATE_INVERTED
     * @param endState   end state of the image, either ANIM_STATE_NORMAL or ANIM_STATE_INVERTED
     * @return a new rotation animation for the expand/collapse image
     */
    private static RotateAnimation getRotation(float startState, float endState) {
        RotateAnimation r = new RotateAnimation(startState, endState, Animation.RELATIVE_TO_SELF, ANIM_PIVOT_VALUE,
                Animation.RELATIVE_TO_SELF, ANIM_PIVOT_VALUE);
        r.setDuration(ANIM_DURATION);
        r.setFillAfter(true);
        return r;
    }

    synchronized void refresh() {
        refreshName();
        refreshArrivalInfoData();
        refreshStopFavorite();
        refreshFilter();
        refreshError();
        refreshArrivalInfoVisibilityAndListeners();
        refreshHeaderSize();
    }

    private void refreshName() {
        String name = mController.getStopName();
        String userName = mController.getUserStopName();

        if (!TextUtils.isEmpty(userName)) {
            mNameView.setText(userName);
        } else if (name != null) {
            mNameView.setText(name);
        }
    }

    /**
     * Refreshes the arrival info data to be displayed in the header based on the most recent
     * arrival info, and sets the number of arrival rows that should be displayed in the header
     */
    private void refreshArrivalInfoData() {
        mArrivalInfo = mController.getArrivalInfo();
        mHeaderArrivalInfo.clear();

        if (mArrivalInfo != null && !mInNameEdit) {
            // Get the indexes for arrival times that should be featured in the header
            ArrayList<Integer> etaIndexes = ArrivalInfo.findPreferredArrivalIndexes(mArrivalInfo);
            if (etaIndexes != null) {
                // We have a non-negative ETA for at least one bus - fill the first arrival row
                final int i1 = etaIndexes.get(0);
                ObaArrivalInfo info1 = mArrivalInfo.get(i1).getInfo();
                boolean isFavorite = ObaContract.RouteHeadsignFavorites.isFavorite(mContext, info1.getRouteId(),
                        info1.getHeadsign(), info1.getStopId());
                mEtaRouteFavorite1
                        .setImageResource(isFavorite ? R.drawable.focus_star_on : R.drawable.focus_star_off);

                mEtaRouteName1.setText(info1.getShortName());
                mEtaRouteDirection1.setText(info1.getHeadsign());
                long eta = mArrivalInfo.get(i1).getEta();
                if (eta == 0) {
                    mEtaArrivalInfo1.setText(mContext.getString(R.string.stop_info_eta_now));
                    mEtaArrivalInfo1.setTextSize(ETA_TEXT_NOW_SIZE_SP);
                    UIUtils.hideViewWithAnimation(mEtaMin1, mShortAnimationDuration);
                } else if (eta > 0) {
                    mEtaArrivalInfo1.setText(Long.toString(eta));
                    mEtaArrivalInfo1.setTextSize(ETA_TEXT_SIZE_SP);
                    UIUtils.showViewWithAnimation(mEtaMin1, mShortAnimationDuration);
                }

                mEtaAndMin1.setBackgroundResource(R.drawable.round_corners_style_b_header_status);
                GradientDrawable d1 = (GradientDrawable) mEtaAndMin1.getBackground();
                final int c1 = mArrivalInfo.get(i1).getColor();
                if (c1 != R.color.stop_info_ontime) {
                    // Show early/late/scheduled color
                    d1.setColor(mResources.getColor(c1));
                } else {
                    // For on-time, use header default color
                    d1.setColor(mResources.getColor(R.color.theme_primary));
                }
                mEtaAndMin1.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mPopup1 != null && mPopup1.isShowing()) {
                            mPopup1.dismiss();
                            return;
                        }
                        mPopup1 = setupPopup(0, c1, mArrivalInfo.get(i1).getStatusText());
                        mPopup1.showAsDropDown(mEtaAndMin1);
                    }
                });

                if (mArrivalInfo.get(i1).getPredicted()) {
                    // We have real-time data - show the indicator
                    UIUtils.showViewWithAnimation(mEtaRealtime1, mShortAnimationDuration);
                } else {
                    // We only have schedule data - hide the indicator
                    mEtaRealtime1.setVisibility(View.INVISIBLE);
                    // If this is frequency-based data, indicate that arrival is approximate
                    if (mArrivalInfo.get(i1).getInfo().getFrequency() != null) {
                        mEtaArrivalInfo1.setText(mResources.getString(R.string.stop_info_frequency_approximate)
                                + mEtaArrivalInfo1.getText());
                    }
                }
                // Save the arrival info for the options menu later
                mHeaderArrivalInfo.add(mArrivalInfo.get(i1));

                // If there is another arrival, fill the second row with it
                if (etaIndexes.size() >= 2) {
                    final int i2 = etaIndexes.get(1);
                    ObaArrivalInfo info2 = mArrivalInfo.get(i2).getInfo();
                    boolean isFavorite2 = ObaContract.RouteHeadsignFavorites.isFavorite(mContext,
                            info2.getRouteId(), info2.getHeadsign(), info2.getStopId());
                    mEtaRouteFavorite2
                            .setImageResource(isFavorite2 ? R.drawable.focus_star_on : R.drawable.focus_star_off);
                    mEtaRouteName2.setText(info2.getShortName());
                    mEtaRouteDirection2.setText(info2.getHeadsign());
                    eta = mArrivalInfo.get(i2).getEta();

                    if (eta == 0) {
                        mEtaArrivalInfo2.setText(mContext.getString(R.string.stop_info_eta_now));
                        mEtaArrivalInfo2.setTextSize(ETA_TEXT_NOW_SIZE_SP);
                        UIUtils.hideViewWithAnimation(mEtaMin2, mShortAnimationDuration);
                    } else if (eta > 0) {
                        mEtaArrivalInfo2.setText(Long.toString(eta));
                        mEtaArrivalInfo2.setTextSize(ETA_TEXT_SIZE_SP);
                        UIUtils.showViewWithAnimation(mEtaMin2, mShortAnimationDuration);
                    }
                    mEtaAndMin2.setBackgroundResource(R.drawable.round_corners_style_b_header_status);
                    GradientDrawable d2 = (GradientDrawable) mEtaAndMin2.getBackground();
                    final int c2 = mArrivalInfo.get(i2).getColor();
                    if (c2 != R.color.stop_info_ontime) {
                        // Show early/late/scheduled color
                        d2.setColor(mResources.getColor(c2));
                    } else {
                        // For on-time, use header default color
                        d2.setColor(mResources.getColor(R.color.theme_primary));
                    }
                    mEtaAndMin2.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (mPopup2 != null && mPopup2.isShowing()) {
                                mPopup2.dismiss();
                                return;
                            }
                            mPopup2 = setupPopup(1, c2, mArrivalInfo.get(i2).getStatusText());
                            mPopup2.showAsDropDown(mEtaAndMin2);
                        }
                    });

                    if (mArrivalInfo.get(i2).getPredicted()) {
                        // We have real-time data - show the indicator
                        UIUtils.showViewWithAnimation(mEtaRealtime2, mShortAnimationDuration);
                    } else {
                        // We only have schedule data - hide the indicator
                        mEtaRealtime2.setVisibility(View.INVISIBLE);
                        // If this is frequency-based data, indicate that arrival is approximate
                        if (mArrivalInfo.get(i2).getInfo().getFrequency() != null) {
                            mEtaArrivalInfo2.setText(mResources.getString(R.string.stop_info_frequency_approximate)
                                    + mEtaArrivalInfo2.getText());
                        }
                    }

                    mNumHeaderArrivals = 2;

                    // Save the arrival info for the options menu later
                    mHeaderArrivalInfo.add(mArrivalInfo.get(i2));
                } else {
                    mNumHeaderArrivals = 1;
                }
            } else {
                // Show abbreviated "no upcoming arrivals" message (e.g., "35+ min")
                int minAfter = mController.getMinutesAfter();
                if (minAfter != -1) {
                    mNoArrivals.setText(UIUtils.getNoArrivalsMessage(mContext, minAfter, false, false));
                } else {
                    minAfter = 35; // Assume 35 minutes, because that's the API default
                    mNoArrivals.setText(UIUtils.getNoArrivalsMessage(mContext, minAfter, false, false));
                }
                mNumHeaderArrivals = 0;
            }
        }
    }

    /**
     * Sets the popup for the status
     *
     * @param index      0 if this is for the top ETA row, 1 if it is for the second
     * @param color      color resource id to use for the popup background
     * @param statusText text to show in the status popup
     * @return a new PopupWindow initialized based on the provided parameters
     */
    private PopupWindow setupPopup(final int index, int color, String statusText) {
        LayoutInflater inflater = LayoutInflater.from(mContext);
        TextView statusView = (TextView) inflater.inflate(R.layout.arrivals_list_tv_template_style_b_status_large,
                null);
        statusView.setBackgroundResource(R.drawable.round_corners_style_b_status);
        GradientDrawable d = (GradientDrawable) statusView.getBackground();
        if (color != R.color.stop_info_ontime) {
            // Show early/late color
            d.setColor(mResources.getColor(color));
        } else {
            // For on-time, use header default color
            d.setColor(mResources.getColor(R.color.theme_primary));
        }
        d.setStroke(UIUtils.dpToPixels(mContext, 1), mResources.getColor(R.color.header_text_color));
        int pSides = UIUtils.dpToPixels(mContext, 5);
        int pTopBottom = UIUtils.dpToPixels(mContext, 2);
        statusView.setPadding(pSides, pTopBottom, pSides, pTopBottom);
        statusView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));
        statusView.measure(TextView.MeasureSpec.UNSPECIFIED, TextView.MeasureSpec.UNSPECIFIED);
        statusView.setText(statusText);
        PopupWindow p = new PopupWindow(statusView, statusView.getWidth(), statusView.getHeight());
        p.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        p.setBackgroundDrawable(new ColorDrawable(mResources.getColor(android.R.color.transparent)));
        p.setOutsideTouchable(true);
        p.setTouchInterceptor(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                boolean touchInView;
                if (index == 0) {
                    touchInView = UIUtils.isTouchInView(mEtaAndMin1, event);
                } else {
                    touchInView = UIUtils.isTouchInView(mEtaAndMin2, event);
                }
                return touchInView;
            }
        });
        return p;
    }

    private void refreshStopFavorite() {
        mStopFavorite.setImageResource(
                mController.isFavoriteStop() ? R.drawable.focus_star_on : R.drawable.focus_star_off);
    }

    /**
     * Refreshes the routes filter, and displays/hides it if necessary
     */
    private void refreshFilter() {
        TextView v = (TextView) mView.findViewById(R.id.filter_text);
        ArrayList<String> routesFilter = mController.getRoutesFilter();
        final int num = (routesFilter != null) ? routesFilter.size() : 0;
        if (num > 0) {
            final int total = mController.getNumRoutes();
            v.setText(mContext.getString(R.string.stop_info_filter_header, num, total));
            // Show the filter text (except when in name edit mode)
            if (mInNameEdit) {
                mFilterGroup.setVisibility(View.GONE);
            } else {
                mFilterGroup.setVisibility(View.VISIBLE);
            }
        } else {
            mFilterGroup.setVisibility(View.GONE);
        }
    }

    /**
     * Returns true if we're filtering routes from the user's view, or false if we're not
     *
     * @return true if we're filtering routes from the user's view, or false if we're not
     */
    private boolean isFilteringRoutes() {
        ArrayList<String> routesFilter = mController.getRoutesFilter();
        final int num = (routesFilter != null) ? routesFilter.size() : 0;
        if (num > 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Hides the progress bar, and replaces it with the correct content depending on sliding
     * panel state and arrival info
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void refreshArrivalInfoVisibilityAndListeners() {
        if (mInNameEdit) {
            // If the user is editing a stop name, we shouldn't show any of these views
            return;
        }
        if (mArrivalInfo == null) {
            // We don't have any arrival info yet, so make sure the progress bar is running
            UIUtils.showViewWithAnimation(mProgressBar, mShortAnimationDuration);
            // Hide all the arrival views
            UIUtils.hideViewWithAnimation(mNoArrivals, mShortAnimationDuration);
            UIUtils.hideViewWithAnimation(mEtaContainer1, mShortAnimationDuration);
            UIUtils.hideViewWithAnimation(mEtaSeparator, mShortAnimationDuration);
            UIUtils.hideViewWithAnimation(mEtaContainer2, mShortAnimationDuration);
            return;
        }

        if (mNumHeaderArrivals == 0) {
            // "No routes" message should be shown
            UIUtils.showViewWithAnimation(mNoArrivals, mShortAnimationDuration);
            // Hide all others
            UIUtils.hideViewWithAnimation(mEtaContainer1, mShortAnimationDuration);
            UIUtils.hideViewWithAnimation(mEtaSeparator, mShortAnimationDuration);
            UIUtils.hideViewWithAnimation(mEtaContainer2, mShortAnimationDuration);
        }

        // Show at least the first row of arrival info, if we have one or more records
        if (mNumHeaderArrivals >= 1) {
            // Show the first row of arrival info
            UIUtils.showViewWithAnimation(mEtaContainer1, mShortAnimationDuration);
            // Hide no arrivals
            UIUtils.hideViewWithAnimation(mNoArrivals, mShortAnimationDuration);

            // Setup tapping on star for first row
            final ObaArrivalInfo info1 = mHeaderArrivalInfo.get(0).getInfo();
            final boolean isRouteFavorite = ObaContract.RouteHeadsignFavorites.isFavorite(mContext,
                    info1.getRouteId(), info1.getHeadsign(), info1.getStopId());
            mEtaRouteFavorite1.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Show dialog for setting route favorite
                    RouteFavoriteDialogFragment dialog = new RouteFavoriteDialogFragment.Builder(info1.getRouteId(),
                            info1.getHeadsign()).setRouteShortName(info1.getShortName())
                                    .setRouteLongName(info1.getRouteLongName()).setStopId(info1.getStopId())
                                    .setFavorite(!isRouteFavorite).build();

                    dialog.setCallback(new RouteFavoriteDialogFragment.Callback() {
                        @Override
                        public void onSelectionComplete(boolean savedFavorite) {
                            if (savedFavorite) {
                                mController.refreshLocal();
                            }
                        }
                    });
                    dialog.show(mFragmentManager, RouteFavoriteDialogFragment.TAG);

                }
            });

            mEtaReminder1.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Add / edit reminder
                    TripInfoActivity.start(mContext, info1.getTripId(), mController.getStopId(), info1.getRouteId(),
                            info1.getShortName(), mController.getStopName(), info1.getScheduledDepartureTime(),
                            info1.getHeadsign());
                }
            });

            // Setup "more" button click for first row
            mEtaMoreVert1.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mController.showListItemMenu(mEtaContainer1, mHeaderArrivalInfo.get(0));
                }
            });

            // Show or hide reminder for first row
            View r = mEtaContainer1.findViewById(R.id.reminder);
            refreshReminder(mHeaderArrivalInfo.get(0).getInfo().getTripId(), r);
        }

        if (mNumHeaderArrivals >= 2) {
            // Also show the 2nd row of arrival info
            UIUtils.showViewWithAnimation(mEtaSeparator, mShortAnimationDuration);
            UIUtils.showViewWithAnimation(mEtaContainer2, mShortAnimationDuration);

            // Setup tapping on star for second row
            final ObaArrivalInfo info2 = mHeaderArrivalInfo.get(1).getInfo();
            final boolean isRouteFavorite2 = ObaContract.RouteHeadsignFavorites.isFavorite(mContext,
                    info2.getRouteId(), info2.getHeadsign(), info2.getStopId());
            mEtaRouteFavorite2.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Show dialog for setting route favorite
                    RouteFavoriteDialogFragment dialog = new RouteFavoriteDialogFragment.Builder(info2.getRouteId(),
                            info2.getHeadsign()).setRouteShortName(info2.getShortName())
                                    .setRouteLongName(info2.getRouteLongName()).setStopId(info2.getStopId())
                                    .setFavorite(!isRouteFavorite2).build();

                    dialog.setCallback(new RouteFavoriteDialogFragment.Callback() {
                        @Override
                        public void onSelectionComplete(boolean savedFavorite) {
                            if (savedFavorite) {
                                mController.refreshLocal();
                            }
                        }
                    });
                    dialog.show(mFragmentManager, RouteFavoriteDialogFragment.TAG);
                }
            });

            mEtaReminder2.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Add / edit reminder
                    TripInfoActivity.start(mContext, info2.getTripId(), mController.getStopId(), info2.getRouteId(),
                            info2.getShortName(), mController.getStopName(), info2.getScheduledDepartureTime(),
                            info2.getHeadsign());
                }
            });

            // Setup "more" button click for second row
            mEtaMoreVert2.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mController.showListItemMenu(mEtaContainer2, mHeaderArrivalInfo.get(1));
                }
            });

            // Show or hide reminder for second row
            View r = mEtaContainer2.findViewById(R.id.reminder);
            refreshReminder(mHeaderArrivalInfo.get(1).getInfo().getTripId(), r);
        } else {
            // Hide the 2nd row of arrival info and related views - we only had one arrival info
            UIUtils.hideViewWithAnimation(mEtaSeparator, mShortAnimationDuration);
            UIUtils.hideViewWithAnimation(mEtaContainer2, mShortAnimationDuration);
        }

        // Hide progress bar
        UIUtils.hideViewWithAnimation(mProgressBar, mShortAnimationDuration);
    }

    /**
     * Shows or hides a reminder in the header, based on whether there is a saved reminder for
     * the given tripId and the current stop
     * @param tripId tripId to search for reminders for in the database
     * @param v reminder view to show or hide, based on whether the there is or isn't a reminder for
     *          this tripId and stop
     */
    void refreshReminder(String tripId, View v) {
        ContentValues values = null;
        if (mTripsForStop != null) {
            values = mTripsForStop.getValues(tripId);
        }
        if (values != null) {
            // A reminder exists for this trip
            v.setVisibility(View.VISIBLE);
        } else {
            v.setVisibility(View.GONE);
        }
    }

    /**
     * Sets the header to the correct size based on the number of arrivals currently
     * shown in the header, and whether the panel is collapsed, anchored, or expanded.
     */
    void refreshHeaderSize() {
        if (mInNameEdit) {
            // When editing the stop name, the header size is always the same
            // Commented out as workaround to #314
            //setHeaderSize(HEADER_HEIGHT_EDIT_NAME_DP);
            return;
        }

        float newSize = 0;

        // Change the header size based on arrival info and if a route filter is used
        if (mNumHeaderArrivals == 0 || mNumHeaderArrivals == 1) {
            newSize = HEADER_HEIGHT_ONE_ARRIVAL_DP;
        } else if (mNumHeaderArrivals == 2) {
            newSize = HEADER_HEIGHT_TWO_ARRIVALS_DP;
        }

        // If we're showing the route filter, than add the offset so this filter view has room
        if (isFilteringRoutes()) {
            newSize += HEADER_OFFSET_FILTER_ROUTES_DP;
        }

        if (newSize != 0) {
            setHeaderSize(newSize);
        }
    }

    /**
     * Re-size the header layout to the provided size, in dp
     *
     * @param newHeightDp the new header layout height, in dp
     */
    void setHeaderSize(float newHeightDp) {
        int heightPixels = UIUtils.dpToPixels(mContext, newHeightDp);
        if (mSlidingPanelController != null) {
            mSlidingPanelController.setPanelHeightPixels(heightPixels);
        } else {
            // If we're in the ArrivalListActivity and not the sliding panel, resize the header here
            mView.getLayoutParams().height = heightPixels;
            mMainContainerView.getLayoutParams().height = heightPixels;
        }
    }

    private static class ResponseError implements AlertList.Alert {

        private final CharSequence mString;

        ResponseError(CharSequence seq) {
            mString = seq;
        }

        @Override
        public String getId() {
            return "STATIC: RESPONSE ERROR";
        }

        @Override
        public int getType() {
            return TYPE_ERROR;
        }

        @Override
        public int getFlags() {
            return 0;
        }

        @Override
        public CharSequence getString() {
            return mString;
        }

        @Override
        public void onClick() {
        }

        @Override
        public int hashCode() {
            return getId().hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            ResponseError other = (ResponseError) obj;
            if (!getId().equals(other.getId())) {
                return false;
            }
            return true;
        }
    }

    private ResponseError mResponseError = null;

    private void refreshError() {
        final long now = System.currentTimeMillis();
        final long responseTime = mController.getLastGoodResponseTime();
        AlertList alerts = mController.getAlertList();
        mHasWarning = false;
        mHasError = false;

        if (mResponseError != null) {
            alerts.remove(mResponseError);
        }

        if ((responseTime) != 0 && ((now - responseTime) >= 2 * DateUtils.MINUTE_IN_MILLIS)) {
            CharSequence relativeTime = DateUtils.getRelativeTimeSpanString(responseTime, now,
                    DateUtils.MINUTE_IN_MILLIS, 0);
            CharSequence s = mContext.getString(R.string.stop_info_old_data, relativeTime);
            mResponseError = new ResponseError(s);
            alerts.insert(mResponseError, 0);
        }

        // If there is a warning or error alert, and show the alert icon in the header

        for (int i = 0; i < alerts.getCount(); i++) {
            AlertList.Alert a = alerts.getItem(i);
            if (a.getType() == AlertList.Alert.TYPE_WARNING) {
                mHasWarning = true;
            }
            if (a.getType() == AlertList.Alert.TYPE_ERROR) {
                mHasError = true;
            }
        }

        if (mHasError) {
            //UIHelp.showViewWithAnimation(mAlertView, mShortAnimationDuration);
            mAlertView.setVisibility(View.VISIBLE);
            mAlertView.setColorFilter(mResources.getColor(R.color.alert_icon_error));
            mAlertView.setContentDescription(mResources.getString(R.string.alert_content_description_error));
        } else if (mHasWarning) {
            //UIHelp.showViewWithAnimation(mAlertView, mShortAnimationDuration);
            mAlertView.setVisibility(View.VISIBLE);
            mAlertView.setColorFilter(mResources.getColor(R.color.alert_icon_warning));
            mAlertView.setContentDescription(mResources.getString(R.string.alert_content_description_warning));
        } else {
            // Don't show the header icon for info-level or no alerts
            //UIHelp.hideViewWithAnimation(mAlertView, mShortAnimationDuration);
            mAlertView.setVisibility(View.GONE);
            mAlertView.setContentDescription("");
        }
    }

    synchronized void beginNameEdit(String initial) {
        // If we can click on this, then we're definitely not
        // editable, so we should go into edit mode.
        mEditNameView.setText((initial != null) ? initial : mNameView.getText());
        mNameContainerView.setVisibility(View.GONE);
        mFilterGroup.setVisibility(View.GONE);
        mStopFavorite.setVisibility(View.GONE);
        mEtaContainer1.setVisibility(View.GONE);
        mEtaSeparator.setVisibility(View.GONE);
        mEtaContainer2.setVisibility(View.GONE);
        mNoArrivals.setVisibility(View.GONE);
        mAlertView.setVisibility(View.GONE);

        // Save mExpandCollapse visibility state
        cachedExpandCollapseViewVisibility = mExpandCollapse.getVisibility();
        if (!UIUtils.canAnimateViewModern()) {
            // View won't disappear without clearing the legacy rotation animation
            mExpandCollapse.clearAnimation();
        }
        mExpandCollapse.setVisibility(View.GONE);
        mEditNameContainerView.setVisibility(View.VISIBLE);

        // Set the entire header size - commented out as workaround to #314
        //setHeaderSize(HEADER_HEIGHT_EDIT_NAME_DP);

        mEditNameView.requestFocus();
        mEditNameView.setSelection(mEditNameView.getText().length());
        mInNameEdit = true;

        // Open soft keyboard if no physical keyboard is open
        UIUtils.openKeyboard(mContext);
    }

    synchronized void endNameEdit() {
        mInNameEdit = false;
        mNameContainerView.setVisibility(View.VISIBLE);
        mEditNameContainerView.setVisibility(View.GONE);
        mStopFavorite.setVisibility(View.VISIBLE);
        mExpandCollapse.setVisibility(cachedExpandCollapseViewVisibility);
        mNoArrivals.setVisibility(View.VISIBLE);
        if (mHasError || mHasWarning) {
            mAlertView.setVisibility(View.VISIBLE);
        }
        // Hide soft keyboard
        UIUtils.closeKeyboard(mContext, mEditNameView);
        refresh();
    }

    /**
     * Closes any open status popups displayed by the header
     */
    public void closeStatusPopups() {
        if (mPopup1 != null) {
            mPopup1.dismiss();
        }
        if (mPopup2 != null) {
            mPopup2.dismiss();
        }
    }
}