uk.org.rivernile.edinburghbustracker.android.fragments.general.FavouriteStopsFragment.java Source code

Java tutorial

Introduction

Here is the source code for uk.org.rivernile.edinburghbustracker.android.fragments.general.FavouriteStopsFragment.java

Source

/*
 * Copyright (C) 2009 - 2013 Niall 'Rivernile' Scott
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors or contributors be held liable for
 * any damages arising from the use of this software.
 *
 * The aforementioned copyright holder(s) hereby grant you a
 * non-transferrable right to use this software for any purpose (including
 * commercial applications), and to modify it and redistribute it, subject to
 * the following conditions:
 *
 *  1. This notice may not be removed or altered from any file it appears in.
 *
 *  2. Any modifications made to this software, except those defined in
 *     clause 3 of this agreement, must be released under this license, and
 *     the source code of any modifications must be made available on a
 *     publically accessible (and locateable) website, or sent to the
 *     original author of this software.
 *
 *  3. Software modifications that do not alter the functionality of the
 *     software but are simply adaptations to a specific environment are
 *     exempt from clause 2.
 */

package uk.org.rivernile.edinburghbustracker.android.fragments.general;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.text.Spanned;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import java.util.HashMap;
import uk.org.rivernile.android.utils.GenericUtils;
import uk.org.rivernile.android.utils.SimpleCursorLoader;
import uk.org.rivernile.edinburghbustracker.android.BusStopDatabase;
import uk.org.rivernile.edinburghbustracker.android.DisplayStopDataActivity;
import uk.org.rivernile.edinburghbustracker.android.R;
import uk.org.rivernile.edinburghbustracker.android.SettingsDatabase;
import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs.DeleteFavouriteDialogFragment;

/**
 * This Fragment shows the user a list of their favourite bus stops. What this
 * Fragment does depends on the ARG_CREATE_SHORTCUT argument.
 * 
 * If ARG_CREATE_SHORTCUT is set to true;
 * 
 * - Do not show the context menu when the user long presses on a list item.
 * - When the user selects a list item, set the Activity result with an Intent
 * which sets the shortcut icon.
 * 
 * If ARG_CREATE_SHORTCUT is set to false;
 * 
 * - Allow the user to bring up a context menu when they long press on a list
 * item.
 * - When the user selects a list item, show the bus times for that bus stop.
 * 
 * @author Niall Scott
 */
public class FavouriteStopsFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<Cursor>, DeleteFavouriteDialogFragment.Callbacks, OnClickListener {

    /** The argument to signify create shortcut mode. */
    public static final String ARG_CREATE_SHORTCUT = "createShortcut";

    private Callbacks callbacks;
    private SimpleCursorAdapter ca;
    private SettingsDatabase sd;
    private boolean isCreateShortcut = false;
    private View progress, txtError;

    /**
     * Create a new instance of this Fragment, specifying whether it should be
     * in shortcuts mode, or favourites mode.
     * 
     * @param isCreateShortcut true if the user wants to add a shortcut, false
     * if not.
     * @return A new instance of this Fragment. 
     */
    public static FavouriteStopsFragment newInstance(final boolean isCreateShortcut) {
        final FavouriteStopsFragment f = new FavouriteStopsFragment();
        final Bundle b = new Bundle();
        b.putBoolean(ARG_CREATE_SHORTCUT, isCreateShortcut);
        f.setArguments(b);

        return f;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);

