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

Java tutorial

Introduction

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

Source

/*
 * Copyright (C) 2012-2015 Paul Watts (paulcwatts@gmail.com), University of South Florida,
 * Benjamin Du (bendu@me.com), and individual contributors.
 *
 * 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.ObaApi;
import org.onebusaway.android.io.elements.ObaArrivalInfo;
import org.onebusaway.android.io.elements.ObaRoute;
import org.onebusaway.android.io.elements.ObaSituation;
import org.onebusaway.android.io.elements.ObaStop;
import org.onebusaway.android.io.request.ObaArrivalInfoResponse;
import org.onebusaway.android.provider.ObaContract;
import org.onebusaway.android.util.ArrayAdapterWithIcon;
import org.onebusaway.android.util.BuildFlavorUtils;
import org.onebusaway.android.util.FragmentUtils;
import org.onebusaway.android.util.LocationUtils;
import org.onebusaway.android.util.MyTextUtils;
import org.onebusaway.android.util.PreferenceUtils;
import org.onebusaway.android.util.UIUtils;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentQueryMap;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnMultiChoiceClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.util.Log;
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.view.accessibility.AccessibilityManager;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

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

//
// We don't use the ListFragment because the support library's version of
// the ListFragment doesn't work well with our header.
//
public class ArrivalsListFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<ObaArrivalInfoResponse>, ArrivalsListHeader.Controller {

    private static final String TAG = "ArrivalsListFragment";

    public static final String STOP_NAME = ".StopName";

    public static final String STOP_DIRECTION = ".StopDir";

    /**
     * Comma-delimited set of routes that serve this stop
     * See {@link UIUtils#serializeRouteDisplayNames(ObaStop,
     * java.util.HashMap)}
     */
    public static final String STOP_ROUTES = ".StopRoutes";

    public static final String STOP_LAT = ".StopLatitude";

    public static final String STOP_LON = ".StopLongitude";

    /**
     * If set to true, the fragment is using a header external to this layout, and shouldn't
     * instantiate its own header view
     */
    public static final String EXTERNAL_HEADER = ".ExternalHeader";

    private static final long RefreshPeriod = 60 * 1000;

    private static int TRIPS_FOR_STOP_LOADER = 1;

    private static int ARRIVALS_LIST_LOADER = 2;

    private ArrivalsListAdapterBase mAdapter;

    private ArrivalsListHeader mHeader;

    private View mHeaderView;

    private View mFooter;

    private View mEmptyList;

    private AlertList mAlertList;

    private ObaStop mStop;

    private String mStopId;

    private Uri mStopUri;

    private ArrayList<String> mRoutesFilter;

    private int mLastResponseLength = -1; // Keep copy locally, since loader overwrites

    // encapsulated info before onLoadFinished() is called
    private boolean mLoadedMoreArrivals = false;

    private boolean mFavorite = false;

    private String mStopUserName;

    private TripsForStopCallback mTripsForStopCallback;

    // The list of situation alerts
    private ArrayList<SituationAlert> mSituationAlerts;

    // Set to true if we're using an external header not in this layout (e.g., if this fragment is in a sliding panel)
    private boolean mExternalHeader = false;

    private Listener mListener;

    ObaArrivalInfo[] mArrivalInfo;

    public interface Listener {

        /**
         * Called when the ListView has been created
         *
         * @param listView the ListView that was just created
         */
        void onListViewCreated(ListView listView);

        /**
         * Called when new arrival times have been retrieved
         *
         * @param response new arrival information
         */
        void onArrivalTimesUpdated(final ObaArrivalInfoResponse response);

        /**
         * Called when the user selects the "Show route on map" for a particular route/trip
         *
         * @param arrivalInfo The arrival information for the route/trip that the user selected
         * @return true if the listener has consumed the event, false otherwise
         */
        boolean onShowRouteOnMapSelected(ArrivalInfo arrivalInfo);

        /**
         * Called when the user selects the "Sort by" option
         */
        void onSortBySelected();
    }

    /**
     * Builds an intent used to set the stop for the ArrivalListFragment directly
     * (i.e., when ArrivalsListActivity is not used)
     */
    public static class IntentBuilder {

        private Intent mIntent;

        public IntentBuilder(Context context, String stopId) {
            mIntent = new Intent(context, ArrivalsListFragment.class);
            mIntent.setData(Uri.withAppendedPath(ObaContract.Stops.CONTENT_URI, stopId));
        }

        /**
         * @param stop   ObaStop to be set
         * @param routes a HashMap of all route display names that may serve this stop - key is
         *               routeId
         */
        public IntentBuilder(Context context, ObaStop stop, HashMap<String, ObaRoute> routes) {
            mIntent = new Intent(context, ArrivalsListFragment.class);
            mIntent.setData(Uri.withAppendedPath(ObaContract.Stops.CONTENT_URI, stop.getId()));
            setStopName(stop.getName());
            setStopDirection(stop.getDirection());
            setStopRoutes(UIUtils.serializeRouteDisplayNames(stop, routes));
            setStopLocation(stop.getLocation());
        }

        public IntentBuilder setStopName(String stopName) {
            mIntent.putExtra(ArrivalsListFragment.STOP_NAME, stopName);
            return this;
        }

        public IntentBuilder setStopDirection(String stopDir) {
            mIntent.putExtra(ArrivalsListFragment.STOP_DIRECTION, stopDir);
            return this;
        }

        public IntentBuilder setStopLocation(Location stopLocation) {
            mIntent.putExtra(ArrivalsListFragment.STOP_LAT, stopLocation.getLatitude());
            mIntent.putExtra(ArrivalsListFragment.STOP_LON, stopLocation.getLongitude());
            return this;
        }

        /**
         * Sets the routes that serve this stop via a comma-delimited set of route display names
         * <p/>
         * See {@link UIUtils#serializeRouteDisplayNames(ObaStop,
         * java.util.HashMap)}
         *
         * @param routes comma-delimited list of route display names that serve this stop
         */
        public IntentBuilder setStopRoutes(String routes) {
            mIntent.putExtra(ArrivalsListFragment.STOP_ROUTES, routes);
            return this;
        }

        public Intent build() {
            return mIntent;
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle savedInstanceState) {
        if (root == null) {
            // Currently in a layout without a container, so no
            // reason to create our view.
            return null;
        }

        initArrivalInfoViews(BuildFlavorUtils.getArrivalInfoStyleFromPreferences(), inflater);

        return inflater.inflate(R.layout.fragment_arrivals_list, null);
    }

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

        // Set the list view properties for Style B
        setListViewProperties(BuildFlavorUtils.getArrivalInfoStyleFromPreferences());

        // We have a menu item to show in action bar.
        setHasOptionsMenu(true);

        mAlertList = new AlertList(getActivity());
        mAlertList.initView(getView().findViewById(R.id.arrivals_alert_list));

        setupHeader(savedInstanceState);

        setupFooter();

        setupEmptyList(null);

        // This sets the stopId and uri
        setStopId();
        setUserInfo();

        // Create an empty adapter we will use to display the loaded data
        instantiateAdapter(BuildFlavorUtils.getArrivalInfoStyleFromPreferences());

        // Start out with a progress indicator.
        setListShown(false);

        mRoutesFilter = ObaContract.StopRouteFilters.get(getActivity(), mStopId);
        mTripsForStopCallback = new TripsForStopCallback();

        //LoaderManager.enableDebugLogging(true);
        LoaderManager mgr = getLoaderManager();

        mgr.initLoader(TRIPS_FOR_STOP_LOADER, null, mTripsForStopCallback);
        mgr.initLoader(ARRIVALS_LIST_LOADER, getArguments(), this);

        // Set initial minutesAfter value in the empty list view
        setEmptyText(
                UIUtils.getNoArrivalsMessage(getActivity(), getArrivalsLoader().getMinutesAfter(), false, false));
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(EXTERNAL_HEADER, mExternalHeader);
    }

    @Override
    public void onPause() {
        mRefreshHandler.removeCallbacks(mRefresh);
        if (mHeader != null) {
            mHeader.onPause();
        }
        super.onPause();
    }

    @Override
    public void onResume() {
        // Make sure we're using the correct adapter based on user preferences, in case they changed
        // after the Fragment was initialized
        checkAdapterStylePreference();

        // Notify listener that ListView is now created
        if (mListener != null) {
            mListener.onListViewCreated(getListView());
        }

        // Try to show any old data just in case we're coming out of sleep
        ArrivalsListLoader loader = getArrivalsLoader();
        if (loader != null) {
            ObaArrivalInfoResponse lastGood = loader.getLastGoodResponse();
            if (lastGood != null) {
                setResponseData(lastGood.getArrivalInfo(), lastGood.getSituations());
            }
        }

        getLoaderManager().restartLoader(TRIPS_FOR_STOP_LOADER, null, mTripsForStopCallback);

        // If our timer would have gone off, then refresh.
        long lastResponseTime = getArrivalsLoader().getLastResponseTime();
        long newPeriod = Math.min(RefreshPeriod, (lastResponseTime + RefreshPeriod) - System.currentTimeMillis());
        // Wait at least one second at least, and the full minute at most.
        //Log.d(TAG, "Refresh period:" + newPeriod);
        if (newPeriod <= 0) {
            refresh();
        } else {
            mRefreshHandler.postDelayed(mRefresh, newPeriod);
        }

        // Refresh the favorite status and stop name, in case we're returning from another view
        setUserInfo();

        if (mHeader != null) {
            mHeader.refresh();
        }

        super.onResume();
    }

    @Override
    public Loader<ObaArrivalInfoResponse> onCreateLoader(int id, Bundle args) {
        return new ArrivalsListLoader(getActivity(), mStopId);
    }

    @Override
    public void onStart() {
        super.onStart();
        ObaAnalytics.reportFragmentStart(this);

        if (Build.VERSION.SDK_INT >= 14) {
            AccessibilityManager am = (AccessibilityManager) getActivity()
                    .getSystemService(getActivity().ACCESSIBILITY_SERVICE);

            Boolean isTalkBackEnabled = am.isTouchExplorationEnabled();
            if (isTalkBackEnabled)
                ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.ACCESSIBILITY.toString(),
                        getString(R.string.analytics_action_touch_exploration),
                        getString(R.string.analytics_label_talkback) + getClass().getSimpleName()
                                + " using TalkBack");
        }
    }

    //
    // This is where the bulk of the initialization takes place to create
    // this screen.
    //
    @Override
    public void onLoadFinished(Loader<ObaArrivalInfoResponse> loader, final ObaArrivalInfoResponse result) {
        UIUtils.showProgress(this, false);

        ObaArrivalInfo[] info = null;
        List<ObaSituation> situations = null;

        if (result.getCode() == ObaApi.OBA_OK) {
            if (mStop == null) {
                mStop = result.getStop();
                addToDB(mStop);
            }
            info = result.getArrivalInfo();
            situations = result.getSituations();
        } else {
            // If there was a last good response, then this is a refresh
            // and we should use a toast. Otherwise, it's a initial
            // page load and we want to display the error in the empty text.
            ObaArrivalInfoResponse lastGood = getArrivalsLoader().getLastGoodResponse();
            if (lastGood != null) {
                // Refresh error
                Toast.makeText(getActivity(), R.string.generic_comm_error_toast, Toast.LENGTH_LONG).show();
                info = lastGood.getArrivalInfo();
                situations = lastGood.getSituations();
            } else {
                setEmptyText(UIUtils.getStopErrorString(getActivity(), result.getCode()));
            }
        }

        setResponseData(info, situations);

        // The list should now be shown.
        if (isResumed()) {
            setListShown(true);
        } else {
            setListShownNoAnimation(true);
        }

        // Clear any pending refreshes
        mRefreshHandler.removeCallbacks(mRefresh);

        // Post an update
        mRefreshHandler.postDelayed(mRefresh, RefreshPeriod);

        // If the user just tried to load more arrivals, determine if we 
        // should show a Toast in the case where no additional arrivals were loaded
        if (mLoadedMoreArrivals) {
            if (info == null || info.length == 0 || mLastResponseLength != info.length) {
                /*
                Don't show the toast, since:
                 1) an error occurred (and user has already seen the error message),
                 2) no records were returned (and empty list message is already shown), or
                 3) more arrivals were actually loaded
                */
                mLoadedMoreArrivals = false;
            } else if (mLastResponseLength == info.length) {
                // No additional arrivals were included in the response, show a toast
                Toast.makeText(getActivity(), UIUtils.getNoArrivalsMessage(getActivity(),
                        getArrivalsLoader().getMinutesAfter(), true, false), Toast.LENGTH_LONG).show();
                mLoadedMoreArrivals = false; // Only show the toast once
            }
        }

        // Notify listener that we have new arrival info
        if (mListener != null) {
            mListener.onArrivalTimesUpdated(result);
        }
    }

    /**
     * Sets the header for this list to be instantiated in another layout, but still controlled by
     * this fragment
     *
     * @param header     header that will be controlled by this fragment
     * @param headerView View that contains this header
     */
    public void setHeader(ArrivalsListHeader header, View headerView) {
        mHeader = header;
        mHeaderView = headerView;
        mHeader.initView(mHeaderView);
        mExternalHeader = true;
    }

    private void setResponseData(ObaArrivalInfo[] info, List<ObaSituation> situations) {
        mArrivalInfo = info;

        // Convert any stop situations into a list of alerts
        if (situations != null) {
            refreshSituations(situations);
        } else {
            refreshSituations(new ArrayList<ObaSituation>());
        }

        if (info != null) {
            // Reset the empty text just in case there is no data.
            setEmptyText(UIUtils.getNoArrivalsMessage(getActivity(), getArrivalsLoader().getMinutesAfter(), false,
                    false));
            mAdapter.setData(info, mRoutesFilter, System.currentTimeMillis());
        }

        if (mHeader != null) {
            mHeader.refresh();
        }
    }

    @Override
    public void onLoaderReset(Loader<ObaArrivalInfoResponse> loader) {
        UIUtils.showProgress(this, false);
        mAdapter.setData(null, mRoutesFilter, System.currentTimeMillis());

        mArrivalInfo = null;

        if (mHeader != null) {
            mHeader.refresh();
        }
    }

    //
    // Action Bar / Options Menu
    //
    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.arrivals_list, menu);
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        String title = mFavorite ? getString(R.string.stop_info_option_removestar)
                : getString(R.string.stop_info_option_addstar);
        menu.findItem(R.id.toggle_favorite).setTitle(title).setTitleCondensed(title);
        if (mExternalHeader) {
            // If we're using an external header, it means that this fragment is being shown
            // in the bottom sliding panel, and therefore the map is already visible.
            // So, we can remove the "Show Map" option
            menu.findItem(R.id.show_on_map).setVisible(false);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        final int id = item.getItemId();
        if (id == R.id.show_on_map) {
            if (mStop != null) {
                HomeActivity.start(getActivity(), mStop.getId(), mStop.getLatitude(), mStop.getLongitude());
            }
            return true;
        } else if (id == R.id.refresh) {
            refresh();
            return true;
        } else if (id == R.id.sort_arrivals) {
            doSortBy();
        } else if (id == R.id.filter) {
            if (mStop != null) {
                showRoutesFilterDialog();
            }
        } else if (id == R.id.edit_name) {
            if (mHeader != null) {
                mHeader.beginNameEdit(null);
            }
        } else if (id == R.id.toggle_favorite) {
            setFavoriteStop(!mFavorite);
            if (mHeader != null) {
                mHeader.refresh();
            }
        } else if (id == R.id.show_stop_details) {
            showStopDetailsDialog();
        } else if (id == R.id.report_stop_problem) {
            if (mStop != null) {
                ReportStopProblemActivity.start(getActivity(), mStop);
            }
        } else if (id == R.id.night_light) {
            NightLightActivity.start(getActivity());
        }
        return false;
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        final ArrivalInfo stop = (ArrivalInfo) getListView().getItemAtPosition(position);
        showListItemMenu(v, stop);
    }

    public void showListItemMenu(View v, final ArrivalInfo arrivalInfo) {
        if (arrivalInfo == null) {
            return;
        }
        Log.d(TAG, "Tapped on route=" + arrivalInfo.getInfo().getShortName() + ", tripId="
                + arrivalInfo.getInfo().getTripId() + ", vehicleId=" + arrivalInfo.getInfo().getVehicleId());

        ArrivalsListLoader loader = getArrivalsLoader();
        if (loader == null) {
            return;
        }
        final ObaArrivalInfoResponse response = loader.getLastGoodResponse();
        if (response == null) {
            return;
        }

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setTitle(R.string.stop_info_item_options_title);

        final String routeId = arrivalInfo.getInfo().getRouteId();
        final ObaRoute route = response.getRoute(routeId);
        final String url = route != null ? route.getUrl() : null;
        final boolean hasUrl = !TextUtils.isEmpty(url);
        // Check to see if the reminder is visible, for whether we show "add" or "edit" reminder
        // (we don't have any other state, so this is good enough)
        View tripView = v.findViewById(R.id.reminder);
        boolean isReminderVisible = tripView != null && tripView.getVisibility() != View.GONE;
        // Check route favorite, for whether we show "Add star" or "Remove star"
        final boolean isRouteFavorite = ObaContract.RouteHeadsignFavorites.isFavorite(getActivity(), routeId,
                arrivalInfo.getInfo().getHeadsign(), arrivalInfo.getInfo().getStopId());

        List<String> items = UIUtils.buildTripOptions(getActivity(), isRouteFavorite, hasUrl, isReminderVisible);
        List<Integer> icons = UIUtils.buildTripOptionsIcons(isRouteFavorite, hasUrl);

        ListAdapter adapter = new ArrayAdapterWithIcon(getActivity(), items, icons);

        builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                if (which == 0) {
                    // Show dialog for setting route favorite
                    RouteFavoriteDialogFragment routeDialog = new RouteFavoriteDialogFragment.Builder(route.getId(),
                            arrivalInfo.getInfo().getHeadsign()).setRouteShortName(route.getShortName())
                                    .setRouteLongName(arrivalInfo.getInfo().getRouteLongName())
                                    .setRouteUrl(route.getUrl()).setStopId(arrivalInfo.getInfo().getStopId())
                                    .setFavorite(!isRouteFavorite).build();

                    routeDialog.setCallback(new RouteFavoriteDialogFragment.Callback() {
                        @Override
                        public void onSelectionComplete(boolean savedFavorite) {
                            if (savedFavorite) {
                                refreshLocal();
                            }
                        }
                    });
                    routeDialog.show(getFragmentManager(), RouteFavoriteDialogFragment.TAG);
                } else if (which == 1) {
                    showRouteOnMap(arrivalInfo);
                } else if (which == 2) {
                    goToTripDetails(arrivalInfo);
                } else if (which == 3) {
                    goToTrip(arrivalInfo);
                } else if (which == 4) {
                    ArrayList<String> routes = new ArrayList<String>(1);
                    routes.add(arrivalInfo.getInfo().getRouteId());
                    setRoutesFilter(routes);
                    if (mHeader != null) {
                        mHeader.refresh();
                    }
                } else if (hasUrl && which == 5) {
                    UIUtils.goToUrl(getActivity(), url);
                } else if ((!hasUrl && which == 5) || (hasUrl && which == 6)) {
                    ReportTripProblemActivity.start(getActivity(), arrivalInfo.getInfo());
                }
            }
        });
        AlertDialog dialog = builder.create();
        dialog.setOwnerActivity(getActivity());
        dialog.show();
    }

    public void showRouteOnMap(ArrivalInfo arrivalInfo) {
        boolean handled = false;
        if (mListener != null) {
            handled = mListener.onShowRouteOnMapSelected(arrivalInfo);
        }
        // If the event hasn't been handled by the listener, start a new activity
        if (!handled) {
            HomeActivity.start(getActivity(), arrivalInfo.getInfo().getRouteId());
        }
    }

    //
    // ActivityListHeader.Controller
    //
    @Override
    public String getStopId() {
        return mStopId;
    }

    @Override
    public Location getStopLocation() {
        Location location = null;
        if (mStop != null) {
            location = mStop.getLocation();
        } else {
            // Check the arguments
            Bundle args = getArguments();
            double latitude = args.getDouble(STOP_LAT);
            double longitude = args.getDouble(STOP_LON);
            if (latitude != 0 && longitude != 0) {
                location = LocationUtils.makeLocation(latitude, longitude);
            }
        }
        return location;
    }

    @Override
    public String getStopName() {
        String name;
        if (mStop != null) {
            name = mStop.getName();
        } else {
            // Check the arguments
            Bundle args = getArguments();
            name = args.getString(STOP_NAME);
        }
        return MyTextUtils.toTitleCase(name);
    }

    @Override
    public String getStopDirection() {
        if (mStop != null) {
            return mStop.getDirection();
        } else {
            // Check the arguments
            Bundle args = getArguments();
            return args.getString(STOP_DIRECTION);
        }
    }

    /**
     * Returns a sorted list (by ETA) of arrival times for the current stop
     *
     * @return a sorted list (by ETA) of arrival times for the current stop
     */
    @Override
    public ArrayList<ArrivalInfo> getArrivalInfo() {
        ArrayList<ArrivalInfo> list = null;

        if (mArrivalInfo != null) {
            list = ArrivalInfo.convertObaArrivalInfo(getActivity(), mArrivalInfo, mRoutesFilter,
                    System.currentTimeMillis());
        }
        return list;
    }

    /**
     * Returns the range of arrival times (i.e., for the next "minutesAfter" minutes), or -1 if
     * this information isn't available
     *
     * @return the range of arrival times (i.e., for the next "minutesAfter" minutes), or -1 if
     * this information isn't available
     */
    @Override
    public int getMinutesAfter() {
        ArrivalsListLoader loader = getArrivalsLoader();
        if (loader != null) {
            return loader.getMinutesAfter();
        } else {
            return -1;
        }
    }

    @Override
    public String getUserStopName() {
        return mStopUserName;
    }

    @Override
    public void setUserStopName(String name) {
        ContentResolver cr = getActivity().getContentResolver();
        ContentValues values = new ContentValues();
        if (TextUtils.isEmpty(name)) {
            values.putNull(ObaContract.Stops.USER_NAME);
            mStopUserName = null;
        } else {
            values.put(ObaContract.Stops.USER_NAME, name);
            mStopUserName = name;
        }
        cr.update(mStopUri, values, null, null);
    }

    @Override
    public ArrayList<String> getRoutesFilter() {
        // If mStop is null, we don't want the ArrivalsListHeader calling
        // getNumRoutes, so if we don't have a stop we pretend we don't have
        // a route filter at all.
        if (mStop != null) {
            return mRoutesFilter;
        } else {
            return null;
        }
    }

    @Override
    public void setRoutesFilter(ArrayList<String> routes) {
        mRoutesFilter = routes;
        ObaContract.StopRouteFilters.set(getActivity(), mStopId, mRoutesFilter);
        refreshLocal();
    }

    @Override
    public long getLastGoodResponseTime() {
        ArrivalsListLoader loader = getArrivalsLoader();
        if (loader == null) {
            return 0;
        }
        return loader.getLastGoodResponseTime();
    }

    @Override
    public List<String> getRouteDisplayNames() {
        if (mStop != null && getArrivalsLoader() != null) {
            ObaArrivalInfoResponse response = getArrivalsLoader().getLastGoodResponse();
            List<ObaRoute> routes = response.getRoutes(mStop.getRouteIds());
            ArrayList<String> displayNames = new ArrayList<String>();
            for (ObaRoute r : routes) {
                displayNames.add(UIUtils.getRouteDisplayName(r));
            }
            return displayNames;
        } else {
            // Check the arguments
            Bundle args = getArguments();
            String serializedRoutes = args.getString(STOP_ROUTES);
            if (serializedRoutes != null) {
                return UIUtils.deserializeRouteDisplayNames(serializedRoutes);
            }
        }
        // If we've gotten this far, we don't have any routeIds to share
        return null;
    }

    @Override
    public int getNumRoutes() {
        if (mStop != null) {
            return mStop.getRouteIds().length;
        } else {
            return 0;
        }
    }

    @Override
    public boolean isFavoriteStop() {
        return mFavorite;
    }

    @Override
    public boolean setFavoriteStop(boolean favorite) {
        if (ObaContract.Stops.markAsFavorite(getActivity(), mStopUri, favorite)) {
            mFavorite = favorite;
        }
        // Apparently we can't rely on onPrepareOptionsMenu to set the
        // menus like we did before...
        getActivity().supportInvalidateOptionsMenu();

        //Analytics
        ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                getString(R.string.analytics_action_edit_field), getString(R.string.analytics_label_edit_field));

        return mFavorite;
    }

    @Override
    public AlertList getAlertList() {
        return mAlertList;
    }

    /**
     * Checks to see if the user has changed the arrival info style preference after this Fragment
     * was initialized
     */
    private void checkAdapterStylePreference() {
        int currentArrivalInfoStyle = BuildFlavorUtils.getArrivalInfoStyleFromPreferences();

        if (currentArrivalInfoStyle == BuildFlavorUtils.ARRIVAL_INFO_STYLE_A
                && !(mAdapter instanceof ArrivalsListAdapterStyleA)) {
            // Change to Style A adapter
            reinitAdapterStyleOnPreferenceChange(BuildFlavorUtils.ARRIVAL_INFO_STYLE_A);
        } else if (currentArrivalInfoStyle == BuildFlavorUtils.ARRIVAL_INFO_STYLE_B
                && !(mAdapter instanceof ArrivalsListAdapterStyleB)) {
            // Change to Style B adapter
            reinitAdapterStyleOnPreferenceChange(BuildFlavorUtils.ARRIVAL_INFO_STYLE_B);
        }
    }

    /**
     * Reinitializes the adapter style after there has been a preference changed
     *
     * @param arrivalInfoStyle the adapter style to change to - should be one of the
     *                         BuildFlavorUtil.ARRIVAL_INFO_STYLE_* contants
     */
    private void reinitAdapterStyleOnPreferenceChange(int arrivalInfoStyle) {
        LayoutInflater inflater = LayoutInflater.from(getActivity());
        // Remove any existing footer view
        if (mFooter != null) {
            getListView().removeFooterView(mFooter);
        }
        CharSequence emptyText = null;
        // Remove any existing empty list view
        if (mEmptyList != null) {
            TextView noArrivals = (TextView) mEmptyList.findViewById(R.id.noArrivals);
            if (noArrivals != null) {
                emptyText = noArrivals.getText();
            }
            ((ViewGroup) getListView().getParent()).removeView(mEmptyList);
        }

        initArrivalInfoViews(arrivalInfoStyle, inflater);
        setupFooter();
        setupEmptyList(emptyText);
        setListViewProperties(arrivalInfoStyle);
        instantiateAdapter(arrivalInfoStyle);
    }

    /**
     * Initializes the adapter views
     *
     * @param arrivalInfoStyle the adapter style to use - should be one of the
     *                         BuildFlavorUtil.ARRIVAL_INFO_STYLE_* contants
     * @param inflater         inflater to use
     */
    private void initArrivalInfoViews(int arrivalInfoStyle, LayoutInflater inflater) {
        // Use a card-styled footer
        mFooter = inflater.inflate(R.layout.arrivals_list_footer_style_b, null);
        mEmptyList = inflater.inflate(R.layout.arrivals_list_empty_style_b, null);
    }

    /**
     * Sets up the footer with the load more arrivals button
     */
    private void setupFooter() {
        // Setup list footer button to load more arrivals (when arrivals are shown)
        Button loadMoreArrivals = (Button) mFooter.findViewById(R.id.load_more_arrivals);
        loadMoreArrivals.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadMoreArrivals();
            }
        });
        getListView().addFooterView(mFooter);
        mFooter.requestLayout();
    }

    /**
     * Sets up the load more arrivals button in the empty list view
     * @param currentText the text that should populate the empty list entry, or null if no text
     *                    should be set at this point
     */
    private void setupEmptyList(CharSequence currentText) {
        Button loadMoreArrivalsEmptyList = (Button) mEmptyList.findViewById(R.id.load_more_arrivals);
        loadMoreArrivalsEmptyList.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadMoreArrivals();
            }
        });
        // Set and add the view that is shown if no arrival information is returned by the REST API
        getListView().setEmptyView(mEmptyList);
        ((ViewGroup) getListView().getParent()).addView(mEmptyList);
        if (currentText != null) {
            setEmptyText(currentText);
        }
    }

    /**
     * Initializes the list view properties
     *
     * @param arrivalInfoStyle the adapter style to use - should be one of the
     *                         BuildFlavorUtil.ARRIVAL_INFO_STYLE_* contants
     */
    private void setListViewProperties(int arrivalInfoStyle) {
        ListView.MarginLayoutParams listParam = (ListView.MarginLayoutParams) getListView().getLayoutParams();
        // Set margins for the CardViews
        listParam.bottomMargin = UIUtils.dpToPixels(getActivity(), 2);
        listParam.topMargin = UIUtils.dpToPixels(getActivity(), 3);
        listParam.leftMargin = UIUtils.dpToPixels(getActivity(), 5);
        listParam.rightMargin = UIUtils.dpToPixels(getActivity(), 5);
        // Set the listview background to give the cards more contrast
        getListView().setBackgroundColor(getResources().getColor(R.color.stop_info_arrival_list_background));
        // Update the layout parameters
        getListView().setLayoutParams(listParam);
    }

    /**
     * Instantiates the adapter based on the style to be used
     *
     * @param arrivalInfoStyle the adapter style to use - should be one of the
     *                         BuildFlavorUtil.ARRIVAL_INFO_STYLE_* contants
     */
    private void instantiateAdapter(int arrivalInfoStyle) {
        if (UIUtils.canSupportArrivalInfoStyleB()) {
            switch (arrivalInfoStyle) {
            case BuildFlavorUtils.ARRIVAL_INFO_STYLE_A:
                mAdapter = new ArrivalsListAdapterStyleA(getActivity());
                break;
            case BuildFlavorUtils.ARRIVAL_INFO_STYLE_B:
                mAdapter = new ArrivalsListAdapterStyleB(getActivity());
                ((ArrivalsListAdapterStyleB) mAdapter).setFragment(this);
                break;
            }
        } else {
            // Always use Style A on Gingerbread
            mAdapter = new ArrivalsListAdapterStyleA(getActivity());
        }

        // We present arrivals as cards, so hide the divider in the listview
        getListView().setDivider(null);
        setListAdapter(mAdapter);
    }

    private void doSortBy() {
        // Switch sort order and show Toast
        Resources r = getActivity().getResources();
        SharedPreferences settings = Application.getPrefs();
        String currentValue = settings.getString(getString(R.string.preference_key_arrival_info_style), null);
        String[] styles = getResources().getStringArray(R.array.arrival_info_style_options);
        int newValue;

        if (currentValue.equalsIgnoreCase(styles[0])) {
            // Currently we're sorting by ETA - change to sorting by route
            newValue = 1;
            //Analytics
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getActivity().getString(R.string.analytics_action_button_press),
                    getActivity().getString(R.string.analytics_label_sort_by_route_arrival));
        } else {
            // Currently we're sorting by route - change to sorting by eta
            newValue = 0;
            //Analytics
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getActivity().getString(R.string.analytics_action_button_press),
                    getActivity().getString(R.string.analytics_label_sort_by_eta_arrival));
        }

        PreferenceUtils.saveString(getResources().getString(R.string.preference_key_arrival_info_style),
                styles[newValue]);
        checkAdapterStylePreference();
        refreshLocal();
        getLoaderManager().restartLoader(TRIPS_FOR_STOP_LOADER, null, mTripsForStopCallback);
        if (mListener != null) {
            mListener.onSortBySelected();
        }
    }

    private void showRoutesFilterDialog() {
        ObaArrivalInfoResponse response = getArrivalsLoader().getLastGoodResponse();
        final List<ObaRoute> routes = response.getRoutes(mStop.getRouteIds());
        final int len = routes.size();
        final ArrayList<String> filter = mRoutesFilter;

        // mRouteIds = new ArrayList<String>(len);
        String[] items = new String[len];
        boolean[] checks = new boolean[len];

        // Go through all the stops, add them to the Ids and Names
        // For each stop, if it is in the enabled list, mark it as checked.
        for (int i = 0; i < len; ++i) {
            final ObaRoute route = routes.get(i);
            // final String id = route.getId();
            // mRouteIds.add(i, id);
            items[i] = UIUtils.getRouteDisplayName(route);
            if (filter.contains(route.getId())) {
                checks[i] = true;
            }
        }
        // Arguments
        Bundle args = new Bundle();
        args.putStringArray(RoutesFilterDialog.ITEMS, items);
        args.putBooleanArray(RoutesFilterDialog.CHECKS, checks);
        RoutesFilterDialog frag = new RoutesFilterDialog();
        frag.setArguments(args);
        frag.show(getActivity().getSupportFragmentManager(), ".RoutesFilterDialog");
    }

    private void showStopDetailsDialog() {
        // Create dialog contents
        final String newLine = "\n";
        String name = getStopName();
        String userName = getUserStopName();
        String title = "";

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

        StringBuilder message = new StringBuilder();

        if (mStop != null) {
            message.append(getString(R.string.stop_details_code, mStop.getStopCode()) + newLine);
        }

        // Routes that serve this stop
        List<String> routeDisplayNames = getRouteDisplayNames();
        if (routeDisplayNames != null) {
            String routes = getString(R.string.stop_info_route_ids_label) + " "
                    + UIUtils.formatRouteDisplayNames(routeDisplayNames, new ArrayList<String>());
            message.append(routes);
        }

        String direction = getStopDirection();
        if (!TextUtils.isEmpty(direction)) {
            message.append(newLine + getString(UIUtils.getStopDirectionText(direction)));
        }

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setTitle(title);
        builder.setMessage(message.toString());
        builder.create().show();

        ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                getString(R.string.analytics_action_button_press),
                getString(R.string.analytics_label_button_press_stop_details));
    }

    /**
     * Sets the listener
     *
     * @param listener the listener
     */
    public void setListener(Listener listener) {
        this.mListener = listener;
    }

    private void setupHeader(Bundle bundle) {
        if (bundle != null) {
            mExternalHeader = bundle.getBoolean(EXTERNAL_HEADER);
        }

        if (mHeader == null && mExternalHeader == false) {
            // We should use the header contained in this fragment's layout, if none was provided
            // by the Activity via setHeader()
            mHeader = new ArrivalsListHeader(getActivity(), this, getFragmentManager());
            mHeaderView = getView().findViewById(R.id.arrivals_list_header);
            mHeader.initView(mHeaderView);
            mHeader.showExpandCollapseIndicator(false);
            // Header is not in a sliding panel, so set collapsed state to false
            mHeader.setSlidingPanelCollapsed(false);
        } else {
            // The header is in another layout (e.g., sliding panel), so we need to remove the header in this layout
            getView().findViewById(R.id.arrivals_list_header).setVisibility(View.GONE);
        }

        if (mHeader != null) {
            mHeader.refresh();
        }
    }

    public static class RoutesFilterDialog extends DialogFragment
            implements OnMultiChoiceClickListener, DialogInterface.OnClickListener {

        static final String ITEMS = ".items";

        static final String CHECKS = ".checks";

        private boolean[] mChecks;

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Bundle args = getArguments();
            String[] items = args.getStringArray(ITEMS);
            mChecks = args.getBooleanArray(CHECKS);
            if (savedInstanceState != null) {
                mChecks = args.getBooleanArray(CHECKS);
            }

            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            return builder.setTitle(R.string.stop_info_filter_title).setMultiChoiceItems(items, mChecks, this)
                    .setPositiveButton(R.string.stop_info_save, this)
                    .setNegativeButton(R.string.stop_info_cancel, null).create();
        }

        @Override
        public void onSaveInstanceState(Bundle outState) {
            outState.putBooleanArray(CHECKS, mChecks);
        }

        @Override
        public void onClick(DialogInterface dialog, int which) {
            Activity act = getActivity();
            ArrivalsListFragment frag = null;

            // Get the fragment we want...
            if (act instanceof ArrivalsListActivity) {
                frag = ((ArrivalsListActivity) act).getArrivalsListFragment();
            } else if (act instanceof HomeActivity) {
                frag = ((HomeActivity) act).getArrivalsListFragment();
            }

            frag.setRoutesFilter(mChecks);
            dialog.dismiss();
        }

        @Override
        public void onClick(DialogInterface arg0, int which, boolean isChecked) {
            mChecks[which] = isChecked;
        }
    }

    private void setRoutesFilter(boolean[] checks) {
        final int len = checks.length;
        final ArrayList<String> newFilter = new ArrayList<String>(len);

        ObaArrivalInfoResponse response = getArrivalsLoader().getLastGoodResponse();
        final List<ObaRoute> routes = response.getRoutes(mStop.getRouteIds());
        assert (routes.size() == len);

        for (int i = 0; i < len; ++i) {
            final ObaRoute route = routes.get(i);
            if (checks[i]) {
                newFilter.add(route.getId());
            }
        }
        // If the size of the filter is the number of routes
        // (i.e., the user selected every checkbox) act then
        // don't select any.
        if (newFilter.size() == len) {
            newFilter.clear();
        }

        setRoutesFilter(newFilter);
    }

    //
    // Navigation
    //
    private void goToTrip(ArrivalInfo stop) {
        ObaArrivalInfo stopInfo = stop.getInfo();
        TripInfoActivity.start(getActivity(), stopInfo.getTripId(), mStopId, stopInfo.getRouteId(),
                stopInfo.getShortName(), mStop.getName(), stopInfo.getScheduledDepartureTime(),
                stopInfo.getHeadsign());
    }

    private void goToTripDetails(ArrivalInfo stop) {
        TripDetailsActivity.start(getActivity(), stop.getInfo().getTripId(), stop.getInfo().getStopId());
    }

    private void goToRoute(ArrivalInfo stop) {
        RouteInfoActivity.start(getActivity(), stop.getInfo().getRouteId());
    }

    //
    // Helpers
    //
    private ArrivalsListLoader getArrivalsLoader() {
        // If the Fragment hasn't been attached to an Activity yet, return null
        if (!isAdded()) {
            return null;
        }
        Loader<ObaArrivalInfoResponse> l = getLoaderManager().getLoader(ARRIVALS_LIST_LOADER);
        return (ArrivalsListLoader) l;
    }

    @Override
    public void setEmptyText(CharSequence text) {
        TextView noArrivals = (TextView) mEmptyList.findViewById(R.id.noArrivals);
        noArrivals.setText(text);
    }

    private void loadMoreArrivals() {
        getArrivalsLoader().incrementMinutesAfter();
        mLoadedMoreArrivals = true;
        refresh();

        //Analytics
        ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                getActivity().getString(R.string.analytics_action_button_press),
                getActivity().getString(R.string.analytics_label_button_press));
    }

    /**
     * Full refresh of data from the OBA server
     */
    private void refresh() {
        if (isAdded()) {
            UIUtils.showProgress(this, true);
            // Get last response length now, since its overwritten within
            // ArrivalsListLoader before onLoadFinished() is called
            ObaArrivalInfoResponse lastGood = getArrivalsLoader().getLastGoodResponse();
            if (lastGood != null) {
                mLastResponseLength = lastGood.getArrivalInfo().length;
            }
            getArrivalsLoader().onContentChanged();
        }
    }

    /**
     * Refreshes ListFragment content using the most recent server response.  Does not trigger
     * another call to the OBA server.
     */
    public void refreshLocal() {
        ArrivalsListLoader loader = getArrivalsLoader();
        if (loader != null) {
            ObaArrivalInfoResponse response = loader.getLastGoodResponse();
            if (response == null) {
                // Nothing to refresh yet
                return;
            }
            mAdapter.setData(response.getArrivalInfo(), mRoutesFilter, System.currentTimeMillis());
        }
        if (mHeader != null) {
            mHeader.refresh();
        }
    }

    private final Handler mRefreshHandler = new Handler();

    private final Runnable mRefresh = new Runnable() {
        public void run() {
            refresh();
        }
    };

    private void setStopId() {
        Uri uri = (Uri) getArguments().getParcelable(FragmentUtils.URI);
        if (uri == null) {
            Log.e(TAG, "No URI in arguments");
            return;
        }
        mStopId = uri.getLastPathSegment();
        mStopUri = uri;
    }

    private static final String[] USER_PROJECTION = { ObaContract.Stops.FAVORITE, ObaContract.Stops.USER_NAME };

    private void setUserInfo() {
        ContentResolver cr = getActivity().getContentResolver();
        Cursor c = cr.query(mStopUri, USER_PROJECTION, null, null, null);
        if (c != null) {
            try {
                if (c.moveToNext()) {
                    mFavorite = (c.getInt(0) == 1);
                    mStopUserName = c.getString(1);
                }
            } finally {
                c.close();
            }
        }
    }

    private void addToDB(ObaStop stop) {
        String name = MyTextUtils.toTitleCase(stop.getName());

        // Update the database
        ContentValues values = new ContentValues();
        values.put(ObaContract.Stops.CODE, stop.getStopCode());
        values.put(ObaContract.Stops.NAME, name);
        values.put(ObaContract.Stops.DIRECTION, stop.getDirection());
        values.put(ObaContract.Stops.LATITUDE, stop.getLatitude());
        values.put(ObaContract.Stops.LONGITUDE, stop.getLongitude());
        if (Application.get().getCurrentRegion() != null) {
            values.put(ObaContract.Stops.REGION_ID, Application.get().getCurrentRegion().getId());
        }
        ObaContract.Stops.insertOrUpdate(getActivity(), stop.getId(), values, true);
    }

    private static final String[] TRIPS_PROJECTION = { ObaContract.Trips._ID, ObaContract.Trips.NAME };

    //
    // The asynchronously loads the trips for stop list.
    //
    private class TripsForStopCallback implements LoaderManager.LoaderCallbacks<Cursor> {

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            return new CursorLoader(getActivity(), ObaContract.Trips.CONTENT_URI, TRIPS_PROJECTION,
                    ObaContract.Trips.STOP_ID + "=?", new String[] { mStopId }, null);
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
            ContentQueryMap map = new ContentQueryMap(c, ObaContract.Trips._ID, true, null);
            // Call back into the adapter and header and say we've finished this.
            mAdapter.setTripsForStop(map);
            mHeader.setTripsForStop(map);
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
        }
    }

    //
    // Situations
    //
    private class SituationAlert implements AlertList.Alert {

        private final ObaSituation mSituation;

        SituationAlert(ObaSituation situation) {
            mSituation = situation;
        }

        @Override
        public String getId() {
            return mSituation.getId();
        }

        @Override
        public int getType() {
            if (ObaSituation.SEVERITY_NO_IMPACT.equals(mSituation.getSeverity())) {
                return TYPE_INFO;
            } else if (ObaSituation.SEVERITY_SEVERE.equals(mSituation.getSeverity())
                    || ObaSituation.SEVERITY_VERY_SEVERE.equals(mSituation.getSeverity())) {
                return TYPE_ERROR;
            } else {
                // Treat all other ObaSituation.SEVERITY_* types as a warning
                return TYPE_WARNING;
            }
        }

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

        @Override
        public CharSequence getString() {
            return mSituation.getSummary();
        }

        @Override
        public void onClick() {
            SituationActivity.start(getActivity(), mSituation);
        }

        @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;
            }
            SituationAlert other = (SituationAlert) obj;
            if (!getId().equals(other.getId())) {
                return false;
            }
            return true;
        }
    }

    private void refreshSituations(List<ObaSituation> situations) {
        // First, remove any existing situations
        if (mSituationAlerts != null) {
            for (SituationAlert alert : mSituationAlerts) {
                mAlertList.remove(alert);
            }
        }
        mSituationAlerts = null;

        if (situations.isEmpty()) {
            // The normal scenario
            return;
        }

        mSituationAlerts = new ArrayList<SituationAlert>();

        for (ObaSituation situation : situations) {
            if (UIUtils.isActiveWindowForSituation(situation, System.currentTimeMillis())) {
                SituationAlert alert = new SituationAlert(situation);
                mSituationAlerts.add(alert);
            }
        }
        mAlertList.addAll(mSituationAlerts);
    }
}