/*
* Copyright (C) 2012 - 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.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnMapClickListener;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.MapsInitializer;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import java.text.NumberFormat;
import uk.org.rivernile.android.utils.LocationUtils;
import uk.org.rivernile.android.utils.SimpleCursorLoader;
import uk.org.rivernile.edinburghbustracker.android.BusStopDatabase;
import uk.org.rivernile.edinburghbustracker.android.PreferencesActivity;
import uk.org.rivernile.edinburghbustracker.android.R;
import uk.org.rivernile.edinburghbustracker.android.SettingsDatabase;
import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs
.DeleteFavouriteDialogFragment;
import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs
.DeleteProximityAlertDialogFragment;
import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs
.DeleteTimeAlertDialogFragment;
/**
* This Fragment shows details for a bus stop. The bus stop code is passed in
* as an argument to this Fragment. A Map is shown at the top of the Fragment
* if the Google Play Services are available, otherwise it is removed.
*
* @author Niall Scott
*/
public class BusStopDetailsFragment extends Fragment
implements LocationListener, SensorEventListener,
LoaderManager.LoaderCallbacks<Cursor>,
DeleteProximityAlertDialogFragment.Callbacks,
DeleteTimeAlertDialogFragment.Callbacks,
DeleteFavouriteDialogFragment.Callbacks {
private static final NumberFormat distanceFormat =
NumberFormat.getInstance();
/** This is the stopCode argument. */
public static final String ARG_STOPCODE = "stopCode";
private static final int REQUEST_PERIOD = 10000;
private static final float MIN_DISTANCE = 3.0f;
private static final float MAP_ZOOM = 15f;
private Callbacks callbacks;
private BusStopDatabase bsd;
private SettingsDatabase sd;
private LocationManager locMan;
private SensorManager sensMan;
private Sensor accelerometer;
private Sensor magnetometer;
private MapView mapView;
private GoogleMap map;
private ImageButton favouriteBtn;
private TextView txtName, txtServices, txtDistance, txtEmpty, txtProxAlert,
txtTimeAlert;
private View layoutContent, layoutDetails, progress;
private int hitboxSize;
private Location lastLocation;
private String stopCode;
private String stopName;
private double latitude;
private double longitude;
private int orientation;
private String locality;
private final float[] distance = new float[2];
private final float[] rotationMatrix = new float[9];
private final float[] headings = new float[3];
private float[] accelerometerValues;
private float[] magnetometerValues;
private GeomagneticField geoField;
private int screenRotation;
private Bitmap needleBitmap;
/**
* Get a new instance of this Fragment. A bus stop code must be supplied.
*
* @param stopCode The bus stop code to show details for.
* @return A new instance of this Fragment.
*/
public static BusStopDetailsFragment newInstance(final String stopCode) {
final BusStopDetailsFragment f = new BusStopDetailsFragment();
final Bundle b = new Bundle();
b.putString(ARG_STOPCODE, stopCode);
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);
final Context context = getActivity().getApplicationContext();
// Get the various resources and services.
bsd = BusStopDatabase.getInstance(context);
sd = SettingsDatabase.getInstance(context);
locMan = (LocationManager)context
.getSystemService(Context.LOCATION_SERVICE);
sensMan = (SensorManager)context
.getSystemService(Context.SENSOR_SERVICE);
accelerometer = sensMan.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magnetometer = sensMan.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
stopCode = getArguments().getString(ARG_STOPCODE);
hitboxSize = getResources()
.getDimensionPixelOffset(R.dimen.star_hitbox_size);
}
/**
* {@inheritDoc}
*/
@Override
public View onCreateView(final LayoutInflater inflater,
final ViewGroup container, final Bundle savedInstanceState) {
// This is the View that contains the root view.
final View v = inflater.inflate(R.layout.busstopdetails, container,
false);
// Get the MapView and send it the onCreate event.
mapView = (MapView)v.findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
layoutContent = v.findViewById(R.id.layoutContent);
layoutDetails = v.findViewById(R.id.layoutDetails);
progress = v.findViewById(R.id.progress);
txtEmpty = (TextView)v.findViewById(android.R.id.empty);
txtName = (TextView)v.findViewById(R.id.txtName);
txtServices = (TextView)v.findViewById(R.id.txtServices);
txtDistance = (TextView)v.findViewById(R.id.txtDistance);
txtProxAlert = (TextView)v.findViewById(R.id.txtProxAlert);
txtTimeAlert = (TextView)v.findViewById(R.id.txtTimeAlert);
Button b = (Button)v.findViewById(R.id.btnBusTimes);
b.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
// Show the bus times.
callbacks.onShowBusTimes(stopCode);
}
});
b = (Button)v.findViewById(R.id.btnStreetView);
b.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
// Show StreetView.
callbacks.onShowStreetView(latitude, longitude);
}
});
favouriteBtn = (ImageButton)v.findViewById(R.id.imgbtnFavourite);
favouriteBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
// Add/remove as favourite.
if(sd.getFavouriteStopExists(stopCode)) {
callbacks.onShowConfirmFavouriteDeletion(stopCode);
} else {
callbacks.onShowAddFavouriteStop(stopCode,
locality != null ?
stopName + ", " + locality : stopName);
}
}
});
txtProxAlert.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
if(sd.isActiveProximityAlert(stopCode)) {
callbacks.onShowConfirmDeleteProximityAlert();
} else {
callbacks.onShowAddProximityAlert(stopCode);
}
}
});
txtTimeAlert.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
if(sd.isActiveTimeAlert(stopCode)) {
callbacks.onShowConfirmDeleteTimeAlert();
} else {
callbacks.onShowAddTimeAlert(stopCode);
}
}
});
return v;
}
/**
* {@inheritDoc}
*/
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Get the screen rotation. This will be needed later to remap the
// coordinate system.
screenRotation = getActivity().getWindowManager().getDefaultDisplay()
.getRotation();
// Initialise the lastLocation to the best known location.
lastLocation = LocationUtils.getBestInitialLocation(locMan);
updateLocation();
map = mapView.getMap();
// The Map can be null if Google Play Services is not available.
if(map != null) {
map.getUiSettings().setMyLocationButtonEnabled(false);
map.setOnMapClickListener(new OnMapClickListener() {
@Override
public void onMapClick(final LatLng point) {
callbacks.onShowBusStopMapWithStopCode(stopCode);
}
});
} else {
// If the MapView is null, then hide it.
mapView.setVisibility(View.GONE);
}
// The loader is restarted each time incase a new bus stop database has
// become available.
getLoaderManager().restartLoader(0, null, this);
}
/**
* {@inheritDoc}
*/
@Override
public void onResume() {
super.onResume();
// Feed back life cycle events back to the MapView.
mapView.onResume();
// Start the location providers if they are enabled.
if(locMan.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
REQUEST_PERIOD, MIN_DISTANCE, this);
}
if(locMan.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER,
REQUEST_PERIOD, MIN_DISTANCE, this);
}
// Start the accelerometer and magnetometer.
startOrientationSensors();
// Show the user's location on the map if we can.
if(map != null) {
map.setMyLocationEnabled(
getActivity().getSharedPreferences(
PreferencesActivity.PREF_FILE, 0)
.getBoolean(PreferencesActivity.PREF_AUTO_LOCATION,
true));
}
if(sd.getFavouriteStopExists(stopCode)) {
favouriteBtn.setImageResource(R.drawable.ic_list_favourite);
favouriteBtn.setContentDescription(
getString(R.string.favourite_rem));
} else {
favouriteBtn.setImageResource(R.drawable.ic_list_unfavourite);
favouriteBtn.setContentDescription(
getString(R.string.favourite_add));
}
if(sd.isActiveProximityAlert(stopCode)) {
txtProxAlert.setText(R.string.busstopdetails_prox_rem);
} else {
txtProxAlert.setText(R.string.busstopdetails_prox_add);
}
if(sd.isActiveTimeAlert(stopCode)) {
txtTimeAlert.setText(R.string.busstopdetails_time_rem);
} else {
txtTimeAlert.setText(R.string.busstopdetails_time_add);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onPause() {
super.onPause();
// Feed back life cycle events back to the MapView.
mapView.onPause();
// When the Activity is being paused, cancel location updates.
locMan.removeUpdates(this);
// ...and Sensor updates.
sensMan.unregisterListener(this);
}
/**
* {@inheritDoc}
*/
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
// Feed back life cycle events back to the MapView.
mapView.onSaveInstanceState(outState);
}
/**
* {@inheritDoc}
*/
@Override
public void onDestroy() {
super.onDestroy();
// Feed back life cycle events back to the MapView.
mapView.onDestroy();
}
/**
* {@inheritDoc}
*/
@Override
public void onLowMemory() {
super.onLowMemory();
// Feed back life cycle events back to the MapView.
mapView.onLowMemory();
}
/**
* {@inheritDoc}
*/
@Override
public Loader<Cursor> onCreateLoader(final int i, final Bundle bundle) {
// Show the progress indicator.
progress.setVisibility(View.VISIBLE);
txtEmpty.setVisibility(View.GONE);
layoutContent.setVisibility(View.GONE);
return new BusStopDetailsLoader(getActivity(), stopCode);
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor c) {
if (isAdded()) {
populateData(c);
} else {
if (c != null) {
c.close();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
// Nothing to do here.
}
/**
* {@inheritDoc}
*/
@Override
public void onLocationChanged(final Location location) {
if(LocationUtils.isBetterLocation(location, lastLocation)) {
lastLocation = location;
// Calculate the GeomagneticField of the current location. This is
// used by the direction indicator.
if(location != null) {
geoField = new GeomagneticField(
Double.valueOf(location.getLatitude()).floatValue(),
Double.valueOf(location.getLongitude()).floatValue(),
Double.valueOf(location.getAltitude()).floatValue(),
System.currentTimeMillis());
}
updateLocation();
}
}
/**
* {@inheritDoc}
*/
@Override
public void onStatusChanged(final String provider, final int status,
final Bundle extras) {
// Nothing to do here.
}
/**
* {@inheritDoc}
*/
@Override
public void onProviderEnabled(final String provider) {
if (LocationManager.GPS_PROVIDER.equals(provider) ||
LocationManager.NETWORK_PROVIDER.equals(provider)) {
locMan.requestLocationUpdates(provider, REQUEST_PERIOD,
MIN_DISTANCE, this);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onProviderDisabled(final String provider) {
// Nothing to do here.
}
/**
* {@inheritDoc}
*/
@Override
public void onSensorChanged(final SensorEvent event) {
final float[] values = event.values;
switch(event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
// If there's no accelerometer values yet, create the values
// array.
if(accelerometerValues == null) {
accelerometerValues = new float[3];
accelerometerValues[0] = values[0];
accelerometerValues[1] = values[1];
accelerometerValues[2] = values[2];
} else {
smoothValues(accelerometerValues, values);
}
break;
case Sensor.TYPE_MAGNETIC_FIELD:
// If there's no magnetometer values yet, create the values
// array.
if(magnetometerValues == null) {
magnetometerValues = new float[3];
magnetometerValues[0] = values[0];
magnetometerValues[1] = values[1];
magnetometerValues[2] = values[2];
} else {
smoothValues(magnetometerValues, values);
}
break;
default:
return;
}
// Make sure the needle is pointing the correct direction.
updateDirectionNeedle();
}
/**
* {@inheritDoc}
*/
@Override
public void onAccuracyChanged(final Sensor sensor, final int accuracy) {
// Nothing to do here.
}
/**
* {@inheritDoc}
*/
@Override
public void onConfirmFavouriteDeletion() {
favouriteBtn.setImageResource(R.drawable.ic_list_unfavourite);
favouriteBtn.setContentDescription(getString(R.string.favourite_add));
}
/**
* {@inheritDoc}
*/
@Override
public void onCancelFavouriteDeletion() {
// Nothing to do.
}
/**
* {@inheritDoc}
*/
@Override
public void onConfirmProximityAlertDeletion() {
txtProxAlert.setText(R.string.busstopdetails_prox_add);
}
/**
* {@inheritDoc}
*/
@Override
public void onCancelProximityAlertDeletion() {
// Nothing to do.
}
/**
* {@inheritDoc}
*/
@Override
public void onConfirmTimeAlertDeletion() {
txtTimeAlert.setText(R.string.busstopdetails_time_add);
}
/**
* {@inheritDoc}
*/
@Override
public void onCancelTimeAlertDeletion() {
// Nothing to do.
}
/**
* This is called when the Loader has finished. It moves the data out of the
* Cursor and in to local fields.
*
* @param c The returned Cursor.
*/
private void populateData(final Cursor c) {
if(c == null) {
showLoadFailedError();
return;
}
if(c.moveToNext()) {
stopName = c.getString(2);
latitude = c.getDouble(3);
longitude = c.getDouble(4);
orientation = c.getInt(5);
locality = c.getString(6);
c.close();
populateView();
} else {
c.close();
showLoadFailedError();
}
}
/**
* Using the data in the local fields of this object, populate the various
* views with data.
*/
private void populateView() {
layoutContent.setVisibility(View.VISIBLE);
txtEmpty.setVisibility(View.GONE);
progress.setVisibility(View.GONE);
layoutDetails.post(new Runnable() {
@Override
public void run() {
final Rect rect = new Rect();
favouriteBtn.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;
}
layoutDetails.setTouchDelegate(new TouchDelegate(rect,
favouriteBtn));
}
});
setMapLocation();
final String name;
if(locality != null) {
name = getString(R.string.busstop_locality_coloured, stopName,
locality, stopCode);
} else {
name = getString(R.string.busstop_coloured, stopName, stopCode);
}
txtName.setText(Html.fromHtml(name));
// Set the services list text.
final String services = bsd.getBusServicesForStopAsString(stopCode);
if(services == null || services.length() == 0) {
txtServices.setText(R.string.busstopdetails_noservices);
} else {
txtServices.setText(BusStopDatabase.getColouredServiceListString(
services));
}
updateLocation();
}
/**
* Set the location of the map.
*/
private void setMapLocation() {
if(map != null) {
// This is a bit of a hack to stop the map crashing the app. The
// CameraUpdateFactory depends on being initialised before use. It
// seems to work well when the Fragment is first run, but if the app
// is killed then later resumed with this Fragment being what is
// returned to, the app crashes. This code forces initialisation to
// happen.
try {
MapsInitializer.initialize(getActivity());
} catch(GooglePlayServicesNotAvailableException e) {
return;
}
// Move the camera to the position of the bus stop marker.
final LatLng position = new LatLng(latitude, longitude);
map.moveCamera(CameraUpdateFactory.newLatLngZoom(position,
MAP_ZOOM));
// Create new MarkerOptions. Not draggable.
final MarkerOptions mo = new MarkerOptions();
mo.draggable(false);
mo.position(position);
// Select the marker icon to use on the map.
switch(orientation) {
case 0:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_n));
break;
case 1:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_ne));
break;
case 2:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_e));
break;
case 3:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_se));
break;
case 4:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_s));
break;
case 5:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_sw));
break;
case 6:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_w));
break;
case 7:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker_nw));
break;
default:
mo.icon(BitmapDescriptorFactory.fromResource(
R.drawable.mapmarker));
break;
}
// Add the marker to the map.
map.addMarker(mo);
}
}
/**
* This method is called when there is a problem loading the bus stop or the
* bus stop does not exist in the database.
*/
private void showLoadFailedError() {
layoutContent.setVisibility(View.GONE);
txtEmpty.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
}
/**
* This method is called when the device detects a new location.
*/
private void updateLocation() {
if(lastLocation == null || stopCode == null) {
// Make sure there's no distance text or direction needle.
txtDistance.setText("");
txtDistance.setCompoundDrawablesWithIntrinsicBounds(null, null,
null, null);
recycleNeedleBitmapIfNotNull(null);
return;
}
// Get the distance between the device's location and the bus stop.
Location.distanceBetween(lastLocation.getLatitude(),
lastLocation.getLongitude(), latitude, longitude, distance);
// Units depends on distance.
if(distance[0] > 1000f) {
distanceFormat.setMaximumFractionDigits(1);
distanceFormat.setParseIntegerOnly(false);
txtDistance.setText(distanceFormat.format(distance[0] / 1000) +
" km");
} else {
distanceFormat.setMaximumFractionDigits(0);
distanceFormat.setParseIntegerOnly(true);
txtDistance.setText(distanceFormat.format(distance[0]) + " m");
}
// There's a new location, the direction needle needs to be updated.
updateDirectionNeedle();
}
/**
* Update the direction needle so that it is pointing towards the bus stop,
* based on the device location and the direction it is facing.
*/
private void updateDirectionNeedle() {
// We need values for location, the accelerometer and magnetometer to
// continue.
if(lastLocation == null || accelerometerValues == null ||
magnetometerValues == null) {
// Make sure the needle isn't showing.
txtDistance.setCompoundDrawablesWithIntrinsicBounds(null, null,
null, null);
recycleNeedleBitmapIfNotNull(null);
return;
}
// Calculating the rotation matrix may fail, for example, if the device
// is in freefall. In that case we cannot continue as the values will
// be unreliable.
if(!SensorManager.getRotationMatrix(rotationMatrix, null,
accelerometerValues, magnetometerValues)) {
return;
}
// The screen rotation was obtained earlier.
switch(screenRotation) {
// There's lots of information about this elsewhere, but briefly;
// The values from the sensors are in the device's coordinate system
// which may be correct if the device is in its natural orientation,
// but it needs to be remapped if the device is rotated.
case Surface.ROTATION_0:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_X, SensorManager.AXIS_Z,
rotationMatrix);
break;
case Surface.ROTATION_90:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_Z, SensorManager.AXIS_MINUS_X,
rotationMatrix);
break;
case Surface.ROTATION_180:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Z,
rotationMatrix);
break;
case Surface.ROTATION_270:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_MINUS_Z, SensorManager.AXIS_X,
rotationMatrix);
break;
}
// Get the X, Y and Z orientations, which are in radians. Covert this
// in to degrees East of North.
SensorManager.getOrientation(rotationMatrix, headings);
double heading = Math.toDegrees(headings[0]);
// If there's a GeomagneticField value, then adjust the heading to take
// this in to account.
if(geoField != null) {
heading -= geoField.getDeclination();
}
// The orientation is in the range of -180 to +180. Convert this in to
// a range of 0 to 360.
final float bearingTo = distance[1] < 0 ?
distance[1] + 360 : distance[1];
// This is the heading to the bus stop.
heading = bearingTo - heading;
// The above calculation may come out as a negative number again. Put
// this back in to the range of 0 to 360.
if(heading < 0) {
heading += 360;
}
// This 'if' statement is required to prevent a crash during device
// rotation. It ensured that the Fragment is still part of the Activity.
if(isAdded()) {
// Get the arrow bitmap from the resources.
final Bitmap needleIn = BitmapFactory.decodeResource(getResources(),
R.drawable.heading_arrow);
// Get an identity matrix and rotate it by the required amount.
final Matrix m = new Matrix();
m.setRotate((float)heading % 360, (float)needleIn.getWidth() / 2,
(float)needleIn.getHeight() / 2);
// Apply the rotation matrix to the Bitmap, to create a new Bitmap.
final Bitmap needleOut = Bitmap.createBitmap(needleIn, 0, 0,
needleIn.getWidth(), needleIn.getHeight(), m, true);
// Recycle the needle read in if it's not the same as the rotated
// needle.
if (needleIn != needleOut) {
needleIn.recycle();
}
// This Bitmap needs to be converted to a Drawable type.
final BitmapDrawable drawable = new BitmapDrawable(getResources(),
needleOut);
// Set the new needle to be on the right hand side of the TextView.
txtDistance.setCompoundDrawablesWithIntrinsicBounds(null, null,
drawable, null);
recycleNeedleBitmapIfNotNull(needleOut);
} else {
// If the Fragment is not added to the Activity, then make sure
// there's no needle.
txtDistance.setCompoundDrawablesWithIntrinsicBounds(null, null,
null, null);
recycleNeedleBitmapIfNotNull(null);
}
}
/**
* Start the sensors used to measure device magnetic orientation.
*/
private void startOrientationSensors() {
// Only use the sensors when both the magnetometer and the accelerometer
// are available.
if(magnetometer != null && accelerometer != null) {
sensMan.registerListener(this, magnetometer,
SensorManager.SENSOR_DELAY_NORMAL);
sensMan.registerListener(this, accelerometer,
SensorManager.SENSOR_DELAY_NORMAL);
}
}
/**
* This method is used to smooth the values coming from the accelerometer
* and the magnetometer. This stops the needle from jumping around.
*
* @param oldValues The existing values.
* @param newValues The new values from the sensor.
*/
private static void smoothValues(final float[] oldValues,
final float[] newValues) {
// If there's no old or new values, then return.
if(oldValues == null || newValues == null) {
return;
}
// Loop through the arrays, smoothing the values.
final int len = oldValues.length;
for(int i = 0; i < len; i++) {
oldValues[i] += 0.2f * (newValues[i] - oldValues[i]);
}
}
/**
* Recycle the needle Bitmap if it's no longer in use.
*
* @param newNeedle The new needle Bitmap. Can be null.
*/
private void recycleNeedleBitmapIfNotNull(final Bitmap newNeedle) {
if (needleBitmap != null) {
needleBitmap.recycle();
}
needleBitmap = newNeedle;
}
/**
* This static class is what loads the bus stop details.
*/
private static class BusStopDetailsLoader extends SimpleCursorLoader {
private final BusStopDatabase bsd;
private final String stopCode;
/**
* Create a new BusStopDetailsLoader, specifying the stopCode.
*
* @param context A Context instance.
* @param stopCode The stopCode to load.
*/
public BusStopDetailsLoader(final Context context,
final String stopCode) {
super(context);
bsd = BusStopDatabase.getInstance(context.getApplicationContext());
this.stopCode = stopCode;
}
/**
* {@inheritDoc}
*/
@Override
public Cursor loadInBackground() {
final Cursor c = bsd.getBusStopByCode(stopCode);
// This ensures the Cursor window is set properly.
if (c != null) {
c.getCount();
}
return c;
}
}
/**
* 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 wishes to view bus stop times.
*
* @param stopCode The bus stop to view times for.
*/
public void onShowBusTimes(String stopCode);
/**
* This is called when the user wishes to see a location on Street View.
*
* @param latitude The latitude of the location to show.
* @param longitude The longitude of the location to show.
*/
public void onShowStreetView(double latitude, double longitude);
/**
* 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 wants to add a new favourite bus stop.
*
* @param stopCode The stop code of the bus stop to add.
* @param stopName The default name to use for the bus stop.
*/
public void onShowAddFavouriteStop(String stopCode, String stopName);
}
}