        try {
            callbacks = (Callbacks) activity;
        } catch (ClassCastException e) {
            throw new IllegalStateException(
                    activity.getClass().getName() + " does not implement " + Callbacks.class.getName());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Cache the Activity instance.
        final Activity activity = getActivity();

        // Get an instance of the SettingsDatabase.
        sd = SettingsDatabase.getInstance(activity.getApplicationContext());
        // Determine the mode this Fragment should be in.
        isCreateShortcut = getArguments().getBoolean(ARG_CREATE_SHORTCUT);
        // Create the ListAdapter.
        if (isCreateShortcut) {
            ca = new FavouritesCursorAdapter(activity, android.R.layout.simple_list_item_2, null,
                    new String[] { SettingsDatabase.FAVOURITE_STOPS_STOPNAME }, new int[] { android.R.id.text1 },
                    null);
        } else {
            ca = new FavouritesCursorAdapter(activity, R.layout.favouritestops_list_item, null,
                    new String[] { SettingsDatabase.FAVOURITE_STOPS_STOPNAME }, new int[] { android.R.id.text1 },
                    this);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
            final Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.favouritestops, container, false);

        progress = v.findViewById(R.id.progress);
        txtError = v.findViewById(R.id.txtError);

        return v;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onActivityCreated(final Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Set the list adapter.
        setListAdapter(ca);

        // What title is set depends on the mode.
        if (isCreateShortcut) {
            getActivity().setTitle(R.string.favouriteshortcut_title);
        } else {
            getActivity().setTitle(R.string.favouritestops_title);
            // Allow the context menu to be shown in normal mode.
            registerForContextMenu(getListView());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onResume() {
        super.onResume();

        // Create the Loader.
        getLoaderManager().restartLoader(0, null, this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onListItemClick(final ListView l, final View v, final int position, final long id) {
        // Get the stopCode and cache the Activity.
        final String stopCode = String.valueOf(id);
        final Activity activity = getActivity();
        Intent intent;

        // What happens when the user selects a list item depends on what mode
        // is active.
        if (isCreateShortcut) {
            // Set the Intent which is used when the shortcut is tapped.
            intent = new Intent(Intent.ACTION_MAIN);
            intent.setClass(activity, DisplayStopDataActivity.class);
            intent.setAction(DisplayStopDataActivity.ACTION_VIEW_STOP_DATA);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intent.putExtra(DisplayStopDataActivity.ARG_STOPCODE, stopCode);

            // Set the Activity result to send back to the launcher, which
            // contains a name, Intent and icon.
            final Intent result = new Intent();
            result.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent);
            result.putExtra(Intent.EXTRA_SHORTCUT_NAME, sd.getNameForStop(stopCode));
            final Parcelable icon = Intent.ShortcutIconResource.fromContext(activity, R.drawable.appicon_favourite);
            result.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);

            // Set the Activity result and exit.
            activity.setResult(Activity.RESULT_OK, result);
            activity.finish();
        } else {
            // View bus stop times.
            callbacks.onShowBusTimes(stopCode);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);

        // Get the menu inflater.
        final MenuInflater inflater = getActivity().getMenuInflater();
        final AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
        final Cursor c = (Cursor) ca.getItem(info.position);
        final String selectedStopCode;

        // Get the stopCode.
        if (c != null) {
            selectedStopCode = String.valueOf(c.getString(0));
        } else {
            selectedStopCode = "";
        }

        // Set the header title of the context menu.
        menu.setHeaderTitle(getString(R.string.busstop, sd.getNameForStop(selectedStopCode), selectedStopCode));
        // Inflate the menu from XML.
        inflater.inflate(R.menu.favouritestops_context_menu, menu);

        // Set the title of the proximity alert item depending whether one is
        // set or not.
        MenuItem item = menu.findItem(R.id.favouritestops_context_menu_prox_alert);

        if (sd.isActiveProximityAlert(selectedStopCode)) {
            item.setTitle(R.string.favouritestops_menu_prox_rem);
        } else {
            item.setTitle(R.string.favouritestops_menu_prox_add);
        }

        // Set the title of the time alert item depending whether one is set or
        // not.
        item = menu.findItem(R.id.favouritestops_context_menu_time_alert);

        if (sd.isActiveTimeAlert(selectedStopCode)) {
            item.setTitle(R.string.favouritestops_menu_time_rem);
        } else {
            item.setTitle(R.string.favouritestops_menu_time_add);
        }

        // If the Google Play Services is not available, then don't show the
        // option to show the stop on the map.
        item = menu.findItem(R.id.favouritestops_context_menu_showonmap);

        if (!GenericUtils.isGoogleMapsAvailable(getActivity())) {
            item.setVisible(false);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onContextItemSelected(final MenuItem item) {
        // Get the info so we can get the stopCode.
        final AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
        final Cursor c = (Cursor) ca.getItem(info.position);
        final String selectedStopCode;

        if (c != null) {
            selectedStopCode = c.getString(0);
        } else {
            selectedStopCode = "";
        }

        switch (item.getItemId()) {
        case R.id.favouritestops_context_menu_modify:
            // Allow the user to edit the name of the favourite stop.
            callbacks.onShowEditFavouriteStop(selectedStopCode);
            return true;
        case R.id.favouritestops_context_menu_delete:
            callbacks.onShowConfirmFavouriteDeletion(selectedStopCode);
            return true;
        case R.id.favouritestops_context_menu_showonmap:
            // Show the selected bus stop on the map.
            callbacks.onShowBusStopMapWithStopCode(selectedStopCode);
            return true;
        case R.id.favouritestops_context_menu_prox_alert:
            // Either show the Activity which allows the user to add a
            // proximity alert, or the DialogFragment to confirm the alert's
            // removal.
            if (sd.isActiveProximityAlert(selectedStopCode)) {
                callbacks.onShowConfirmDeleteProximityAlert();
            } else {
                callbacks.onShowAddProximityAlert(selectedStopCode);
            }

            return true;
        case R.id.favouritestops_context_menu_time_alert:
            // Either show the Activity which allows the user to add a time
            // alert, or the DialogFragment to confirm the alert's removal.
            if (sd.isActiveTimeAlert(selectedStopCode)) {
                callbacks.onShowConfirmDeleteTimeAlert();
            } else {
                callbacks.onShowAddTimeAlert(selectedStopCode);
            }

            return true;
        default:
            return super.onContextItemSelected(item);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Loader<Cursor> onCreateLoader(final int i, final Bundle bundle) {
        progress.setVisibility(View.VISIBLE);
        txtError.setVisibility(View.GONE);

        // Return the only Loader of this Fragment.
        return new FavouritesCursorLoader(getActivity());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoadFinished(final Loader<Cursor> loader, final Cursor c) {
        if (isAdded()) {
            // When loading is complete, swap the Cursor. The old Cursor is
            // automatically closed.
            ca.swapCursor(c);

            if (c == null || c.getCount() == 0) {
                progress.setVisibility(View.GONE);
                txtError.setVisibility(View.VISIBLE);
            }
        } else {
            if (c != null) {
                c.close();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoaderReset(final Loader<Cursor> loader) {
        // Give the adapter a null Cursor when the Loader is reset.
        ca.swapCursor(null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onConfirmFavouriteDeletion() {
        // When the user deletes a favourite bus stop, reload the Cursor.
        getLoaderManager().restartLoader(0, null, this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCancelFavouriteDeletion() {
        // Do nothing.
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onClick(final View v) {
        final int position = getListView().getPositionForView(v);
        if (position != AdapterView.INVALID_POSITION) {
            final Cursor c = ca.getCursor();
            if (c != null && c.moveToPosition(position)) {
                callbacks.onShowConfirmFavouriteDeletion(c.getString(0));
            }
        }
    }

    /**
     * This Loader loads the user's list of favourite bus stops.
     */
    public static class FavouritesCursorLoader extends SimpleCursorLoader {

        private final SettingsDatabase sd;

        /**
         * Create a new instance of this Loader.
         * 
         * @param context A Context object.
         */
        public FavouritesCursorLoader(final Context context) {
            super(context);

            sd = SettingsDatabase.getInstance(context.getApplicationContext());
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Cursor loadInBackground() {
            final Cursor c = sd.getAllFavouriteStops();

            // This ensure's the Cursor's window is set properly.
            if (c != null)
                c.getCount();

            return c;
        }
    }

    /**
     * This is a custom Cursor adapter to add a services list to the row being
     * displayed.
     */
    private class FavouritesCursorAdapter extends SimpleCursorAdapter {

        private BusStopDatabase bsd;
        private final HashMap<String, Spanned> serviceListings = new HashMap<String, Spanned>();
        private final OnClickListener starClickListener;
        private int hitboxSize;

        /**
         * Create a new FavouritesCursorAdapter.
         * 
         * @param context A Context instance.
         * @param layout The layout reference ID.
         * @param c The Cursor to populate the list from.
         * @param from An array of Strings to map to UI components.
         * @param to An array of resource IDs to map the from Strings to.
         * @param starClickListener The callback for when the star is clicked.
         */
        public FavouritesCursorAdapter(final Context context, final int layout, final Cursor c, final String[] from,
                final int[] to, final OnClickListener starClickListener) {
            super(context, layout, c, from, to);

            bsd = BusStopDatabase.getInstance(context.getApplicationContext());
            this.starClickListener = starClickListener;
            hitboxSize = context.getResources().getDimensionPixelOffset(R.dimen.star_hitbox_size);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
            final View v = super.newView(context, cursor, parent);
            final ImageButton imgbtnFavourite = (ImageButton) v.findViewById(R.id.imgbtnFavourite);
            if (imgbtnFavourite != null) {
                imgbtnFavourite.setFocusable(false);
                imgbtnFavourite.setFocusableInTouchMode(false);
                imgbtnFavourite.setOnClickListener(starClickListener);

                v.post(new Runnable() {
                    @Override
                    public void run() {
                        final Rect rect = new Rect();
                        imgbtnFavourite.getHitRect(rect);
                        // Assume it's a square
                        final int adjustBy = (int) ((hitboxSize - (rect.bottom - rect.top)) / 2);

                        if (adjustBy > 0) {
                            rect.top -= adjustBy;
                            rect.bottom += adjustBy;
                            rect.left -= adjustBy;
                            rect.right += adjustBy;
                        }

                        v.setTouchDelegate(new TouchDelegate(rect, imgbtnFavourite));
                    }
                });
            }

            return v;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void bindView(final View v, final Context context, final Cursor cursor) {
            super.bindView(v, context, cursor);

            final String stopCode = cursor.getString(0);
            final TextView services = (TextView) v.findViewById(android.R.id.text2);

            // Look to see if the service list is in the cache. If not, get it
            // from the database.
            Spanned s = serviceListings.get(stopCode);
            if (s == null) {
                s = BusStopDatabase.getColouredServiceListString(bsd.getBusServicesForStopAsString(stopCode));
                if (s != null) {
                    services.setText(s);
                    serviceListings.put(stopCode, s);
                }
            } else {
                services.setText(s);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Cursor swapCursor(final Cursor newCursor) {
            // If the Cursor is being swapped, clear the service listings cache.
            serviceListings.clear();

            return super.swapCursor(newCursor);
        }
    }

    /**
     * Any Activities which host this Fragment must implement this interface to
     * handle navigation events.
     */
    public static interface Callbacks {

        /**
         * This is called when it should be confirmed with the user that they
         * want to delete a favourite bus stop.
         * 
         * @param stopCode The bus stop that the user may want to delete.
         */
        public void onShowConfirmFavouriteDeletion(String stopCode);

        /**
         * This is called when it should be confirmed with the user that they
         * want to delete the proximity alert.
         */
        public void onShowConfirmDeleteProximityAlert();

        /**
         * This is called when it should be confirmed with the user that they
         * want to delete the time alert.
         */
        public void onShowConfirmDeleteTimeAlert();

        /**
         * This is called when the user wants to edit a favourite bus stop.
         * 
         * @param stopCode The stop code of the bus stop to add.
         */
        public void onShowEditFavouriteStop(String stopCode);

        /**
         * This is called when the user wants to view the interface to add a new
         * proximity alert.
         * 
         * @param stopCode The stopCode the proximity alert should be added for.
         */
        public void onShowAddProximityAlert(String stopCode);

        /**
         * This is called when the user wants to view the interface to add a new
         * time alert.
         * 
         * @param stopCode The stopCode the time alert should be added for.
         */
        public void onShowAddTimeAlert(String stopCode);

        /**
         * This is called when the user wants to view the bus stop map centered
         * on a specific bus stop.
         * 
         * @param stopCode The stopCode that the map should center on.
         */
        public void onShowBusStopMapWithStopCode(String stopCode);

        /**
         * This is called when the user wishes to view bus stop times.
         * 
         * @param stopCode The bus stop to view times for.
         */
        public void onShowBusTimes(String stopCode);
    }
}