diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 6b255feb1..4e258a5fd 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -147,17 +147,6 @@ limitations under the License. - - - - - - - - - - - - - - @@ -211,17 +189,6 @@ limitations under the License. - - - - - - - - - - - - - - - - - diff --git a/MyTracks/res/drawable/left_arrow_stats_stateful.xml b/MyTracks/res/drawable/left_arrow_stats_stateful.xml deleted file mode 100644 index 5b3169af4..000000000 --- a/MyTracks/res/drawable/left_arrow_stats_stateful.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/MyTracks/res/drawable/left_arrow_track_stateful.xml b/MyTracks/res/drawable/left_arrow_track_stateful.xml deleted file mode 100644 index 3ae27009c..000000000 --- a/MyTracks/res/drawable/left_arrow_track_stateful.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/MyTracks/res/drawable/right_arrow_chart_stateful.xml b/MyTracks/res/drawable/right_arrow_chart_stateful.xml deleted file mode 100644 index eeb6b33f8..000000000 --- a/MyTracks/res/drawable/right_arrow_chart_stateful.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/MyTracks/res/drawable/right_arrow_stats_stateful.xml b/MyTracks/res/drawable/right_arrow_stats_stateful.xml deleted file mode 100644 index 3ade4f429..000000000 --- a/MyTracks/res/drawable/right_arrow_stats_stateful.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/MyTracks/res/drawable/right_arrow_track_stateful.xml b/MyTracks/res/drawable/right_arrow_track_stateful.xml deleted file mode 100644 index 9e5c364fd..000000000 --- a/MyTracks/res/drawable/right_arrow_track_stateful.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/MyTracks/res/layout/track_detail.xml b/MyTracks/res/layout/track_detail.xml new file mode 100644 index 000000000..1226d9ec0 --- /dev/null +++ b/MyTracks/res/layout/track_detail.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/MyTracks/res/values/navigation_icons.xml b/MyTracks/res/values/navigation_icons.xml deleted file mode 100644 index e641291c9..000000000 --- a/MyTracks/res/values/navigation_icons.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - @drawable/left_arrow_chart_stateful - @drawable/left_arrow_track_stateful - @drawable/left_arrow_stats_stateful - - - - @drawable/right_arrow_stats_stateful - @drawable/right_arrow_chart_stateful - @drawable/right_arrow_track_stateful - - diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 0445af499..89e65a436 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -2303,6 +2303,10 @@ limitations under the License. + + Chart + @@ -2314,6 +2318,14 @@ limitations under the License. Android Market."> You need Google Earth to play the track. Do you want to install from the Market? + + Map + + + Stats + Track diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java deleted file mode 100644 index 265f6e4d8..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ /dev/null @@ -1,486 +0,0 @@ -/* - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.ChartView.Mode; -import com.google.android.apps.mytracks.content.MyTracksLocation; -import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackDataHub; -import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; -import com.google.android.apps.mytracks.content.TrackDataListener; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.stats.DoubleBuffer; -import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; -import com.google.common.annotations.VisibleForTesting; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.location.Location; -import android.os.Bundle; -import android.speech.tts.TextToSpeech; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.widget.CheckBox; -import android.widget.LinearLayout; -import android.widget.RadioGroup; -import android.widget.ZoomControls; - -import java.util.ArrayList; -import java.util.EnumSet; - -/** - * An activity that displays a chart from the track point provider. - * - * @author Sandor Dornbush - * @author Rodrigo Damazio - */ -public class ChartActivity extends Activity implements TrackDataListener { - - private static final int DIALOG_CHART_SETTINGS_ID = 0; - - private final DoubleBuffer elevationBuffer = - new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - private final DoubleBuffer speedBuffer = - new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); - private final ArrayList pendingPoints = new ArrayList(); - - private TrackDataHub dataHub; - - // Stats gathered from received data. - private double profileLength = 0; - private long startTime = -1; - private Location lastLocation; - private double trackMaxSpeed; - - // Modes of operation - private boolean metricUnits = true; - private boolean reportSpeed = true; - - /* - * UI elements: - */ - private ChartView chartView; - private LinearLayout busyPane; - private ZoomControls zoomControls; - - /** - * A runnable that can be posted to the UI thread. It will remove the spinner - * (if any), enable/disable zoom controls and orange pointer as appropriate - * and redraw. - */ - private final Runnable updateChart = new Runnable() { - @Override - public void run() { - // Get a local reference in case it's set to null concurrently with this. - TrackDataHub localDataHub = dataHub; - if (localDataHub == null || isFinishing()) { - return; - } - - busyPane.setVisibility(View.GONE); - zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); - zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); - chartView.setShowPointer(localDataHub.isRecordingSelected()); - chartView.invalidate(); - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - Log.w(TAG, "ChartActivity.onCreate"); - super.onCreate(savedInstanceState); - - // The volume we want to control is the Text-To-Speech volume - setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); - - setContentView(R.layout.mytracks_charts); - ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart); - chartView = new ChartView(this); - LayoutParams params = - new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); - layout.addView(chartView, params); - - busyPane = (LinearLayout) findViewById(R.id.elevation_busypane); - zoomControls = (ZoomControls) findViewById(R.id.elevation_zoom); - zoomControls.setOnZoomInClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - zoomIn(); - } - }); - zoomControls.setOnZoomOutClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - zoomOut(); - } - }); - } - - @Override - protected void onResume() { - super.onResume(); - - dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); - dataHub.registerTrackDataListener(this, EnumSet.of( - ListenerDataType.SELECTED_TRACK_CHANGED, - ListenerDataType.TRACK_UPDATES, - ListenerDataType.POINT_UPDATES, - ListenerDataType.SAMPLED_OUT_POINT_UPDATES, - ListenerDataType.WAYPOINT_UPDATES, - ListenerDataType.DISPLAY_PREFERENCES)); - } - - @Override - protected void onPause() { - dataHub.unregisterTrackDataListener(this); - dataHub = null; - - super.onPause(); - } - - private void zoomIn() { - chartView.zoomIn(); - zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); - zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); - } - - private void zoomOut() { - chartView.zoomOut(); - zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); - zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); - } - - private void setMode(Mode newMode) { - if (chartView.getMode() != newMode) { - chartView.setMode(newMode); - dataHub.reloadDataForListener(this); - } - } - - @Override - protected Dialog onCreateDialog(int id) { - if (id != DIALOG_CHART_SETTINGS_ID) { - return null; - } - - View view = getLayoutInflater().inflate(R.layout.chart_settings, null); - final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.chart_settings_x); - radioGroup.check(chartView.getMode() == Mode.BY_DISTANCE - ? R.id.chart_settings_by_distance : R.id.chart_settings_by_time); - - final CheckBox[] checkBoxes = new CheckBox[ChartView.NUM_SERIES]; - checkBoxes[ChartView.ELEVATION_SERIES] = (CheckBox) view.findViewById( - R.id.chart_settings_elevation); - checkBoxes[ChartView.SPEED_SERIES] = (CheckBox) view.findViewById(R.id.chart_settings_speed); - checkBoxes[ChartView.POWER_SERIES] = (CheckBox) view.findViewById(R.id.chart_settings_power); - checkBoxes[ChartView.CADENCE_SERIES] = (CheckBox) view.findViewById( - R.id.chart_settings_cadence); - checkBoxes[ChartView.HEART_RATE_SERIES] = (CheckBox) view.findViewById( - R.id.chart_settings_heart_rate); - - // set checkboxes values - for (int i = 0; i < ChartView.NUM_SERIES; i++) { - checkBoxes[i].setChecked(chartView.isChartValueSeriesEnabled(i)); - } - checkBoxes[ChartView.SPEED_SERIES].setText(reportSpeed - ? R.string.stat_speed : R.string.stat_pace); - - return new AlertDialog.Builder(this) - .setCancelable(true) - .setNegativeButton(R.string.generic_cancel, null) - .setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - setMode(radioGroup.getCheckedRadioButtonId() == R.id.chart_settings_by_distance - ? Mode.BY_DISTANCE - : Mode.BY_TIME); - for (int i = 0; i < ChartView.NUM_SERIES; i++) { - chartView.setChartValueSeriesEnabled(i, checkBoxes[i].isChecked()); - } - chartView.postInvalidate(); - } - }) - .setTitle(R.string.menu_chart_settings) - .setView(view) - .create(); - } - - /** - * Shows the chart settings dialog - */ - public void showChartSettingsDialog() { - showDialog(DIALOG_CHART_SETTINGS_ID); - } - - /** - * Given a location, creates a new data point for the chart. A data point is - * an array double[3 or 6], where: - * data[0] = the time or distance - * data[1] = the elevation - * data[2] = the speed - * and possibly: - * data[3] = power - * data[4] = cadence - * data[5] = heart rate - * - * This must be called in order for each point. - * - * @param location the location to get data for (this method takes ownership of that location) - * @param result the resulting point to fill out - */ - @VisibleForTesting - void fillDataPoint(Location location, double result[]) { - double timeOrDistance = Double.NaN, - elevation = Double.NaN, - speed = Double.NaN, - power = Double.NaN, - cadence = Double.NaN, - heartRate = Double.NaN; - - if (location instanceof MyTracksLocation && - ((MyTracksLocation) location).getSensorDataSet() != null) { - SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet(); - if (sensorData.hasPower() - && sensorData.getPower().getState() == Sensor.SensorState.SENDING - && sensorData.getPower().hasValue()) { - power = sensorData.getPower().getValue(); - } - if (sensorData.hasCadence() - && sensorData.getCadence().getState() == Sensor.SensorState.SENDING - && sensorData.getCadence().hasValue()) { - cadence = sensorData.getCadence().getValue(); - } - if (sensorData.hasHeartRate() - && sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING - && sensorData.getHeartRate().hasValue()) { - heartRate = sensorData.getHeartRate().getValue(); - } - } - - // TODO: Account for segment splits? - Mode mode = chartView.getMode(); - switch (mode) { - case BY_DISTANCE: - if (lastLocation != null) { - double d = lastLocation.distanceTo(location); - if (metricUnits) { - profileLength += d; - } else { - profileLength += d * UnitConversions.KM_TO_MI; - } - } - timeOrDistance = profileLength * UnitConversions.M_TO_KM; - break; - case BY_TIME: - if (startTime == -1) { - // Base case - startTime = location.getTime(); - } - timeOrDistance = (location.getTime() - startTime); - break; - default: - Log.w(TAG, "ChartActivity unknown mode: " + mode); - } - - elevationBuffer.setNext(metricUnits - ? location.getAltitude() - : location.getAltitude() * UnitConversions.M_TO_FT); - elevation = elevationBuffer.getAverage(); - - if (lastLocation == null) { - if (Math.abs(location.getSpeed() - 128) > 1) { - speedBuffer.setNext(location.getSpeed()); - } - } else if (TripStatisticsBuilder.isValidSpeed( - location.getTime(), location.getSpeed(), lastLocation.getTime(), - lastLocation.getSpeed(), speedBuffer) - && (location.getSpeed() <= trackMaxSpeed)) { - speedBuffer.setNext(location.getSpeed()); - } - speed = speedBuffer.getAverage() * UnitConversions.MS_TO_KMH; - if (!metricUnits) { - speed *= UnitConversions.KM_TO_MI; - } - if (!reportSpeed) { - if (speed != 0) { - // Format as hours per unit - speed = (60.0 / speed); - } else { - speed = Double.NaN; - } - } - - // Keep a copy so the location can be reused. - lastLocation = location; - - if (result != null) { - result[0] = timeOrDistance; - result[1] = elevation; - result[2] = speed; - result[3] = power; - result[4] = cadence; - result[5] = heartRate; - } - } - - @Override - public void onProviderStateChange(ProviderState state) { - // We don't care. - } - - @Override - public void onCurrentLocationChanged(Location loc) { - // We don't care. - } - - @Override - public void onCurrentHeadingChanged(double heading) { - // We don't care. - } - - @Override - public void onSelectedTrackChanged(Track track, boolean isRecording) { - runOnUiThread(new Runnable() { - @Override - public void run() { - busyPane.setVisibility(View.VISIBLE); - } - }); - } - - @Override - public void onTrackUpdated(Track track) { - if (track == null || track.getStatistics() == null) { - trackMaxSpeed = 0.0; - return; - } - - trackMaxSpeed = track.getStatistics().getMaxSpeed(); - } - - @Override - public void clearTrackPoints() { - profileLength = 0; - lastLocation = null; - startTime = -1; - elevationBuffer.reset(); - chartView.reset(); - speedBuffer.reset(); - pendingPoints.clear(); - - runOnUiThread(new Runnable() { - @Override - public void run() { - chartView.resetScroll(); - } - }); - } - - @Override - public void onNewTrackPoint(Location loc) { - if (LocationUtils.isValidLocation(loc)) { - double[] point = new double[6]; - fillDataPoint(loc, point); - pendingPoints.add(point); - } - } - - @Override - public void onSampledOutTrackPoint(Location loc) { - // Still account for the point in the smoothing buffers. - fillDataPoint(loc, null); - } - - @Override - public void onSegmentSplit() { - // Do nothing. - } - - @Override - public void onNewTrackPointsDone() { - chartView.addDataPoints(pendingPoints); - pendingPoints.clear(); - runOnUiThread(updateChart); - } - - @Override - public void clearWaypoints() { - chartView.clearWaypoints(); - } - - @Override - public void onNewWaypoint(Waypoint wpt) { - chartView.addWaypoint(wpt); - } - - @Override - public void onNewWaypointsDone() { - runOnUiThread(updateChart); - } - - @Override - public boolean onUnitsChanged(boolean metric) { - if (metricUnits == metric) { - return false; - } - metricUnits = metric; - chartView.setMetricUnits(metricUnits); - runOnUiThread(new Runnable() { - @Override - public void run() { - chartView.requestLayout(); - } - }); - return true; - } - - @Override - public boolean onReportSpeedChanged(boolean speed) { - if (reportSpeed == speed) { - return false; - } - reportSpeed = speed; - chartView.setReportSpeed(speed, this); - runOnUiThread(new Runnable() { - @Override - public void run() { - chartView.requestLayout(); - } - }); - return true; - } - - @VisibleForTesting - ChartView getChartView() { - return chartView; - } - - @VisibleForTesting - void setTrackMaxSpeed(double maxSpeed) { - trackMaxSpeed = maxSpeed; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartFragment.java b/MyTracks/src/com/google/android/apps/mytracks/ChartFragment.java new file mode 100644 index 000000000..cc46dc13d --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartFragment.java @@ -0,0 +1,518 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks; + +import com.google.android.apps.mytracks.ChartView.Mode; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.stats.DoubleBuffer; +import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; + +import android.location.Location; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; +import android.widget.ZoomControls; + +import java.util.ArrayList; +import java.util.EnumSet; + +/** + * A fragment to display track chart to the user. + * + * @author Sandor Dornbush + * @author Rodrigo Damazio + */ +public class ChartFragment extends Fragment implements TrackDataListener { + + // Android reports 128 when the speed is invalid + private static final int INVALID_SPEED = 128; + private final DoubleBuffer elevationBuffer = new DoubleBuffer( + Constants.ELEVATION_SMOOTHING_FACTOR); + private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); + private final ArrayList pendingPoints = new ArrayList(); + + private TrackDataHub trackDataHub; + + // Stats gathered from the received data + private double totalDistance = 0.0; + private long startTime = -1L; + private Location lastLocation = null; + private double trackMaxSpeed = 0.0; + + // Modes of operation + private boolean metricUnits = true; + private boolean reportSpeed = true; + + // UI elements + private ChartView chartView; + private LinearLayout busyPane; + private ZoomControls zoomControls; + + /** + * A runnable that will remove the spinner (if any), enable/disable zoom + * controls and orange pointer as appropriate and redraw. + */ + private final Runnable updateChart = new Runnable() { + @Override + public void run() { + if (trackDataHub == null) { + return; + } + + busyPane.setVisibility(View.GONE); + zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); + zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); + chartView.setShowPointer(isRecording()); + chartView.invalidate(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + /* + * Create a chartView here to store data thus won't need to reload all the + * data on every onStart or onResume. + */ + chartView = new ChartView(getActivity()); + }; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.mytracks_charts, container, false); + busyPane = (LinearLayout) view.findViewById(R.id.elevation_busypane); + zoomControls = (ZoomControls) view.findViewById(R.id.elevation_zoom); + zoomControls.setOnZoomInClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + zoomIn(); + } + }); + zoomControls.setOnZoomOutClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + zoomOut(); + } + }); + return view; + } + + @Override + public void onStart() { + super.onStart(); + ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.elevation_chart); + LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + layout.addView(chartView, layoutParams); + } + + @Override + public void onResume() { + super.onResume(); + resumeTrackDataHub(); + getActivity().runOnUiThread(updateChart); + } + + @Override + public void onPause() { + super.onPause(); + pauseTrackDataHub(); + } + + @Override + public void onStop() { + super.onStop(); + ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.elevation_chart); + layout.removeView(chartView); + } + + /** + * Sets the chart view mode. + * + * @param mode the chart view mode + */ + public void setMode(Mode mode) { + if (chartView.getMode() != mode) { + chartView.setMode(mode); + reloadTrackDataHub(); + } + } + + /** + * Gets the chart view mode. + */ + public Mode getMode() { + return chartView.getMode(); + } + + /** + * Enables or disables the chart value series. + * + * @param index the index of the series + * @param enabled true to enable, false to disable + */ + public void setChartValueSeriesEnabled(int index, boolean enabled) { + chartView.setChartValueSeriesEnabled(index, enabled); + } + + /** + * Returns true if the chart value series is enabled. + * + * @param index the index of the series + */ + public boolean isChartValueSeriesEnabled(int index) { + return chartView.isChartValueSeriesEnabled(index); + } + + /** + * Returns true to report speed instead of pace. + */ + public boolean isReportSpeed() { + return reportSpeed; + } + + /** + * Updates the chart. + */ + public void update() { + chartView.postInvalidate(); + } + + @Override + public void onProviderStateChange(ProviderState state) { + // We don't care. + } + + @Override + public void onCurrentLocationChanged(Location loc) { + // We don't care. + } + + @Override + public void onCurrentHeadingChanged(double heading) { + // We don't care. + } + + @Override + public void onSelectedTrackChanged(Track track, boolean isRecording) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + busyPane.setVisibility(View.VISIBLE); + } + }); + } + + @Override + public void onTrackUpdated(Track track) { + if (track == null || track.getStatistics() == null) { + trackMaxSpeed = 0.0; + return; + } + trackMaxSpeed = track.getStatistics().getMaxSpeed(); + } + + @Override + public void clearTrackPoints() { + totalDistance = 0.0; + startTime = -1L; + lastLocation = null; + + elevationBuffer.reset(); + speedBuffer.reset(); + pendingPoints.clear(); + + chartView.reset(); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + chartView.resetScroll(); + } + }); + } + + @Override + public void onNewTrackPoint(Location location) { + if (LocationUtils.isValidLocation(location)) { + double[] data = new double[6]; + fillDataPoint(location, data); + pendingPoints.add(data); + } + } + + @Override + public void onSampledOutTrackPoint(Location location) { + if (LocationUtils.isValidLocation(location)) { + // Still account for the point in the smoothing buffers. + fillDataPoint(location, null); + } + } + + @Override + public void onSegmentSplit() { + // Do nothing. + } + + @Override + public void onNewTrackPointsDone() { + chartView.addDataPoints(pendingPoints); + pendingPoints.clear(); + getActivity().runOnUiThread(updateChart); + } + + @Override + public void clearWaypoints() { + chartView.clearWaypoints(); + } + + @Override + public void onNewWaypoint(Waypoint waypoint) { + if (waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) { + chartView.addWaypoint(waypoint); + } + } + + @Override + public void onNewWaypointsDone() { + getActivity().runOnUiThread(updateChart); + } + + @Override + public boolean onUnitsChanged(boolean metric) { + if (metricUnits == metric) { + return false; + } + metricUnits = metric; + chartView.setMetricUnits(metricUnits); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + chartView.requestLayout(); + } + }); + return true; + } + + @Override + public boolean onReportSpeedChanged(boolean speed) { + if (reportSpeed == speed) { + return false; + } + reportSpeed = speed; + chartView.setReportSpeed(speed, getActivity()); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + chartView.requestLayout(); + } + }); + return true; + } + + /** + * Resumes the trackDataHub. Needs to be synchronized because trackDataHub can be + * accessed by multiple threads. + */ + private synchronized void resumeTrackDataHub() { + trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub.registerTrackDataListener(this, EnumSet.of( + ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.TRACK_UPDATES, + ListenerDataType.WAYPOINT_UPDATES, + ListenerDataType.POINT_UPDATES, + ListenerDataType.SAMPLED_OUT_POINT_UPDATES, + ListenerDataType.DISPLAY_PREFERENCES)); + } + + /** + * Pauses the trackDataHub. Needs to be synchronized because trackDataHub can be + * accessed by multiple threads. + */ + private synchronized void pauseTrackDataHub() { + trackDataHub.unregisterTrackDataListener(this); + trackDataHub = null; + } + + /** + * Returns true if recording. Needs to be synchronized because trackDataHub + * can be accessed by multiple threads. + */ + private synchronized boolean isRecording() { + return trackDataHub != null && trackDataHub.isRecordingSelected(); + } + + /** + * Reloads the trackDataHub. Needs to be synchronized because trackDataHub can be + * accessed by multiple threads. + */ + private synchronized void reloadTrackDataHub() { + if (trackDataHub != null) { + trackDataHub.reloadDataForListener(this); + } + } + + /** + * To zoom in. + */ + private void zoomIn() { + chartView.zoomIn(); + zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); + zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); + } + + /** + * To zoom out. + */ + private void zoomOut() { + chartView.zoomOut(); + zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); + zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); + } + + /** + * Given a location, fill in a data point, an array of double[6].
+ * data[0] = time/distance
+ * data[1] = elevation
+ * data[2] = speed
+ * data[3] = power
+ * data[4] = cadence
+ * data[5] = heart rate
+ * + * @param location the location + * @param data the data point to fill in, can be null + */ + @VisibleForTesting + void fillDataPoint(Location location, double data[]) { + double timeOrDistance = Double.NaN; + double elevation = Double.NaN; + double speed = Double.NaN; + double power = Double.NaN; + double cadence = Double.NaN; + double heartRate = Double.NaN; + + // TODO: Use TripStatisticsBuilder + if (chartView.getMode() == Mode.BY_DISTANCE) { + if (lastLocation != null) { + double distance = lastLocation.distanceTo(location) * UnitConversions.M_TO_KM; + if (metricUnits) { + totalDistance += distance; + } else { + totalDistance += distance * UnitConversions.KM_TO_MI; + } + } + timeOrDistance = totalDistance; + } else { + if (startTime == -1L) { + startTime = location.getTime(); + } + timeOrDistance = location.getTime() - startTime; + } + + elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() + * UnitConversions.M_TO_FT); + elevation = elevationBuffer.getAverage(); + + if (lastLocation == null) { + if (Math.abs(location.getSpeed() - INVALID_SPEED) > 1) { + speedBuffer.setNext(location.getSpeed()); + } + } else if (TripStatisticsBuilder.isValidSpeed(location.getTime(), location.getSpeed(), + lastLocation.getTime(), lastLocation.getSpeed(), speedBuffer) + && (location.getSpeed() <= trackMaxSpeed)) { + speedBuffer.setNext(location.getSpeed()); + } + speed = speedBuffer.getAverage() * UnitConversions.MS_TO_KMH; + if (!metricUnits) { + speed *= UnitConversions.KM_TO_MI; + } + if (!reportSpeed) { + speed = speed == 0 ? 0.0 : 60.0 / speed; + } + + if (location instanceof MyTracksLocation + && ((MyTracksLocation) location).getSensorDataSet() != null) { + SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet(); + if (sensorDataSet.hasPower() && sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING + && sensorDataSet.getPower().hasValue()) { + power = sensorDataSet.getPower().getValue(); + } + if (sensorDataSet.hasCadence() + && sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING + && sensorDataSet.getCadence().hasValue()) { + cadence = sensorDataSet.getCadence().getValue(); + } + if (sensorDataSet.hasHeartRate() + && sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING + && sensorDataSet.getHeartRate().hasValue()) { + heartRate = sensorDataSet.getHeartRate().getValue(); + } + } + + if (data != null) { + data[0] = timeOrDistance; + data[1] = elevation; + data[2] = speed; + data[3] = power; + data[4] = cadence; + data[5] = heartRate; + } + lastLocation = location; + } + + @VisibleForTesting + ChartView getChartView() { + return chartView; + } + + @VisibleForTesting + void setChartView(ChartView view) { + chartView = view; + } + + @VisibleForTesting + void setTrackMaxSpeed(double value) { + trackMaxSpeed = value; + } + + @VisibleForTesting + void setMetricUnits(boolean value) { + metricUnits = value; + } + + @VisibleForTesting + void setReportSpeed(boolean value) { + reportSpeed = value; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java deleted file mode 100644 index 1be7597b7..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java +++ /dev/null @@ -1,558 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackDataHub; -import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; -import com.google.android.apps.mytracks.content.TrackDataListener; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.GeoRect; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.maps.GeoPoint; -import com.google.android.maps.MapController; -import com.google.android.maps.MapView; -import com.google.android.maps.mytracks.R; - -import android.content.Intent; -import android.location.Location; -import android.os.Bundle; -import android.provider.Settings; -import android.speech.tts.TextToSpeech; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.EnumSet; - -/** - * The map view activity of the MyTracks application. - * - * @author Leif Hendrik Wilden - * @author Rodrigo Damazio - */ -public class MapActivity extends com.google.android.maps.MapActivity - implements View.OnTouchListener, View.OnClickListener, - TrackDataListener { - - // Saved instance state keys: - // --------------------------- - - private static final String KEY_CURRENT_LOCATION = "currentLocation"; - private static final String KEY_KEEP_MY_LOCATION_VISIBLE = "keepMyLocationVisible"; - - private TrackDataHub dataHub; - - /** - * True if the map should be scrolled so that the pointer is always in the - * visible area. - */ - private boolean keepMyLocationVisible; - - /** - * True if we've already zoomed into the current location. - * Only relevant when {@link #keepMyLocationVisible} is true. - */ - private boolean myLocationWasZoomedIn; - - /** - * The ID of a track on which we want to show a waypoint. - * The waypoint will be shown as soon as the track is loaded. - */ - private long showWaypointTrackId; - - /** - * The ID of a waypoint which we want to show. - * The waypoint will be shown as soon as its track is loaded. - */ - private long showWaypointId; - - /** - * The track that's currently selected. - * This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after - * actual track data has been received. - */ - private long selectedTrackId; - - /** - * The current pointer location. - * This is kept to quickly center on it when the user requests. - */ - private Location currentLocation; - - // UI elements: - // ------------- - - private RelativeLayout screen; - private MapView mapView; - private MapOverlay mapOverlay; - private LinearLayout messagePane; - private TextView messageText; - private LinearLayout busyPane; - - /** - * We are not displaying driving directions. Just an arbitrary track that is - * not associated to any licensed mapping data. Therefore it should be okay to - * return false here and still comply with the terms of service. - */ - @Override - protected boolean isRouteDisplayed() { - return false; - } - - /** - * We are displaying a location. This needs to return true in order to comply - * with the terms of service. - */ - @Override - protected boolean isLocationDisplayed() { - return true; - } - - // Application life cycle: - // ------------------------ - - @Override - protected void onCreate(Bundle bundle) { - Log.d(TAG, "MapActivity.onCreate"); - super.onCreate(bundle); - - // The volume we want to control is the Text-To-Speech volume - setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); - - // Inflate the layout: - setContentView(R.layout.mytracks_layout); - - // Remove the window's background because the MapView will obscure it - getWindow().setBackgroundDrawable(null); - - // Set up a map overlay: - screen = (RelativeLayout) findViewById(R.id.screen); - mapView = (MapView) findViewById(R.id.map); - mapView.requestFocus(); - mapOverlay = new MapOverlay(this); - mapView.getOverlays().add(mapOverlay); - mapView.setOnTouchListener(this); - mapView.setBuiltInZoomControls(true); - messagePane = (LinearLayout) findViewById(R.id.messagepane); - messageText = (TextView) findViewById(R.id.messagetext); - busyPane = (LinearLayout) findViewById(R.id.busypane); - } - - @Override - protected void onRestoreInstanceState(Bundle bundle) { - Log.d(TAG, "MapActivity.onRestoreInstanceState"); - if (bundle != null) { - super.onRestoreInstanceState(bundle); - keepMyLocationVisible = - bundle.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false); - if (bundle.containsKey(KEY_CURRENT_LOCATION)) { - currentLocation = (Location) bundle.getParcelable(KEY_CURRENT_LOCATION); - if (currentLocation != null) { - showCurrentLocation(); - } - } else { - currentLocation = null; - } - } - } - - @Override - protected void onResume() { - Log.d(TAG, "MapActivity.onResume"); - super.onResume(); - - dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); - dataHub.registerTrackDataListener(this, EnumSet.of( - ListenerDataType.SELECTED_TRACK_CHANGED, - ListenerDataType.POINT_UPDATES, - ListenerDataType.WAYPOINT_UPDATES, - ListenerDataType.LOCATION_UPDATES, - ListenerDataType.COMPASS_UPDATES)); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - Log.d(TAG, "MapActivity.onSaveInstanceState"); - outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible); - if (currentLocation != null) { - outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation); - } - super.onSaveInstanceState(outState); - } - - @Override - protected void onPause() { - Log.d(TAG, "MapActivity.onPause"); - - dataHub.unregisterTrackDataListener(this); - dataHub = null; - - super.onPause(); - } - - /** - * Tests if a location is visible. - * - * @param location a given location - * @return true if the given location is within the visible map area - */ - private boolean locationIsVisible(Location location) { - if (location == null || mapView == null) { - return false; - } - GeoPoint center = mapView.getMapCenter(); - int latSpan = mapView.getLatitudeSpan(); - int lonSpan = mapView.getLongitudeSpan(); - - // Bottom of map view is obscured by zoom controls/buttons. - // Subtract a margin from the visible area: - GeoPoint marginBottom = mapView.getProjection().fromPixels( - 0, mapView.getHeight()); - GeoPoint marginTop = mapView.getProjection().fromPixels(0, - mapView.getHeight() - - mapView.getZoomButtonsController().getZoomControls().getHeight()); - int margin = - Math.abs(marginTop.getLatitudeE6() - marginBottom.getLatitudeE6()); - GeoRect r = new GeoRect(center, latSpan, lonSpan); - r.top += margin; - - GeoPoint geoPoint = LocationUtils.getGeoPoint(location); - return r.contains(geoPoint); - } - - /** - * Centers (and keeps centered) the map on the current location. - */ - public void showMyLocation() { - dataHub.forceUpdateLocation(); - keepMyLocationVisible = true; - myLocationWasZoomedIn = false; - if (currentLocation != null) { - showCurrentLocation(); - } - } - - /** - * Moves the location pointer to the current location and center the map if - * the current location is outside the visible area. - */ - private void showCurrentLocation() { - if (mapOverlay == null || mapView == null) { - return; - } - - mapOverlay.setMyLocation(currentLocation); - mapView.postInvalidate(); - - if (currentLocation != null && keepMyLocationVisible && !locationIsVisible(currentLocation)) { - GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation); - MapController controller = mapView.getController(); - controller.animateTo(geoPoint); - if (!myLocationWasZoomedIn && mapView.getZoomLevel() < 18) { - // Only zoom in the first time we show the location. - myLocationWasZoomedIn = true; - controller.setZoom(18); - } - } - } - - @Override - public void onTrackUpdated(Track track) { - // We don't care. - } - - /** - * Zooms and pans the map so that the given track is visible. - * - * @param track the track - */ - private void zoomMapToBoundaries(Track track) { - if (mapView == null) { - return; - } - - if (track == null || track.getNumberOfPoints() < 2) { - return; - } - - TripStatistics stats = track.getStatistics(); - int bottom = stats.getBottom(); - int left = stats.getLeft(); - int latSpanE6 = stats.getTop() - bottom; - int lonSpanE6 = stats.getRight() - left; - if (latSpanE6 > 0 - && latSpanE6 < 180E6 - && lonSpanE6 > 0 - && lonSpanE6 < 360E6) { - keepMyLocationVisible = false; - GeoPoint center = new GeoPoint( - bottom + latSpanE6 / 2, - left + lonSpanE6 / 2); - if (LocationUtils.isValidGeoPoint(center)) { - mapView.getController().setCenter(center); - mapView.getController().zoomToSpan(latSpanE6, lonSpanE6); - } - } - } - - /** - * Zooms and pans the map so that the given waypoint is visible. - */ - public void showWaypoint(long waypointId) { - MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(this); - Waypoint wpt = providerUtils.getWaypoint(waypointId); - if (wpt != null && wpt.getLocation() != null) { - keepMyLocationVisible = false; - GeoPoint center = new GeoPoint( - (int) (wpt.getLocation().getLatitude() * 1E6), - (int) (wpt.getLocation().getLongitude() * 1E6)); - mapView.getController().setCenter(center); - mapView.getController().setZoom(20); - mapView.invalidate(); - } - } - - /** - * Zooms and pans the map so that the given waypoint is visible, when the given track is loaded. - * If the track is already loaded, it does that immediately. - * - * @param trackId the ID of the track on which to show the waypoint - * @param waypointId the ID of the waypoint to show - */ - public void showWaypoint(long trackId, long waypointId) { - synchronized (this) { - if (trackId == selectedTrackId) { - showWaypoint(waypointId); - return; - } - - showWaypointTrackId = trackId; - showWaypointId = waypointId; - } - } - - /** - * Does the proper zooming/panning for a just-loaded track. - * This may be either zooming to a waypoint that has been previously selected, or - * zooming to the whole track. - * - * @param track the loaded track - */ - private void zoomLoadedTrack(Track track) { - synchronized (this) { - if (track.getId() == showWaypointTrackId) { - // There's a waypoint to show in this track. - showWaypoint(showWaypointId); - - showWaypointId = 0L; - showWaypointTrackId = 0L; - } else { - // Zoom out to show the whole track. - zoomMapToBoundaries(track); - } - } - } - - @Override - public void onSelectedTrackChanged(final Track track, final boolean isRecording) { - runOnUiThread(new Runnable() { - @Override - public void run() { - boolean trackSelected = track != null; - mapOverlay.setTrackDrawingEnabled(trackSelected); - - if (trackSelected) { - busyPane.setVisibility(View.VISIBLE); - - synchronized (this) { - // Need to get the track ID only at this point, to prevent a race condition - // among showWaypoint, zoomLoadedTrack and dataHub.loadTrack. - selectedTrackId = track.getId(); - zoomLoadedTrack(track); - } - - mapOverlay.setShowEndMarker(!isRecording); - busyPane.setVisibility(View.GONE); - } - mapView.invalidate(); - } - }); - } - - /** - * Returns whether the map is currently in satellite view mode. - */ - public boolean isSatelliteView() { - return mapView.isSatellite(); - } - - /** - * Changes whether the map should be in satellite view mode. - */ - public void setSatelliteView(boolean sat) { - mapView.setSatellite(sat); - } - - @Override - public void onClick(View v) { - if (v == messagePane) { - startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } - } - - /** - * We want the pointer to become visible again in case of the next location - * update: - */ - @Override - public boolean onTouch(View view, MotionEvent event) { - if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) { - if (!locationIsVisible(currentLocation)) { - keepMyLocationVisible = false; - } - } - return false; - } - - @Override - public void onProviderStateChange(ProviderState state) { - final int messageId; - final boolean isGpsDisabled; - switch (state) { - case DISABLED: - messageId = R.string.gps_need_to_enable; - isGpsDisabled = true; - break; - case NO_FIX: - case BAD_FIX: - messageId = R.string.gps_wait_for_fix; - isGpsDisabled = false; - break; - case GOOD_FIX: - // Nothing to show. - messageId = -1; - isGpsDisabled = false; - break; - default: - throw new IllegalArgumentException("Unexpected state: " + state); - } - - runOnUiThread(new Runnable() { - @Override - public void run() { - if (messageId != -1) { - messageText.setText(messageId); - messagePane.setVisibility(View.VISIBLE); - - if (isGpsDisabled) { - // Give a warning about this state. - Toast.makeText(MapActivity.this, - R.string.gps_not_found, - Toast.LENGTH_LONG).show(); - - // Make clicking take the user to the location settings. - messagePane.setOnClickListener(MapActivity.this); - } else { - messagePane.setOnClickListener(null); - } - } else { - messagePane.setVisibility(View.GONE); - } - - screen.requestLayout(); - } - }); - } - - @Override - public void onCurrentLocationChanged(Location location) { - currentLocation = location; - showCurrentLocation(); - } - - @Override - public void onCurrentHeadingChanged(double heading) { - synchronized (this) { - if (mapOverlay.setHeading((float) heading)) { - mapView.postInvalidate(); - } - } - } - - @Override - public void clearWaypoints() { - mapOverlay.clearWaypoints(); - } - - @Override - public void onNewWaypoint(Waypoint waypoint) { - if (LocationUtils.isValidLocation(waypoint.getLocation())) { - // TODO: Optimize locking inside addWaypoint - mapOverlay.addWaypoint(waypoint); - } - } - - @Override - public void onNewWaypointsDone() { - mapView.postInvalidate(); - } - - @Override - public void clearTrackPoints() { - mapOverlay.clearPoints(); - } - - @Override - public void onNewTrackPoint(Location loc) { - mapOverlay.addLocation(loc); - } - - @Override - public void onSegmentSplit() { - mapOverlay.addSegmentSplit(); - } - - @Override - public void onSampledOutTrackPoint(Location loc) { - // We don't care. - } - - @Override - public void onNewTrackPointsDone() { - mapView.postInvalidate(); - } - - @Override - public boolean onUnitsChanged(boolean metric) { - // We don't care. - return false; - } - - @Override - public boolean onReportSpeedChanged(boolean reportSpeed) { - // We don't care. - return false; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapFragment.java b/MyTracks/src/com/google/android/apps/mytracks/MapFragment.java new file mode 100644 index 000000000..f35a20e12 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MapFragment.java @@ -0,0 +1,518 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.GeoRect; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.maps.GeoPoint; +import com.google.android.maps.MapController; +import com.google.android.maps.MapView; +import com.google.android.maps.mytracks.R; + +import android.content.Intent; +import android.location.Location; +import android.os.Bundle; +import android.provider.Settings; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.EnumSet; + +/** + * A fragment to display map to the user. + * + * @author Leif Hendrik Wilden + * @author Rodrigo Damazio + */ +public class MapFragment extends Fragment + implements View.OnTouchListener, View.OnClickListener, TrackDataListener { + + private static final String KEY_CURRENT_LOCATION = "currentLocation"; + private static final String KEY_KEEP_MY_LOCATION_VISIBLE = "keepMyLocationVisible"; + + private TrackDataHub trackDataHub; + + // True to keep my location visible. + private boolean keepMyLocationVisible; + + // True to zoom to my location. Only apply when keepMyLocationVisible is true. + private boolean zoomToMyLocation; + + // The track id of the waypoint to show. + private long waypointTrackId; + + // The waypoint id to show + private long waypointId; + + // The current selected track id. Set in onSelectedTrackChanged. + private long currentSelectedTrackId; + + // The current location. Set in onCurrentLocationChanged. + private Location currentLocation; + + // UI elements + private View mapViewContainer; + private MapOverlay mapOverlay; + private RelativeLayout screen; + private MapView mapView; + private LinearLayout messagePane; + private TextView messageText; + private LinearLayout busyPane; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mapViewContainer = ((TrackDetailActivity) getActivity()).getMapViewContainer(); + + mapOverlay = new MapOverlay(getActivity()); + + screen = (RelativeLayout) mapViewContainer.findViewById(R.id.screen); + mapView = (MapView) mapViewContainer.findViewById(R.id.map); + mapView.requestFocus(); + mapView.setOnTouchListener(this); + mapView.setBuiltInZoomControls(true); + mapView.getOverlays().add(mapOverlay); + messagePane = (LinearLayout) mapViewContainer.findViewById(R.id.messagepane); + messageText = (TextView) mapViewContainer.findViewById(R.id.messagetext); + busyPane = (LinearLayout) mapViewContainer.findViewById(R.id.busypane); + + return mapViewContainer; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (savedInstanceState != null) { + keepMyLocationVisible = savedInstanceState.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false); + currentLocation = (Location) savedInstanceState.getParcelable(KEY_CURRENT_LOCATION); + if (currentLocation != null) { + updateCurrentLocation(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + resumeTrackDataHub(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible); + if (currentLocation != null) { + outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation); + } + } + + @Override + public void onPause() { + super.onPause(); + pauseTrackDataHub(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ViewGroup parentViewGroup = (ViewGroup) mapViewContainer.getParent(); + if (parentViewGroup != null) { + parentViewGroup.removeView(mapViewContainer); + } + } + + /** + * Shows my location. + */ + public void showMyLocation() { + updateTrackDataHub(); + keepMyLocationVisible = true; + zoomToMyLocation = true; + if (currentLocation != null) { + updateCurrentLocation(); + } + } + + /** + * Shows the waypoint. + * + * @param id the waypoint id + */ + public void showWaypoint(long id) { + MyTracksProviderUtils MyTracksProviderUtils = Factory.get(getActivity()); + Waypoint waypoint = MyTracksProviderUtils.getWaypoint(id); + if (waypoint != null && waypoint.getLocation() != null) { + keepMyLocationVisible = false; + GeoPoint center = new GeoPoint((int) (waypoint.getLocation().getLatitude() * 1E6), + (int) (waypoint.getLocation().getLongitude() * 1E6)); + mapView.getController().setCenter(center); + mapView.getController().setZoom(mapView.getMaxZoomLevel()); + mapView.invalidate(); + } + } + + /** + * Shows the waypoint. + * + * @param trackId the track id + * @param id the waypoint id + */ + public void showWaypoint(long trackId, long id) { + /* + * Synchronize to prevent race condition in changing waypointTrackId and + * waypointId variables. + */ + synchronized (this) { + if (trackId == currentSelectedTrackId) { + showWaypoint(id); + waypointTrackId = -1L; + waypointId = -1L; + return; + } + waypointTrackId = trackId; + waypointId = id; + } + } + + /** + * Returns true if in satellite mode. + */ + public boolean isSatelliteView() { + return mapView.isSatellite(); + } + + /** + * Sets the satellite mode + * + * @param enabled true for satellite mode, false for map mode + */ + public void setSatelliteView(boolean enabled) { + mapView.setSatellite(enabled); + } + + @Override + public boolean onTouch(View view, MotionEvent event) { + if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) { + if (!isVisible(currentLocation)) { + /* + * Only set to false when no longer visible. Thus can keep showing the + * current location with the next location update. + */ + keepMyLocationVisible = false; + } + } + return false; + } + + @Override + public void onClick(View v) { + if (v == messagePane) { + startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + } + + @Override + public void onProviderStateChange(ProviderState state) { + final int messageId; + final boolean isGpsDisabled; + switch (state) { + case DISABLED: + messageId = R.string.gps_need_to_enable; + isGpsDisabled = true; + break; + case NO_FIX: + case BAD_FIX: + messageId = R.string.gps_wait_for_fix; + isGpsDisabled = false; + break; + case GOOD_FIX: + messageId = -1; + isGpsDisabled = false; + break; + default: + throw new IllegalArgumentException("Unexpected state: " + state); + } + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageId != -1) { + messageText.setText(messageId); + messagePane.setVisibility(View.VISIBLE); + + if (isGpsDisabled) { + Toast.makeText(getActivity(), R.string.gps_not_found, Toast.LENGTH_LONG).show(); + + // Click to show the location source settings + messagePane.setOnClickListener(MapFragment.this); + } else { + messagePane.setOnClickListener(null); + } + } else { + messagePane.setVisibility(View.GONE); + } + screen.requestLayout(); + } + }); + } + + @Override + public void onCurrentLocationChanged(Location location) { + currentLocation = location; + updateCurrentLocation(); + } + + @Override + public void onCurrentHeadingChanged(double heading) { + if (mapOverlay.setHeading((float) heading)) { + mapView.postInvalidate(); + } + } + + @Override + public void onSelectedTrackChanged(final Track track, final boolean isRecording) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + boolean hasTrack = track != null; + mapOverlay.setTrackDrawingEnabled(hasTrack); + + if (hasTrack) { + busyPane.setVisibility(View.VISIBLE); + + synchronized (this) { + /* + * Synchronize to prevent race condition in changing waypointTrackId + * and waypointId variables. + */ + currentSelectedTrackId = track.getId(); + updateMap(track); + } + mapOverlay.setShowEndMarker(!isRecording); + busyPane.setVisibility(View.GONE); + } + mapView.invalidate(); + } + }); + } + + @Override + public void onTrackUpdated(Track track) { + // We don't care. + } + + @Override + public void clearTrackPoints() { + mapOverlay.clearPoints(); + } + + @Override + public void onNewTrackPoint(Location location) { + if (LocationUtils.isValidLocation(location)) { + mapOverlay.addLocation(location); + } + } + + @Override + public void onSampledOutTrackPoint(Location loc) { + // We don't care. + } + + @Override + public void onSegmentSplit() { + mapOverlay.addSegmentSplit(); + } + + @Override + public void onNewTrackPointsDone() { + mapView.postInvalidate(); + } + + @Override + public void clearWaypoints() { + mapOverlay.clearWaypoints(); + } + + @Override + public void onNewWaypoint(Waypoint waypoint) { + if (waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) { + // TODO: Optimize locking inside addWaypoint + mapOverlay.addWaypoint(waypoint); + } + } + + @Override + public void onNewWaypointsDone() { + mapView.postInvalidate(); + } + + @Override + public boolean onUnitsChanged(boolean metric) { + // We don't care. + return false; + } + + @Override + public boolean onReportSpeedChanged(boolean reportSpeed) { + // We don't care. + return false; + } + + /** + * Resumes the trackDataHub. Needs to be synchronized because trackDataHub can be + * accessed by multiple threads. + */ + private synchronized void resumeTrackDataHub() { + trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub.registerTrackDataListener(this, EnumSet.of( + ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.WAYPOINT_UPDATES, + ListenerDataType.POINT_UPDATES, + ListenerDataType.LOCATION_UPDATES, + ListenerDataType.COMPASS_UPDATES)); + } + + /** + * Pauses the trackDataHub. Needs to be synchronized because trackDataHub can be + * accessed by multiple threads. + */ + private synchronized void pauseTrackDataHub() { + trackDataHub.unregisterTrackDataListener(this); + trackDataHub = null; + } + + /** + * Updates the trackDataHub. Needs to be synchronized because trackDataHub can be + * accessed by multiple threads. + */ + private synchronized void updateTrackDataHub() { + trackDataHub.forceUpdateLocation(); + } + + /** + * Updates the map by either zooming to the requested waypoint or showing the track. + * + * @param track the track + */ + private void updateMap(Track track) { + if (track.getId() == waypointTrackId) { + // Show the waypoint + showWaypoint(waypointId); + + waypointTrackId = -1L; + waypointId = -1L; + } else { + // Show the track + showTrack(track); + } + } + + /** + * Returns true if the location is visible. + * + * @param location the location + */ + private boolean isVisible(Location location) { + if (location == null || mapView == null) { + return false; + } + GeoPoint mapCenter = mapView.getMapCenter(); + int latitudeSpan = mapView.getLatitudeSpan(); + int longitudeSpan = mapView.getLongitudeSpan(); + + /* + * The bottom of the mapView is obscured by the zoom controls, subtract its + * height from the visible area. + */ + GeoPoint zoomControlBottom = mapView.getProjection().fromPixels(0, mapView.getHeight()); + GeoPoint zoomControlTop = mapView.getProjection().fromPixels( + 0, mapView.getHeight() - mapView.getZoomButtonsController().getZoomControls().getHeight()); + int zoomControlMargin = Math.abs(zoomControlTop.getLatitudeE6() + - zoomControlBottom.getLatitudeE6()); + GeoRect geoRect = new GeoRect(mapCenter, latitudeSpan, longitudeSpan); + geoRect.top += zoomControlMargin; + + GeoPoint geoPoint = LocationUtils.getGeoPoint(location); + return geoRect.contains(geoPoint); + } + + /** + * Updates the current location and centers it if necessary. + */ + private void updateCurrentLocation() { + if (mapOverlay == null || mapView == null) { + return; + } + + mapOverlay.setMyLocation(currentLocation); + mapView.postInvalidate(); + + if (currentLocation != null && keepMyLocationVisible && !isVisible(currentLocation)) { + GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation); + MapController mapController = mapView.getController(); + mapController.animateTo(geoPoint); + if (zoomToMyLocation) { + // Only zoom in the first time we show the location. + zoomToMyLocation = false; + if (mapView.getZoomLevel() < mapView.getMaxZoomLevel()) { + mapController.setZoom(mapView.getMaxZoomLevel()); + } + } + } + } + + /** + * Shows the track. + * + * @param track the track + */ + private void showTrack(Track track) { + if (mapView == null || track == null || track.getNumberOfPoints() < 2) { + return; + } + + TripStatistics tripStatistics = track.getStatistics(); + int bottom = tripStatistics.getBottom(); + int left = tripStatistics.getLeft(); + int latitudeSpanE6 = tripStatistics.getTop() - bottom; + int longitudeSpanE6 = tripStatistics.getRight() - left; + if (latitudeSpanE6 > 0 && latitudeSpanE6 < 180E6 && longitudeSpanE6 > 0 + && longitudeSpanE6 < 360E6) { + keepMyLocationVisible = false; + GeoPoint center = new GeoPoint(bottom + latitudeSpanE6 / 2, left + longitudeSpanE6 / 2); + if (LocationUtils.isValidGeoPoint(center)) { + mapView.getController().setCenter(center); + mapView.getController().zoomToSpan(latitudeSpanE6, longitudeSpanE6); + } + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/NavControls.java b/MyTracks/src/com/google/android/apps/mytracks/NavControls.java deleted file mode 100644 index 655a5fab8..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/NavControls.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; - -/** - * Creates previous and next arrows for a given activity. - * - * @author Leif Hendrik Wilden - */ -public class NavControls { - - private static final int KEEP_VISIBLE_MILLIS = 4000; - private static final boolean FADE_CONTROLS = true; - - private static final Animation SHOW_NEXT_ANIMATION = - new AlphaAnimation(0F, 1F); - private static final Animation HIDE_NEXT_ANIMATION = - new AlphaAnimation(1F, 0F); - private static final Animation SHOW_PREV_ANIMATION = - new AlphaAnimation(0F, 1F); - private static final Animation HIDE_PREV_ANIMATION = - new AlphaAnimation(1F, 0F); - - /** - * A touchable image view. - * When touched it changes the navigation control icons accordingly. - */ - private class TouchLayout extends RelativeLayout implements Runnable { - private final boolean isLeft; - private final ImageView icon; - - public TouchLayout(Context context, boolean isLeft) { - super(context); - this.isLeft = isLeft; - this.icon = new ImageView(context); - icon.setVisibility(View.GONE); - addView(icon); - } - - public void setIcon(Drawable drawable) { - icon.setImageDrawable(drawable); - icon.setVisibility(View.VISIBLE); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - setPressed(true); - shiftIcons(isLeft); - - // Call the user back - handler.post(this); - - break; - case MotionEvent.ACTION_UP: - setPressed(false); - break; - } - return super.onTouchEvent(event); - } - - @Override - public void run() { - touchRunnable.run(); - setPressed(false); - } - } - - private final Handler handler = new Handler(); - private final Runnable dismissControls = new Runnable() { - public void run() { - hide(); - } - }; - - private final TouchLayout prevImage; - private final TouchLayout nextImage; - private final TypedArray leftIcons; - private final TypedArray rightIcons; - private final Runnable touchRunnable; - - private boolean isVisible = false; - private int currentIcons; - - public NavControls(Context context, ViewGroup container, - TypedArray leftIcons, TypedArray rightIcons, - Runnable touchRunnable) { - this.leftIcons = leftIcons; - this.rightIcons = rightIcons; - this.touchRunnable = touchRunnable; - - if (leftIcons.length() != rightIcons.length() || leftIcons.length() < 1) { - throw new IllegalArgumentException("Invalid icons specified"); - } - if (touchRunnable == null) { - throw new NullPointerException("Runnable cannot be null"); - } - - LayoutParams prevParams = new LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - LayoutParams nextParams = new LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - - prevParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); - nextParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); - prevParams.addRule(RelativeLayout.CENTER_VERTICAL); - nextParams.addRule(RelativeLayout.CENTER_VERTICAL); - - nextImage = new TouchLayout(context, false); - prevImage = new TouchLayout(context, true); - nextImage.setLayoutParams(nextParams); - prevImage.setLayoutParams(prevParams); - nextImage.setVisibility(View.INVISIBLE); - prevImage.setVisibility(View.INVISIBLE); - - container.addView(prevImage); - container.addView(nextImage); - - prevImage.setIcon(leftIcons.getDrawable(0)); - nextImage.setIcon(rightIcons.getDrawable(0)); - this.currentIcons = 0; - } - - private void keepVisible() { - if (isVisible && FADE_CONTROLS) { - handler.removeCallbacks(dismissControls); - handler.postDelayed(dismissControls, KEEP_VISIBLE_MILLIS); - } - } - - public void show() { - if (!isVisible) { - SHOW_PREV_ANIMATION.setDuration(500); - SHOW_PREV_ANIMATION.startNow(); - prevImage.setPressed(false); - prevImage.setAnimation(SHOW_PREV_ANIMATION); - prevImage.setVisibility(View.VISIBLE); - - SHOW_NEXT_ANIMATION.setDuration(500); - SHOW_NEXT_ANIMATION.startNow(); - nextImage.setPressed(false); - nextImage.setAnimation(SHOW_NEXT_ANIMATION); - nextImage.setVisibility(View.VISIBLE); - - isVisible = true; - keepVisible(); - } else { - keepVisible(); - } - } - - public void hideNow() { - handler.removeCallbacks(dismissControls); - isVisible = false; - prevImage.clearAnimation(); - prevImage.setVisibility(View.INVISIBLE); - nextImage.clearAnimation(); - nextImage.setVisibility(View.INVISIBLE); - } - - public void hide() { - isVisible = false; - prevImage.setAnimation(HIDE_PREV_ANIMATION); - HIDE_PREV_ANIMATION.setDuration(500); - HIDE_PREV_ANIMATION.startNow(); - prevImage.setVisibility(View.INVISIBLE); - - nextImage.setAnimation(HIDE_NEXT_ANIMATION); - HIDE_NEXT_ANIMATION.setDuration(500); - HIDE_NEXT_ANIMATION.startNow(); - nextImage.setVisibility(View.INVISIBLE); - } - - public int getCurrentIcons() { - return currentIcons; - } - - private void shiftIcons(boolean isLeft) { - // Increment or decrement by one, with wrap around - currentIcons = (currentIcons + leftIcons.length() + - (isLeft ? -1 : 1)) % leftIcons.length(); - prevImage.setIcon(leftIcons.getDrawable(currentIcons)); - nextImage.setIcon(rightIcons.getDrawable(currentIcons)); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java deleted file mode 100644 index ad7a046f6..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackDataHub; -import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; -import com.google.android.apps.mytracks.content.TrackDataListener; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.services.ServiceUtils; -import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.location.Location; -import android.os.Bundle; -import android.speech.tts.TextToSpeech; -import android.util.DisplayMetrics; -import android.util.Log; -import android.widget.ScrollView; -import android.widget.TextView; - -import java.util.EnumSet; - -/** - * An activity that displays track statistics to the user. - * - * @author Sandor Dornbush - * @author Rodrigo Damazio - */ -public class StatsActivity extends Activity implements TrackDataListener { - /** - * A runnable for posting to the UI thread. Will update the total time field. - */ - private final Runnable updateResults = new Runnable() { - public void run() { - if (dataHub != null && dataHub.isRecordingSelected()) { - utils.setTime(R.id.total_time_register, - System.currentTimeMillis() - startTime); - } - } - }; - - private StatsUtilities utils; - private UIUpdateThread thread; - - /** - * The start time of the selected track. - */ - private long startTime = -1; - - private TrackDataHub dataHub; - private SharedPreferences preferences; - - /** - * A thread that updates the total time field every second. - */ - private class UIUpdateThread extends Thread { - - public UIUpdateThread() { - super(); - Log.i(TAG, "Created UI update thread"); - } - - @Override - public void run() { - Log.i(TAG, "Started UI update thread"); - while (ServiceUtils.isRecording(StatsActivity.this, null, preferences)) { - runOnUiThread(updateResults); - try { - Thread.sleep(1000L); - } catch (InterruptedException e) { - Log.w(TAG, "StatsActivity: Caught exception on sleep.", e); - break; - } - } - Log.w(TAG, "UIUpdateThread finished."); - } - } - - /** Called when the activity is first created. */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - preferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - utils = new StatsUtilities(this); - - // The volume we want to control is the Text-To-Speech volume - setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); - - setContentView(R.layout.stats); - - ScrollView sv = ((ScrollView) findViewById(R.id.scrolly)); - sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET); - - showUnknownLocation(); - updateLabels(); - - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); - if (metrics.heightPixels > 600) { - ((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f); - } - } - - @Override - protected void onResume() { - super.onResume(); - - dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); - dataHub.registerTrackDataListener(this, EnumSet.of( - ListenerDataType.SELECTED_TRACK_CHANGED, - ListenerDataType.TRACK_UPDATES, - ListenerDataType.LOCATION_UPDATES, - ListenerDataType.DISPLAY_PREFERENCES)); - } - - @Override - protected void onPause() { - dataHub.unregisterTrackDataListener(this); - dataHub = null; - - if (thread != null) { - thread.interrupt(); - thread = null; - } - - super.onStop(); - } - - @Override - public boolean onUnitsChanged(boolean metric) { - if (utils.isMetricUnits() == metric) { - return false; - } - utils.setMetricUnits(metric); - updateLabels(); - return true; - } - - @Override - public boolean onReportSpeedChanged(boolean speed) { - if (utils.isReportSpeed() == speed) { - return false; - } - utils.setReportSpeed(speed); - updateLabels(); - return true; - } - - private void updateLabels() { - runOnUiThread(new Runnable() { - @Override - public void run() { - utils.updateUnits(); - utils.setSpeedLabel(R.id.speed_label, R.string.stat_speed, R.string.stat_pace); - utils.setSpeedLabels(); - } - }); - } - - /** - * Updates the given location fields (latitude, longitude, altitude) and all - * other fields. - * - * @param l may be null (will set location fields to unknown) - */ - private void showLocation(Location l) { - utils.setAltitude(R.id.elevation_register, l.getAltitude()); - utils.setLatLong(R.id.latitude_register, l.getLatitude()); - utils.setLatLong(R.id.longitude_register, l.getLongitude()); - utils.setSpeed(R.id.speed_register, l.getSpeed() * UnitConversions.MS_TO_KMH); - } - - private void showUnknownLocation() { - utils.setUnknown(R.id.elevation_register); - utils.setUnknown(R.id.latitude_register); - utils.setUnknown(R.id.longitude_register); - utils.setUnknown(R.id.speed_register); - } - - @Override - public void onSelectedTrackChanged(Track track, boolean isRecording) { - /* - * Checks if this activity needs to update live track data or not. - * If so, make sure that: - * a) a thread keeps updating the total time - * b) a location listener is registered - * c) a content observer is registered - * Otherwise unregister listeners, observers, and kill update thread. - */ - final boolean startThread = (thread == null) && isRecording; - final boolean killThread = (thread != null) && (!isRecording); - if (startThread) { - thread = new UIUpdateThread(); - thread.start(); - } else if (killThread) { - thread.interrupt(); - thread = null; - } - } - - @Override - public void onCurrentLocationChanged(final Location loc) { - TrackDataHub localDataHub = dataHub; - if (localDataHub != null && localDataHub.isRecordingSelected()) { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (loc != null) { - showLocation(loc); - } else { - showUnknownLocation(); - } - } - }); - } - } - - @Override - public void onCurrentHeadingChanged(double heading) { - // We don't care. - } - - @Override - public void onProviderStateChange(ProviderState state) { - switch (state) { - case DISABLED: - case NO_FIX: - runOnUiThread(new Runnable() { - @Override - public void run() { - showUnknownLocation(); - } - }); - break; - } - } - - @Override - public void onTrackUpdated(final Track track) { - TrackDataHub localDataHub = dataHub; - final boolean recordingSelected = localDataHub != null && localDataHub.isRecordingSelected(); - runOnUiThread(new Runnable() { - @Override - public void run() { - if (track == null || track.getStatistics() == null) { - utils.setAllToUnknown(); - return; - } - - startTime = track.getStatistics().getStartTime(); - if (!recordingSelected) { - utils.setTime(R.id.total_time_register, - track.getStatistics().getTotalTime()); - showUnknownLocation(); - } - utils.setAllStats(track.getStatistics()); - } - }); - } - - @Override - public void clearWaypoints() { - // We don't care. - } - - @Override - public void onNewWaypoint(Waypoint wpt) { - // We don't care. - } - - @Override - public void onNewWaypointsDone() { - // We don't care. - } - - @Override - public void clearTrackPoints() { - // We don't care. - } - - @Override - public void onNewTrackPoint(Location loc) { - // We don't care. - } - - @Override - public void onSegmentSplit() { - // We don't care. - } - - @Override - public void onSampledOutTrackPoint(Location loc) { - // We don't care. - } - - @Override - public void onNewTrackPointsDone() { - // We don't care. - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsFragment.java b/MyTracks/src/com/google/android/apps/mytracks/StatsFragment.java new file mode 100644 index 000000000..5cb3b3627 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsFragment.java @@ -0,0 +1,322 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.services.ServiceUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.location.Location; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +import java.util.EnumSet; + +/** + * A fragment to display track statistics to the user. + * + * @author Sandor Dornbush + * @author Rodrigo Damazio + */ +public class StatsFragment extends Fragment implements TrackDataListener { + + private static final String TAG = StatsFragment.class.getSimpleName(); + + private SharedPreferences sharedPreferences; + private StatsUtilities statsUtilities; + private TrackDataHub trackDataHub; + private UiUpdateThread uiUpdateThread; + + // The start time of the current track. + private long startTime = -1L; + + // A runnable to update the total time field. + private final Runnable updateTotalTime = new Runnable() { + public void run() { + if (isRecording()) { + statsUtilities.setTime(R.id.total_time_register, System.currentTimeMillis() - startTime); + } + } + }; + + /** + * A thread that updates the total time field every second. + */ + private class UiUpdateThread extends Thread { + + @Override + public void run() { + Log.d(TAG, "UI update thread started"); + while (ServiceUtils.isRecording(getActivity(), null, sharedPreferences)) { + getActivity().runOnUiThread(updateTotalTime); + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + Log.d(TAG, "UI update thread caught exception", e); + break; + } + } + Log.d(TAG, "UI update thread finished"); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sharedPreferences = getActivity() + .getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + statsUtilities = new StatsUtilities(getActivity()); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.stats, container, false); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ScrollView scrollView = (ScrollView) getActivity().findViewById(R.id.scrolly); + scrollView.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET); + updateLabels(); + setLocationUnknown(); + } + + @Override + public void onResume() { + super.onResume(); + resumeTrackDataHub(); + } + + @Override + public void onPause() { + super.onPause(); + pauseTrackDataHub(); + if (uiUpdateThread != null) { + uiUpdateThread.interrupt(); + uiUpdateThread = null; + } + } + + @Override + public void onProviderStateChange(ProviderState state) { + if (state == ProviderState.DISABLED || state == ProviderState.NO_FIX) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + setLocationUnknown(); + } + }); + } + } + + @Override + public void onCurrentLocationChanged(final Location location) { + if (isRecording()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (location != null) { + setLocation(location); + } else { + setLocationUnknown(); + } + } + }); + } + } + + @Override + public void onCurrentHeadingChanged(double heading) { + // We don't care. + } + + @Override + public void onSelectedTrackChanged(Track track, boolean isRecording) { + if (uiUpdateThread == null && isRecording) { + uiUpdateThread = new UiUpdateThread(); + uiUpdateThread.start(); + } else if (uiUpdateThread != null && !isRecording) { + uiUpdateThread.interrupt(); + uiUpdateThread = null; + } + } + + @Override + public void onTrackUpdated(final Track track) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (track == null || track.getStatistics() == null) { + statsUtilities.setAllToUnknown(); + return; + } + + startTime = track.getStatistics().getStartTime(); + if (!isRecording()) { + statsUtilities.setTime(R.id.total_time_register, track.getStatistics().getTotalTime()); + setLocationUnknown(); + } + statsUtilities.setAllStats(track.getStatistics()); + } + }); + } + + @Override + public void clearTrackPoints() { + // We don't care. + } + + @Override + public void onNewTrackPoint(Location loc) { + // We don't care. + } + + @Override + public void onSampledOutTrackPoint(Location loc) { + // We don't care. + } + + @Override + public void onSegmentSplit() { + // We don't care. + } + + @Override + public void onNewTrackPointsDone() { + // We don't care. + } + + @Override + public void clearWaypoints() { + // We don't care. + } + + @Override + public void onNewWaypoint(Waypoint wpt) { + // We don't care. + } + + @Override + public void onNewWaypointsDone() { + // We don't care. + } + + @Override + public boolean onUnitsChanged(boolean metric) { + if (statsUtilities.isMetricUnits() == metric) { + return false; + } + statsUtilities.setMetricUnits(metric); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + updateLabels(); + } + }); + return true; + } + + @Override + public boolean onReportSpeedChanged(boolean speed) { + if (statsUtilities.isReportSpeed() == speed) { + return false; + } + statsUtilities.setReportSpeed(speed); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + updateLabels(); + } + }); + return true; + } + + /** + * Resumes the trackDataHub. Needs to be synchronized because trackDataHub can + * be accessed by multiple threads. + */ + private synchronized void resumeTrackDataHub() { + trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub.registerTrackDataListener(this, EnumSet.of( + ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.TRACK_UPDATES, + ListenerDataType.LOCATION_UPDATES, + ListenerDataType.DISPLAY_PREFERENCES)); + } + + /** + * Pauses the trackDataHub. Needs to be synchronized because trackDataHub can + * be accessed by multiple threads. + */ + private synchronized void pauseTrackDataHub() { + trackDataHub.unregisterTrackDataListener(this); + trackDataHub = null; + } + + /** + * Returns true if recording. Needs to be synchronized because trackDataHub + * can be accessed by multiple threads. + */ + private synchronized boolean isRecording() { + return trackDataHub != null && trackDataHub.isRecordingSelected(); + } + + /** + * Updates the labels. + */ + private void updateLabels() { + statsUtilities.updateUnits(); + statsUtilities.setSpeedLabel(R.id.speed_label, R.string.stat_speed, R.string.stat_pace); + statsUtilities.setSpeedLabels(); + } + + /** + * Sets the current location. + * + * @param location the current location + */ + private void setLocation(Location location) { + statsUtilities.setAltitude(R.id.elevation_register, location.getAltitude()); + statsUtilities.setLatLong(R.id.latitude_register, location.getLatitude()); + statsUtilities.setLatLong(R.id.longitude_register, location.getLongitude()); + statsUtilities.setSpeed(R.id.speed_register, location.getSpeed() * UnitConversions.MS_TO_KMH); + } + + /** + * Sets the current location to unknown. + */ + private void setLocationUnknown() { + statsUtilities.setUnknown(R.id.elevation_register); + statsUtilities.setUnknown(R.id.latitude_register); + statsUtilities.setUnknown(R.id.longitude_register); + statsUtilities.setUnknown(R.id.speed_register); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/TabManager.java b/MyTracks/src/com/google/android/apps/mytracks/TabManager.java new file mode 100644 index 000000000..5159eacb1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/TabManager.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.view.View; +import android.widget.TabHost; + +import java.util.HashMap; + +/** + * This is a helper class that implements a generic mechanism for associating + * fragments with the tabs in a tab host. It relies on a trick. Normally a tab + * host has a simple API for supplying a View or Intent that each tab will show. + * This is not sufficient for switching between fragments. So instead we make + * the content part of the tab host 0dp high (it is not shown) and the + * TabManager supplies its own dummy view to show as the tab content. It listens + * to changes in tabs, and takes care of switch to the correct fragment shown in + * a separate content area whenever the selected tab changes. + * + * Copied from the Fragment Tabs example in the API 4+ Support Demos. + * + * @author Jimmy Shih + */ +public class TabManager implements TabHost.OnTabChangeListener { + private final FragmentActivity mActivity; + private final TabHost mTabHost; + private final int mContainerId; + private final HashMap mTabs = new HashMap(); + TabInfo mLastTab; + + static final class TabInfo { + private final String tag; + private final Class clss; + private final Bundle args; + private Fragment fragment; + + TabInfo(String _tag, Class _class, Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + @Override + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + public TabManager(FragmentActivity activity, TabHost tabHost, int containerId) { + mActivity = activity; + mTabHost = tabHost; + mContainerId = containerId; + mTabHost.setOnTabChangedListener(this); + } + + public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { + tabSpec.setContent(new DummyTabFactory(mActivity)); + String tag = tabSpec.getTag(); + + TabInfo info = new TabInfo(tag, clss, args); + + // Check to see if we already have a fragment for this tab, probably + // from a previously saved state. If so, deactivate it, because our + // initial state is that a tab isn't shown. + info.fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag); + if (info.fragment != null && !info.fragment.isDetached()) { + FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction(); + ft.detach(info.fragment); + ft.commit(); + } + + mTabs.put(tag, info); + mTabHost.addTab(tabSpec); + } + + @Override + public void onTabChanged(String tabId) { + TabInfo newTab = mTabs.get(tabId); + if (mLastTab != newTab) { + FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction(); + if (mLastTab != null) { + if (mLastTab.fragment != null) { + ft.detach(mLastTab.fragment); + } + } + if (newTab != null) { + if (newTab.fragment == null) { + newTab.fragment = Fragment.instantiate(mActivity, newTab.clss.getName(), newTab.args); + ft.add(mContainerId, newTab.fragment, newTab.tag); + } else { + ft.attach(newTab.fragment); + } + } + + mLastTab = newTab; + ft.commit(); + mActivity.getSupportFragmentManager().executePendingTransactions(); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java index 6c4909365..325cbac94 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java @@ -36,7 +36,6 @@ import com.google.android.maps.mytracks.R; import android.app.AlertDialog; import android.app.Dialog; -import android.app.TabActivity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -44,21 +43,21 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.os.RemoteException; import android.speech.tts.TextToSpeech; +import android.support.v4.app.FragmentActivity; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; -import android.view.ViewGroup.LayoutParams; -import android.widget.RelativeLayout; +import android.widget.CheckBox; +import android.widget.RadioGroup; import android.widget.TabHost; +import android.widget.TabHost.TabSpec; import android.widget.Toast; import java.util.List; @@ -69,22 +68,24 @@ import java.util.List; * @author Leif Hendrik Wilden * @author Rodrigo Damazio */ -@SuppressWarnings("deprecation") -public class TrackDetailActivity extends TabActivity implements OnTouchListener { +public class TrackDetailActivity extends FragmentActivity { public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_WAYPOINT_ID = "waypoint_id"; private static final String TAG = TrackDetailActivity.class.getSimpleName(); private static final String MARKET_URL_PREFIX = "market://details?id="; + private static final String CURRENT_TAG_KEY = "tab"; private static final int DIALOG_INSTALL_EARTH_ID = 0; private static final int DIALOG_DELETE_CURRENT_ID = 1; - + private static final int DIALOG_CHART_SETTINGS_ID = 2; + private SharedPreferences sharedPreferences; private TrackDataHub trackDataHub; private TrackRecordingServiceConnection trackRecordingServiceConnection; - private NavControls navControls; + private TabHost tabHost; + private TabManager tabManager; private long trackId; private MenuItem stopRecording; @@ -96,12 +97,8 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener private MenuItem edit; private MenuItem delete; - private final Runnable changeTab = new Runnable() { - public void run() { - getTabHost().setCurrentTab(navControls.getCurrentIcons()); - } - }; - + private View mapViewContainer; + /* * Note that sharedPreferenceChangeListener cannot be an anonymous inner * class. Anonymous inner class will get garbage collected. @@ -119,42 +116,58 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener } }; + /** + * We are not displaying driving directions. Just an arbitrary track that is + * not associated to any licensed mapping data. Therefore it should be okay to + * return false here and still comply with the terms of service. + */ + @Override + protected boolean isRouteDisplayed() { + return false; + } + + /** + * We are displaying a location. This needs to return true in order to comply + * with the terms of service. + */ + @Override + protected boolean isLocationDisplayed() { + return true; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); - ApiAdapterFactory.getApiAdapter().configureActionBarHomeAsUp(this); + ApiAdapterFactory.getApiAdapter().configureActionBarHomeAsUp(this); + setContentView(R.layout.track_detail); sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); trackDataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); - final Resources res = getResources(); - final TabHost tabHost = getTabHost(); - tabHost.addTab(tabHost.newTabSpec(MAP_TAB_TAG) - .setIndicator("Map", res.getDrawable(android.R.drawable.ic_menu_mapmode)) - .setContent(new Intent(this, MapActivity.class))); - tabHost.addTab(tabHost.newTabSpec(STATS_TAB_TAG) - .setIndicator("Stats", res.getDrawable(R.drawable.ic_menu_statistics)) - .setContent(new Intent(this, StatsActivity.class))); - tabHost.addTab(tabHost.newTabSpec(CHART_TAB_TAG) - .setIndicator("Chart", res.getDrawable(R.drawable.menu_elevation)) - .setContent(new Intent(this, ChartActivity.class))); + mapViewContainer = getLayoutInflater().inflate(R.layout.mytracks_layout, null); + + tabHost = (TabHost) findViewById(android.R.id.tabhost); + tabHost.setup(); + tabManager = new TabManager(this, tabHost, R.id.realtabcontent); + TabSpec mapTabSpec = tabHost.newTabSpec(MAP_TAB_TAG).setIndicator( + getString(R.string.track_detail_map_tab), + getResources().getDrawable(android.R.drawable.ic_menu_mapmode)); + tabManager.addTab(mapTabSpec, MapFragment.class, null); + TabSpec chartTabSpec = tabHost.newTabSpec(CHART_TAB_TAG).setIndicator( + getString(R.string.track_detail_chart_tab), + getResources().getDrawable(R.drawable.menu_elevation)); + tabManager.addTab(chartTabSpec, ChartFragment.class, null); + TabSpec statsTabSpec = tabHost.newTabSpec(STATS_TAB_TAG).setIndicator( + getString(R.string.track_detail_stats_tab), + getResources().getDrawable(R.drawable.ic_menu_statistics)); + tabManager.addTab(statsTabSpec, StatsFragment.class, null); - // Hide the tab widget itself. We'll use overlayed prev/next buttons to - // switch between the tabs: - tabHost.getTabWidget().setVisibility(View.GONE); - - RelativeLayout layout = new RelativeLayout(this); - LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); - layout.setLayoutParams(params); - navControls = new NavControls(this, layout, getResources().obtainTypedArray(R.array.left_icons), - getResources().obtainTypedArray(R.array.right_icons), changeTab); - navControls.show(); - tabHost.addView(layout); - layout.setOnTouchListener(this); + if (savedInstanceState != null) { + tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAG_KEY)); + } // Get the trackid Intent intent = getIntent(); @@ -168,16 +181,22 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener // Get the waypointId long waypointId = intent.getLongExtra(EXTRA_WAYPOINT_ID, -1L); if (waypointId != -1L) { - MapActivity mapActivity = getMapActivity(); - if (mapActivity != null) { - getTabHost().setCurrentTab(0); - mapActivity.showWaypoint(trackId, waypointId); + MapFragment mapFragmet = (MapFragment) getSupportFragmentManager().findFragmentByTag(MAP_TAB_TAG); + if (mapFragmet != null) { + tabHost.setCurrentTab(0); + mapFragmet.showWaypoint(trackId, waypointId); } else { - Log.e(TAG, "MapActivity is null"); + Log.e(TAG, "MapFragment is null"); } } } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(CURRENT_TAG_KEY, tabHost.getCurrentTabTag()); + } + @Override protected void onStart() { super.onStart(); @@ -227,11 +246,55 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener startTrackListActivity(); } }); + case DIALOG_CHART_SETTINGS_ID: + return createChartSettingsDialog(); default: return null; } } + private Dialog createChartSettingsDialog() { + final ChartFragment chartFragment = (ChartFragment) getSupportFragmentManager() + .findFragmentByTag(CHART_TAB_TAG); + View view = getLayoutInflater().inflate(R.layout.chart_settings, null); + final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.chart_settings_x); + radioGroup + .check(chartFragment.getMode() == ChartView.Mode.BY_DISTANCE ? R.id.chart_settings_by_distance + : R.id.chart_settings_by_time); + + final CheckBox[] checkBoxes = new CheckBox[ChartView.NUM_SERIES]; + checkBoxes[ChartView.ELEVATION_SERIES] = (CheckBox) view + .findViewById(R.id.chart_settings_elevation); + checkBoxes[ChartView.SPEED_SERIES] = (CheckBox) view.findViewById(R.id.chart_settings_speed); + checkBoxes[ChartView.POWER_SERIES] = (CheckBox) view.findViewById(R.id.chart_settings_power); + checkBoxes[ChartView.CADENCE_SERIES] = (CheckBox) view + .findViewById(R.id.chart_settings_cadence); + checkBoxes[ChartView.HEART_RATE_SERIES] = (CheckBox) view + .findViewById(R.id.chart_settings_heart_rate); + + // set checkboxes values + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + checkBoxes[i].setChecked(chartFragment.isChartValueSeriesEnabled(i)); + } + checkBoxes[ChartView.SPEED_SERIES].setText(chartFragment.isReportSpeed() ? R.string.stat_speed + : R.string.stat_pace); + + return new AlertDialog.Builder(this).setCancelable(true) + .setNegativeButton(R.string.generic_cancel, null) + .setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + chartFragment + .setMode(radioGroup.getCheckedRadioButtonId() == R.id.chart_settings_by_distance ? ChartView.Mode.BY_DISTANCE + : ChartView.Mode.BY_TIME); + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + chartFragment.setChartValueSeriesEnabled(i, checkBoxes[i].isChecked()); + } + chartFragment.update(); + } + }).setTitle(R.string.menu_chart_settings).setView(view).create(); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.track_detail, menu); @@ -269,13 +332,14 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener @Override public boolean onPrepareOptionsMenu(Menu menu) { - String currentTabTag = getTabHost().getCurrentTabTag(); + String currentTabTag = tabHost.getCurrentTabTag(); menu.findItem(R.id.track_detail_chart_settings).setVisible(CHART_TAB_TAG.equals(currentTabTag)); menu.findItem(R.id.track_detail_my_location).setVisible(MAP_TAB_TAG.equals(currentTabTag)); // Set map or satellite mode - MapActivity mapActivity = getMapActivity(); - boolean isSatelliteMode = mapActivity != null ? mapActivity.isSatelliteView() : false; + MapFragment mapFragment = (MapFragment) getSupportFragmentManager() + .findFragmentByTag(MAP_TAB_TAG); + boolean isSatelliteMode = mapFragment != null ? mapFragment.isSatelliteView() : false; menu.findItem(R.id.track_detail_satellite_mode).setVisible(MAP_TAB_TAG.equals(currentTabTag)) .setTitle(isSatelliteMode ? R.string.menu_map_mode : R.string.menu_satellite_mode); @@ -284,7 +348,7 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener @Override public boolean onOptionsItemSelected(MenuItem item) { - MapActivity mapActivity; + MapFragment mapFragment; Intent intent; switch (item.getItemId()) { case android.R.id.home: @@ -361,22 +425,19 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener showDialog(DIALOG_DELETE_CURRENT_ID); return true; case R.id.track_detail_my_location: - mapActivity = getMapActivity(); - if (mapActivity != null) { - mapActivity.showMyLocation(); + mapFragment = (MapFragment) getSupportFragmentManager().findFragmentByTag(MAP_TAB_TAG); + if (mapFragment != null) { + mapFragment.showMyLocation(); } return true; case R.id.track_detail_satellite_mode: - mapActivity = getMapActivity(); - if (mapActivity != null) { - mapActivity.setSatelliteView(!mapActivity.isSatelliteView()); + mapFragment = (MapFragment) getSupportFragmentManager().findFragmentByTag(MAP_TAB_TAG); + if (mapFragment != null) { + mapFragment.setSatelliteView(!mapFragment.isSatelliteView()); } return true; case R.id.track_detail_chart_settings: - ChartActivity chartActivity = getChartActivity(); - if (chartActivity != null) { - chartActivity.showChartSettingsDialog(); - } + showDialog(DIALOG_CHART_SETTINGS_ID); return true; case R.id.track_detail_sensor_state: startActivity(new Intent(this, SensorStateActivity.class)); @@ -401,10 +462,11 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener if (results != null) { long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1L); if (waypointId != -1L) { - MapActivity mapActivity = (MapActivity) getLocalActivityManager().getActivity(MAP_TAB_TAG); - if (mapActivity != null) { - getTabHost().setCurrentTab(0); - mapActivity.showWaypoint(waypointId); + MapFragment mapFragment = (MapFragment) getSupportFragmentManager() + .findFragmentByTag(MAP_TAB_TAG); + if (mapFragment != null) { + tabHost.setCurrentTab(0); + mapFragment.showWaypoint(waypointId); } } } @@ -440,12 +502,11 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener return super.onTrackballEvent(event); } - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - navControls.show(); - } - return false; + /** + * @return the mapViewContainer + */ + public View getMapViewContainer() { + return mapViewContainer; } /** @@ -557,18 +618,4 @@ public class TrackDetailActivity extends TabActivity implements OnTouchListener } return false; } - - /** - * Gets the map activity, can be null. - */ - private MapActivity getMapActivity() { - return (MapActivity) getLocalActivityManager().getActivity(MAP_TAB_TAG); - } - - /** - * Gets the chart activity, can be null. - */ - private ChartActivity getChartActivity() { - return (ChartActivity) getLocalActivityManager().getActivity(CHART_TAB_TAG); - } } \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartFragmentTest.java similarity index 54% rename from MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/ChartFragmentTest.java index 1ed95955a..69418ddd6 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartFragmentTest.java @@ -13,152 +13,100 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; -import android.app.Instrumentation; import android.location.Location; -import android.test.ActivityInstrumentationTestCase2; -import android.view.View; -import android.widget.ZoomControls; +import android.test.AndroidTestCase; /** - * Tests {@link ChartActivity}. - * + * Tests {@link ChartFragment}. + * * @author Youtao Liu */ -public class ChartActivityTest extends ActivityInstrumentationTestCase2 { +public class ChartFragmentTest extends AndroidTestCase { - private Instrumentation instrumentation; - private ChartActivity chartActivity; - private View zoomIn; - private View zoomOut; - private int currentZoomLevel; + private ChartFragment chartFragment; - // 10 is same with the default value in ChartView - private final int MAX_ZOOM_LEVEL = 10; - private final int MIN_ZOOM_LEVEL = 1; - // The ratio from meter/second to kilometer/hour, the conversion is 60 * 60 / - // 1000 = 3.6. - private final double METER_PER_SECOND_TO_KILOMETER_PER_HOUR = 3.6; - private final double KILOMETER_TO_METER = 1000.0; - private final double HOURS_PER_UNIT = 60; - - public ChartActivityTest() { - super(ChartActivity.class); - } + private final double HOURS_PER_UNIT = 60.0; @Override protected void setUp() throws Exception { - super.setUp(); - instrumentation = getInstrumentation(); - chartActivity = getActivity(); + chartFragment = new ChartFragment(); + chartFragment.setChartView(new ChartView(getContext())); } /** - * Tests zooming in and zooming out. - */ - public void testZoomInAndZoomOut() { - currentZoomLevel = chartActivity.getChartView().getZoomLevel(); - chartActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - ZoomControls zoomControls = (ZoomControls) chartActivity.findViewById(R.id.elevation_zoom); - zoomIn = zoomControls.getChildAt(0); - zoomOut = zoomControls.getChildAt(1); - // Invoke following two methods method to initial the display of - // ZoomControls. - zoomControls.setIsZoomInEnabled(chartActivity.getChartView().canZoomIn()); - zoomControls.setIsZoomOutEnabled(chartActivity.getChartView().canZoomOut()); - // Click zoomIn button to disable. - for (int i = currentZoomLevel; i > MIN_ZOOM_LEVEL; i--) { - zoomIn.performClick(); - } - } - }); - instrumentation.waitForIdleSync(); - assertEquals(false, zoomIn.isEnabled()); - assertEquals(true, zoomOut.isEnabled()); - chartActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - // Click to the second max zoom level - for (int i = MIN_ZOOM_LEVEL; i < MAX_ZOOM_LEVEL - 1; i++) { - zoomOut.performClick(); - assertEquals(true, zoomIn.isEnabled()); - assertEquals(true, zoomOut.isEnabled()); - } - zoomOut.performClick(); - } - }); - instrumentation.waitForIdleSync(); - assertEquals(true, zoomIn.isEnabled()); - assertEquals(false, zoomOut.isEnabled()); - } - - /** - * Tests the logic to get the incorrect values of sensor in {@link - * ChartActivity#fillDataPoint(Location, double[])}. + * Tests the logic to get the incorrect values of sensor in + * {@link ChartFragment#fillDataPoint(Location, double[])} */ public void testFillDataPoint_sensorIncorrect() { MyTracksLocation myTracksLocation = TrackStubUtils.createMyTracksLocation(); + // No input. - double[] point = fillDataPointTestHelper(myTracksLocation, false); + double[] point = fillDataPointTestHelper(myTracksLocation); assertEquals(Double.NaN, point[3]); assertEquals(Double.NaN, point[4]); assertEquals(Double.NaN, point[5]); // Input incorrect state. // Creates SensorData. - Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder().setValue(20) - .setState(Sensor.SensorState.NONE); - Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder().setValue(20) - .setState(Sensor.SensorState.NONE); - Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder().setValue(20) - .setState(Sensor.SensorState.NONE); + Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder() + .setValue(20).setState(Sensor.SensorState.NONE); + Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder() + .setValue(20).setState(Sensor.SensorState.NONE); + Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder() + .setValue(20).setState(Sensor.SensorState.NONE); // Creates SensorDataSet. SensorDataSet sensorDataSet = myTracksLocation.getSensorDataSet(); - sensorDataSet = sensorDataSet.toBuilder().setPower(powerData).setCadence(cadenceData) - .setHeartRate(heartRateData).build(); + sensorDataSet = sensorDataSet.toBuilder() + .setPower(powerData) + .setCadence(cadenceData) + .setHeartRate(heartRateData) + .build(); myTracksLocation.setSensorData(sensorDataSet); // Test. - point = fillDataPointTestHelper(myTracksLocation, false); + point = fillDataPointTestHelper(myTracksLocation); assertEquals(Double.NaN, point[3]); assertEquals(Double.NaN, point[4]); assertEquals(Double.NaN, point[5]); } /** - * Tests the logic to get the correct values of sensor in {@link - * ChartActivity#fillDataPoint(Location, double[])}. + * Tests the logic to get the correct values of sensor in + * {@link ChartFragment#fillDataPoint(Location, double[])}. */ public void testFillDataPoint_sensorCorrect() { MyTracksLocation myTracksLocation = TrackStubUtils.createMyTracksLocation(); // No input. - double[] point = fillDataPointTestHelper(myTracksLocation, false); + double[] point = fillDataPointTestHelper(myTracksLocation); assertEquals(Double.NaN, point[3]); assertEquals(Double.NaN, point[4]); assertEquals(Double.NaN, point[5]); // Creates SensorData. - Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder().setValue(20) - .setState(Sensor.SensorState.SENDING); - Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder().setValue(20) - .setState(Sensor.SensorState.SENDING); - Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder().setValue(20) - .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder() + .setValue(20).setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder() + .setValue(20).setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder() + .setValue(20).setState(Sensor.SensorState.SENDING); + // Creates SensorDataSet. SensorDataSet sensorDataSet = myTracksLocation.getSensorDataSet(); - sensorDataSet = sensorDataSet.toBuilder().setPower(powerData).setCadence(cadenceData) - .setHeartRate(heartRateData).build(); + sensorDataSet = sensorDataSet.toBuilder() + .setPower(powerData) + .setCadence(cadenceData) + .setHeartRate(heartRateData) + .build(); myTracksLocation.setSensorData(sensorDataSet); // Test. - point = fillDataPointTestHelper(myTracksLocation, false); + point = fillDataPointTestHelper(myTracksLocation); assertEquals(20.0, point[3]); assertEquals(20.0, point[4]); assertEquals(20.0, point[5]); @@ -166,61 +114,63 @@ public class ChartActivityTest extends ActivityInstrumentationTestCase2