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

Java tutorial

Introduction

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

Source

/*
 * Copyright (C) 2012-2015 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 com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;

import com.sothree.slidinguppanel.SlidingUpPanelLayout;

import org.onebusaway.android.BuildConfig;
import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.ObaAnalytics;
import org.onebusaway.android.io.elements.ObaRegion;
import org.onebusaway.android.io.elements.ObaRoute;
import org.onebusaway.android.io.elements.ObaStop;
import org.onebusaway.android.io.request.ObaArrivalInfoResponse;
import org.onebusaway.android.map.MapModeController;
import org.onebusaway.android.map.MapParams;
import org.onebusaway.android.map.googlemapsv2.BaseMapFragment;
import org.onebusaway.android.region.ObaRegionsTask;
import org.onebusaway.android.tripservice.TripService;
import org.onebusaway.android.util.FragmentUtils;
import org.onebusaway.android.util.LocationUtils;
import org.onebusaway.android.util.PreferenceUtils;
import org.onebusaway.android.util.RegionUtils;
import org.onebusaway.android.util.ShowcaseViewUtils;
import org.onebusaway.android.util.UIUtils;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.FragmentManager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.ImageView;
import android.widget.ListView;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;

import static org.onebusaway.android.ui.NavigationDrawerFragment.NAVDRAWER_ITEM_HELP;
import static org.onebusaway.android.ui.NavigationDrawerFragment.NAVDRAWER_ITEM_MY_REMINDERS;
import static org.onebusaway.android.ui.NavigationDrawerFragment.NAVDRAWER_ITEM_NEARBY;
import static org.onebusaway.android.ui.NavigationDrawerFragment.NAVDRAWER_ITEM_SEND_FEEDBACK;
import static org.onebusaway.android.ui.NavigationDrawerFragment.NAVDRAWER_ITEM_SETTINGS;
import static org.onebusaway.android.ui.NavigationDrawerFragment.NAVDRAWER_ITEM_STARRED_STOPS;
import static org.onebusaway.android.ui.NavigationDrawerFragment.NavigationDrawerCallbacks;

public class HomeActivity extends AppCompatActivity implements BaseMapFragment.OnFocusChangedListener,
        ArrivalsListFragment.Listener, NavigationDrawerCallbacks {

    interface SlidingPanelController {

        /**
         * Sets the height of the sliding panel in pixels
         *
         * @param heightInPixels height of panel in pixels
         */
        void setPanelHeightPixels(int heightInPixels);

        /**
         * Returns the current height of the sliding panel in pixels, or -1 if the panel isn't yet
         * initialized
         *
         * @return the current height of the sliding panel in pixels, or -1 if the panel isn't yet
         * initialized
         */
        int getPanelHeightPixels();
    }

    public static final String TWITTER_URL = "http://mobile.twitter.com/onebusaway";

    public static final String STOP_ID = ".StopId";

    private static final int HELP_DIALOG = 1;

    private static final int WHATSNEW_DIALOG = 2;

    //One week, in milliseconds
    private static final long REGION_UPDATE_THRESHOLD = 1000 * 60 * 60 * 24 * 7;

    private static final String TAG = "HomeActivity";

    Context mContext;

    ArrivalsListFragment mArrivalsListFragment;

    ArrivalsListHeader mArrivalsListHeader;

    View mArrivalsListHeaderView;

    View mArrivalsListHeaderSubView;

    private FloatingActionButton mFabMyLocation;

    private static int MY_LOC_DEFAULT_BOTTOM_MARGIN;

    private static final int MY_LOC_BTN_ANIM_DURATION = 100; // ms

    Animation mMyLocationAnimation;

    /**
     * GoogleApiClient being used for Location Services
     */
    protected GoogleApiClient mGoogleApiClient;

    // Bottom Sliding panel
    SlidingUpPanelLayout mSlidingPanel;

    /**
     * Fragment managing the behaviors, interactions and presentation of the navigation drawer.
     */
    private NavigationDrawerFragment mNavigationDrawerFragment;

    /**
     * Currently selected navigation drawer position (so we don't unnecessarily swap fragments
     * if the same item is selected).  Initialized to -1 so the initial callback from
     * NavigationDrawerFragment always instantiates the fragments
     */
    private int mCurrentNavDrawerPosition = -1;

    /**
     * Fragments that can be selected as main content via the NavigationDrawer
     */
    MyStarredStopsFragment mMyStarredStopsFragment;

    BaseMapFragment mMapFragment;

    MyRemindersFragment mMyRemindersFragment;

    /**
     * Control which menu options are shown per fragment menu groups
     */
    private boolean mShowStarredStopsMenu = false;

    private boolean mShowArrivalsMenu = false;

    /**
     * Stop that has current focus on the map.  We retain a reference to the StopId,
     * since during rapid rotations its possible that a reference to a ObaStop object in
     * mFocusedStop can still be null, and we don't want to lose the state of which stopId is in
     * focus.  We also need access to the focused stop properties, hence why we also have
     * mFocusedStop
     */
    String mFocusedStopId = null;

    ObaStop mFocusedStop = null;

    ImageView mExpandCollapse = null;

    /**
     * Starts the MapActivity with a particular stop focused with the center of
     * the map at a particular point.
     *
     * @param context The context of the activity.
     * @param focusId The stop to focus.
     * @param lat     The latitude of the map center.
     * @param lon     The longitude of the map center.
     */
    public static final void start(Context context, String focusId, double lat, double lon) {
        context.startActivity(makeIntent(context, focusId, lat, lon));
    }

    /**
     * Starts the MapActivity in "RouteMode", which shows stops along a route,
     * and does not get new stops when the user pans the map.
     *
     * @param context The context of the activity.
     * @param routeId The route to show.
     */
    public static final void start(Context context, String routeId) {
        context.startActivity(makeIntent(context, routeId));
    }

    /**
     * Returns an intent that will start the MapActivity with a particular stop
     * focused with the center of the map at a particular point.
     *
     * @param context The context of the activity.
     * @param focusId The stop to focus.
     * @param lat     The latitude of the map center.
     * @param lon     The longitude of the map center.
     */
    public static final Intent makeIntent(Context context, String focusId, double lat, double lon) {
        Intent myIntent = new Intent(context, HomeActivity.class);
        myIntent.putExtra(MapParams.STOP_ID, focusId);
        myIntent.putExtra(MapParams.CENTER_LAT, lat);
        myIntent.putExtra(MapParams.CENTER_LON, lon);
        return myIntent;
    }

    /**
     * Returns an intent that starts the MapActivity in "RouteMode", which shows
     * stops along a route, and does not get new stops when the user pans the
     * map.
     *
     * @param context The context of the activity.
     * @param routeId The route to show.
     */
    public static final Intent makeIntent(Context context, String routeId) {
        Intent myIntent = new Intent(context, HomeActivity.class);
        myIntent.putExtra(MapParams.MODE, MapParams.MODE_ROUTE);
        myIntent.putExtra(MapParams.ZOOM_TO_ROUTE, true);
        myIntent.putExtra(MapParams.ROUTE_ID, routeId);
        return myIntent;
    }

    private int mWhatsNewMessage = R.string.main_help_whatsnew;

    SlidingPanelController mSlidingPanelController;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mContext = this;

        setupNavigationDrawer();

        setupSlidingPanel();

        setupMapState(savedInstanceState);

        setupMyLocationButton();

        setupGooglePlayServices();

        UIUtils.setupActionBar(this);

        autoShowWhatsNew();

        checkRegionStatus();

        // Tutorials - only one will show at a time, so "Welcome" is shown before "Recents"
        ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_WELCOME, this, null);
        ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_RECENT_STOPS_ROUTES, this, null);
    }

    @Override
    public void onStart() {
        super.onStart();
        // Make sure GoogleApiClient is connected, if available
        if (mGoogleApiClient != null && !mGoogleApiClient.isConnected()) {
            mGoogleApiClient.connect();
        }
        ObaAnalytics.reportActivityStart(this);
        if (Build.VERSION.SDK_INT >= 14) {
            AccessibilityManager am = (AccessibilityManager) getSystemService(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");
            }
        }
    }

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

        // Make sure header has sliding panel state
        if (mArrivalsListHeader != null && mSlidingPanel != null) {
            mArrivalsListHeader.setSlidingPanelCollapsed(isSlidingPanelCollapsed());
        }
    }

    @Override
    public void onStop() {
        // Tear down GoogleApiClient
        if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
            mGoogleApiClient.disconnect();
        }
        super.onStop();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mFocusedStopId != null) {
            outState.putString(STOP_ID, mFocusedStopId);
        }
    }

    @Override
    public void onNavigationDrawerItemSelected(int position) {
        goToNavDrawerItem(position);
    }

    private void goToNavDrawerItem(int item) {
        // Update the main content by replacing fragments
        switch (item) {
        case NAVDRAWER_ITEM_STARRED_STOPS:
            if (mCurrentNavDrawerPosition != NAVDRAWER_ITEM_STARRED_STOPS) {
                showStarredStopsFragment();
                mCurrentNavDrawerPosition = item;
                ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                        getString(R.string.analytics_action_button_press),
                        getString(R.string.analytics_label_button_press_star));
            }
            break;
        case NAVDRAWER_ITEM_NEARBY:
            if (mCurrentNavDrawerPosition != NAVDRAWER_ITEM_NEARBY) {
                showMapFragment();
                mCurrentNavDrawerPosition = item;
                ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                        getString(R.string.analytics_action_button_press),
                        getString(R.string.analytics_label_button_press_nearby));
            }
            break;
        case NAVDRAWER_ITEM_MY_REMINDERS:
            if (mCurrentNavDrawerPosition != NAVDRAWER_ITEM_MY_REMINDERS) {
                showMyRemindersFragment();
                mCurrentNavDrawerPosition = item;
                ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                        getString(R.string.analytics_action_button_press),
                        getString(R.string.analytics_label_button_press_reminders));
            }
            break;
        case NAVDRAWER_ITEM_SETTINGS:
            Intent preferences = new Intent(HomeActivity.this, PreferencesActivity.class);
            startActivity(preferences);
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getString(R.string.analytics_action_button_press),
                    getString(R.string.analytics_label_button_press_settings));
            break;
        case NAVDRAWER_ITEM_HELP:
            showDialog(HELP_DIALOG);
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getString(R.string.analytics_action_button_press),
                    getString(R.string.analytics_label_button_press_help));
            break;
        case NAVDRAWER_ITEM_SEND_FEEDBACK:
            Log.d(TAG, "TODO - show send feedback fragment");
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getString(R.string.analytics_action_button_press),
                    getString(R.string.analytics_label_button_press_feedback));
            break;
        }
        invalidateOptionsMenu();
    }

    private void showMapFragment() {
        FragmentManager fm = getSupportFragmentManager();
        /**
         * Hide everything that shouldn't be shown
         */
        hideStarredStopsFragment();
        hideReminderFragment();
        mShowStarredStopsMenu = false;
        /**
         * Show fragment (we use show instead of replace to keep the map state)
         */
        if (mMapFragment == null) {
            // First check to see if an instance of BaseMapFragment already exists (see #356)
            mMapFragment = (BaseMapFragment) fm.findFragmentByTag(BaseMapFragment.TAG);

            if (mMapFragment == null) {
                // No existing fragment was found, so create a new one
                Log.d(TAG, "Creating new BaseMapFragment");
                mMapFragment = BaseMapFragment.newInstance();
                fm.beginTransaction().add(R.id.main_fragment_container, mMapFragment, BaseMapFragment.TAG).commit();
            }
        }
        // Register listener for map focus callbacks
        mMapFragment.setOnFocusChangeListener(this);
        getSupportFragmentManager().beginTransaction().show(mMapFragment).commit();
        showMyLocationButton();
        mShowArrivalsMenu = true;
        if (mFocusedStopId != null && mSlidingPanel != null) {
            // if we've focused on a stop, then show the panel that was previously hidden
            mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
        }
        setTitle(getResources().getString(R.string.navdrawer_item_nearby));
    }

    private void showStarredStopsFragment() {
        FragmentManager fm = getSupportFragmentManager();
        /**
         * Hide everything that shouldn't be shown
         */
        hideMyLocationButton();
        hideMapFragment();
        hideReminderFragment();
        hideSlidingPanel();
        mShowArrivalsMenu = false;
        /**
         * Show fragment (we use show instead of replace to keep the map state)
         */
        mShowStarredStopsMenu = true;
        if (mMyStarredStopsFragment == null) {
            // First check to see if an instance of MyStarredStopsFragment already exists (see #356)
            mMyStarredStopsFragment = (MyStarredStopsFragment) fm.findFragmentByTag(MyStarredStopsFragment.TAG);

            if (mMyStarredStopsFragment == null) {
                // No existing fragment was found, so create a new one
                Log.d(TAG, "Creating new MyStarredStopsFragment");
                mMyStarredStopsFragment = new MyStarredStopsFragment();
                fm.beginTransaction()
                        .add(R.id.main_fragment_container, mMyStarredStopsFragment, MyStarredStopsFragment.TAG)
                        .commit();
            }
        }
        fm.beginTransaction().show(mMyStarredStopsFragment).commit();
        setTitle(getResources().getString(R.string.navdrawer_item_starred_stops));
    }

    private void showMyRemindersFragment() {
        FragmentManager fm = getSupportFragmentManager();
        /**
         * Hide everything that shouldn't be shown
         */
        hideMyLocationButton();
        hideStarredStopsFragment();
        hideMapFragment();
        hideSlidingPanel();
        mShowArrivalsMenu = false;
        mShowStarredStopsMenu = false;
        /**
         * Show fragment (we use show instead of replace to keep the map state)
         */
        if (mMyRemindersFragment == null) {
            // First check to see if an instance of MyRemindersFragment already exists (see #356)
            mMyRemindersFragment = (MyRemindersFragment) fm.findFragmentByTag(MyRemindersFragment.TAG);

            if (mMyRemindersFragment == null) {
                // No existing fragment was found, so create a new one
                Log.d(TAG, "Creating new MyRemindersFragment");
                mMyRemindersFragment = new MyRemindersFragment();
                fm.beginTransaction()
                        .add(R.id.main_fragment_container, mMyRemindersFragment, MyRemindersFragment.TAG).commit();
            }
        }
        fm.beginTransaction().show(mMyRemindersFragment).commit();
        setTitle(getResources().getString(R.string.navdrawer_item_my_reminders));
    }

    private void hideMapFragment() {
        FragmentManager fm = getSupportFragmentManager();
        mMapFragment = (BaseMapFragment) fm.findFragmentByTag(BaseMapFragment.TAG);
        if (mMapFragment != null && !mMapFragment.isHidden()) {
            fm.beginTransaction().hide(mMapFragment).commit();
        }
    }

    private void hideStarredStopsFragment() {
        FragmentManager fm = getSupportFragmentManager();
        mMyStarredStopsFragment = (MyStarredStopsFragment) fm.findFragmentByTag(MyStarredStopsFragment.TAG);
        if (mMyStarredStopsFragment != null && !mMyStarredStopsFragment.isHidden()) {
            fm.beginTransaction().hide(mMyStarredStopsFragment).commit();
        }
    }

    private void hideReminderFragment() {
        FragmentManager fm = getSupportFragmentManager();
        mMyRemindersFragment = (MyRemindersFragment) fm.findFragmentByTag(MyRemindersFragment.TAG);
        if (mMyRemindersFragment != null && !mMyRemindersFragment.isHidden()) {
            fm.beginTransaction().hide(mMyRemindersFragment).commit();
        }
    }

    private void hideSlidingPanel() {
        if (mSlidingPanel != null) {
            mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN);
        }
    }

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

        UIUtils.setupSearch(this, menu);

        // Initialize fragment menu visibility here, so we don't have overlap between the various fragments
        setupOptionsMenu(menu);

        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        // Manage fragment menu visibility here, so we don't have overlap between the various fragments
        setupOptionsMenu(menu);

        return true;
    }

    private void setupOptionsMenu(Menu menu) {
        menu.setGroupVisible(R.id.main_options_menu_group, true);
        menu.setGroupVisible(R.id.arrival_list_menu_group, mShowArrivalsMenu);
        menu.setGroupVisible(R.id.starred_stop_menu_group, mShowStarredStopsMenu);
    }

    @Override
    @SuppressWarnings("deprecation")
    public boolean onOptionsItemSelected(MenuItem item) {
        Log.d(TAG, "onOptionsItemSelected");
        final int id = item.getItemId();
        if (id == R.id.search) {
            onSearchRequested();
            //Analytics
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getString(R.string.analytics_action_button_press),
                    getString(R.string.analytics_label_button_press_search_box));
            return true;
        } else if (id == R.id.recent_stops_routes) {
            Intent myIntent = new Intent(this, MyRecentStopsAndRoutesActivity.class);
            startActivity(myIntent);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
        case HELP_DIALOG:
            return createHelpDialog();

        case WHATSNEW_DIALOG:
            return createWhatsNewDialog();
        }
        return super.onCreateDialog(id);
    }

    @SuppressWarnings("deprecation")
    private Dialog createHelpDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.main_help_title);
        // If a custom API URL is set, hide Contact Us, as we don't have a contact email to use
        int options;
        if (TextUtils.isEmpty(Application.get().getCustomApiUrl())) {
            options = R.array.main_help_options;
        } else {
            // Hide "Contact Us"
            options = R.array.main_help_options_no_contact_us;
        }
        builder.setItems(options, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                switch (which) {
                case 0:
                    ShowcaseViewUtils.resetAllTutorials();
                    NavHelp.goHome(HomeActivity.this);
                    break;
                case 1:
                    String twitterUrl = TWITTER_URL;
                    if (Application.get().getCurrentRegion() != null
                            && !TextUtils.isEmpty(Application.get().getCurrentRegion().getTwitterUrl())) {
                        twitterUrl = Application.get().getCurrentRegion().getTwitterUrl();
                    }
                    UIUtils.goToUrl(HomeActivity.this, twitterUrl);
                    ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                            getString(R.string.analytics_action_switch),
                            getString(R.string.analytics_label_app_switch));
                    break;
                case 2:
                    AgenciesActivity.start(HomeActivity.this);
                    break;
                case 3:
                    showDialog(WHATSNEW_DIALOG);
                    break;
                case 4:
                    UIUtils.sendContactEmail(HomeActivity.this, mGoogleApiClient);
                    break;
                }
            }
        });
        return builder.create();
    }

    @SuppressWarnings("deprecation")
    private Dialog createWhatsNewDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.main_help_whatsnew_title);
        builder.setIcon(R.mipmap.ic_launcher);
        builder.setMessage(mWhatsNewMessage);
        builder.setNeutralButton(R.string.main_help_close, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                dismissDialog(WHATSNEW_DIALOG);
            }
        });
        return builder.create();
    }

    private static final String WHATS_NEW_VER = "whatsNewVer";

    @SuppressWarnings("deprecation")
    private void autoShowWhatsNew() {
        SharedPreferences settings = Application.getPrefs();

        // Get the current app version.
        PackageManager pm = getPackageManager();
        PackageInfo appInfo = null;
        try {
            appInfo = pm.getPackageInfo(getPackageName(), PackageManager.GET_META_DATA);
        } catch (NameNotFoundException e) {
            // Do nothing, perhaps we'll get to show it again? Or never.
            return;
        }

        final int oldVer = settings.getInt(WHATS_NEW_VER, 0);
        final int newVer = appInfo.versionCode;

        if (oldVer < newVer) {
            mWhatsNewMessage = R.string.main_help_whatsnew;
            showDialog(WHATSNEW_DIALOG);

            // Updates will remove the alarms. This should put them back.
            // (Unfortunately I can't find a way to reschedule them without
            // having the app run again).
            TripService.scheduleAll(this);
            PreferenceUtils.saveInt(WHATS_NEW_VER, appInfo.versionCode);
        }
    }

    /**
     * Called by the BaseMapFragment when a stop obtains focus, or no stops have focus
     *
     * @param stop   the ObaStop that obtained focus, or null if no stop is in focus
     * @param routes a HashMap of all route display names that serve this stop - key is routeId
     * @param location the user touch location on the map, or null if the focus was otherwise
     *                 cleared programmatically
     */
    @Override
    public void onFocusChanged(ObaStop stop, HashMap<String, ObaRoute> routes, Location location) {
        // Check to see if we're already focused on this same stop - if so, we shouldn't do anything
        if (mFocusedStopId != null && stop != null && mFocusedStopId.equalsIgnoreCase(stop.getId())) {
            return;
        }

        mFocusedStop = stop;

        if (stop != null) {
            mFocusedStopId = stop.getId();
            // A stop on the map was just tapped, show it in the sliding panel
            updateArrivalListFragment(stop.getId(), stop, routes);

            //Analytics
            ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                    getString(R.string.analytics_action_button_press),
                    getString(R.string.analytics_label_button_press_map_icon));
        } else {
            // No stop is in focus (e.g., user tapped on the map), so hide the panel
            // and clear the currently focused stopId
            mFocusedStopId = null;
            moveMyLocationButton();
            mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN);
            if (mArrivalsListFragment != null) {
                FragmentManager fm = getSupportFragmentManager();
                fm.beginTransaction().remove(mArrivalsListFragment).commit();
            }
            mShowArrivalsMenu = false;
        }
    }

    /**
     * Called by the ArrivalsListFragment when the ListView is created
     *
     * @param listView the ListView that was just created
     */
    @Override
    public void onListViewCreated(ListView listView) {
        // Set the scrollable view in the sliding panel
        mSlidingPanel.setScrollableView(listView);
    }

    /**
     * Called by the ArrivalsListFragment when we have new updated arrival information
     *
     * @param response new arrival information
     */
    @Override
    public void onArrivalTimesUpdated(ObaArrivalInfoResponse response) {
        if (response == null || response.getStop() == null) {
            return;
        }

        // If we're missing any local references (e.g., if orientation just changed), store the values
        if (mFocusedStopId == null) {
            mFocusedStopId = response.getStop().getId();
        }
        if (mFocusedStop == null) {
            mFocusedStop = response.getStop();

            // Since mFocusedStop was null, the layout changed, and we should recenter map on stop
            if (mMapFragment != null && mSlidingPanel != null) {
                mMapFragment.setMapCenter(mFocusedStop.getLocation(), true,
                        mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.ANCHORED);
            }

            // ...and we should add a focus marker for this stop
            if (mMapFragment != null) {
                mMapFragment.setFocusStop(mFocusedStop, response.getRoutes());
            }
        }

        // Header might have changed height, so make sure my location button is set above the header
        moveMyLocationButton();

        // Show arrival info related tutorials
        showArrivalInfoTutorials(response);
    }

    /**
     * Triggers the various tutorials related to arrival info and the sliding panel header
     *
     * @param response arrival info, which is required for some tutorials
     */
    private void showArrivalInfoTutorials(ObaArrivalInfoResponse response) {
        // If we're already showing a ShowcaseView, we don't want to stack another on top
        if (ShowcaseViewUtils.isShowcaseViewShowing()) {
            return;
        }

        // If we can't see the map or sliding panel, we can't see the arrival info, so return
        if (mMapFragment.isHidden() || !mMapFragment.isVisible()
                || mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) {
            return;
        }

        /**
         * The ShowcaseViewUtils.showTutorial() method takes care of checking if a tutorial has
         * already been shown, so we can just list the tutorials in order of how they should be
         * shown to the user.
         */

        // Show the tutorial explaining arrival times
        ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_HEADER_ARRIVAL_INFO, this, response);

        // Show the starred stop tutorial
        if (isSlidingPanelCollapsed()) {
            ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_HEADER_STAR_STOP, this, response);
        }

        // Show the starred routes tutorial
        ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_HEADER_STAR_ROUTE, this, response);

        // Show the "more" arrival info tutorial
        ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_INFO_MORE, this, response);

        if (!isSlidingPanelCollapsed()
                && mArrivalsListFragment.getListAdapter() instanceof ArrivalsListAdapterStyleB) {
            ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_STYLE_B_SHOW_ROUTE, this, response);
        }
    }

    /**
     * Called by the ArrivalListFragment 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
     */
    @Override
    public boolean onShowRouteOnMapSelected(ArrivalInfo arrivalInfo) {
        // If the panel is fully expanded, change it to anchored so the user can see the map
        if (mSlidingPanel != null) {
            mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
        }

        Bundle bundle = new Bundle();
        bundle.putBoolean(MapParams.ZOOM_TO_ROUTE, false);
        bundle.putBoolean(MapParams.ZOOM_INCLUDE_CLOSEST_VEHICLE, true);
        bundle.putString(MapParams.ROUTE_ID, arrivalInfo.getInfo().getRouteId());
        mMapFragment.setMapMode(MapParams.MODE_ROUTE, bundle);

        ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_VEHICLE_ICONS, this, null);

        return true;
    }

    /**
     * Called when the user selects the "Sort by" option in ArrivalsListFragment
     */
    @Override
    public void onSortBySelected() {
        // If the sliding panel isn't open, then open it to show what we're sorting
        if (mSlidingPanel != null) {
            if (isSlidingPanelCollapsed()) {
                mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.ANCHORED);
            }
        }
    }

    @Override
    public void onBackPressed() {
        // If we're showing a tutorial, we don't want the user to be able to use the back button
        if (ShowcaseViewUtils.isShowcaseViewShowing()) {
            return;
        }

        // Collapse the panel when the user presses the back button
        if (mSlidingPanel != null) {
            // Collapse the sliding panel if its anchored or expanded
            if (mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED
                    || mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.ANCHORED) {
                mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
                return;
            }
            // Clear focused stop and close the sliding panel if its collapsed
            if (mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) {
                // Clear the stop focus in map fragment, which will trigger a callback to close the
                // panel via BaseMapFragment.OnFocusChangedListener in this.onFocusChanged()
                mMapFragment.setFocusStop(null, null);
                return;
            }
        }
        super.onBackPressed();
    }

    private void updateArrivalListFragment(String stopId, ObaStop stop, HashMap<String, ObaRoute> routes) {
        FragmentManager fm = getSupportFragmentManager();
        Intent intent;

        mArrivalsListFragment = new ArrivalsListFragment();
        mArrivalsListFragment.setListener(this);

        // Set the header for the arrival list to be the top of the sliding panel
        mArrivalsListHeader = new ArrivalsListHeader(this, mArrivalsListFragment, getSupportFragmentManager());
        mArrivalsListFragment.setHeader(mArrivalsListHeader, mArrivalsListHeaderView);
        mArrivalsListHeader.setSlidingPanelController(mSlidingPanelController);
        mArrivalsListHeader.setSlidingPanelCollapsed(isSlidingPanelCollapsed());
        mShowArrivalsMenu = true;
        mExpandCollapse = (ImageView) mArrivalsListHeaderView.findViewById(R.id.expand_collapse);

        if (stop != null && routes != null) {
            // Use ObaStop and ObaRoute objects, since we can pre-populate some of the fields
            // before getting an API response
            intent = new ArrivalsListFragment.IntentBuilder(this, stop, routes).build();
        } else {
            // All we have is a stopId (likely started from Intent or after rotating device)
            // Some fields will be blank until we get an API response
            intent = new ArrivalsListFragment.IntentBuilder(this, stopId).build();
        }

        mArrivalsListFragment.setArguments(FragmentUtils.getIntentArgs(intent));
        fm.beginTransaction().replace(R.id.slidingFragment, mArrivalsListFragment).commit();
        if (mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) {
            mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
        }
        moveMyLocationButton();
    }

    /**
     * Checks region status, which can potentially including forcing a reload of region
     * info from the server.  Also includes auto-selection of closest region.
     */
    private void checkRegionStatus() {
        //First check for custom API URL set by user via Preferences, since if that is set we don't need region info from the REST API
        if (!TextUtils.isEmpty(Application.get().getCustomApiUrl())) {
            return;
        }

        // Check if region is hard-coded for this build flavor
        if (BuildConfig.USE_FIXED_REGION) {
            ObaRegion r = RegionUtils.getRegionFromBuildFlavor();
            // Set the hard-coded region
            RegionUtils.saveToProvider(this, Collections.singletonList(r));
            Application.get().setCurrentRegion(r);
            // Disable any region auto-selection in preferences
            PreferenceUtils.saveBoolean(getString(R.string.preference_key_auto_select_region), false);
            return;
        }

        boolean forceReload = false;
        boolean showProgressDialog = true;

        //If we don't have region info selected, or if enough time has passed since last region info update,
        //force contacting the server again
        if (Application.get().getCurrentRegion() == null
                || new Date().getTime() - Application.get().getLastRegionUpdateDate() > REGION_UPDATE_THRESHOLD) {
            forceReload = true;
            Log.d(TAG, "Region info has expired (or does not exist), forcing a reload from the server...");
        }

        if (Application.get().getCurrentRegion() != null) {
            //We already have region info locally, so just check current region status quietly in the background
            showProgressDialog = false;
        }

        //Check region status, possibly forcing a reload from server and checking proximity to current region
        ObaRegionsTask task = new ObaRegionsTask(this, mMapFragment, forceReload, showProgressDialog);
        task.execute();
    }

    private void setupMyLocationButton() {
        // Initialize the My Location button
        mFabMyLocation = (FloatingActionButton) findViewById(R.id.btnMyLocation);
        mFabMyLocation.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                if (mMapFragment != null) {
                    mMapFragment.setMyLocation(true, true);
                    ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.UI_ACTION.toString(),
                            getString(R.string.analytics_action_button_press),
                            getString(R.string.analytics_label_button_press_location));
                }
            }
        });
        ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) mFabMyLocation.getLayoutParams();
        MY_LOC_DEFAULT_BOTTOM_MARGIN = p.bottomMargin;
        if (mCurrentNavDrawerPosition == NAVDRAWER_ITEM_NEARBY) {
            showMyLocationButton();
        } else {
            hideMyLocationButton();
        }
    }

    synchronized private void moveMyLocationButton() {
        if (mFabMyLocation == null) {
            return;
        }
        if (mMyLocationAnimation != null
                && (mMyLocationAnimation.hasStarted() && !mMyLocationAnimation.hasEnded())) {
            // We're already animating - do nothing
            return;
        }

        if (mMyLocationAnimation != null) {
            mMyLocationAnimation.reset();
        }

        // Post this to a handler to allow the header to settle before animating the button
        final Handler h = new Handler();
        h.postDelayed(new Runnable() {
            @Override
            public void run() {
                final ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) mFabMyLocation
                        .getLayoutParams();

                int tempMargin = MY_LOC_DEFAULT_BOTTOM_MARGIN;

                if (mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) {
                    tempMargin += mSlidingPanel.getPanelHeight();
                    if (p.bottomMargin == tempMargin) {
                        // Button is already in the right position, do nothing
                        return;
                    }
                } else {
                    if (mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) {
                        if (p.bottomMargin == tempMargin) {
                            // Button is already in the right position, do nothing
                            return;
                        }
                    }
                }

                final int goalMargin = tempMargin;
                final int currentMargin = p.bottomMargin;

                // TODO - this doesn't seem to be animating?? Why not?  Or is it just my device...
                mMyLocationAnimation = new Animation() {
                    @Override
                    protected void applyTransformation(float interpolatedTime, Transformation t) {
                        int bottom;
                        if (goalMargin > currentMargin) {
                            bottom = currentMargin
                                    + (int) (Math.abs(currentMargin - goalMargin) * interpolatedTime);
                        } else {
                            bottom = currentMargin
                                    - (int) (Math.abs(currentMargin - goalMargin) * interpolatedTime);
                        }
                        UIUtils.setMargins(mFabMyLocation, p.leftMargin, p.topMargin, p.rightMargin, bottom);
                    }
                };
                mMyLocationAnimation.setDuration(MY_LOC_BTN_ANIM_DURATION);
                mFabMyLocation.startAnimation(mMyLocationAnimation);
            }
        }, 100);
    }

    private void showMyLocationButton() {
        if (mFabMyLocation == null) {
            return;
        }
        if (mFabMyLocation.getVisibility() != View.VISIBLE) {
            mFabMyLocation.setVisibility(View.VISIBLE);
        }
    }

    private void hideMyLocationButton() {
        if (mFabMyLocation == null) {
            return;
        }
        if (mFabMyLocation.getVisibility() != View.GONE) {
            mFabMyLocation.setVisibility(View.GONE);
        }
    }

    private void setupNavigationDrawer() {
        mNavigationDrawerFragment = (NavigationDrawerFragment) getSupportFragmentManager()
                .findFragmentById(R.id.navigation_drawer);

        // Set up the drawer.
        mNavigationDrawerFragment.setUp(R.id.navigation_drawer,
                (DrawerLayout) findViewById(R.id.nav_drawer_left_pane));

        // Was this activity started to show a route or stop on the map? If so, switch to MapFragment
        Bundle bundle = getIntent().getExtras();
        if (bundle != null) {
            String routeId = bundle.getString(MapParams.ROUTE_ID);
            String stopId = bundle.getString(MapParams.STOP_ID);
            if (routeId != null || stopId != null) {
                mNavigationDrawerFragment.selectItem(NAVDRAWER_ITEM_NEARBY);
            }
        }
    }

    private void setupGooglePlayServices() {
        // Init Google Play Services as early as possible in the Fragment lifecycle to give it time
        if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
            mGoogleApiClient = LocationUtils.getGoogleApiClientWithCallbacks(this);
            mGoogleApiClient.connect();
        }
    }

    private void setupSlidingPanel() {
        mSlidingPanel = (SlidingUpPanelLayout) findViewById(R.id.bottom_sliding_layout);
        mArrivalsListHeaderView = findViewById(R.id.arrivals_list_header);
        mArrivalsListHeaderSubView = mArrivalsListHeaderView.findViewById(R.id.main_header_content);

        mSlidingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN); // Don't show the panel until we have content
        mSlidingPanel.setOverlayed(true);
        mSlidingPanel.setAnchorPoint(MapModeController.OVERLAY_PERCENTAGE);
        mSlidingPanel.setPanelSlideListener(new SlidingUpPanelLayout.PanelSlideListener() {
            @Override
            public void onPanelSlide(View panel, float slideOffset) {
                Log.d(TAG, "onPanelSlide, offset " + slideOffset);
                mArrivalsListHeader.closeStatusPopups();
            }

            @Override
            public void onPanelExpanded(View panel) {
                Log.d(TAG, "onPanelExpanded");
                if (mArrivalsListHeader != null) {
                    mArrivalsListHeader.setSlidingPanelCollapsed(false);
                    mArrivalsListHeader.refresh();
                }

                // Accessibility
                if (mExpandCollapse != null) {
                    mExpandCollapse.setContentDescription(
                            mContext.getResources().getString(R.string.stop_header_sliding_panel_open));
                }

                ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_SORT, HomeActivity.this, null);
            }

            @Override
            public void onPanelCollapsed(View panel) {
                Log.d(TAG, "onPanelCollapsed");
                if (mMapFragment != null) {
                    mMapFragment.getMapView().setPadding(null, null, null, mSlidingPanel.getPanelHeight());
                }
                if (mArrivalsListHeader != null) {
                    mArrivalsListHeader.setSlidingPanelCollapsed(true);
                    mArrivalsListHeader.refresh();
                }
                moveMyLocationButton();

                // Accessibility
                if (mExpandCollapse != null) {
                    mExpandCollapse.setContentDescription(
                            mContext.getResources().getString(R.string.stop_header_sliding_panel_collapsed));
                }
            }

            @Override
            public void onPanelAnchored(View panel) {
                Log.d(TAG, "onPanelAnchored");
                if (mMapFragment != null) {
                    mMapFragment.getMapView().setPadding(null, null, null, mSlidingPanel.getPanelHeight());
                }
                if (mFocusedStop != null && mMapFragment != null) {
                    mMapFragment.setMapCenter(mFocusedStop.getLocation(), true, true);
                }
                if (mArrivalsListHeader != null) {
                    mArrivalsListHeader.setSlidingPanelCollapsed(false);
                    mArrivalsListHeader.refresh();
                }

                // Accessibility
                if (mExpandCollapse != null) {
                    mExpandCollapse.setContentDescription(
                            mContext.getResources().getString(R.string.stop_header_sliding_panel_open));
                }

                ShowcaseViewUtils.showTutorial(ShowcaseViewUtils.TUTORIAL_ARRIVAL_SORT, HomeActivity.this, null);
            }

            @Override
            public void onPanelHidden(View panel) {
                Log.d(TAG, "onPanelHidden");
                // We need to hide the panel when switching between fragments via the navdrawer,
                // so we shouldn't put anything here that causes us to lose the state of the
                // MapFragment or the ArrivalListFragment (e.g., removing the ArrivalListFragment)
                if (mMapFragment != null) {
                    mMapFragment.getMapView().setPadding(null, null, null, 0);
                }

                // Accessibility - reset it here so its ready for next showing
                if (mExpandCollapse != null) {
                    mExpandCollapse.setContentDescription(
                            mContext.getResources().getString(R.string.stop_header_sliding_panel_collapsed));
                }
            }
        });

        mSlidingPanelController = new SlidingPanelController() {
            @Override
            public void setPanelHeightPixels(int heightInPixels) {
                if (mSlidingPanel != null) {
                    if (mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.DRAGGING
                            || mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) {
                        // Don't resize header yet - see #294 - header size will be refreshed on panel state change
                        return;
                    }
                    if (mSlidingPanel.getPanelHeight() != heightInPixels) {
                        mSlidingPanel.setPanelHeight(heightInPixels);
                        mArrivalsListHeaderView.getLayoutParams().height = heightInPixels;
                        mArrivalsListHeaderSubView.getLayoutParams().height = heightInPixels;
                    }
                }
            }

            @Override
            public int getPanelHeightPixels() {
                if (mSlidingPanel != null) {
                    return mSlidingPanel.getPanelHeight();
                }
                return -1;
            }
        };
    }

    /**
     * Sets up the initial map state, based on a previous savedInstanceState for this activity,
     * or an Intent that was passed into this activity
     */
    private void setupMapState(Bundle savedInstanceState) {
        String stopId;
        // Check savedInstanceState to see if there is a previous state for this activity
        if (savedInstanceState != null) {
            // We're recreating an instance with a previous state, so show the focused stop in panel
            stopId = savedInstanceState.getString(STOP_ID);

            if (stopId != null) {
                mFocusedStopId = stopId;
                // We don't have an ObaStop or ObaRoute mapping, so just pass in null for those
                updateArrivalListFragment(stopId, null, null);
            }
        } else {
            // Check intent passed into Activity
            Bundle bundle = getIntent().getExtras();
            if (bundle != null) {
                // Did this activity start to focus on a stop?  If so, set focus and show arrival info
                stopId = bundle.getString(MapParams.STOP_ID);
                double lat = bundle.getDouble(MapParams.CENTER_LAT);
                double lon = bundle.getDouble(MapParams.CENTER_LON);

                if (stopId != null && lat != 0.0 && lon != 0.0) {
                    mFocusedStopId = stopId;
                    updateArrivalListFragment(stopId, null, null);
                }
            }
        }
    }

    /**
     * Our definition of collapsed is consistent with SlidingPanel pre-v3.0.0 definition - we don't
     * consider the panel changing from the hidden state to the collapsed state to be a "collapsed"
     * event.  v3.0.0 and higher fire the "collapsed" event when coming from the hidden state.
     * This method provides us with a collapsed state that is consistent with the pre-v3.0.0
     * definition
     * of a collapse event, to make our event model consistent with post v3.0.0 SlidingPanel.
     *
     * @return true if the panel isn't expanded or anchored, false if it is not
     */
    private boolean isSlidingPanelCollapsed() {
        return !(mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED
                || mSlidingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.ANCHORED);
    }

    public ArrivalsListFragment getArrivalsListFragment() {
        return mArrivalsListFragment;
    }
}