/* * Copyright (c) 2013, Will Szumski * Copyright (c) 2013, Doug Szumski * * This file is part of Cyclismo. * * Cyclismo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Cyclismo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Cyclismo. If not, see <http://www.gnu.org/licenses/>. */ package org.cowboycoders.cyclismo.turbo; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.location.Location; import android.location.LocationManager; import android.os.Binder; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; import org.cowboycoders.ant.Node; import org.cowboycoders.ant.interfaces.AndroidAntTransceiver; import org.cowboycoders.ant.interfaces.AntRadioPoweredOffException; import org.cowboycoders.ant.interfaces.AntRadioServiceNotInstalledException; import org.cowboycoders.ant.interfaces.ServiceAlreadyClaimedException; import org.cowboycoders.ant.sensors.DataSourceCombiner; import org.cowboycoders.ant.sensors.HeartRateListener; import org.cowboycoders.ant.sensors.HeartRateMonitor; import org.cowboycoders.cyclismo.Constants; import org.cowboycoders.cyclismo.R; import org.cowboycoders.cyclismo.TrackEditActivity; import org.cowboycoders.cyclismo.content.Bike; import org.cowboycoders.cyclismo.content.CyclismoProviderUtils; import org.cowboycoders.cyclismo.content.MyTracksProviderUtils; import org.cowboycoders.cyclismo.content.User; import org.cowboycoders.cyclismo.services.TrackRecordingServiceConnection; import org.cowboycoders.cyclismo.util.IntentUtils; import org.cowboycoders.cyclismo.util.PreferenceEntry; import org.cowboycoders.cyclismo.util.PreferencesUtils; import org.cowboycoders.cyclismo.util.TrackRecordingServiceConnectionUtils; import org.cowboycoders.cyclismo.util.UnitConversions; import org.cowboycoders.turbotrainers.AntTurboTrainer; import org.cowboycoders.turbotrainers.CourseTracker; import org.cowboycoders.turbotrainers.DummyTrainer; import org.cowboycoders.turbotrainers.Mode; import org.cowboycoders.turbotrainers.Parameters; import org.cowboycoders.turbotrainers.TooFewAntChannelsAvailableException; import org.cowboycoders.turbotrainers.TurboCommunicationException; import org.cowboycoders.turbotrainers.TurboTrainerDataListener; import org.cowboycoders.turbotrainers.TurboTrainerInterface; import org.cowboycoders.turbotrainers.bushido.brake.BushidoBrake; import org.cowboycoders.turbotrainers.bushido.brake.ConstantResistanceController; import org.cowboycoders.turbotrainers.bushido.headunit.BushidoHeadunit; import org.fluxoid.utils.LatLongAlt; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TurboService extends Service { public static final int TIMEOUT_HRM_STALE_DATA = 10 * 1000; // milliseconds private static final int DEFAULT_BIKE_WEIGHT = 7; private static final int DEFAULT_USER_WEIGHT = 60; private static final int RESULT_ERROR = 0; public static final int PRECEDENCE_HRM_STRAP = 20; public static final int PRECEDENCE_TURBO_HRM = 100; private Binder turboBinder = new TurboBinder(); //relocate to R public static final int NOTIFCATION_ID_STARTUP = 0; public static final int NOTIFCATION_ID_SHUTDOWN = 1; public static String TAG = "TurboService"; public static String COURSE_TRACK_ID = "COURSE_TRACK_ID"; public static double TARGET_TRACKPOINT_DISTANCE_METRES = 0.1; protected AntLoggerImpl antLogger; private TurboRegistry turboRegistry; // FIXME: Something must be checking for GPS_PROVIDER only? // we want to use SimulatedLocationProvider.NAME private final static String MOCK_LOCATION_PROVIDER = LocationManager.GPS_PROVIDER; private int gpsAccuracy = 5; // m // private List<LatLongAlt> latLongAlts; private boolean running = false; private ServiceConnection mConnection; // ant service connection private float scaleFactor = 1.0f; //multiply positive gradients by this value private AndroidAntTransceiver transceiver; private Map<String, Mode> uiModeToTurboMode; // private double distanceBetweenPoints; // private double lastSubmittedDistance = 0; // private int currentLatLongIndex = 1; private static String WAKE_LOCK = TurboService.class.getSimpleName(); private boolean mIsBound = false; private boolean attachAntLogger = false; private Node antNode; // Notification that we are running public static int ONGOING_NOTIFICATION = 1786; double lastRecordedSpeed = 0.0; // kmh private TurboTrainerInterface turboTrainer; private Integer targetPower; private Lock parameterBuilderLock = new ReentrantLock(); private Parameters.Builder parameterBuilder; private HeartRateMonitor hrm; private DataSourceCombiner dsc = new DataSourceCombiner(new HeartRateListener() { /** * @param heartRate new heart rate value */ @Override public void onValueChange(int heartRate) { if (heartRate > 0) { Intent intent = new Intent(getString(R.string.sensor_data_heart_rate)); intent.putExtra(getString(R.string.sensor_data_double_value), (double) heartRate); sendBroadcast(intent); } } }, TIMEOUT_HRM_STALE_DATA); public void setParameterBuilder(Parameters.Builder builder) { try { parameterBuilderLock.lock(); parameterBuilder = builder; } finally { parameterBuilderLock.unlock(); } } private HeartRateListener hrmListener = new HeartRateListener() { /** * @param hr new heart rate value */ @Override public void onValueChange(int hr) { Log.d(TAG, "hr(strap): " + hr); dsc.update(hr, PRECEDENCE_HRM_STRAP); } }; private TurboTrainerDataListener dataListener = new TurboTrainerDataListener() { boolean updating = false; Lock updatingLock = new ReentrantLock(); @Override public void onSpeedChange(double speed) { // synchronized to keep speed in alignment with distance Intent intent = new Intent(getString(R.string.sensor_data_speed_kmh)); intent.putExtra(getString(R.string.sensor_data_double_value), speed); sendBroadcast(intent); try { updatingLock.lock(); if (updating) return; updating = true; } finally { updatingLock.unlock(); } lastRecordedSpeed = speed; Log.v(TAG, "new speed: " + speed); courseTracker.updateSpeed(speed); try { updatingLock.lock(); updating = false; } finally { updatingLock.unlock(); } } @Override public void onPowerChange(double power) { Intent intent = new Intent(getString(R.string.sensor_data_power)); intent.putExtra(getString(R.string.sensor_data_double_value), power); sendBroadcast(intent); } @Override public void onCadenceChange(double cadence) { Log.d(TAG, "cadence: " + cadence); Intent intent = new Intent(getString(R.string.sensor_data_cadence)); intent.putExtra(getString(R.string.sensor_data_double_value), cadence); sendBroadcast(intent); } @Override public void onDistanceChange(double distance) { // NOTE: this method currently doesn't make use of distance passed in; other than to log // it's value // synchronized to keep speed in alignment with distance : may need // changing if threads are queueing and order becomes // unpredictable try { updatingLock.lock(); if (updating) return; updating = true; } finally { updatingLock.unlock(); } double courseTrackerDistance = courseTracker.getDistance(); // Log the cumulative distance as it is used by some tools (Strava etc.) to calculate speed Log.d(TAG, "sensor distance: " + distance); Log.d(TAG, "courseTracker distance" + courseTrackerDistance); Intent intent = new Intent(getString(R.string.sensor_data_distance)); intent.putExtra(getString(R.string.sensor_data_double_value), courseTrackerDistance); sendBroadcast(intent); LatLongAlt currentLocation = courseTracker.getNearestLocation(); updateLocation(currentLocation); // returns 0.0 if finished for warm down double gradient = courseTracker.getCurrentGradient(); if (gradient > 0) { gradient = gradient * scaleFactor; } Mode mode = uiModeToTurboMode.get(selectedCourseMode); try { parameterBuilderLock.lock(); switch (mode) { case TARGET_POWER: // TODO: Add course tracking power here, like for distance turboTrainer.setParameters(parameterBuilder.buildTargetPower(targetPower)); break; case TARGET_SPEED: // TODO case TARGET_SLOPE: default: turboTrainer.setParameters(parameterBuilder.buildTargetSlope(gradient)); } } finally { parameterBuilderLock.unlock(); } Log.d(TAG, "new gradient: " + gradient); if (courseTracker.hasFinished()) { doFinish(); } try { updatingLock.lock(); updating = false; } finally { updatingLock.unlock(); } } @Override public void onHeartRateChange(double heartRate) { dsc.update((int) heartRate, PRECEDENCE_TURBO_HRM); Log.d(TAG, "hr(turbo): " + heartRate); } }; private WakeLock wakeLock; private CourseTracker courseTracker; private long recordingTrackId; private SharedPreferences preferences; private String selectedTurboTrainer; private String selectedCourseMode; public void doFinish() { // FIXME: preferences is a bad choice for the guard // preferences is non-null when start() has been called, but not in calibrate mode if (preferences != null) { preferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); // if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { Intent intent = IntentUtils.newIntent(getBaseContext(), TrackEditActivity.class) .putExtra(TrackEditActivity.EXTRA_TRACK_ID, recordingTrackId) .putExtra(TrackEditActivity.EXTRA_NEW_TRACK, true).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getApplication().startActivity(intent); TrackRecordingServiceConnectionUtils .stopRecording(this, trackRecordingServiceConnection, false); // } } this.stopSelf(); } void doUnbindService() { if (mIsBound) { unbindService(mConnection); mConnection = null; mIsBound = false; } } /* * (non-Javadoc) * @see android.app.Service#onStartCommand(android.content.Intent, int, int) */ @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); return START_STICKY; } public synchronized void start(final long trackId, final Context context) { if (running) { return; } running = true; uiModeToTurboMode = getUiModeToJTurboModeMap(); preferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); preferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); syncScaleFactor(); // Get selections from preferences. Don't use defaults because the UI enforces a selection. selectedTurboTrainer = preferences.getString(getApplication().getString( R.string.turbotrainer_selected), null); selectedCourseMode = preferences.getString(getApplication().getString( R.string.course_mode), null); targetPower = preferences.getInt(getApplication().getString( R.string.constant_course_power), 0); Log.d(TAG, "Constant course power: " + targetPower); // accessing database so should be put into a task double userWeight; double bikeWeight; Bike selectedBike; CyclismoProviderUtils providerUtils = MyTracksProviderUtils.Factory.getCyclimso(context); User currentUser = providerUtils.getUser(PreferencesUtils.getLong(context, R.string.settings_select_user_current_selection_key)); if (currentUser != null) { userWeight = currentUser.getWeight(); selectedBike = providerUtils.getBike(PreferencesUtils.getLong(context, R.string.settings_select_bike_current_selection_key)); } else { Log.w(TAG, "using default user weight"); userWeight = DEFAULT_USER_WEIGHT; //kg selectedBike = null; } if (selectedBike != null) { bikeWeight = selectedBike.getWeight(); } else { Log.w(TAG, "using default bike weight"); bikeWeight = DEFAULT_BIKE_WEIGHT; //kg } setParameterBuilder(new Parameters.Builder(userWeight, bikeWeight)); Log.d(TAG, "user weight: " + userWeight); Log.d(TAG, "bike weight: " + bikeWeight); attachAntLogger = PreferencesUtils.getBoolean(context, R.string.settings_ant_diagnostic_logging_state_key, false); wakeLock.acquire(); recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key); List<LatLongAlt> latLongAlts = null; context.getClass(); CourseLoader cl = new CourseLoader(context, trackId); try { latLongAlts = cl.getLatLongAlts(); } catch (InterruptedException e) { Log.e(TAG, "interrupted whilst loading course"); handleException(e, "Error loading course", true, NOTIFCATION_ID_STARTUP); } this.courseTracker = new CourseTracker(latLongAlts, TARGET_TRACKPOINT_DISTANCE_METRES); Log.d(TAG, "latlong length: " + latLongAlts.size()); // start in background as otherwise it is destroyed in onDestory() before we // can disconnect startServiceInBackround(); doBindService(); registerRecordingReceiver(); showStartNotification(); // currentLatLongIndex = latLongAlts.size() -3; puts in near end so we can // test ending } private void showStartNotification() { Intent notificationIntent = new Intent(this, TurboService.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); Notification noti = new NotificationCompat.Builder(this) .setContentTitle(getString(R.string.turbo_mode_service_running_notification_title)) .setContentText(getString(R.string.turbo_mode_service_running_notification_text)) .setSmallIcon(R.drawable.track_bike).setContentIntent(pendingIntent).setOngoing(true) .build(); // build api 16 only ;/ startForeground(ONGOING_NOTIFICATION, noti); } public synchronized void startBushidoCalibrate(BushidoBrake.CalibrationCallback callback, TurboTrainerDataListener listener) { startServiceInBackround(); this.doBindForBushidoCalibrate(callback, listener); showStartNotification(); } private static interface ErrorProneCall { void call() throws Exception; } private static void retryErrorProneCall(ErrorProneCall errorProneCall, int maxRetries) throws Exception { int retries = 0; while (true) { try { errorProneCall.call(); break; } catch (Exception e) { if (++retries >= maxRetries) { throw e; } } } } private ErrorProneCall startTurbo = new ErrorProneCall() { @Override public void call() throws TurboCommunicationException, InterruptedException, TimeoutException { // TODO: Catch exception when this fails and print a toast turboTrainer.start(); } }; private ErrorProneCall startHrm = new ErrorProneCall() { @Override public void call() throws TurboCommunicationException, InterruptedException, TimeoutException { hrm.start(); } }; private ErrorProneCall startBushidoBrake = new ErrorProneCall() { @Override public void call() throws TurboCommunicationException, InterruptedException, TimeoutException { ((BushidoBrake) turboTrainer).startConnection(); } }; private class BushidoCalibrateConnection implements ServiceConnection { private TurboTrainerDataListener dataListener; private BushidoBrake.CalibrationCallback calibrationListener; private BushidoCalibrateConnection(BushidoBrake.CalibrationCallback calibrationListener, TurboTrainerDataListener dataListener) { this.calibrationListener = calibrationListener; this.dataListener = dataListener; } public void onServiceConnected(ComponentName className, IBinder binder) { initAntWireless((AntHubService.LocalBinder) binder); ConstantResistanceController mapper = new ConstantResistanceController(); mapper.setAbsoluteResistance(100); final BushidoBrake bushidoBrake = new BushidoBrake(); bushidoBrake.setMode(Mode.TARGET_SLOPE); bushidoBrake.setNode(antNode); bushidoBrake.overrideDefaultResistanceController(mapper); TurboService.this.turboTrainer = bushidoBrake; new Thread() { public void run() { try { retryErrorProneCall(startBushidoBrake, 10); bushidoBrake.registerDataListener(dataListener); bushidoBrake.calibrate(calibrationListener, 120); } catch (Exception e) { handleException(e, "Error trying to calibrate your turbo trainer", true, NOTIFCATION_ID_STARTUP); } } }.start(); Toast.makeText(TurboService.this, "Connected to AntHub service", Toast.LENGTH_SHORT).show(); } public void onServiceDisconnected(ComponentName className) { Toast.makeText(TurboService.this, "Disconnected from AntHub service", Toast.LENGTH_SHORT) .show(); } } private class TurboConnection implements ServiceConnection { public void onServiceConnected(ComponentName className, IBinder binder) { TurboService.this.turboTrainer = getTurboTrainer((AntHubService.LocalBinder) binder); if (turboTrainer == null) { Toast.makeText(TurboService.this, "Please select/reselect your turbo from the settings menu and try again", Toast.LENGTH_SHORT).show(); return; } new Thread() { public void run() { try { // slight hack to get around timeout errors on my tablet retryErrorProneCall(startTurbo, 10); if (hrm != null) { // ANT has been initialised retryErrorProneCall(startHrm, 10); } turboTrainer.registerDataListener(dataListener); unpauseRecording(); LatLongAlt loc = courseTracker.getNearestLocation(); updateLocation(loc); } catch (Exception e) { handleException(e, "Error initiliasing turbo trainer", true, NOTIFCATION_ID_STARTUP); } } }.start(); Toast.makeText(TurboService.this, "Connected to AntHub service", Toast.LENGTH_SHORT).show(); } public void onServiceDisconnected(ComponentName className) { Toast.makeText(TurboService.this, "Disconnected from AntHub service", Toast.LENGTH_SHORT) .show(); } } private void unpauseRecording() { Intent intent = new Intent().setAction(this .getString(R.string.turbo_service_action_course_start)); sendBroadcast(intent); } private void broadcastLocation(Location loc) { Intent intent = new Intent().setAction(this .getString(R.string.turbo_service_action_location_update)); intent.putExtra(getString(R.string.turbo_service_data_location_update), loc); sendBroadcast(intent); } private void initAntWireless(AntHubService.LocalBinder binder) { if (antNode != null) return; AntHubService s = ((AntHubService.LocalBinder) binder).getService(); antNode = s.getNode(); if (attachAntLogger) { antLogger = new AntLoggerImpl(this); antNode.registerAntLogger(antLogger); } transceiver = s.getTransceiver(); } private void initAntSensors() { if (hrm != null) return; hrm = new HeartRateMonitor(antNode); hrm.registerListener(hrmListener); } private void initAnt(AntHubService.LocalBinder binder) { initAntWireless(binder); initAntSensors(); } private TrackRecordingServiceConnection trackRecordingServiceConnection; void doBindService() { assert mConnection == null : "expecting mConnection to be null"; mConnection = new TurboConnection(); bindService(new Intent(this, AntHubService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBound = true; } void doBindForBushidoCalibrate(BushidoBrake.CalibrationCallback calibrationCallback, TurboTrainerDataListener listener) { assert mConnection == null : "expecting mConnection to be null"; mConnection = new BushidoCalibrateConnection(calibrationCallback, listener); bindService(new Intent(this, AntHubService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBound = true; } /** * Returns modes supported by the Turbo trainer for use in the turbo * mode selection menu. This is used to display the modes supported * by the turbo in the UI. * * @param jTurboModes the modes supported by the turbo in JTurbo. * @return modes as preference entries for use in the UI. */ private PreferenceEntry getUiModesForTurbo(final Mode [] jTurboModes) { PreferenceEntry uiModes = new PreferenceEntry(); for (Mode mode: jTurboModes) { switch(mode) { case TARGET_SLOPE: uiModes.addPreferenceEntry( getString(R.string.settings_courses_mode_simulation), getString(R.string.settings_courses_mode_simulation_value) ); break; case TARGET_SPEED: uiModes.addPreferenceEntry( getString(R.string.settings_courses_mode_constant_speed), getString(R.string.settings_courses_mode_constant_speed_value) ); break; case TARGET_POWER: uiModes.addPreferenceEntry( getString(R.string.settings_courses_mode_constant_power), getString(R.string.settings_courses_mode_constant_power_value) ); break; } } return uiModes; } private Map<String, Mode> getUiModeToJTurboModeMap() { Map<String, Mode> uiModeToTurboMode = new HashMap<>(); uiModeToTurboMode.put( getString(R.string.settings_courses_mode_simulation_value), Mode.TARGET_SLOPE); uiModeToTurboMode.put( getString(R.string.settings_courses_mode_constant_speed_value), Mode.TARGET_SPEED); uiModeToTurboMode.put( getString(R.string.settings_courses_mode_constant_power_value), Mode.TARGET_POWER); return uiModeToTurboMode; } private TurboRegistry getTurboRegistry() { if (turboRegistry == null) { turboRegistry = new TurboRegistry(); registerTurbos(); } return turboRegistry; } private void registerTurbos() { turboRegistry.register( getString(R.string.turbotrainer_tacx_bushido_headunit_value), new BushidoHeadunit() ); turboRegistry.register( getString(R.string.turbotrainer_tacx_bushido_brake_value), new BushidoBrake() ); turboRegistry.register( getString(R.string.turbotrainer_dummy_value), new DummyTrainer() ); } /** * Get the supported turbo modes for the UI. * * @param turboSelectValue is the mode selected in the UI. * @return turbo capabilities from JTurbo. Can be null if turbo not supported. */ public PreferenceEntry getCourseModesForTurbo(final String turboSelectValue) { TurboTrainerInterface turboSelected = getTurboRegistry().getTurboTrainer(turboSelectValue); Mode[] modes = turboSelected.modesSupported(); return getUiModesForTurbo(modes); } protected TurboTrainerInterface getTurboTrainer(AntHubService.LocalBinder binder) { Log.d(TAG, selectedTurboTrainer); Mode mode = uiModeToTurboMode.get(selectedCourseMode); TurboRegistry turboRegistry = getTurboRegistry(); TurboTrainerInterface turbo = turboRegistry.getTurboTrainer(selectedTurboTrainer); turbo.setMode(mode); if (turboRegistry.usesAnt(selectedTurboTrainer)) { initAnt(binder); AntTurboTrainer antTurbo = (AntTurboTrainer) turbo; antTurbo.setNode(antNode); } return turbo; } private void startServiceInBackround() { Intent intent = new Intent(this, AntHubService.class); this.startService(intent); } private synchronized void updateLocation(LatLongAlt pos) { try { float locSpeed = (float) (lastRecordedSpeed / UnitConversions.MS_TO_KMH); final long timestamp = System.currentTimeMillis(); Log.v(TAG, "location timestamp: " + timestamp); Location loc = new Location(MOCK_LOCATION_PROVIDER); Log.d(TAG, "alt: " + pos.getAltitude()); Log.d(TAG, "lat: " + pos.getLatitude()); Log.d(TAG, "long: " + pos.getLongitude()); loc.setLatitude(pos.getLatitude()); loc.setLongitude(pos.getLongitude()); loc.setAltitude(pos.getAltitude()); // TODO(dszumski) one possible way to correct // long timeCorrection = (long) (1000.0 * (delta / lastRecordedSpeed)); // loc.setTime(timestamp - timeCorrection); loc.setTime(timestamp); loc.setSpeed(locSpeed); loc.setAccuracy(gpsAccuracy); Method locationJellyBeanFixMethod = null; try { locationJellyBeanFixMethod = Location.class.getMethod("makeComplete"); } catch (NoSuchMethodException e) { // ignore } if (locationJellyBeanFixMethod != null) { locationJellyBeanFixMethod.invoke(loc); } unpauseRecording(); // automatically resume on location updates broadcastLocation(loc); Log.d(TAG, "updated location"); } catch (SecurityException e) { // is this possible now we aren't using mock locations? handleException(e, "Error updating location", true, NOTIFCATION_ID_STARTUP); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } @Override public synchronized void onDestroy() { //FIXME: should not do this in calibrate mode unregisterRecordingReceiver(); running = false; new Thread() { public void run() { shutDownTurbo(); } }.start(); // no longer need ant this.doUnbindService(); super.onDestroy(); } // this takes time private boolean shutDownTurbo() { boolean shutDownSuccess = true; if (turboTrainer != null) { try { turboTrainer.unregisterDataListener(dataListener); turboTrainer.stop(); if (hrm != null) { hrm.stop(); } } catch (Exception e) { handleException(e, "Error shutting down turbo", false, NOTIFCATION_ID_SHUTDOWN); shutDownSuccess = false; } String shutdownMessage; if (shutDownSuccess) { shutdownMessage = "Shutdown turbo trainer sucessfully"; } else { shutdownMessage = "Error shutting down turbo trainer"; } // NOU IN UI // Toast.makeText(this.getBaseContext(), shutdownMessage, // Toast.LENGTH_LONG).show(); Log.i(TAG, shutdownMessage); } Intent intent = new Intent().setAction(this.getString(R.string.anthub_action_shutdown)); sendBroadcast(intent); if (wakeLock.isHeld()) { wakeLock.release(); } // trackRecordingServiceConnection.unbind(); return shutDownSuccess; } public class TurboBinder extends Binder { public TurboService getService() { return TurboService.this; } } @Override public IBinder onBind(Intent arg0) { return turboBinder; } /* * (non-Javadoc) * @see android.app.Service#onCreate() */ @Override public void onCreate() { super.onCreate(); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback); // trackRecordingServiceConnection.startAndBind(); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); this.gpsAccuracy = getApplicationContext().getResources().getInteger(R.integer.SIMULATED_LOCATION_ACCURACY); this.wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, TurboService.WAKE_LOCK); } // can th is be null private final Runnable bindChangedCallback = new Runnable() { @Override public void run() { // After binding changes (is available), update the total time in // trackController. } }; // start track controller stuff private final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(TRACK_STOPPED_ACTION)) { TurboService.this.doFinish(); } else if (action.equals(TRACK_PAUSED_ACTION)) { // paused } } }; private static String TRACK_STOPPED_ACTION; private static String TRACK_PAUSED_ACTION; public void registerRecordingReceiver() { TRACK_STOPPED_ACTION = TurboService.this.getString(R.string.track_stopped_broadcast_action); TRACK_PAUSED_ACTION = TurboService.this.getString(R.string.track_paused_broadcast_action); IntentFilter filter = new IntentFilter(); filter.addAction(TRACK_STOPPED_ACTION); filter.addAction(TRACK_PAUSED_ACTION); registerReceiver(receiver, filter); } public void unregisterRecordingReceiver() { try { unregisterReceiver(receiver); } catch (IllegalArgumentException e) { //FIXME: catch-all shoudln't be necessary if we had better logic Log.d(TAG, "unable to unregister Recording receiver"); } } // probably should show an activity with detail? // TODO: move strings to R private void handleException(Exception e, String title, boolean finish, int id) { title = title == null ? "An Error occured communicating with your turbotrainer" : title; NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.track_bike) .setContentTitle(title); Notification notification = null; Log.e(TAG, "Exception thrown in " + TurboService.class.getSimpleName() + ": ", e); String exceptionMessage = e.getMessage() == null ? "" : e.getMessage(); if (e instanceof ServiceAlreadyClaimedException) { if (transceiver != null) { transceiver.requestForceClaimInterface(getString(R.string.app_name)); } mBuilder.setContentText("Someone has already claimed the interface : attempting to force claim"); notification = mBuilder.build(); } else if (e instanceof AntRadioServiceNotInstalledException) { mBuilder.setContentText("AntRadioSerivce not installed. Please install."); notification = mBuilder.build(); } else if (e instanceof TooFewAntChannelsAvailableException) { mBuilder.setContentText("Too few ant channels available"); notification = mBuilder.build(); } else if (e instanceof AntRadioPoweredOffException) { mBuilder.setContentText("Ant radio is powered off"); notification = mBuilder.build(); } else if (e instanceof Exception) { //too long : just show message? StringBuilder message = new StringBuilder(); Throwable cause = e.getCause(); message.append("Exception: " + e.getMessage()); if (!exceptionMessage.equals("")) { message.append("\n" + "Message : " + exceptionMessage); } if (cause != null) { message.append("\nCaused by: "); message.append(cause.toString()); if (cause.getMessage() != null) { message.append(" : " + cause.getMessage()); } while ((cause = cause.getCause()) != null) { message.append(", "); message.append(cause.toString()); if (cause.getMessage() != null) { message.append(" : " + cause.getMessage()); } } } mBuilder.setContentText(message); notification = mBuilder.build(); } NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.notify(id, notification); if (finish) { Intent intent = new Intent().setAction(this.getString(R.string.turbo_service_action_exception_thrown)); sendBroadcast(intent); doFinish(); } } /* * Note that sharedPreferenceChangeListenr 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.equals(getString(R.string.settings_turbotrainer_generic_scale_factor_key))) { syncScaleFactor(); } else if (key.equals(getString(R.string.turbotrainer_selected))) { selectedTurboTrainer = preferences.getString( getString(R.string.turbotrainer_selected), getString(R.string.turbotrainer_tacx_bushido_headunit_value)); } } }; private void syncScaleFactor() { scaleFactor = Float.parseFloat(preferences.getString(getString(R.string.settings_turbotrainer_generic_scale_factor_key), "1.0")); } }