/* * Copyright 2011 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.content; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.database.Cursor; import android.location.Location; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import java.util.Collections; import java.util.EnumSet; import java.util.Set; /** * Track data hub. Receives data from {@link DataSource} and distributes it to * {@link TrackDataListener} after some processing. * * @author Rodrigo Damazio */ public class TrackDataHub implements DataSourceListener { private static final String TAG = TrackDataHub.class.getSimpleName(); /** * Maximum number of waypoints to displayed. */ @VisibleForTesting static final int MAX_DISPLAYED_WAYPOINTS = 128; /** * Target number of track points displayed by the map overlay. We may display * more than this number of points. */ public static final int TARGET_DISPLAYED_TRACK_POINTS = 5000; private final Context context; private final TrackDataManager trackDataManager; private final MyTracksProviderUtils myTracksProviderUtils; private final int targetNumPoints; private boolean started; private HandlerThread handlerThread; private Handler handler; private DataSource dataSource; private DataSourceManager dataSourceManager; // Preference values private long selectedTrackId; private long recordingTrackId; private boolean recordingTrackPaused; private boolean metricUnits; private boolean reportSpeed; private int recordingGpsAccuracy; private int recordingDistanceInterval; private int mapType; // Track points sampling state private int numLoadedPoints; private long firstSeenLocationId; private long lastSeenLocationId; /** * Creates a new instance. */ public synchronized static TrackDataHub newInstance(Context context) { return new TrackDataHub(context, new TrackDataManager(), MyTracksProviderUtils.Factory.get( context), TARGET_DISPLAYED_TRACK_POINTS); } /** * Constructor. * * @param context the context * @param trackDataManager the track data manager * @param myTracksProviderUtils the my tracks provider utils * @param targetNumPoints the target number of points */ @VisibleForTesting TrackDataHub(Context context, TrackDataManager trackDataManager, MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) { this.context = context; this.trackDataManager = trackDataManager; this.myTracksProviderUtils = myTracksProviderUtils; this.targetNumPoints = targetNumPoints; resetSamplingState(); } /** * Starts. */ public void start() { if (started) { Log.i(TAG, "TrackDataHub already started, ignoring start."); return; } started = true; handlerThread = new HandlerThread("TrackDataHubHandlerThread"); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); dataSource = newDataSource(); dataSourceManager = new DataSourceManager(dataSource, this); notifyPreferenceChanged(null); runInHanderThread(new Runnable() { @Override public void run() { if (dataSourceManager != null) { dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); loadDataForAll(); } } }); } /** * Stops. */ public void stop() { if (!started) { Log.i(TAG, "TrackDataHub not started, ignoring stop."); return; } started = false; dataSourceManager.unregisterAllListeners(); if (handlerThread != null) { handlerThread.getLooper().quit(); handlerThread = null; } handler = null; dataSource = null; dataSourceManager = null; } /** * Loads a track. * * @param trackId the track id */ public void loadTrack(final long trackId) { runInHanderThread(new Runnable() { @Override public void run() { if (trackId == selectedTrackId) { Log.i(TAG, "Not reloading track " + trackId); return; } selectedTrackId = trackId; loadDataForAll(); } }); } /** * Registers a {@link TrackDataListener}. * * @param trackDataListener the track data listener * @param trackDataTypes the track data types */ public void registerTrackDataListener( final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) { runInHanderThread(new Runnable() { @Override public void run() { trackDataManager.registerListener(trackDataListener, trackDataTypes); if (dataSourceManager != null) { dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); loadDataForListener(trackDataListener); } } }); } /** * Unregisters a {@link TrackDataListener}. * * @param trackDataListener the track data listener */ public void unregisterTrackDataListener(final TrackDataListener trackDataListener) { runInHanderThread(new Runnable() { @Override public void run() { trackDataManager.unregisterListener(trackDataListener); if (dataSourceManager != null) { dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); } } }); } /** * Reloads data for a {@link TrackDataListener}. */ public void reloadDataForListener(final TrackDataListener trackDataListener) { runInHanderThread(new Runnable() { @Override public void run() { loadDataForListener(trackDataListener); } }); } /** * Returns true if the selected track is recording. */ public boolean isSelectedTrackRecording() { return selectedTrackId == recordingTrackId && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; } /** * Returns true if the selected track is paused. */ public boolean isSelectedTrackPaused() { return selectedTrackId == recordingTrackId && recordingTrackPaused; } @Override public void notifyTracksTableUpdated() { runInHanderThread(new Runnable() { @Override public void run() { notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); } }); } @Override public void notifyWaypointsTableUpdated() { runInHanderThread(new Runnable() { @Override public void run() { notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); } }); } @Override public void notifyTrackPointsTableUpdated() { runInHanderThread(new Runnable() { @Override public void run() { notifyTrackPointsTableUpdate( true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE), trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); } }); } @Override public void notifyPreferenceChanged(final String key) { runInHanderThread(new Runnable() { @Override public void run() { if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) { recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); } if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) { recordingTrackPaused = PreferencesUtils.getBoolean( context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); } if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) { metricUnits = PreferencesUtils.isMetricUnits(context); if (key != null) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { if (trackDataListener.onMetricUnitsChanged(metricUnits)) { loadDataForListener(trackDataListener); } } } } if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) { reportSpeed = PreferencesUtils.isReportSpeed(context); if (key != null) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { if (trackDataListener.onReportSpeedChanged(reportSpeed)) { loadDataForListener(trackDataListener); } } } } if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) { recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT); if (key != null) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) { loadDataForListener(trackDataListener); } } } } if (key == null || key.equals( PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) { recordingDistanceInterval = PreferencesUtils.getInt( context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT); if (key != null) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) { loadDataForListener(trackDataListener); } } } } if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.map_type_key))) { mapType = PreferencesUtils.getInt( context, R.string.map_type_key, PreferencesUtils.MAP_TYPE_DEFAUlT); if (key != null) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { if (trackDataListener.onMapTypeChanged(mapType)) { loadDataForListener(trackDataListener); } } } } } }); } /** * Loads data for all listeners. To be run in the {@link #handler} thread. */ private void loadDataForAll() { resetSamplingState(); if (trackDataManager.getNumberOfListeners() == 0) { return; } for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { trackDataListener.onMetricUnitsChanged(metricUnits); trackDataListener.onReportSpeedChanged(reportSpeed); trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy); trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval); trackDataListener.onMapTypeChanged(mapType); } notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); for (TrackDataListener listener : trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) { listener.clearTrackPoints(); } notifyTrackPointsTableUpdate(true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE), trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); } /** * Loads data for a listener. To be run in the {@link #handler} thread. * * @param trackDataListener the track data listener. */ private void loadDataForListener(TrackDataListener trackDataListener) { Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener); EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener); if (trackDataTypes.contains(TrackDataType.PREFERENCE)) { trackDataListener.onMetricUnitsChanged(metricUnits); trackDataListener.onReportSpeedChanged(reportSpeed); trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy); trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval); trackDataListener.onMapTypeChanged(mapType); } if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) { notifyTracksTableUpdate(trackDataListeners); } boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE); boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE); if (hasSampledIn || hasSampledOut) { trackDataListener.clearTrackPoints(); boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1; if (isOnlyListener) { resetSamplingState(); } Set<TrackDataListener> sampledInListeners = trackDataListeners; Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.<TrackDataListener> emptySet(); notifyTrackPointsTableUpdate(isOnlyListener, sampledInListeners, sampledOutListeners); } if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) { notifyWaypointsTableUpdate(trackDataListeners); } } /** * Notifies track table update. To be run in the {@link #handler} thread. * * @param trackDataListeners the track data listeners to notify */ private void notifyTracksTableUpdate(Set<TrackDataListener> trackDataListeners) { if (trackDataListeners.isEmpty()) { return; } Track track = myTracksProviderUtils.getTrack(selectedTrackId); for (TrackDataListener trackDataListener : trackDataListeners) { trackDataListener.onTrackUpdated(track); } } /** * Notifies waypoint table update. Currently, reloads all the waypoints up to * {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler} * thread. * * @param trackDataListeners the track data listeners to notify */ private void notifyWaypointsTableUpdate(Set<TrackDataListener> trackDataListeners) { if (trackDataListeners.isEmpty()) { return; } for (TrackDataListener trackDataListener : trackDataListeners) { trackDataListener.clearWaypoints(); } Cursor cursor = null; try { cursor = myTracksProviderUtils.getWaypointCursor( selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS); if (cursor != null && cursor.moveToFirst()) { do { Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor); if (!LocationUtils.isValidLocation(waypoint.getLocation())) { continue; } for (TrackDataListener trackDataListener : trackDataListeners) { trackDataListener.onNewWaypoint(waypoint); } } while (cursor.moveToNext()); } } finally { if (cursor != null) { cursor.close(); } } for (TrackDataListener trackDataListener : trackDataListeners) { trackDataListener.onNewWaypointsDone(); } } /** * Notifies track points table update. To be run in the {@link #handler} * thread. * * @param updateSamplingState true to update the sampling state * @param sampledInListeners the sampled-in listeners * @param sampledOutListeners the sampled-out listeners */ private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) { if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) { return; } if (updateSamplingState && numLoadedPoints >= targetNumPoints) { // Reload and resample the track at a lower frequency. Log.i(TAG, "Resampling track after " + numLoadedPoints + " points."); resetSamplingState(); for (TrackDataListener listener : sampledInListeners) { listener.clearTrackPoints(); } } int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0; long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L; long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L; long maxPointId = updateSamplingState ? -1L : lastSeenLocationId; long lastTrackPointId = myTracksProviderUtils.getLastTrackPointId(selectedTrackId); int samplingFrequency = -1; boolean includeNextPoint = false; LocationIterator locationIterator = null; try { locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); while (locationIterator.hasNext()) { Location location = locationIterator.next(); long locationId = locationIterator.getLocationId(); // Stop if past the last wanted point if (maxPointId != -1L && locationId > maxPointId) { break; } if (localFirstSeenLocationId == -1) { localFirstSeenLocationId = locationId; } if (samplingFrequency == -1) { long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenLocationId); samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints); } if (!LocationUtils.isValidLocation(location)) { // TODO: also include the last valid point before a split for (TrackDataListener trackDataListener : sampledInListeners) { trackDataListener.onSegmentSplit(location); includeNextPoint = true; } } else { // Also include the last point if the selected track is not recording. if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) { includeNextPoint = false; for (TrackDataListener trackDataListener : sampledInListeners) { trackDataListener.onSampledInTrackPoint(location); } } else { for (TrackDataListener trackDataListener : sampledOutListeners) { trackDataListener.onSampledOutTrackPoint(location); } } } localNumLoadedPoints++; localLastSeenLocationId = locationId; } } finally { if (locationIterator != null) { locationIterator.close(); } } if (updateSamplingState) { numLoadedPoints = localNumLoadedPoints; firstSeenLocationId = localFirstSeenLocationId; lastSeenLocationId = localLastSeenLocationId; } for (TrackDataListener listener : sampledInListeners) { listener.onNewTrackPointsDone(); } } /** * Resets the track points sampling states. */ private void resetSamplingState() { numLoadedPoints = 0; firstSeenLocationId = -1L; lastSeenLocationId = -1L; } /** * Creates a {@link DataSource}. */ @VisibleForTesting protected DataSource newDataSource() { return new DataSource(context); } /** * Run in the handler thread. * * @param runnable the runnable */ @VisibleForTesting protected void runInHanderThread(Runnable runnable) { if (handler == null) { // Use a Throwable to ensure the stack trace is logged. Log.d(TAG, "handler is null.", new Throwable()); return; } handler.post(runnable); } /** * Gets the value selectedTrackId. * * @return the selectedTrackId */ public long getSelectedTrackId() { return selectedTrackId; } /** * Gets the recordingGpsAccuracy. */ @VisibleForTesting int getRecordingGpsAccuracy() { return recordingGpsAccuracy; } /** * Gets the metricUnits. * * @return the metricUnits */ @VisibleForTesting boolean isMetricUnits() { return metricUnits; } /** * Gets the reportSpeed. * * @return the reportSpeed */ @VisibleForTesting boolean isReportSpeed() { return reportSpeed; } }