/* * 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); } }