diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/SensorStateActivity.java b/myTracks/src/main/java/com/google/android/apps/mytracks/SensorStateActivity.java index f38d38154..444e27e9f 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/SensorStateActivity.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/SensorStateActivity.java @@ -55,11 +55,7 @@ public class SensorStateActivity extends AbstractActivity { // Check if service is available and recording boolean isRecording = false; if (trackRecordingService != null) { - try { - isRecording = trackRecordingService.isRecording(); - } catch (RemoteException e) { - Log.e(TAG, "Unable to determine if the track recording service is recording.", e); - } + isRecording = trackRecordingService.isRecording(); } if (!isRecording) { @@ -151,20 +147,13 @@ public class SensorStateActivity extends AbstractActivity { if (trackRecordingService == null) { Log.d(TAG, "Cannot get the track recording service."); } else { - try { - sensorState = Sensor.SensorState.valueOf(trackRecordingService.getSensorState()); - } catch (RemoteException e) { - Log.e(TAG, "Cannote read sensor state.", e); - sensorState = Sensor.SensorState.NONE; - } + sensorState = Sensor.SensorState.valueOf(trackRecordingService.getSensorState()); try { byte[] buff = trackRecordingService.getSensorData(); if (buff != null) { sensorDataSet = Sensor.SensorDataSet.parseFrom(buff); } - } catch (RemoteException e) { - Log.e(TAG, "Cannot read sensor data set.", e); } catch (InvalidProtocolBufferException e) { Log.e(TAG, "Cannot read sensor data set.", e); } diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/TrackController.java b/myTracks/src/main/java/com/google/android/apps/mytracks/TrackController.java index 46831b76d..d474fd441 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/TrackController.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/TrackController.java @@ -146,11 +146,6 @@ public class TrackController { */ private long getTotalTime() { ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); - try { - return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L; - } catch (RemoteException e) { - Log.e(TAG, "exception", e); - return 0L; - } + return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L; } } diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/TrackListActivity.java b/myTracks/src/main/java/com/google/android/apps/mytracks/TrackListActivity.java index 73849e4da..7edc50772 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/TrackListActivity.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/TrackListActivity.java @@ -115,30 +115,19 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType } if (startNewRecording) { startGps = false; - try { - long trackId = service.startNewTrack(); - startNewRecording = false; - Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class) - .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); - startActivity(intent); - Toast.makeText( - TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT) - .show(); - } catch (RemoteException e) { - Toast.makeText( - TrackListActivity.this, R.string.track_list_record_error, Toast.LENGTH_LONG).show(); - Log.e(TAG, "Unable to start a new recording.", e); - } + + long trackId = service.startNewTrack(); + startNewRecording = false; + Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class) + .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); + startActivity(intent); + Toast.makeText( + TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT) + .show(); } if (startGps) { - try { - service.startGps(); - startGps = false; - } catch (RemoteException e) { - Toast.makeText(TrackListActivity.this, R.string.gps_starting_error, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Unable to start gps"); - } + service.startGps(); + startGps = false; } } }; @@ -477,11 +466,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType } else { ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); if (trackRecordingService != null) { - try { - trackRecordingService.stopGps(); - } catch (RemoteException e) { - Log.e(TAG, "Unable to stop gps.", e); - } + trackRecordingService.stopGps(); } trackRecordingServiceConnection.unbindAndStop(); } diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/ControlRecordingService.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/ControlRecordingService.java index 64c4a5da3..5edf74dc0 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/ControlRecordingService.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/ControlRecordingService.java @@ -60,7 +60,7 @@ public class ControlRecordingService extends IntentService implements ServiceCon @Override public void onServiceConnected(ComponentName name, IBinder service) { - trackRecordingService = ITrackRecordingService.Stub.asInterface(service); + trackRecordingService = (ITrackRecordingService)service; notifyConnected(); } @@ -109,18 +109,14 @@ public class ControlRecordingService extends IntentService implements ServiceCon void onHandleIntent(Intent intent, ITrackRecordingService service) { String action = intent.getAction(); if (action != null) { - try { - if (action.equals(getString(R.string.track_action_start))) { - service.startNewTrack(); - } else if (action.equals(getString(R.string.track_action_end))) { - service.endCurrentTrack(); - } else if (action.equals(getString(R.string.track_action_pause))) { - service.pauseCurrentTrack(); - } else if (action.equals(getString(R.string.track_action_resume))) { - service.resumeCurrentTrack(); - } - } catch (RemoteException e) { - Log.d(TAG, "ControlRecordingService onHandleIntent RemoteException", e); + if (action.equals(getString(R.string.track_action_start))) { + service.startNewTrack(); + } else if (action.equals(getString(R.string.track_action_end))) { + service.endCurrentTrack(); + } else if (action.equals(getString(R.string.track_action_pause))) { + service.pauseCurrentTrack(); + } else if (action.equals(getString(R.string.track_action_resume))) { + service.resumeCurrentTrack(); } } unbindService(this); diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl b/myTracks/src/main/java/com/google/android/apps/mytracks/services/ITrackRecordingService.java similarity index 93% rename from myTracks/src/main/java/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl rename to myTracks/src/main/java/com/google/android/apps/mytracks/services/ITrackRecordingService.java index 59b80b566..512e125d8 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/ITrackRecordingService.java @@ -15,13 +15,15 @@ */ package com.google.android.apps.mytracks.services; +import android.location.Location; + import com.google.android.apps.mytracks.content.WaypointCreationRequest; /** * MyTracks service. * This service is the process that actually records and manages tracks. */ -interface ITrackRecordingService { +public interface ITrackRecordingService { /** * Starts gps. @@ -81,7 +83,7 @@ interface ITrackRecordingService { * @param request the details of the waypoint to be inserted * @return the ID of the inserted waypoint */ - long insertWaypoint(in WaypointCreationRequest request); + long insertWaypoint(WaypointCreationRequest request); /** * Inserts a track point in the current recording track. @@ -91,7 +93,7 @@ interface ITrackRecordingService { * * @param location the track point to be inserted */ - void insertTrackPoint(in Location location); + void insertTrackPoint(Location location); /** * Gets the current sensor data. Returns null if there is no data. diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingService.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingService.java index 7921ac44e..28603ea3a 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -39,7 +39,6 @@ import android.os.PowerManager.WakeLock; import android.os.Process; import android.util.Log; -import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; import androidx.core.app.TaskStackBuilder; @@ -80,1181 +79,1151 @@ import java.util.concurrent.Executors; /** * A background service that registers a location listener and records track * points. Track points are saved to the {@link MyTracksProvider}. - * + * * @author Leif Hendrik Wilden */ public class TrackRecordingService extends Service { - /** - * The name of extra intent property to indicate whether we want to resume a - * previously recorded track. - */ - public static final String RESUME_TRACK_EXTRA_NAME = "com.google.android.apps.mytracks.RESUME_TRACK"; + /** + * The name of extra intent property to indicate whether we want to resume a + * previously recorded track. + */ + public static final String RESUME_TRACK_EXTRA_NAME = "com.google.android.apps.mytracks.RESUME_TRACK"; - public static final double PAUSE_LATITUDE = 100.0; - public static final double RESUME_LATITUDE = 200.0; - - /** - * Anything faster than that (in meters per second) will be considered moving. - */ - public static final double MAX_NO_MOVEMENT_SPEED = 0.224; - - private static final String TAG = TrackRecordingService.class.getSimpleName(); + public static final double PAUSE_LATITUDE = 100.0; + public static final double RESUME_LATITUDE = 200.0; - // 1 minute in milliseconds - private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS); + /** + * Anything faster than that (in meters per second) will be considered moving. + */ + public static final double MAX_NO_MOVEMENT_SPEED = 0.224; - @VisibleForTesting - static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; + private static final String TAG = TrackRecordingService.class.getSimpleName(); - // The following variables are set in onCreate: - private ExecutorService executorService; - private Context context; - private MyTracksProviderUtils myTracksProviderUtils; - private Handler handler; - private MyTracksLocationManager myTracksLocationManager; - private PeriodicTaskExecutor voiceExecutor; - private PeriodicTaskExecutor splitExecutor; - private SharedPreferences sharedPreferences; - private long recordingTrackId; - private boolean recordingTrackPaused; - private LocationListenerPolicy locationListenerPolicy; - private int recordingDistanceInterval; - private int maxRecordingDistance; - private int recordingGpsAccuracy; - private int autoResumeTrackTimeout; - private long currentRecordingInterval; + // 1 minute in milliseconds + private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS); - // The following variables are set when recording: - private TripStatisticsUpdater trackTripStatisticsUpdater; - private TripStatisticsUpdater markerTripStatisticsUpdater; - private WakeLock wakeLock; - private SensorManager sensorManager; - private Location lastLocation; - private boolean currentSegmentHasLocation; - private boolean isIdle; // true if idle + @VisibleForTesting + static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; - private ServiceBinder binder = new ServiceBinder(this); + // The following variables are set in onCreate: + private ExecutorService executorService; + private Context context; + private MyTracksProviderUtils myTracksProviderUtils; + private Handler handler; + private MyTracksLocationManager myTracksLocationManager; + private PeriodicTaskExecutor voiceExecutor; + private PeriodicTaskExecutor splitExecutor; + private SharedPreferences sharedPreferences; + private long recordingTrackId; + private boolean recordingTrackPaused; + private LocationListenerPolicy locationListenerPolicy; + private int recordingDistanceInterval; + private int maxRecordingDistance; + private int recordingGpsAccuracy; + private int autoResumeTrackTimeout; + private long currentRecordingInterval; - /* - * Note that sharedPreferenceChangeListener cannot be an anonymous inner - * class. Anonymous inner class will get garbage collected. - */ - private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { - @Override + // The following variables are set when recording: + private TripStatisticsUpdater trackTripStatisticsUpdater; + private TripStatisticsUpdater markerTripStatisticsUpdater; + private WakeLock wakeLock; + private SensorManager sensorManager; + private Location lastLocation; + private boolean currentSegmentHasLocation; + private boolean isIdle; // true if idle + + private ServiceBinder binder = new ServiceBinder(this); + + /* + * Note that sharedPreferenceChangeListener cannot be an anonymous inner + * class. Anonymous inner class will get garbage collected. + */ + private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { + @Override public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) { - long trackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); - /* - * Only through the TrackRecordingService can one stop a recording - * and set the recordingTrackId to -1L. - */ - if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { - recordingTrackId = trackId; + if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) { + long trackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); + /* + * Only through the TrackRecordingService can one stop a recording + * and set the recordingTrackId to -1L. + */ + if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { + recordingTrackId = trackId; + } } - } - 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))) { - boolean metricUnits = PreferencesUtils.isMetricUnits(context); - voiceExecutor.setMetricUnits(metricUnits); - splitExecutor.setMetricUnits(metricUnits); - } - if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.voice_frequency_key))) { - voiceExecutor.setTaskFrequency(PreferencesUtils.getInt( - context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT)); - } - if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.split_frequency_key))) { - splitExecutor.setTaskFrequency(PreferencesUtils.getInt( - context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); - } - if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.min_recording_interval_key))) { - int minRecordingInterval = PreferencesUtils.getInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); - switch (minRecordingInterval) { - case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE: - // Choose battery life over moving time accuracy. - locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5); - break; - case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: - // Get all the updates. - locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0); - break; - default: - locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND); + 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))) { + boolean metricUnits = PreferencesUtils.isMetricUnits(context); + voiceExecutor.setMetricUnits(metricUnits); + splitExecutor.setMetricUnits(metricUnits); + } + if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.voice_frequency_key))) { + voiceExecutor.setTaskFrequency(PreferencesUtils.getInt( + context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT)); + } + if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.split_frequency_key))) { + splitExecutor.setTaskFrequency(PreferencesUtils.getInt( + context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); + } + if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.min_recording_interval_key))) { + int minRecordingInterval = PreferencesUtils.getInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); + switch (minRecordingInterval) { + case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE: + // Choose battery life over moving time accuracy. + locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5); + break; + case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: + // Get all the updates. + locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0); + break; + default: + locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND); + } + } + 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 || key.equals(PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) { + maxRecordingDistance = PreferencesUtils.getInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); + } + 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 || key.equals(PreferencesUtils.getKey(context, R.string.auto_resume_track_timeout_key))) { + autoResumeTrackTimeout = PreferencesUtils.getInt(context, R.string.auto_resume_track_timeout_key, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); } - } - 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 || key.equals(PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) { - maxRecordingDistance = PreferencesUtils.getInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); - } - 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 || key.equals(PreferencesUtils.getKey(context, R.string.auto_resume_track_timeout_key))) { - autoResumeTrackTimeout = PreferencesUtils.getInt(context,R.string.auto_resume_track_timeout_key, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); - } } - }; + }; - private LocationListener locationListener = new LocationListener() { + private LocationListener locationListener = new LocationListener() { - @Override - public void onLocationChanged(final Location location) { - if (myTracksLocationManager == null - || executorService == null - || executorService.isShutdown() - || executorService.isTerminated()) { - return; - } - executorService.submit(new Runnable() { + @Override + public void onLocationChanged(final Location location) { + if (myTracksLocationManager == null + || executorService == null + || executorService.isShutdown() + || executorService.isTerminated()) { + return; + } + executorService.submit(new Runnable() { + @Override + public void run() { + onLocationChangedAsync(location); + } + }); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + Log.w(TAG, "LocationListener.onStatusChanged(): is not implemented."); + } + + @Override + public void onProviderEnabled(String provider) { + Log.w(TAG, "LocationListener.onProviderEnabled(): is not implemented."); + } + + @Override + public void onProviderDisabled(String provider) { + Log.w(TAG, "LocationListener.onProviderDisabled(): is not implemented."); + } + }; + + private final Runnable registerLocationRunnable = new Runnable() { @Override public void run() { - onLocationChangedAsync(location); + if (isRecording() && !isPaused()) { + registerLocationListener(); + } + handler.postDelayed(this, ONE_MINUTE); } - }); - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - Log.w(TAG, "LocationListener.onStatusChanged(): is not implemented."); - } - - @Override - public void onProviderEnabled(String provider) { - Log.w(TAG, "LocationListener.onProviderEnabled(): is not implemented."); - } - - @Override - public void onProviderDisabled(String provider) { - Log.w(TAG, "LocationListener.onProviderDisabled(): is not implemented."); - } - }; - - private final Runnable registerLocationRunnable = new Runnable() { - @Override - public void run() { - if (isRecording() && !isPaused()) { - registerLocationListener(); - } - handler.postDelayed(this, ONE_MINUTE); - } - }; - - /* - * Note that this service, through the AndroidManifest.xml, is configured to - * allow both MyTracks and third party apps to invoke it. For the onCreate - * callback, we cannot tell whether the caller is MyTracks or a third party - * app, thus it cannot start/stop a recording or write/update MyTracks - * database. - */ - @Override - public void onCreate() { - super.onCreate(); - executorService = Executors.newSingleThreadExecutor(); - context = this; - myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this); - handler = new Handler(); - myTracksLocationManager = new MyTracksLocationManager(this, handler.getLooper()); - voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); - splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory()); - sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - - // onSharedPreferenceChanged might not set recordingTrackId. - recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; - - // Require voiceExecutor and splitExecutor to be created. - sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null); - - handler.post(registerLocationRunnable); - - /* - * Try to restart the previous recording track in case the service has been - * restarted by the system, which can sometimes happen. - */ - Track track = myTracksProviderUtils.getTrack(recordingTrackId); - if (track != null) { - restartTrack(track); - } else { - if (isRecording()) { - Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId); - updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); - } - showNotification(false); - } - } - - /* - * Note that this service, through the AndroidManifest.xml, is configured to - * allow both MyTracks and third party apps to invoke it. For the - * onStartCommand callback, we cannot tell whether the caller is MyTracks or a - * third party app, thus it cannot start/stop a recording or write/update - * MyTracks database. - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - handleStartCommand(intent, startId); - return START_STICKY; - } - - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - @Override - public void onDestroy() { - if (sensorManager != null) { - SensorManagerFactory.releaseSystemSensorManager(); - sensorManager = null; - } - - // Reverse order from onCreate - showNotification(false); - - handler.removeCallbacks(registerLocationRunnable); - unregisterLocationListener(); - - // unregister sharedPreferences before shutting down splitExecutor and voiceExecutor - sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - - try { - splitExecutor.shutdown(); - } finally { - splitExecutor = null; - } - - try { - voiceExecutor.shutdown(); - } finally { - voiceExecutor = null; - } - - myTracksLocationManager = null; - myTracksProviderUtils = null; - - binder.detachFromService(); - binder = null; - - // This should be the next to last operation - releaseWakeLock(); + }; /* - * Shutdown the executorService last to avoid sending events to a dead - * executor. + * Note that this service, through the AndroidManifest.xml, is configured to + * allow both MyTracks and third party apps to invoke it. For the onCreate + * callback, we cannot tell whether the caller is MyTracks or a third party + * app, thus it cannot start/stop a recording or write/update MyTracks + * database. */ - executorService.shutdown(); - super.onDestroy(); - } + @Override + public void onCreate() { + super.onCreate(); + executorService = Executors.newSingleThreadExecutor(); + context = this; + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this); + handler = new Handler(); + myTracksLocationManager = new MyTracksLocationManager(this, handler.getLooper()); + voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); + splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory()); + sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - /** - * Returns true if the service is recording. - */ - public boolean isRecording() { - return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; - } + // onSharedPreferenceChanged might not set recordingTrackId. + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; - /** - * Returns true if the current recording is paused. - */ - public boolean isPaused() { - return recordingTrackPaused; - } + // Require voiceExecutor and splitExecutor to be created. + sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null); - /** - * Gets the trip statistics. - */ - public TripStatistics getTripStatistics() { - if (trackTripStatisticsUpdater == null) { - return null; - } - return trackTripStatisticsUpdater.getTripStatistics(); - } + handler.post(registerLocationRunnable); - /** - * Inserts a waypoint. - * - * @param waypointCreationRequest the waypoint creation request - * @return the waypoint id - */ - public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { - if (!isRecording() || isPaused()) { - return -1L; - } - - WaypointType waypointType = waypointCreationRequest.getType(); - boolean isStatistics = waypointType == WaypointType.STATISTICS; - - // Get name - String name; - if (waypointCreationRequest.getName() != null) { - name = waypointCreationRequest.getName(); - } else { - int nextWaypointNumber = myTracksProviderUtils.getNextWaypointNumber(recordingTrackId, waypointType); - if (nextWaypointNumber == -1) { - nextWaypointNumber = 0; - } - name = getString(isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format, nextWaypointNumber); - } - - // Get category - String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest.getCategory() : ""; - - // Get tripStatistics, description, and icon - TripStatistics tripStatistics; - String description; - String icon; - if (isStatistics) { - long now = System.currentTimeMillis(); - markerTripStatisticsUpdater.updateTime(now); - tripStatistics = markerTripStatisticsUpdater.getTripStatistics(); - markerTripStatisticsUpdater = new TripStatisticsUpdater(now); - description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics); - icon = getString(R.string.marker_statistics_icon_url); - } else { - tripStatistics = null; - description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest.getDescription() : ""; - //TODO Bundle icon? - icon = getString(R.string.marker_waypoint_icon_url); - } - - // Get length and duration - double length; - long duration; - Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId); - if (location != null && trackTripStatisticsUpdater != null) { - TripStatistics stats = trackTripStatisticsUpdater.getTripStatistics(); - length = stats.getTotalDistance(); - duration = stats.getTotalTime(); - } else { - if (!waypointCreationRequest.isTrackStatistics()) { - return -1L; - } - // For track statistics, make it an impossible location - location = new Location(""); - location.setLatitude(100); - location.setLongitude(180); - length = 0.0; - duration = 0L; - } - - String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : ""; - - // Insert waypoint - Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, waypointType, length, duration, -1L, -1L, location, tripStatistics, photoUrl); - Uri uri = myTracksProviderUtils.insertWaypoint(waypoint); - return Long.parseLong(uri.getLastPathSegment()); - } - - /** - * Starts the service as a foreground service. - * - * @param pendingIntent the notification pending intent - * @param messageId the notification message id - */ - @VisibleForTesting - protected void startForegroundService(PendingIntent pendingIntent, int messageId) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel notificationChannel = new NotificationChannel(getString(R.string.my_tracks_app_name), getString(R.string.my_tracks_app_name), NotificationManager.IMPORTANCE_DEFAULT); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - assert manager != null; - manager.createNotificationChannel(notificationChannel); - } - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.my_tracks_app_name)) - .setContentIntent(pendingIntent) - .setContentText(getString(messageId)) - .setContentTitle(getString(R.string.my_tracks_app_name)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_stat_notify_recording) - .setCategory(Notification.CATEGORY_SERVICE) - .setWhen(System.currentTimeMillis()); - startForeground(1, builder.build()); - } - - /** - * Stops the service as a foreground service. - */ - @VisibleForTesting - protected void stopForegroundService() { - stopForeground(true); - } - - /** - * Handles start command. - * - * @param intent the intent - * @param startId the start id - */ - private void handleStartCommand(Intent intent, int startId) { - // Check if the service is called to resume track (from phone reboot) - if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false) && !shouldResumeTrack()) { - Log.i(TAG, "Stop resume track."); - updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); - stopSelfResult(startId); - } - } - - /** - * Returns true if should resume. - */ - private boolean shouldResumeTrack() { - Track track = myTracksProviderUtils.getTrack(recordingTrackId); - - if (track == null) { - Log.d(TAG, "Not resuming. Track is null."); - return false; - } - int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT); - if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { - Log.d(TAG, "Not resuming. Exceeded maximum retry attempts."); - return false; - } - PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retries + 1); - - if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER) { - Log.d(TAG, "Not resuming. Auto-resume track timeout set to never."); - return false; - } else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) { - Log.d(TAG, "Resuming. Auto-resume track timeout set to always."); - return true; - } - - if (track.getTripStatistics() == null) { - Log.d(TAG, "Not resuming. No trip statistics."); - return false; - } - long stopTime = track.getTripStatistics().getStopTime(); - return stopTime > 0 && (System.currentTimeMillis() - stopTime) <= autoResumeTrackTimeout * ONE_MINUTE; - } - - /** - * Starts a new track. - * - * @return the track id - */ - private long startNewTrack() { - if (isRecording()) { - Log.d(TAG, "Ignore startNewTrack. Already recording."); - return -1L; - } - long now = System.currentTimeMillis(); - trackTripStatisticsUpdater = new TripStatisticsUpdater(now); - markerTripStatisticsUpdater = new TripStatisticsUpdater(now); - - // Insert a track - Track track = new Track(); - Uri uri = myTracksProviderUtils.insertTrack(track); - long trackId = Long.parseLong(uri.getLastPathSegment()); - - // Update shared preferences - updateRecordingState(trackId, false); - PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0); - - // Update database - track.setId(trackId); - track.setName(TrackNameUtils.getTrackName(this, trackId, now, null)); - - String category = PreferencesUtils.getString( - this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT); - track.setCategory(category); - track.setIcon(TrackIconUtils.getIconValue(this, category)); - track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics()); - myTracksProviderUtils.updateTrack(track); - insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); - - startRecording(true); - return trackId; - } - - /** - * Restart a track. - * - * @param track the track - */ - private void restartTrack(Track track) { - Log.d(TAG, "Restarting track: " + track.getId()); - - TripStatistics tripStatistics = track.getTripStatistics(); - trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime()); - - long markerStartTime; - Waypoint waypoint = myTracksProviderUtils.getLastWaypoint(recordingTrackId, WaypointType.STATISTICS); - if (waypoint != null && waypoint.getTripStatistics() != null) { - markerStartTime = waypoint.getTripStatistics().getStopTime(); - } else { - markerStartTime = tripStatistics.getStartTime(); - } - markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime); - - LocationIterator locationIterator = null; - try { - locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); - - while (locationIterator.hasNext()) { - Location location = locationIterator.next(); - trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); - if (location.getTime() > markerStartTime) { - markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); - } - } - } catch (RuntimeException e) { - Log.e(TAG, "RuntimeException", e); - } finally { - if (locationIterator != null) { - locationIterator.close(); - } - } - startRecording(true); - } - - /** - * Resumes current track. - */ - private void resumeCurrentTrack() { - if (!isRecording() || !isPaused()) { - Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused."); - return; - } - - // Update shared preferences - recordingTrackPaused = false; - PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false); - - // Update database - Track track = myTracksProviderUtils.getTrack(recordingTrackId); - if (track != null) { - Location resume = new Location(LocationManager.GPS_PROVIDER); - resume.setLongitude(0); - resume.setLatitude(RESUME_LATITUDE); - resume.setTime(System.currentTimeMillis()); - insertLocation(track, resume, null); - } - - startRecording(false); - } - - /** - * Common code for starting a new track, resuming a track, or restarting after - * phone reboot. - * - * @param trackStarted true if track is started, false if track is resumed - */ - private void startRecording(boolean trackStarted) { - // Update instance variables - sensorManager = SensorManagerFactory.getSystemSensorManager(this); - lastLocation = null; - currentSegmentHasLocation = false; - isIdle = false; - - startGps(); - sendTrackBroadcast(trackStarted ? R.string.track_started_broadcast_action : R.string.track_resumed_broadcast_action, recordingTrackId); - - // Restore periodic tasks - voiceExecutor.restore(); - splitExecutor.restore(); - } - - /** - * Starts gps. - */ - private void startGps() { - wakeLock = SystemUtils.acquireWakeLock(this, wakeLock); - registerLocationListener(); - showNotification(true); - } - - /** - * Ends the current track. - */ - private void endCurrentTrack() { - if (!isRecording()) { - Log.d(TAG, "Ignore endCurrentTrack. Not recording."); - return; - } - - // Need to remember the recordingTrackId before setting it to -1L - long trackId = recordingTrackId; - boolean paused = recordingTrackPaused; - - // Update shared preferences - updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); - - // Update database - Track track = myTracksProviderUtils.getTrack(trackId); - if (track != null) { - // If not paused, add the last location - if (!paused) { - insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId)); - - // Update the recording track time - updateRecordingTrack(track, myTracksProviderUtils.getLastTrackPointId(trackId), false); - } - - String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime(), myTracksProviderUtils.getFirstValidTrackPoint(trackId)); - if (trackName != null && !trackName.equals(track.getName())) { - track.setName(trackName); - myTracksProviderUtils.updateTrack(track); - } - } - endRecording(true, trackId); - } - - /** - * Pauses the current track. - */ - private void pauseCurrentTrack() { - if (!isRecording() || isPaused()) { - Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused."); - return; - } - - // Update shared preferences - recordingTrackPaused = true; - PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true); - - // Update database - Track track = myTracksProviderUtils.getTrack(recordingTrackId); - if (track != null) { - insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(track.getId())); - - Location pause = new Location(LocationManager.GPS_PROVIDER); - pause.setLongitude(0); - pause.setLatitude(PAUSE_LATITUDE); - pause.setTime(System.currentTimeMillis()); - insertLocation(track, pause, null); - } - - endRecording(false, recordingTrackId); - } - - /** - * Common code for ending a track or pausing a track. - * - * @param trackStopped true if track is stopped, false if track is paused - * @param trackId the track id - */ - private void endRecording(boolean trackStopped, long trackId) { - // Shutdown periodic tasks - voiceExecutor.shutdown(); - splitExecutor.shutdown(); - - // Update instance variables - if (sensorManager != null) { - SensorManagerFactory.releaseSystemSensorManager(); - sensorManager = null; - } - lastLocation = null; - - sendTrackBroadcast(trackStopped ? R.string.track_stopped_broadcast_action : R.string.track_paused_broadcast_action, trackId); - stopGps(trackStopped); - } - - /** - * Stops gps. - * - * @param stop true to stop self - */ - private void stopGps(boolean stop) { - unregisterLocationListener(); - showNotification(false); - releaseWakeLock(); - if (stop) { - stopSelf(); - } - } - - /** - * Gets the last valid track point in the current segment. Returns null if not - * available. - * - * @param trackId the track id - */ - private Location getLastValidTrackPointInCurrentSegment(long trackId) { - if (!currentSegmentHasLocation) { - return null; - } - return myTracksProviderUtils.getLastValidTrackPoint(trackId); - } - - /** - * Updates the recording states. - * - * @param trackId the recording track id - * @param paused true if the recording is paused - */ - private void updateRecordingState(long trackId, boolean paused) { - recordingTrackId = trackId; - PreferencesUtils.setLong(this, R.string.recording_track_id_key, trackId); - recordingTrackPaused = paused; - PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused); - } - - /** - * Called when location changed. - * - * @param location the location - */ - private void onLocationChangedAsync(Location location) { - try { - if (!isRecording() || isPaused()) { - Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused."); - return; - } - - Track track = myTracksProviderUtils.getTrack(recordingTrackId); - if (track == null) { - Log.w(TAG, "Ignore onLocationChangedAsync. No track."); - return; - } - - if (!LocationUtils.isValidLocation(location)) { - Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid."); - return; - } - - if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) { - Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy."); - return; - } - - //TODO Necessary? - // Fix for phones that do not set the time field - if (location.getTime() == 0L) { - location.setTime(System.currentTimeMillis()); - } - - Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId()); - long idleTime = 0L; - if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) { - idleTime = location.getTime() - lastValidTrackPoint.getTime(); - } - locationListenerPolicy.updateIdleTime(idleTime); - if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) { - registerLocationListener(); - } - - SensorDataSet sensorDataSet = getSensorDataSet(); - if (sensorDataSet != null) { - location = new MyTracksLocation(location, sensorDataSet); - } - - // Always insert the first segment location - if (!currentSegmentHasLocation) { - insertLocation(track, location, null); - currentSegmentHasLocation = true; - lastLocation = location; - return; - } - - if (!LocationUtils.isValidLocation(lastValidTrackPoint)) { /* - * Should not happen. The current segment should have a location. Just - * insert the current location. + * Try to restart the previous recording track in case the service has been + * restarted by the system, which can sometimes happen. */ - insertLocation(track, location, null); - lastLocation = location; - return; - } - - double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint); - if (distanceToLastTrackLocation > maxRecordingDistance) { - insertLocation(track, lastLocation, lastValidTrackPoint); - - Location pause = new Location(LocationManager.GPS_PROVIDER); - pause.setLongitude(0); - pause.setLatitude(PAUSE_LATITUDE); - pause.setTime(lastLocation.getTime()); - insertLocation(track, pause, null); - - insertLocation(track, location, null); - isIdle = false; - } else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) { - insertLocation(track, lastLocation, lastValidTrackPoint); - insertLocation(track, location, null); - isIdle = false; - } else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) { - insertLocation(track, lastLocation, lastValidTrackPoint); - insertLocation(track, location, null); - isIdle = true; - } else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) { - insertLocation(track, lastLocation, lastValidTrackPoint); - insertLocation(track, location, null); - isIdle = false; - } else { - Log.d(TAG, "Not recording location, idle"); - } - lastLocation = location; - } catch (Error e) { - Log.e(TAG, "Error in onLocationChangedAsync", e); - throw e; - } catch (RuntimeException e) { - Log.e(TAG, "RuntimeException in onLocationChangedAsync", e); - throw e; - } - } - - /** - * Inserts a location. - * - * @param track the track - * @param location the location - * @param lastValidTrackPoint the last valid track point, can be null - */ - private void insertLocation(Track track, Location location, Location lastValidTrackPoint) { - if (location == null) { - Log.w(TAG, "Ignore insertLocation. loation is null."); - return; - } - // Do not insert if inserted already - if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == location.getTime()) { - Log.w(TAG, "Ignore insertLocation. location time same as last valid track point time."); - return; + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + if (track != null) { + restartTrack(track); + } else { + if (isRecording()) { + Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId); + updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); + } + showNotification(false); + } } - try { - Uri uri = myTracksProviderUtils.insertTrackPoint(location, track.getId()); - long trackPointId = Long.parseLong(uri.getLastPathSegment()); - trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); - markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); - updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location)); - } catch (SQLiteException e) { - /* - * Insert failed, most likely because of SqlLite error code 5 - * (SQLite_BUSY). This is expected to happen extremely rarely (if our - * listener gets invoked twice at about the same time). - */ - Log.w(TAG, "SQLiteException", e); - } - voiceExecutor.update(); - splitExecutor.update(); - sendTrackBroadcast(R.string.track_update_broadcast_action, track.getId()); - } - - /** - * Updates the recording track time. Also updates the startId and the stopId. - * Increase the number of points if it is a new and valid track point. - * - * @param track the track - * @param lastTrackPointId the last track point id - * @param increaseNumberOfPoints true to increase the number of points - */ - private void updateRecordingTrack(Track track, long lastTrackPointId, boolean increaseNumberOfPoints) { - if (lastTrackPointId >= 0) { - if (track.getStartId() < 0) { - track.setStartId(lastTrackPointId); - } - track.setStopId(lastTrackPointId); - } - if (increaseNumberOfPoints) { - track.setNumberOfPoints(track.getNumberOfPoints() + 1); - } - - trackTripStatisticsUpdater.updateTime(System.currentTimeMillis()); - track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics()); - myTracksProviderUtils.updateTrack(track); - } - - private SensorDataSet getSensorDataSet() { - if (sensorManager == null || !sensorManager.isEnabled() || !sensorManager.isSensorDataSetValid()) { - return null; - } - return sensorManager.getSensorDataSet(); - } - - /** - * Registers the location listener. - */ - private void registerLocationListener() { - if (myTracksLocationManager == null) { - Log.e(TAG, "locationManager is null."); - return; - } - try { - long interval = locationListenerPolicy.getDesiredPollingInterval(); - myTracksLocationManager.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance(), locationListener); - currentRecordingInterval = interval; - } catch (RuntimeException e) { - Log.e(TAG, "Could not register location listener.", e); - } - } - - /** - * Unregisters the location manager. - */ - private void unregisterLocationListener() { - if (myTracksLocationManager == null) { - Log.e(TAG, "locationManager is null."); - return; - } - myTracksLocationManager.removeLocationUpdates(locationListener); - } - - /** - * Releases the wake lock. - */ - private void releaseWakeLock() { - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - wakeLock = null; - } - } - - /** - * Shows the notification. - * - * @param isGpsStarted true if GPS is started - */ - private void showNotification(boolean isGpsStarted) { - if (isRecording()) { - if (isPaused()) { - stopForegroundService(); - } else { - Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class) - .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId); - PendingIntent pendingIntent = TaskStackBuilder.create(this) - .addParentStack(TrackDetailActivity.class).addNextIntent(intent) - .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - startForegroundService(pendingIntent, R.string.track_record_notification); - } - } else { - // Not recording - if (isGpsStarted) { - Intent intent = IntentUtils.newIntent(this, TrackListActivity.class); - PendingIntent pendingIntent = TaskStackBuilder.create(this) - .addNextIntent(intent).getPendingIntent(0, 0); - startForegroundService(pendingIntent, R.string.gps_starting); - } else { - stopForegroundService(); - } - } - } - - /** - * Sends track broadcast. - * - * @param actionId the intent action id - * @param trackId the track id - */ - private void sendTrackBroadcast(int actionId, long trackId) { - Intent intent = new Intent().setAction(getString(actionId)) - .putExtra(getString(R.string.track_id_broadcast_extra), trackId); - sendBroadcast(intent, getString(R.string.permission_notification_value)); - if (PreferencesUtils.getBoolean( - this, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT)) { - sendBroadcast(intent, getString(R.string.broadcast_notifications_permission)); - } - } - - /** - * TODO: There is a bug in Android that leaks Binder instances. This bug is - * especially visible if we have a non-static class, as there is no way to - * nullify reference to the outer class (the service). A workaround is to use - * a static class and explicitly clear service and detach it from the - * underlying Binder. With this approach, we minimize the leak to 24 bytes per - * each service instance. For more details, see the following bug: - * http://code.google.com/p/android/issues/detail?id=6426. - */ - private static class ServiceBinder extends ITrackRecordingService.Stub { - private TrackRecordingService trackRecordingService; - private DeathRecipient deathRecipient; - - public ServiceBinder(TrackRecordingService trackRecordingService) { - this.trackRecordingService = trackRecordingService; + /* + * Note that this service, through the AndroidManifest.xml, is configured to + * allow both MyTracks and third party apps to invoke it. For the + * onStartCommand callback, we cannot tell whether the caller is MyTracks or a + * third party app, thus it cannot start/stop a recording or write/update + * MyTracks database. + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handleStartCommand(intent, startId); + return START_STICKY; } @Override - public boolean isBinderAlive() { - return trackRecordingService != null; + public IBinder onBind(Intent intent) { + return null; } @Override - public boolean pingBinder() { - return isBinderAlive(); + public void onDestroy() { + if (sensorManager != null) { + SensorManagerFactory.releaseSystemSensorManager(); + sensorManager = null; + } + + // Reverse order from onCreate + showNotification(false); + + handler.removeCallbacks(registerLocationRunnable); + unregisterLocationListener(); + + // unregister sharedPreferences before shutting down splitExecutor and voiceExecutor + sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); + + try { + splitExecutor.shutdown(); + } finally { + splitExecutor = null; + } + + try { + voiceExecutor.shutdown(); + } finally { + voiceExecutor = null; + } + + myTracksLocationManager = null; + myTracksProviderUtils = null; + + binder.detachFromService(); + binder = null; + + // This should be the next to last operation + releaseWakeLock(); + + /* + * Shutdown the executorService last to avoid sending events to a dead + * executor. + */ + executorService.shutdown(); + super.onDestroy(); } - @Override - public void linkToDeath(@NonNull DeathRecipient recipient, int flags) { - deathRecipient = recipient; - } - - @Override - public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { - if (!isBinderAlive()) { - return false; - } - deathRecipient = null; - return true; - } - - @Override - public void startGps() { - if (!canAccess()) { - return; - } - if (!trackRecordingService.isRecording()) { - trackRecordingService.startGps(); - } - } - - public void stopGps() { - if (!canAccess()) { - return; - } - if (!trackRecordingService.isRecording()) { - trackRecordingService.stopGps(true); - } - } - - @Override - public long startNewTrack() { - if (!canAccess()) { - return -1L; - } - return trackRecordingService.startNewTrack(); - } - - @Override - public void pauseCurrentTrack() { - if (!canAccess()) { - return; - } - trackRecordingService.pauseCurrentTrack(); - } - - @Override - public void resumeCurrentTrack() { - if (!canAccess()) { - return; - } - trackRecordingService.resumeCurrentTrack(); - } - - @Override - public void endCurrentTrack() { - if (!canAccess()) { - return; - } - trackRecordingService.endCurrentTrack(); - } - - @Override + /** + * Returns true if the service is recording. + */ public boolean isRecording() { - if (!canAccess()) { - return false; - } - return trackRecordingService.isRecording(); + return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; } - @Override + /** + * Returns true if the current recording is paused. + */ public boolean isPaused() { - if (!canAccess()) { - return false; - } - return trackRecordingService.isPaused(); + return recordingTrackPaused; } - @Override - public long getRecordingTrackId() { - if (!canAccess()) { - return -1L; - } - return trackRecordingService.recordingTrackId; + /** + * Gets the trip statistics. + */ + public TripStatistics getTripStatistics() { + if (trackTripStatisticsUpdater == null) { + return null; + } + return trackTripStatisticsUpdater.getTripStatistics(); } - @Override - public long getTotalTime() { - if (!canAccess()) { - return 0; - } - TripStatisticsUpdater updater = trackRecordingService.trackTripStatisticsUpdater; - if (updater == null) { - return 0; - } - if (!trackRecordingService.isPaused()) { - updater.updateTime(System.currentTimeMillis()); - } - return updater.getTripStatistics().getTotalTime(); - } - - @Override + /** + * Inserts a waypoint. + * + * @param waypointCreationRequest the waypoint creation request + * @return the waypoint id + */ public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { - if (!canAccess()) { - return -1L; - } - return trackRecordingService.insertWaypoint(waypointCreationRequest); - } + if (!isRecording() || isPaused()) { + return -1L; + } - @Override - public void insertTrackPoint(Location location) { - if (!canAccess()) { - return; - } - //TODO trackRecordingService.locationListener.onLocationChanged(location); - } + WaypointType waypointType = waypointCreationRequest.getType(); + boolean isStatistics = waypointType == WaypointType.STATISTICS; - @Override - public byte[] getSensorData() { - if (!canAccess()) { - return null; - } - if (trackRecordingService.sensorManager == null) { - Log.d(TAG, "sensorManager is null."); - return null; - } - if (trackRecordingService.getSensorDataSet() == null) { - Log.d(TAG, "Sensor data set is null."); - return null; - } - return trackRecordingService.sensorManager.getSensorDataSet().toByteArray(); - } + // Get name + String name; + if (waypointCreationRequest.getName() != null) { + name = waypointCreationRequest.getName(); + } else { + int nextWaypointNumber = myTracksProviderUtils.getNextWaypointNumber(recordingTrackId, waypointType); + if (nextWaypointNumber == -1) { + nextWaypointNumber = 0; + } + name = getString(isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format, nextWaypointNumber); + } - @Override - public int getSensorState() { - if (!canAccess()) { - return Sensor.SensorState.NONE.getNumber(); - } - if (trackRecordingService.sensorManager == null) { - Log.d(TAG, "sensorManager is null."); - return Sensor.SensorState.NONE.getNumber(); - } - return trackRecordingService.sensorManager.getSensorState().getNumber(); + // Get category + String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest.getCategory() : ""; + + // Get tripStatistics, description, and icon + TripStatistics tripStatistics; + String description; + String icon; + if (isStatistics) { + long now = System.currentTimeMillis(); + markerTripStatisticsUpdater.updateTime(now); + tripStatistics = markerTripStatisticsUpdater.getTripStatistics(); + markerTripStatisticsUpdater = new TripStatisticsUpdater(now); + description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics); + icon = getString(R.string.marker_statistics_icon_url); + } else { + tripStatistics = null; + description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest.getDescription() : ""; + //TODO Bundle icon? + icon = getString(R.string.marker_waypoint_icon_url); + } + + // Get length and duration + double length; + long duration; + Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId); + if (location != null && trackTripStatisticsUpdater != null) { + TripStatistics stats = trackTripStatisticsUpdater.getTripStatistics(); + length = stats.getTotalDistance(); + duration = stats.getTotalTime(); + } else { + if (!waypointCreationRequest.isTrackStatistics()) { + return -1L; + } + // For track statistics, make it an impossible location + location = new Location(""); + location.setLatitude(100); + location.setLongitude(180); + length = 0.0; + duration = 0L; + } + + String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : ""; + + // Insert waypoint + Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, waypointType, length, duration, -1L, -1L, location, tripStatistics, photoUrl); + Uri uri = myTracksProviderUtils.insertWaypoint(waypoint); + return Long.parseLong(uri.getLastPathSegment()); } /** - * Returns true if the RPC caller is from the same application or if the - * "Allow access" setting indicates that another app can invoke this - * service's RPCs. + * Starts the service as a foreground service. + * + * @param pendingIntent the notification pending intent + * @param messageId the notification message id */ - private boolean canAccess() { - // As a precondition for access, must check if the service is available. - if (trackRecordingService == null) { - throw new IllegalStateException("The track recording service has been detached!"); - } - if (Process.myPid() == Binder.getCallingPid()) { - return true; - } else { - return PreferencesUtils.getBoolean(trackRecordingService, R.string.allow_access_key, - PreferencesUtils.ALLOW_ACCESS_DEFAULT); - } + @VisibleForTesting + protected void startForegroundService(PendingIntent pendingIntent, int messageId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(getString(R.string.my_tracks_app_name), getString(R.string.my_tracks_app_name), NotificationManager.IMPORTANCE_DEFAULT); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + assert manager != null; + manager.createNotificationChannel(notificationChannel); + } + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.my_tracks_app_name)) + .setContentIntent(pendingIntent) + .setContentText(getString(messageId)) + .setContentTitle(getString(R.string.my_tracks_app_name)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_stat_notify_recording) + .setCategory(Notification.CATEGORY_SERVICE) + .setWhen(System.currentTimeMillis()); + startForeground(1, builder.build()); } /** - * Detaches from the track recording service. Clears the reference to the - * outer class to minimize the leak. + * Stops the service as a foreground service. */ - private void detachFromService() { - trackRecordingService = null; - attachInterface(null, null); - - if (deathRecipient != null) { - deathRecipient.binderDied(); - } + @VisibleForTesting + protected void stopForegroundService() { + stopForeground(true); + } + + /** + * Handles start command. + * + * @param intent the intent + * @param startId the start id + */ + private void handleStartCommand(Intent intent, int startId) { + // Check if the service is called to resume track (from phone reboot) + if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false) && !shouldResumeTrack()) { + Log.i(TAG, "Stop resume track."); + updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); + stopSelfResult(startId); + } + } + + /** + * Returns true if should resume. + */ + private boolean shouldResumeTrack() { + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + + if (track == null) { + Log.d(TAG, "Not resuming. Track is null."); + return false; + } + int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT); + if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { + Log.d(TAG, "Not resuming. Exceeded maximum retry attempts."); + return false; + } + PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retries + 1); + + if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER) { + Log.d(TAG, "Not resuming. Auto-resume track timeout set to never."); + return false; + } else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) { + Log.d(TAG, "Resuming. Auto-resume track timeout set to always."); + return true; + } + + if (track.getTripStatistics() == null) { + Log.d(TAG, "Not resuming. No trip statistics."); + return false; + } + long stopTime = track.getTripStatistics().getStopTime(); + return stopTime > 0 && (System.currentTimeMillis() - stopTime) <= autoResumeTrackTimeout * ONE_MINUTE; + } + + /** + * Starts a new track. + * + * @return the track id + */ + private long startNewTrack() { + if (isRecording()) { + Log.d(TAG, "Ignore startNewTrack. Already recording."); + return -1L; + } + long now = System.currentTimeMillis(); + trackTripStatisticsUpdater = new TripStatisticsUpdater(now); + markerTripStatisticsUpdater = new TripStatisticsUpdater(now); + + // Insert a track + Track track = new Track(); + Uri uri = myTracksProviderUtils.insertTrack(track); + long trackId = Long.parseLong(uri.getLastPathSegment()); + + // Update shared preferences + updateRecordingState(trackId, false); + PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0); + + // Update database + track.setId(trackId); + track.setName(TrackNameUtils.getTrackName(this, trackId, now, null)); + + String category = PreferencesUtils.getString( + this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT); + track.setCategory(category); + track.setIcon(TrackIconUtils.getIconValue(this, category)); + track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics()); + myTracksProviderUtils.updateTrack(track); + insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); + + startRecording(true); + return trackId; + } + + /** + * Restart a track. + * + * @param track the track + */ + private void restartTrack(Track track) { + Log.d(TAG, "Restarting track: " + track.getId()); + + TripStatistics tripStatistics = track.getTripStatistics(); + trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime()); + + long markerStartTime; + Waypoint waypoint = myTracksProviderUtils.getLastWaypoint(recordingTrackId, WaypointType.STATISTICS); + if (waypoint != null && waypoint.getTripStatistics() != null) { + markerStartTime = waypoint.getTripStatistics().getStopTime(); + } else { + markerStartTime = tripStatistics.getStartTime(); + } + markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime); + + LocationIterator locationIterator = null; + try { + locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + + while (locationIterator.hasNext()) { + Location location = locationIterator.next(); + trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); + if (location.getTime() > markerStartTime) { + markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); + } + } + } catch (RuntimeException e) { + Log.e(TAG, "RuntimeException", e); + } finally { + if (locationIterator != null) { + locationIterator.close(); + } + } + startRecording(true); + } + + /** + * Resumes current track. + */ + private void resumeCurrentTrack() { + if (!isRecording() || !isPaused()) { + Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused."); + return; + } + + // Update shared preferences + recordingTrackPaused = false; + PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false); + + // Update database + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + if (track != null) { + Location resume = new Location(LocationManager.GPS_PROVIDER); + resume.setLongitude(0); + resume.setLatitude(RESUME_LATITUDE); + resume.setTime(System.currentTimeMillis()); + insertLocation(track, resume, null); + } + + startRecording(false); + } + + /** + * Common code for starting a new track, resuming a track, or restarting after + * phone reboot. + * + * @param trackStarted true if track is started, false if track is resumed + */ + private void startRecording(boolean trackStarted) { + // Update instance variables + sensorManager = SensorManagerFactory.getSystemSensorManager(this); + lastLocation = null; + currentSegmentHasLocation = false; + isIdle = false; + + startGps(); + sendTrackBroadcast(trackStarted ? R.string.track_started_broadcast_action : R.string.track_resumed_broadcast_action, recordingTrackId); + + // Restore periodic tasks + voiceExecutor.restore(); + splitExecutor.restore(); + } + + /** + * Starts gps. + */ + private void startGps() { + wakeLock = SystemUtils.acquireWakeLock(this, wakeLock); + registerLocationListener(); + showNotification(true); + } + + /** + * Ends the current track. + */ + private void endCurrentTrack() { + if (!isRecording()) { + Log.d(TAG, "Ignore endCurrentTrack. Not recording."); + return; + } + + // Need to remember the recordingTrackId before setting it to -1L + long trackId = recordingTrackId; + boolean paused = recordingTrackPaused; + + // Update shared preferences + updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); + + // Update database + Track track = myTracksProviderUtils.getTrack(trackId); + if (track != null) { + // If not paused, add the last location + if (!paused) { + insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId)); + + // Update the recording track time + updateRecordingTrack(track, myTracksProviderUtils.getLastTrackPointId(trackId), false); + } + + String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime(), myTracksProviderUtils.getFirstValidTrackPoint(trackId)); + if (trackName != null && !trackName.equals(track.getName())) { + track.setName(trackName); + myTracksProviderUtils.updateTrack(track); + } + } + endRecording(true, trackId); + } + + /** + * Pauses the current track. + */ + private void pauseCurrentTrack() { + if (!isRecording() || isPaused()) { + Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused."); + return; + } + + // Update shared preferences + recordingTrackPaused = true; + PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true); + + // Update database + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + if (track != null) { + insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(track.getId())); + + Location pause = new Location(LocationManager.GPS_PROVIDER); + pause.setLongitude(0); + pause.setLatitude(PAUSE_LATITUDE); + pause.setTime(System.currentTimeMillis()); + insertLocation(track, pause, null); + } + + endRecording(false, recordingTrackId); + } + + /** + * Common code for ending a track or pausing a track. + * + * @param trackStopped true if track is stopped, false if track is paused + * @param trackId the track id + */ + private void endRecording(boolean trackStopped, long trackId) { + // Shutdown periodic tasks + voiceExecutor.shutdown(); + splitExecutor.shutdown(); + + // Update instance variables + if (sensorManager != null) { + SensorManagerFactory.releaseSystemSensorManager(); + sensorManager = null; + } + lastLocation = null; + + sendTrackBroadcast(trackStopped ? R.string.track_stopped_broadcast_action : R.string.track_paused_broadcast_action, trackId); + stopGps(trackStopped); + } + + /** + * Stops gps. + * + * @param stop true to stop self + */ + private void stopGps(boolean stop) { + unregisterLocationListener(); + showNotification(false); + releaseWakeLock(); + if (stop) { + stopSelf(); + } + } + + /** + * Gets the last valid track point in the current segment. Returns null if not + * available. + * + * @param trackId the track id + */ + private Location getLastValidTrackPointInCurrentSegment(long trackId) { + if (!currentSegmentHasLocation) { + return null; + } + return myTracksProviderUtils.getLastValidTrackPoint(trackId); + } + + /** + * Updates the recording states. + * + * @param trackId the recording track id + * @param paused true if the recording is paused + */ + private void updateRecordingState(long trackId, boolean paused) { + recordingTrackId = trackId; + PreferencesUtils.setLong(this, R.string.recording_track_id_key, trackId); + recordingTrackPaused = paused; + PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused); + } + + /** + * Called when location changed. + * + * @param location the location + */ + private void onLocationChangedAsync(Location location) { + try { + if (!isRecording() || isPaused()) { + Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused."); + return; + } + + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + if (track == null) { + Log.w(TAG, "Ignore onLocationChangedAsync. No track."); + return; + } + + if (!LocationUtils.isValidLocation(location)) { + Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid."); + return; + } + + if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) { + Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy."); + return; + } + + //TODO Necessary? + // Fix for phones that do not set the time field + if (location.getTime() == 0L) { + location.setTime(System.currentTimeMillis()); + } + + Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId()); + long idleTime = 0L; + if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) { + idleTime = location.getTime() - lastValidTrackPoint.getTime(); + } + locationListenerPolicy.updateIdleTime(idleTime); + if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) { + registerLocationListener(); + } + + SensorDataSet sensorDataSet = getSensorDataSet(); + if (sensorDataSet != null) { + location = new MyTracksLocation(location, sensorDataSet); + } + + // Always insert the first segment location + if (!currentSegmentHasLocation) { + insertLocation(track, location, null); + currentSegmentHasLocation = true; + lastLocation = location; + return; + } + + if (!LocationUtils.isValidLocation(lastValidTrackPoint)) { + /* + * Should not happen. The current segment should have a location. Just + * insert the current location. + */ + insertLocation(track, location, null); + lastLocation = location; + return; + } + + double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint); + if (distanceToLastTrackLocation > maxRecordingDistance) { + insertLocation(track, lastLocation, lastValidTrackPoint); + + Location pause = new Location(LocationManager.GPS_PROVIDER); + pause.setLongitude(0); + pause.setLatitude(PAUSE_LATITUDE); + pause.setTime(lastLocation.getTime()); + insertLocation(track, pause, null); + + insertLocation(track, location, null); + isIdle = false; + } else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) { + insertLocation(track, lastLocation, lastValidTrackPoint); + insertLocation(track, location, null); + isIdle = false; + } else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) { + insertLocation(track, lastLocation, lastValidTrackPoint); + insertLocation(track, location, null); + isIdle = true; + } else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) { + insertLocation(track, lastLocation, lastValidTrackPoint); + insertLocation(track, location, null); + isIdle = false; + } else { + Log.d(TAG, "Not recording location, idle"); + } + lastLocation = location; + } catch (Error e) { + Log.e(TAG, "Error in onLocationChangedAsync", e); + throw e; + } catch (RuntimeException e) { + Log.e(TAG, "RuntimeException in onLocationChangedAsync", e); + throw e; + } + } + + /** + * Inserts a location. + * + * @param track the track + * @param location the location + * @param lastValidTrackPoint the last valid track point, can be null + */ + private void insertLocation(Track track, Location location, Location lastValidTrackPoint) { + if (location == null) { + Log.w(TAG, "Ignore insertLocation. loation is null."); + return; + } + // Do not insert if inserted already + if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == location.getTime()) { + Log.w(TAG, "Ignore insertLocation. location time same as last valid track point time."); + return; + } + + try { + Uri uri = myTracksProviderUtils.insertTrackPoint(location, track.getId()); + long trackPointId = Long.parseLong(uri.getLastPathSegment()); + trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); + markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); + updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location)); + } catch (SQLiteException e) { + /* + * Insert failed, most likely because of SqlLite error code 5 + * (SQLite_BUSY). This is expected to happen extremely rarely (if our + * listener gets invoked twice at about the same time). + */ + Log.w(TAG, "SQLiteException", e); + } + voiceExecutor.update(); + splitExecutor.update(); + sendTrackBroadcast(R.string.track_update_broadcast_action, track.getId()); + } + + /** + * Updates the recording track time. Also updates the startId and the stopId. + * Increase the number of points if it is a new and valid track point. + * + * @param track the track + * @param lastTrackPointId the last track point id + * @param increaseNumberOfPoints true to increase the number of points + */ + private void updateRecordingTrack(Track track, long lastTrackPointId, boolean increaseNumberOfPoints) { + if (lastTrackPointId >= 0) { + if (track.getStartId() < 0) { + track.setStartId(lastTrackPointId); + } + track.setStopId(lastTrackPointId); + } + if (increaseNumberOfPoints) { + track.setNumberOfPoints(track.getNumberOfPoints() + 1); + } + + trackTripStatisticsUpdater.updateTime(System.currentTimeMillis()); + track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics()); + myTracksProviderUtils.updateTrack(track); + } + + private SensorDataSet getSensorDataSet() { + if (sensorManager == null || !sensorManager.isEnabled() || !sensorManager.isSensorDataSetValid()) { + return null; + } + return sensorManager.getSensorDataSet(); + } + + /** + * Registers the location listener. + */ + private void registerLocationListener() { + if (myTracksLocationManager == null) { + Log.e(TAG, "locationManager is null."); + return; + } + try { + long interval = locationListenerPolicy.getDesiredPollingInterval(); + myTracksLocationManager.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance(), locationListener); + currentRecordingInterval = interval; + } catch (RuntimeException e) { + Log.e(TAG, "Could not register location listener.", e); + } + } + + /** + * Unregisters the location manager. + */ + private void unregisterLocationListener() { + if (myTracksLocationManager == null) { + Log.e(TAG, "locationManager is null."); + return; + } + myTracksLocationManager.removeLocationUpdates(locationListener); + } + + /** + * Releases the wake lock. + */ + private void releaseWakeLock() { + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + wakeLock = null; + } + } + + /** + * Shows the notification. + * + * @param isGpsStarted true if GPS is started + */ + private void showNotification(boolean isGpsStarted) { + if (isRecording()) { + if (isPaused()) { + stopForegroundService(); + } else { + Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class) + .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId); + PendingIntent pendingIntent = TaskStackBuilder.create(this) + .addParentStack(TrackDetailActivity.class).addNextIntent(intent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + startForegroundService(pendingIntent, R.string.track_record_notification); + } + } else { + // Not recording + if (isGpsStarted) { + Intent intent = IntentUtils.newIntent(this, TrackListActivity.class); + PendingIntent pendingIntent = TaskStackBuilder.create(this) + .addNextIntent(intent).getPendingIntent(0, 0); + startForegroundService(pendingIntent, R.string.gps_starting); + } else { + stopForegroundService(); + } + } + } + + /** + * Sends track broadcast. + * + * @param actionId the intent action id + * @param trackId the track id + */ + private void sendTrackBroadcast(int actionId, long trackId) { + Intent intent = new Intent().setAction(getString(actionId)) + .putExtra(getString(R.string.track_id_broadcast_extra), trackId); + sendBroadcast(intent, getString(R.string.permission_notification_value)); + if (PreferencesUtils.getBoolean( + this, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT)) { + sendBroadcast(intent, getString(R.string.broadcast_notifications_permission)); + } + } + + /** + * TODO: There is a bug in Android that leaks Binder instances. This bug is + * especially visible if we have a non-static class, as there is no way to + * nullify reference to the outer class (the service). A workaround is to use + * a static class and explicitly clear service and detach it from the + * underlying Binder. With this approach, we minimize the leak to 24 bytes per + * each service instance. For more details, see the following bug: + * http://code.google.com/p/android/issues/detail?id=6426. + */ + private static class ServiceBinder implements ITrackRecordingService { + private TrackRecordingService trackRecordingService; + + public ServiceBinder(TrackRecordingService trackRecordingService) { + this.trackRecordingService = trackRecordingService; + } + + @Override + public void startGps() { + if (!canAccess()) { + return; + } + if (!trackRecordingService.isRecording()) { + trackRecordingService.startGps(); + } + } + + public void stopGps() { + if (!canAccess()) { + return; + } + if (!trackRecordingService.isRecording()) { + trackRecordingService.stopGps(true); + } + } + + @Override + public long startNewTrack() { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.startNewTrack(); + } + + @Override + public void pauseCurrentTrack() { + if (!canAccess()) { + return; + } + trackRecordingService.pauseCurrentTrack(); + } + + @Override + public void resumeCurrentTrack() { + if (!canAccess()) { + return; + } + trackRecordingService.resumeCurrentTrack(); + } + + @Override + public void endCurrentTrack() { + if (!canAccess()) { + return; + } + trackRecordingService.endCurrentTrack(); + } + + @Override + public boolean isRecording() { + if (!canAccess()) { + return false; + } + return trackRecordingService.isRecording(); + } + + @Override + public boolean isPaused() { + if (!canAccess()) { + return false; + } + return trackRecordingService.isPaused(); + } + + @Override + public long getRecordingTrackId() { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.recordingTrackId; + } + + @Override + public long getTotalTime() { + if (!canAccess()) { + return 0; + } + TripStatisticsUpdater updater = trackRecordingService.trackTripStatisticsUpdater; + if (updater == null) { + return 0; + } + if (!trackRecordingService.isPaused()) { + updater.updateTime(System.currentTimeMillis()); + } + return updater.getTripStatistics().getTotalTime(); + } + + @Override + public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.insertWaypoint(waypointCreationRequest); + } + + @Override + public void insertTrackPoint(Location location) { + if (!canAccess()) { + return; + } + //TODO trackRecordingService.locationListener.onLocationChanged(location); + } + + @Override + public byte[] getSensorData() { + if (!canAccess()) { + return null; + } + if (trackRecordingService.sensorManager == null) { + Log.d(TAG, "sensorManager is null."); + return null; + } + if (trackRecordingService.getSensorDataSet() == null) { + Log.d(TAG, "Sensor data set is null."); + return null; + } + return trackRecordingService.sensorManager.getSensorDataSet().toByteArray(); + } + + @Override + public int getSensorState() { + if (!canAccess()) { + return Sensor.SensorState.NONE.getNumber(); + } + if (trackRecordingService.sensorManager == null) { + Log.d(TAG, "sensorManager is null."); + return Sensor.SensorState.NONE.getNumber(); + } + return trackRecordingService.sensorManager.getSensorState().getNumber(); + } + + /** + * Returns true if the RPC caller is from the same application or if the + * "Allow access" setting indicates that another app can invoke this + * service's RPCs. + */ + private boolean canAccess() { + // As a precondition for access, must check if the service is available. + if (trackRecordingService == null) { + throw new IllegalStateException("The track recording service has been detached!"); + } + if (Process.myPid() == Binder.getCallingPid()) { + return true; + } else { + return PreferencesUtils.getBoolean(trackRecordingService, R.string.allow_access_key, + PreferencesUtils.ALLOW_ACCESS_DEFAULT); + } + } + + /** + * Detaches from the track recording service. Clears the reference to the + * outer class to minimize the leak. + */ + private void detachFromService() { + trackRecordingService = null; + } } - } } diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java index a89812508..4fccb64b3 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java @@ -56,7 +56,7 @@ public class TrackRecordingServiceConnection { } catch (RemoteException e) { Log.e(TAG, "Failed to bind a death recipient.", e); } - setTrackRecordingService(ITrackRecordingService.Stub.asInterface(service)); + setTrackRecordingService((ITrackRecordingService)service); } @Override @@ -119,7 +119,7 @@ public class TrackRecordingServiceConnection { * Gets the track recording service if bound. Returns null otherwise */ public ITrackRecordingService getServiceIfBound() { - if (trackRecordingService != null && !trackRecordingService.asBinder().isBinderAlive()) { + if (trackRecordingService != null) { setTrackRecordingService(null); return null; } diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/util/TrackRecordingServiceConnectionUtils.java b/myTracks/src/main/java/com/google/android/apps/mytracks/util/TrackRecordingServiceConnectionUtils.java index 3dac602c5..a8c27a784 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/util/TrackRecordingServiceConnectionUtils.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/util/TrackRecordingServiceConnectionUtils.java @@ -71,13 +71,9 @@ public class TrackRecordingServiceConnectionUtils { * @param trackRecordingServiceConnection the track recording service */ public static void resumeTrack(TrackRecordingServiceConnection trackRecordingServiceConnection) { - try { - ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); - if (service != null) { - service.resumeCurrentTrack(); - } - } catch (RemoteException e) { - Log.e(TAG, "Unable to resume track.", e); + ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); + if (service != null) { + service.resumeCurrentTrack(); } } @@ -88,13 +84,9 @@ public class TrackRecordingServiceConnectionUtils { * connection */ public static void pauseTrack(TrackRecordingServiceConnection trackRecordingServiceConnection) { - try { - ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); - if (service != null) { - service.pauseCurrentTrack(); - } - } catch (RemoteException e) { - Log.e(TAG, "Unable to resume track.", e); + ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); + if (service != null) { + service.pauseCurrentTrack(); } } @@ -181,8 +173,6 @@ public class TrackRecordingServiceConnectionUtils { Toast.makeText(context, R.string.marker_add_success, Toast.LENGTH_SHORT).show(); return markerId; } - } catch (RemoteException e) { - Log.e(TAG, "Unable to add marker", e); } catch (IllegalStateException e) { Log.e(TAG, "Unable to add marker.", e); }