616 lines
No EOL
22 KiB
Java
Executable file
616 lines
No EOL
22 KiB
Java
Executable file
/**
|
|
* TrackerService.java
|
|
* Implements the app's movement tracker service
|
|
* The TrackerService creates a Track object and displays a notification
|
|
*
|
|
* This file is part of
|
|
* TRACKBOOK - Movement Recorder for Android
|
|
*
|
|
* Copyright (c) 2016-18 - Y20K.org
|
|
* Licensed under the MIT-License
|
|
* http://opensource.org/licenses/MIT
|
|
*
|
|
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
|
* https://github.com/osmdroid/osmdroid
|
|
*/
|
|
|
|
package org.y20k.trackbook;
|
|
|
|
import android.app.Notification;
|
|
import android.app.NotificationManager;
|
|
import android.app.Service;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.database.ContentObserver;
|
|
import android.hardware.Sensor;
|
|
import android.hardware.SensorEvent;
|
|
import android.hardware.SensorEventListener;
|
|
import android.hardware.SensorManager;
|
|
import android.location.Location;
|
|
import android.location.LocationListener;
|
|
import android.location.LocationManager;
|
|
import android.os.AsyncTask;
|
|
import android.os.Binder;
|
|
import android.os.Bundle;
|
|
import android.os.CountDownTimer;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.preference.PreferenceManager;
|
|
import android.widget.Toast;
|
|
|
|
import org.y20k.trackbook.core.Track;
|
|
import org.y20k.trackbook.helpers.LocationHelper;
|
|
import org.y20k.trackbook.helpers.LogHelper;
|
|
import org.y20k.trackbook.helpers.NotificationHelper;
|
|
import org.y20k.trackbook.helpers.StorageHelper;
|
|
import org.y20k.trackbook.helpers.TrackbookKeys;
|
|
|
|
import java.util.List;
|
|
|
|
import androidx.core.app.NotificationCompat;
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
|
|
import static android.hardware.Sensor.TYPE_STEP_COUNTER;
|
|
|
|
|
|
/**
|
|
* TrackerService class
|
|
*/
|
|
public class TrackerService extends Service implements TrackbookKeys, SensorEventListener {
|
|
|
|
/* Define log tag */
|
|
private static final String LOG_TAG = TrackerService.class.getSimpleName();
|
|
|
|
|
|
/* Main class variables */
|
|
private Track mTrack;
|
|
private CountDownTimer mTimer;
|
|
private LocationManager mLocationManager;
|
|
private SensorManager mSensorManager;
|
|
private float mStepCountOffset;
|
|
private LocationListener mGPSListener = null;
|
|
private LocationListener mNetworkListener = null;
|
|
private SettingsContentObserver mSettingsContentObserver;
|
|
private Location mCurrentBestLocation;
|
|
private Notification mNotification;
|
|
private NotificationCompat.Builder mNotificationBuilder;
|
|
private NotificationManager mNotificationManager;
|
|
private boolean mTrackerServiceRunning;
|
|
private boolean mLocationSystemSetting;
|
|
private boolean mResumedFlag;
|
|
|
|
private final IBinder mBinder = new LocalBinder(); // todo move to onCreate
|
|
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
|
|
// prepare notification channel and get NotificationManager
|
|
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
NotificationHelper.createNotificationChannel(this);
|
|
|
|
// acquire reference to Location Manager
|
|
mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
|
|
|
|
// acquire reference to Sensor Manager
|
|
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
|
|
|
|
// get state of location system setting
|
|
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(getApplicationContext());
|
|
|
|
// create content observer for changes in System Settings
|
|
mSettingsContentObserver = new SettingsContentObserver(new Handler());
|
|
|
|
// initialize the resume flag
|
|
mResumedFlag = false;
|
|
}
|
|
|
|
|
|
@Override
|
|
public IBinder onBind(Intent intent) {
|
|
// a client is binding to the service with bindService()
|
|
return mBinder;
|
|
}
|
|
|
|
|
|
@Override
|
|
public boolean onUnbind(Intent intent) {
|
|
// return true if you would like to have the service's onRebind(Intent) method later called when new clients bind to it.
|
|
return true;
|
|
}
|
|
|
|
|
|
@Override
|
|
public void onRebind(Intent intent) {
|
|
// a client is binding to the service with bindService(), after onUnbind() has already been called
|
|
}
|
|
|
|
|
|
@Override
|
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
|
|
// SERVICE RESTART (via START_STICKY)
|
|
if (intent == null) {
|
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PREFS_TRACKER_SERVICE_RUNNING, false)) {
|
|
LogHelper.w(LOG_TAG, "Trackbook has been killed by the operating system. Trying to resume recording.");
|
|
resumeTracking(LocationHelper.determineLastKnownLocation(mLocationManager));
|
|
}
|
|
}
|
|
// ACTION STOP
|
|
else if (ACTION_STOP.equals(intent.getAction())) {
|
|
stopTracking();
|
|
}
|
|
// ACTION RESUME
|
|
else if (ACTION_RESUME.equals(intent.getAction())) {
|
|
resumeTracking(LocationHelper.determineLastKnownLocation(mLocationManager));
|
|
}
|
|
|
|
// START_STICKY is used for services that are explicitly started and stopped as needed
|
|
return START_STICKY;
|
|
}
|
|
|
|
|
|
@Override
|
|
public void onTaskRemoved(Intent rootIntent) {
|
|
super.onTaskRemoved(rootIntent);
|
|
LogHelper.v(LOG_TAG, "onTaskRemoved called.");
|
|
}
|
|
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
LogHelper.v(LOG_TAG, "onDestroy called.");
|
|
|
|
if (mTrackerServiceRunning) {
|
|
stopTracking();
|
|
}
|
|
|
|
// remove TrackerService from foreground state
|
|
stopForeground(true);
|
|
|
|
super.onDestroy();
|
|
}
|
|
|
|
|
|
@Override
|
|
public void onSensorChanged(SensorEvent sensorEvent) {
|
|
// save the step count offset (steps previously recorded by the system) and subtract any steps recorded during this session in case the app was killed
|
|
if (mStepCountOffset == 0) {
|
|
mStepCountOffset = (sensorEvent.values[0] - 1) - mTrack.getStepCount();
|
|
}
|
|
|
|
// calculate step count
|
|
float stepCount = sensorEvent.values[0] - mStepCountOffset;
|
|
|
|
// set step count in track
|
|
mTrack.setStepCount(stepCount);
|
|
}
|
|
|
|
|
|
@Override
|
|
public void onAccuracyChanged(Sensor sensor, int i) {
|
|
|
|
}
|
|
|
|
|
|
/* Start tracking location */
|
|
public void startTracking(Location lastLocation) {
|
|
if (mLocationSystemSetting) {
|
|
LogHelper.v(LOG_TAG, "Start tracking");
|
|
|
|
// create a new track - if requested
|
|
mTrack = new Track();
|
|
|
|
// get last location
|
|
if (lastLocation != null) {
|
|
mCurrentBestLocation = lastLocation;
|
|
} else {
|
|
mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager);
|
|
}
|
|
|
|
// begin recording
|
|
startMovementRecording();
|
|
|
|
} else {
|
|
LogHelper.i(LOG_TAG, "Location Setting is turned off.");
|
|
Toast.makeText(getApplicationContext(), R.string.toast_message_location_offline, Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
|
|
|
|
/* Resume tracking after stop/pause */
|
|
public void resumeTracking(Location lastLocation) {
|
|
if (mLocationSystemSetting) {
|
|
LogHelper.v(LOG_TAG, "Recording resumed");
|
|
|
|
// switch the resume flag
|
|
mResumedFlag = true;
|
|
|
|
// create a new track - if requested
|
|
StorageHelper storageHelper = new StorageHelper(this);
|
|
if (storageHelper.tempFileExists()) {
|
|
// load temp track file
|
|
mTrack = storageHelper.loadTrack(FILE_TEMP_TRACK);
|
|
// try to mark last waypoint as stopover
|
|
int lastWayPoint = mTrack.getWayPoints().size() - 1;
|
|
if (lastWayPoint >= 0) {
|
|
mTrack.getWayPoints().get(lastWayPoint).setIsStopOver(true);
|
|
}
|
|
} else {
|
|
// fallback, if tempfile did not exist
|
|
LogHelper.e(LOG_TAG, "Unable to find previously saved track temp file.");
|
|
mTrack = new Track();
|
|
}
|
|
|
|
// get last location
|
|
mCurrentBestLocation = lastLocation;
|
|
// FALLBACK: use last recorded location
|
|
if (mCurrentBestLocation == null && mTrack.getSize() > 0) {
|
|
mCurrentBestLocation = mTrack.getWayPointLocation(mTrack.getSize() -1);
|
|
}
|
|
|
|
// begin recording
|
|
startMovementRecording();
|
|
|
|
} else {
|
|
LogHelper.i(LOG_TAG, "Location Setting is turned off.");
|
|
Toast.makeText(getApplicationContext(), R.string.toast_message_location_offline, Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
|
|
|
|
/* Stop tracking location */
|
|
public void stopTracking() {
|
|
LogHelper.v(LOG_TAG, "Recording stopped");
|
|
|
|
// catches a bug that leaves the ui in a incorrect state after a crash
|
|
if (!mTrackerServiceRunning) {
|
|
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_SAVE);
|
|
broadcastTrackingStateChange();
|
|
return;
|
|
}
|
|
|
|
// store current date and time
|
|
mTrack.setRecordingEnd();
|
|
|
|
// stop timer
|
|
mTimer.cancel();
|
|
|
|
// broadcast an updated track
|
|
broadcastTrackUpdate();
|
|
|
|
// save a temp file in case the activity has been killed
|
|
SaveTempTrackAsyncHelper saveTempTrackAsyncHelper = new SaveTempTrackAsyncHelper();
|
|
saveTempTrackAsyncHelper.execute();
|
|
|
|
// change notification
|
|
displayNotification(false);
|
|
|
|
// reset resume flag
|
|
mResumedFlag = false;
|
|
|
|
// remove listeners
|
|
stopFindingLocation();
|
|
mSensorManager.unregisterListener(this);
|
|
|
|
// disable content observer for changes in System Settings
|
|
this.getContentResolver().unregisterContentObserver(mSettingsContentObserver);
|
|
|
|
// remove TrackerService from foreground state
|
|
stopForeground(false);
|
|
}
|
|
|
|
|
|
/* Dismiss notification */
|
|
public void dismissNotification() {
|
|
// save state
|
|
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_DEFAULT);
|
|
// cancel notification
|
|
mNotificationManager.cancel(TRACKER_SERVICE_NOTIFICATION_ID); // todo check if necessary?
|
|
stopForeground(true);
|
|
}
|
|
|
|
|
|
/* Starts to record movements */
|
|
private void startMovementRecording() {
|
|
// initialize step counter
|
|
mStepCountOffset = 0;
|
|
|
|
// add last location as WayPoint to track
|
|
addWayPointToTrack();
|
|
|
|
// put up notification
|
|
displayNotification(true);
|
|
|
|
// create gps and network location listeners
|
|
startFindingLocation();
|
|
|
|
// start timer that periodically request a location update
|
|
startRequestingLocationChanges();
|
|
|
|
// start counting steps
|
|
startStepCounter();
|
|
|
|
// register content observer for changes in System Settings
|
|
this.getContentResolver().registerContentObserver(android.provider.Settings.Secure.CONTENT_URI, true, mSettingsContentObserver);
|
|
|
|
// start service in foreground
|
|
startForeground(TRACKER_SERVICE_NOTIFICATION_ID, mNotification);
|
|
}
|
|
|
|
|
|
/* Registers a step counter listener */
|
|
private void startStepCounter() {
|
|
boolean stepCounterAvailable = mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI);
|
|
if (stepCounterAvailable) {
|
|
LogHelper.v(LOG_TAG, "Pedometer sensor available: Registering listener.");
|
|
} else {
|
|
LogHelper.i(LOG_TAG, "Pedometer sensor not available.");
|
|
mTrack.setStepCount(-1);
|
|
}
|
|
}
|
|
|
|
|
|
/* Set timer to periodically retrieve new locations and to prevent endless tracking */
|
|
private void startRequestingLocationChanges() {
|
|
final long previouslyRecordedDuration = mTrack.getTrackDuration();
|
|
mTimer = new CountDownTimer(EIGHT_HOURS_IN_MILLISECONDS, FIFTEEN_SECONDS_IN_MILLISECONDS) {
|
|
@Override
|
|
public void onTick(long millisUntilFinished) {
|
|
// update track duration - and add duration from previously interrupted / paused session
|
|
long duration = EIGHT_HOURS_IN_MILLISECONDS - millisUntilFinished + previouslyRecordedDuration;
|
|
mTrack.setDuration(duration);
|
|
// try to add WayPoint to Track
|
|
addWayPointToTrack();
|
|
// update notification
|
|
mNotification = NotificationHelper.getUpdatedNotification(TrackerService.this, mNotificationBuilder, mTrack);
|
|
mNotificationManager.notify(TRACKER_SERVICE_NOTIFICATION_ID, mNotification);
|
|
// save a temp file in case the service has been killed by the system
|
|
SaveTempTrackAsyncHelper saveTempTrackAsyncHelper = new SaveTempTrackAsyncHelper();
|
|
saveTempTrackAsyncHelper.execute();
|
|
}
|
|
|
|
@Override
|
|
public void onFinish() {
|
|
// stop tracking after eight hours
|
|
stopTracking();
|
|
}
|
|
};
|
|
mTimer.start();
|
|
}
|
|
|
|
|
|
/* Display notification */
|
|
private void displayNotification(boolean trackingState) {
|
|
mNotificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL);
|
|
mNotification = NotificationHelper.getNotification(this, mNotificationBuilder, mTrack, trackingState);
|
|
mNotificationManager.notify(TRACKER_SERVICE_NOTIFICATION_ID, mNotification); // todo check if necessary in pre Android O
|
|
}
|
|
|
|
|
|
/* Adds a new WayPoint to current track */
|
|
private void addWayPointToTrack() {
|
|
|
|
boolean success = false;
|
|
Location previousLocation = null;
|
|
int trackSize = mTrack.getWayPoints().size();
|
|
|
|
if (trackSize == 0) {
|
|
// if accurate AND current
|
|
if (LocationHelper.isAccurate(mCurrentBestLocation) && LocationHelper.isCurrent(mCurrentBestLocation)) {
|
|
// add first location to track
|
|
success = mTrack.addWayPoint(previousLocation, mCurrentBestLocation);
|
|
} else {
|
|
// just send a broadcast indicating that current location fix not not suited
|
|
broadcastTrackUpdate();
|
|
}
|
|
} else {
|
|
// get location of previous WayPoint
|
|
previousLocation = mTrack.getWayPointLocation(trackSize - 1);
|
|
|
|
// default value for average speed
|
|
float averageSpeed = 0f;
|
|
|
|
// compute average speed if new location came from network provider
|
|
if (trackSize > 1 && mCurrentBestLocation.getProvider().equals(LocationManager.NETWORK_PROVIDER)) {
|
|
Location firstWayPoint = mTrack.getWayPointLocation(0);
|
|
float distance = firstWayPoint.distanceTo(previousLocation);
|
|
long timeDifference = previousLocation.getElapsedRealtimeNanos() - firstWayPoint.getElapsedRealtimeNanos();
|
|
averageSpeed = distance / ((float) timeDifference / ONE_SECOND_IN_NANOSECOND);
|
|
}
|
|
|
|
// if accurate AND new
|
|
if (LocationHelper.isAccurate(mCurrentBestLocation) && LocationHelper.isNewWayPoint(previousLocation, mCurrentBestLocation, averageSpeed)) {
|
|
// add current best location to track
|
|
success = mTrack.addWayPoint(previousLocation, mCurrentBestLocation);
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
if (mResumedFlag) {
|
|
// mark last location as stop over
|
|
int lastWayPoint = mTrack.getWayPoints().size() - 2;
|
|
mTrack.getWayPoints().get(lastWayPoint).setIsStopOver(true);
|
|
mResumedFlag = false;
|
|
} else {
|
|
// update distance, if not resumed
|
|
mTrack.updateDistance(previousLocation, mCurrentBestLocation);
|
|
}
|
|
|
|
// send local broadcast if new WayPoint was added
|
|
broadcastTrackUpdate();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* Broadcasts a track update */
|
|
private void broadcastTrackUpdate() {
|
|
if (mTrack != null) {
|
|
Intent i = new Intent();
|
|
i.setAction(ACTION_TRACK_UPDATED);
|
|
i.putExtra(EXTRA_TRACK, mTrack);
|
|
i.putExtra(EXTRA_LAST_LOCATION, mCurrentBestLocation);
|
|
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i);
|
|
}
|
|
}
|
|
|
|
|
|
/* Creates a location listener */
|
|
private LocationListener createLocationListener() {
|
|
return new LocationListener() {
|
|
public void onLocationChanged(Location location) {
|
|
// check if the new location is better
|
|
if (LocationHelper.isBetterLocation(location, mCurrentBestLocation)) {
|
|
// save location
|
|
mCurrentBestLocation = location;
|
|
}
|
|
}
|
|
|
|
public void onStatusChanged(String provider, int status, Bundle extras) {
|
|
LogHelper.v(LOG_TAG, "Location provider status change: " + provider + " | " + status);
|
|
}
|
|
|
|
public void onProviderEnabled(String provider) {
|
|
LogHelper.v(LOG_TAG, "Location provider enabled: " + provider);
|
|
}
|
|
|
|
public void onProviderDisabled(String provider) {
|
|
LogHelper.v(LOG_TAG, "Location provider disabled: " + provider);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
/* Creates gps and network location listeners */
|
|
private void startFindingLocation() {
|
|
|
|
// register location listeners and request updates
|
|
List locationProviders = mLocationManager.getAllProviders();
|
|
if (locationProviders.contains(LocationManager.GPS_PROVIDER)) {
|
|
mGPSListener = createLocationListener();
|
|
mTrackerServiceRunning = true;
|
|
}
|
|
if (locationProviders.contains(LocationManager.NETWORK_PROVIDER)) {
|
|
mNetworkListener = createLocationListener();
|
|
mTrackerServiceRunning = true;
|
|
}
|
|
LocationHelper.registerLocationListeners(mLocationManager, mGPSListener, mNetworkListener);
|
|
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_RECORDING);
|
|
|
|
// notify MainActivity
|
|
broadcastTrackingStateChange();
|
|
}
|
|
|
|
|
|
/* Removes gps and network location listeners */
|
|
private void stopFindingLocation() {
|
|
// remove listeners
|
|
LocationHelper.removeLocationListeners(mLocationManager, mGPSListener, mNetworkListener);
|
|
mTrackerServiceRunning = false;
|
|
saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_SAVE);
|
|
|
|
// notify MainActivity
|
|
broadcastTrackingStateChange();
|
|
}
|
|
|
|
|
|
/* Sends a broadcast with tracking changed */
|
|
private void broadcastTrackingStateChange() {
|
|
Intent i = new Intent();
|
|
i.setAction(ACTION_TRACKING_STATE_CHANGED);
|
|
i.putExtra(EXTRA_TRACK, mTrack);
|
|
i.putExtra(EXTRA_LAST_LOCATION, mCurrentBestLocation);
|
|
i.putExtra(EXTRA_TRACKING_STATE, mTrackerServiceRunning);
|
|
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i);
|
|
}
|
|
|
|
|
|
/* Saves state of Tracker Service and floating Action Button */
|
|
private void saveTrackerServiceState(boolean trackerServiceRunning, int fabState) {
|
|
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
|
|
SharedPreferences.Editor editor = settings.edit();
|
|
editor.putBoolean(PREFS_TRACKER_SERVICE_RUNNING, trackerServiceRunning);
|
|
editor.putInt(PREFS_FAB_STATE, fabState);
|
|
editor.apply();
|
|
}
|
|
|
|
|
|
/**
|
|
* Inner class: Local Binder that returns this service
|
|
*/
|
|
public class LocalBinder extends Binder {
|
|
TrackerService getService() {
|
|
// return this instance of TrackerService so clients can call public methods
|
|
return TrackerService.this;
|
|
}
|
|
}
|
|
/**
|
|
* End of inner class
|
|
*/
|
|
|
|
|
|
/**
|
|
* Inner class: SettingsContentObserver is a custom ContentObserver for changes in Android Settings
|
|
*/
|
|
public class SettingsContentObserver extends ContentObserver {
|
|
|
|
public SettingsContentObserver(Handler handler) {
|
|
super(handler);
|
|
}
|
|
|
|
@Override
|
|
public boolean deliverSelfNotifications() {
|
|
return super.deliverSelfNotifications();
|
|
}
|
|
|
|
@Override
|
|
public void onChange(boolean selfChange) {
|
|
super.onChange(selfChange);
|
|
LogHelper.v(LOG_TAG, "System Setting change detected.");
|
|
|
|
// check if location setting was changed
|
|
boolean previousLocationSystemSetting = mLocationSystemSetting;
|
|
mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(getApplicationContext());
|
|
if (previousLocationSystemSetting != mLocationSystemSetting && !mLocationSystemSetting && mTrackerServiceRunning) {
|
|
LogHelper.v(LOG_TAG, "Location Setting turned off while tracking service running.");
|
|
if (mTrack != null) {
|
|
stopTracking();
|
|
}
|
|
stopForeground(true);
|
|
}
|
|
}
|
|
|
|
}
|
|
/**
|
|
* End of inner class
|
|
*/
|
|
|
|
|
|
/**
|
|
* Inner class: Saves track to external storage using AsyncTask
|
|
*/
|
|
private class SaveTempTrackAsyncHelper extends AsyncTask<Void, Void, Void> {
|
|
|
|
@Override
|
|
protected Void doInBackground(Void... voids) {
|
|
LogHelper.v(LOG_TAG, "Saving temporary track object in background.");
|
|
// save track object
|
|
StorageHelper storageHelper = new StorageHelper(TrackerService.this);
|
|
storageHelper.saveTrack(mTrack, FILE_TEMP_TRACK);
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Void aVoid) {
|
|
super.onPostExecute(aVoid);
|
|
LogHelper.v(LOG_TAG, "Saving finished.");
|
|
}
|
|
}
|
|
/**
|
|
* End of inner class
|
|
*/
|
|
|
|
} |