Java tutorial
/** This file is part of Personal Trainer. Personal Trainer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Personal Trainer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Personal Trainer. If not, see <http://www.gnu.org/licenses/>. (C) Copyright 2012: Daniel Kvist, Henrik Hugo, Gustaf Werlinder, Patrik Thitusson, Markus Schutzer */ package se.team05.activity; import java.util.ArrayList; import java.util.List; import se.team05.R; import se.team05.content.ParcelableGeoPoint; import se.team05.content.Result; import se.team05.content.Route; import se.team05.content.Track; import se.team05.data.DatabaseHandler; import se.team05.dialog.AlertDialogFactory; import se.team05.dialog.EditCheckPointDialog; import se.team05.dialog.SaveRouteDialog; import se.team05.listener.MapLocationListener; import se.team05.listener.MapOnGestureListener; import se.team05.listener.RouteActivityButtonListener; import se.team05.overlay.CheckPoint; import se.team05.overlay.CheckPointOverlay; import se.team05.overlay.RouteOverlay; import se.team05.util.Utils; import se.team05.view.EditRouteMapView; import android.app.Activity; import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.BitmapFactory; import android.location.Criteria; import android.location.LocationManager; import android.os.Bundle; import android.os.PowerManager.WakeLock; import android.support.v4.app.NavUtils; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapActivity; import com.google.android.maps.MyLocationOverlay; import com.google.android.maps.Overlay; /** * The main use of this activity and of this application in general is that the * user is supposed to run a route of his or her choice and then be able to save * it. As the main idea is that a user will run a route, we will from here on * refer to this action as "running" or "exercise". * * This activity Presents a map to the user. In the main menu the user gets the * choice of running a new route or an existing one he/she has saved from * earlier and thusly this activity will serve both functions. If the user * chooses to run a new run he/she will be presented with a map and the * possibility to record a new route. Recording will paint the path that the * user undertakes, as well as time distance and speed. The user can also place * checkpoints, which the user can use to activate music or sound at a given * location. Both checkpoints and paths is represented by geopoints. After * completion, the possibility to save this route will appear and the user gets * transferred to the start screen. If the user chooses an old route instead, * the old one will be painted in grey at the start and a new path in blue will * gradually get painted as the user moves along. When the user is done he or * she will instead be presented with the possibility to save the result * * @author Markus Schutzer, Patrik Thituson, Daniel Kvist */ public class RouteActivity extends MapActivity implements EditCheckPointDialog.Callbacks, SaveRouteDialog.Callbacks, CheckPointOverlay.Callbacks, MapOnGestureListener.Callbacks, MapLocationListener.Callbacks, Utils.Callbacks, RouteActivityButtonListener.Callbacks { private static final int USER_ROUTE_COLOR = 78; private static final int RECORDED_ROUTE_COLOR = 10; private static final String TAG = "Personal trainer"; private static final String BUNDLE_ACTIVE_DIALOG = "activeDialog"; private static final String BUNDLE_SAVE_RESULT_CHECKED = "isSaveResultChecked"; private static final String BUNDLE_STARTED = "started"; private static final int DIALOG_NONE = -1; private static final int DIALOG_SAVE_ROUTE = 0; private static final int DIALOG_SAVE_RESULT = 1; private static final int DIALOG_CHECKPOINT = 2; private static final int NOTIFICATION_ID = 2; private List<Overlay> overlays; private Route route; private WakeLock wakeLock; private MapLocationListener mapLocationListener; private LocationManager locationManager; private EditRouteMapView mapView; private MyLocationOverlay myLocationOverlay; private CheckPointOverlay checkPointOverlay; private EditCheckPointDialog checkPointDialog; private SaveRouteDialog saveRouteDialog; private AlertDialog saveResultDialog; private DatabaseHandler databaseHandler; private CheckPoint currentCheckPoint; private Button stopAndSaveButton; private Button startButton; private TextView speedView; private TextView distanceView; private TextView timeView; private TextView calorieView; /** * Will present a map to the user and will also display a dot representing * the user's location. Also contains three buttons of which one * (startRunButton) will start the recording of the user's movement and will * paint the track accordingly on the map. The button stopAndSaveButton will * finish the run and save it. As of now it is recorded in the memory but * later it will have database functionality. The button addCheckPointButton * will place a checkpoint at the user's current location. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_route); getActionBar().setDisplayHomeAsUpEnabled(true); databaseHandler = new DatabaseHandler(this); route = new Route(getString(R.string.new_route), getString(R.string.route_description), this); wakeLock = Utils.acquireWakeLock(this); checkPointOverlay = new CheckPointOverlay(getResources().getDrawable(R.drawable.ic_launcher), this); setupMapView(); route.setId(getIntent().getLongExtra(Route.EXTRA_ID, -1)); if (!route.isNewRoute()) { route = databaseHandler.getRoute(route.getId()); route.setCheckPoints(databaseHandler.getCheckPoints(route.getId())); } setTitle(route.getName()); } /** * This method restores the instance after a configuration change has * happen. Important data is saved in the OnSavedInstanceState and contains * more data if a dialog is shown. * * @param savedInstanceState */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); route = savedInstanceState.getParcelable("route"); RouteOverlay routeOverlay3 = new RouteOverlay(route.getGeoPoints(), USER_ROUTE_COLOR); overlays.add(routeOverlay3); checkPointOverlay = new CheckPointOverlay(getResources().getDrawable(R.drawable.ic_launcher), this); checkPointOverlay.setCheckPoints(route.getCheckPoints()); int activeDialog = savedInstanceState.getInt(BUNDLE_ACTIVE_DIALOG); switch (activeDialog) { case DIALOG_CHECKPOINT: currentCheckPoint = savedInstanceState.getParcelable("currentCheckPoint"); //checkPointOverlay.addCheckPoint(currentCheckPoint); showCheckPointDialog(currentCheckPoint, EditCheckPointDialog.MODE_EDIT); break; case DIALOG_SAVE_ROUTE: showSaveRouteDialog(); saveRouteDialog.setSaveResultChecked(savedInstanceState.getBoolean(BUNDLE_SAVE_RESULT_CHECKED)); break; case DIALOG_SAVE_RESULT: showSaveResultDialog(route.getId()); break; } } /** * Sets up the mapview and fetches the overlays that are used to draw on the * map. */ private void setupMapView() { mapView = (EditRouteMapView) findViewById(R.id.mapview); mapView.setBuiltInZoomControls(true); mapView.setOnGestureListener(new MapOnGestureListener(this)); overlays = mapView.getOverlays(); } /** * Sets up the checkpoint overlay and draws the route that the user has * recorded. */ private void setupRouteAndCheckPoints() { if (!route.isNewRoute()) { RouteOverlay recordedRouteOverlay = new RouteOverlay(route.getGeoPoints(), RECORDED_ROUTE_COLOR); overlays.add(recordedRouteOverlay); checkPointOverlay.setCheckPoints(route.getCheckPoints()); } overlays.add(checkPointOverlay); } /** * Initiates the textviews and buttons in the view and adds listeners to * them. */ private void setupTextViewsAndButtons() { RouteActivityButtonListener clickListener = new RouteActivityButtonListener(this); distanceView = (TextView) findViewById(R.id.show_distance_textview); speedView = (TextView) findViewById(R.id.show_speed_textview); timeView = (TextView) findViewById(R.id.show_time_textview); calorieView = (TextView) findViewById(R.id.show_calorie_textview); Button addCheckPointButton = (Button) findViewById(R.id.add_checkpoint); Button showResultButton = (Button) findViewById(R.id.show_result_button); stopAndSaveButton = (Button) findViewById(R.id.stop_button); startButton = (Button) findViewById(R.id.start_button); stopAndSaveButton.setOnClickListener(clickListener); startButton.setOnClickListener(clickListener); if (route.isNewRoute()) { addCheckPointButton.setOnClickListener(clickListener); } else { showResultButton.setOnClickListener(clickListener); showResultButton.setVisibility(View.VISIBLE); addCheckPointButton.setVisibility(View.GONE); } boolean started = getIntent().getBooleanExtra(BUNDLE_STARTED, false); if (started) { startButton.setVisibility(View.GONE); stopAndSaveButton.setVisibility(View.VISIBLE); route.setStarted(true); } } /** * Sets up the location manager, location lsitener and some criteria that * ask for the gps provider (fine). Then add overlay for my location and the * trace of the user's route. */ private void setupMyLocationAndListener() { locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); mapLocationListener = new MapLocationListener(this, route.isNewRoute(), route.getCheckPoints()); locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mapLocationListener); Criteria criteria = new Criteria(); criteria.setAccuracy(Criteria.ACCURACY_FINE); criteria.setCostAllowed(false); String providerName = locationManager.getBestProvider(criteria, true); if (providerName != null) { Log.d(TAG, getString(R.string.provider_) + providerName); } myLocationOverlay = new MyLocationOverlay(this, mapView); overlays.add(myLocationOverlay); RouteOverlay userRouteOverlay = new RouteOverlay(route.getGeoPoints(), USER_ROUTE_COLOR); overlays.add(userRouteOverlay); } /** * Starts a new alert dialog which shows the distance and time and asks the * user if the results should be saved or not. * * @param rid */ private void showSaveResultDialog(long rid) { Result result = new Result(rid, System.currentTimeMillis() / 1000, route.getTimePassed(), (int) route.getTotalDistance(), 0); saveResultDialog = AlertDialogFactory.newSaveResultDialog(this, route, result); saveResultDialog.show(); route.setStarted(false); } /** * This is called when the user has selected a media from the media * selection activity. A list of tracks is then passed back as a result * which this method then saves into the database. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == MediaSelectorActivity.REQUEST_MEDIA && resultCode == RESULT_OK) { ArrayList<Track> selectedTracks = data .getParcelableArrayListExtra(MediaSelectorActivity.EXTRA_SELECTED_ITEMS); currentCheckPoint.addTracks(selectedTracks); } } /** * Unused method, must implement this because of MapActivity inheritance. * Might be implemented in a later stage. */ @Override protected boolean isRouteDisplayed() { return false; } /** * This will be called when user changes location. It will create a new * GeoPoint consisting of longitude and latitude represented by integers and * add it to the route. It will also get the user's speed and total distance * traveled and convert this data into strings to be presented on the * screen. As of now this method will be called once every three seconds, * this number is a tradeoff between fast updates which would be needed for * doing fast paced activities like cycling and slower like walking. Slow * activities could do with a lesser update interval and as such preserve * battery life but as of this version the user does not have the * possibility to choose what kind of activity to undertake and thus the * value is hard coded. * * @param location * the new location of the user */ @Override public void onLocationChanged(ParcelableGeoPoint geoPoint, String userSpeed, String userDistance, float totalDistance) { if (route.isStarted()) { route.getGeoPoints().add(geoPoint); route.setTotalDistance(totalDistance, this); speedView.setText(userSpeed); distanceView.setText(userDistance + getString(R.string.km)); calorieView.setText(String.valueOf(route.getCalories()) + getString(R.string.kcal)); mapView.postInvalidate(); } } /** * When our activity resumes, we want to register for location updates. */ protected void onResume() { super.onResume(); setupRouteAndCheckPoints(); setupTextViewsAndButtons(); setupMyLocationAndListener(); if (route.isStarted()) { onStartRouteClick(); } myLocationOverlay.enableMyLocation(); myLocationOverlay.runOnFirstFix(new Runnable() { @Override public void run() { mapView.getController().animateTo(myLocationOverlay.getMyLocation()); } }); mapView.getController().setZoom(17); mapView.postInvalidate(); } /** * When our activity pauses, we want to remove listening for location * updates */ protected void onPause() { super.onPause(); myLocationOverlay.disableMyLocation(); } /** * Starts the timer for a new route and change the button Start to * StopAndSave */ @Override public void onStartRouteClick() { initNotification(); wakeLock = Utils.acquireWakeLock(this); route.setStarted(true); startButton.setVisibility(View.GONE); stopAndSaveButton.setVisibility(View.VISIBLE); Utils.startTimer(this); } /** * Stops the route and timer and toggles the buttons. Also releases the wake * lock. */ @Override public void onStopRouteClick() { Utils.stopTimer(); route.setStarted(false); startButton.setVisibility(View.VISIBLE); stopAndSaveButton.setVisibility(View.GONE); wakeLock = Utils.releaseWakeLock(); if (!route.isNewRoute()) { mapLocationListener.stopService(); showSaveResultDialog(route.getId()); } else { showSaveRouteDialog(); } cancelNotification(); } /** * Shows the create checkpoint dialog and passes in the current geo point if * my location is enabled. */ @Override public void onAddCheckPointClick() { if (myLocationOverlay.isMyLocationEnabled()) { GeoPoint geoPoint = myLocationOverlay.getMyLocation(); if (geoPoint != null) { createCheckPoint(geoPoint); } } } /** * Launches the activity which shows a list of results for this route. */ @Override public void onShowResultClick() { Intent intent = new Intent(this, ListExistingResultsActivity.class); intent.putExtra(Route.EXTRA_ID, route.getId()); startActivity(intent); } /** * Creates a new result and initates the saveRouteDialog */ private void showSaveRouteDialog() { saveRouteDialog = new SaveRouteDialog(this, this, route); saveRouteDialog.show(); } /** * Method that gets called to update the UI with how much time that has * passed and presents this to the user. Will use field timePassed to * determine time while not altering the timePassed variable if we want to * pass that value to the database. */ @Override public void onTimerTick() { timeView.setText(route.getTimePassedAsString()); route.setTimePassed(route.getTimePassed() + 1); } /** * This method deletes the checkpoint with all its tracks from the database. * It also calls on the checkpoint overlay to delete its current selected * checkpoint from the view. We then need to invalidate the map view for it * to update. */ @Override public void onDeleteCheckPoint(long checkPointId) { route.removeCheckPoint(checkPointId); databaseHandler.deleteCheckPoint(checkPointId); databaseHandler.deleteTracksByCid(checkPointId); checkPointOverlay.deleteCheckPoint(); mapView.postInvalidate(); } /** * Callback from the checkpoint dialog that tells the activity that the user * has pressed the save button and thus the checkpoint with all its data * should now be saved into the route. If the checkPoint exists it updates * the checkPoint. */ @Override public void onSaveCheckPoint(CheckPoint checkPoint) { route.addCheckPoint(checkPoint); long cid = checkPoint.getId(); if (cid > 0) { databaseHandler.updateCheckPoint(checkPoint); } } /** * When a checkpoint is tapped this method calls the showCheckPointDialog * method with MODE_EDIT which marks it as a edit dialog. This method also * sets the current checkpoint to the last tapped. */ @Override public void onCheckPointTap(CheckPoint checkPoint) { currentCheckPoint = checkPoint; showCheckPointDialog(checkPoint, EditCheckPointDialog.MODE_EDIT); } /** * Initiates a new checkpoint dialog * * @param checkPoint * @param mode */ private void showCheckPointDialog(CheckPoint checkPoint, int mode) { checkPointDialog = new EditCheckPointDialog(this, checkPoint, mode); checkPointDialog.show(); } /** * Creates a checkpoint with the geopoint and adds it to the checkpoint * overlay, it also calls showCheckPointDialog with the MODE_ADD which marks * it as a add dialog. This method also saves the newly created checkpoint * as the current checkpoint for future reference. * * @param geoPoint */ private void createCheckPoint(GeoPoint geoPoint) { ParcelableGeoPoint parcelableGeoPoint = new ParcelableGeoPoint(geoPoint.getLatitudeE6(), geoPoint.getLongitudeE6()); CheckPoint checkPoint = new CheckPoint(parcelableGeoPoint); currentCheckPoint = checkPoint; checkPointOverlay.addCheckPoint(checkPoint); showCheckPointDialog(checkPoint, EditCheckPointDialog.MODE_ADD); mapView.postInvalidate(); } /** * The onTap method zooms in on double tap and creates a geopoint on single * tap which it sends to createCheckPoint */ @Override public void onTap(int x, int y, int eventType) { switch (eventType) { case MapOnGestureListener.EVENT_DOUBLE_TAP: mapView.getController().zoomInFixing(x, y); break; case MapOnGestureListener.EVENT_SINGLE_TAP: if ((checkPointDialog == null || !checkPointDialog.isShowing()) && route.isNewRoute()) { GeoPoint geoPoint = mapView.getProjection().fromPixels(x, y); createCheckPoint(geoPoint); } break; } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } /** * This method is called when an item in the action bar (options menu) has * been pressed. Currently this only takes the user to the parent activity * (main activity). */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: if (route.isStarted()) { AlertDialogFactory.newConfirmBackDialog(this).show(); } else { NavUtils.navigateUpFromSameTask(this); } return true; case R.id.settings: if (route.isStarted()) { AlertDialogFactory .newAlertMessageDialog(this, getString(R.string.stop_route), getString(R.string.you_must_stop_your_route_before_you_can_enter_the_settings)) .show(); } else { launchActivity(SettingsActivity.class); } return true; } return super.onOptionsItemSelected(item); } /** * This method is called from the save geoPointList dialog when the user has * pressed the "save" button. It creates a new geoPointList with the * information given in the dialog and then saves it to the database. After * that, the user is taken back to the main activity. */ @Override public void onSaveRoute(Route route, boolean saveResult) { route.setId(databaseHandler.saveRoute(route)); if (saveResult) { Result result = new Result(route.getId(), (int) System.currentTimeMillis() / 1000, route.getTimePassed(), (int) route.getTotalDistance(), route.getCalories()); result.setRid(route.getId()); result.setTimestamp(System.currentTimeMillis() / 1000); databaseHandler.saveResult(result); } databaseHandler.saveGeoPoints(route.getId(), route.getGeoPoints()); for (CheckPoint checkPoint : route.getCheckPoints()) { checkPoint.setRid(route.getId()); checkPoint.setId(databaseHandler.saveCheckPoint(checkPoint)); for (Track track : checkPoint.getTracks()) { databaseHandler.saveTrack(checkPoint.getId(), track); } } launchActivity(MainActivity.class); } /** * Called when a route is being dismissed. This method just launches the * main activity. */ @Override public void onDismissRoute() { databaseHandler.deleteRoute(route); databaseHandler.deleteCheckPoints(route.getId()); launchActivity(MainActivity.class); } /** * Called by the system when the activity is shut down completely. Releases * the wake lock and stops listening for location updates. */ @Override public void onDestroy() { wakeLock = Utils.releaseWakeLock(); locationManager.removeUpdates(mapLocationListener); super.onDestroy(); } /** * Private helper method to launch the passed in activity. */ private void launchActivity(Class<? extends Activity> c) { Intent intent = new Intent(this, c); this.startActivity(intent); } /** * Saves all important data to be able to handle configuration changes. if a * dialog is shown it saves some extra fields to be able to restore the * dialog with its properties and fields */ @Override protected void onSaveInstanceState(final Bundle outState) { if (route.isStarted()) { Utils.stopTimer(); } int activeDialog = getActiveDialog(); outState.putParcelable("route", route); outState.putInt(BUNDLE_ACTIVE_DIALOG, activeDialog); if (activeDialog == DIALOG_CHECKPOINT) { checkPointDialog.stopRecording(); currentCheckPoint = checkPointDialog.getCheckPoint(); outState.putParcelable("currentCheckPoint", currentCheckPoint); } else if (activeDialog == DIALOG_SAVE_ROUTE) { route = saveRouteDialog.getRoute(); outState.putBoolean(BUNDLE_SAVE_RESULT_CHECKED, saveRouteDialog.isSaveResultChecked()); } } /** * Private helper method to assist in calculating which, if any, dialog is * shown * * @return the dialog shown constant */ private int getActiveDialog() { int activeDialog = DIALOG_NONE; if (checkPointDialog != null && checkPointDialog.isShowing()) { activeDialog = DIALOG_CHECKPOINT; } if (saveRouteDialog != null && saveRouteDialog.isShowing()) { activeDialog = DIALOG_SAVE_ROUTE; } if (saveResultDialog != null && saveResultDialog.isShowing()) { activeDialog = DIALOG_SAVE_RESULT; } return activeDialog; } /** * Shows a alert dialog when back button is pressed to confirm that the user * wants to discard the route. This is implemented to prevent the user to * hit the back button by mistake and quit the route. */ @Override public void onBackPressed() { if (route.isStarted()) { AlertDialogFactory.newConfirmBackDialog(this).show(); } else { super.onBackPressed(); } } /** * Initiates a notification with the application launcher icon as a graphic * and custom messages for the ticker, title and text. * */ private void initNotification() { Context context = getApplicationContext(); Intent notificationIntent = new Intent(context, RouteActivity.class); notificationIntent.putExtra(BUNDLE_STARTED, true); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); Resources resources = context.getResources(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context); builder.setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher)) .setTicker(getString(R.string.your_route_is_being_recorded)).setWhen(System.currentTimeMillis()) .setOngoing(true).setContentTitle(getString(R.string.your_route_is_being_recorded)) .setContentText(getString(R.string.click_here_to_enter_application_and_finish_it)); Notification notification = builder.getNotification(); notificationManager.notify(NOTIFICATION_ID, notification); } /** * Cancels the notification and removes it from the notification area. */ public void cancelNotification() { NotificationManager notificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE); notificationManager.cancel(NOTIFICATION_ID); } }