public class LogCollectorService extends Service {
    private static final String PREFIX = LogCollectorService.class.getName() + ".";
    public static final String ACTION_START_COLLECTING = PREFIX + "ACTION_START_COLLECTING";
    public static final String ACTION_STOP_COLLECTING = PREFIX + "ACTION_STOP_COLLECTING";

    private static final int NOTIFICATION_ID = 1;
    private Uri mCollectingRideUri;
    protected Location mLastLocation;
    private Float mLastCadence;
    private Integer mLastHeartRate;
    private SharedPreferences mPreferences;
    private AndroidWearSender mAndroidWearSender = null;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("intent=" + StringUtil.toString(intent));
        if (intent == null) return Service.START_STICKY;
        String action = intent.getAction();
        if (ACTION_START_COLLECTING.equals(action)) {
            startCollecting(intent.getData());
        } else if (ACTION_STOP_COLLECTING.equals(action)) {
            stopCollecting(intent.getData());
        }
        return Service.START_STICKY;
    }

    private void startCollecting(Uri rideUri) {
        Context context = getApplicationContext();
        Schedulers.io().scheduleDirect(() -> {
            // Smartwatches support (if enabled in prefs)
            if (mPreferences.getBoolean(Constants.PREF_ANDROID_WEAR, Constants.PREF_ANDROID_WEAR_DEFAULT)) {
                mAndroidWearSender = new AndroidWearSender();
                mAndroidWearSender.startSending(context);
            }

            // First, pause current ride if any
            if (mCollectingRideUri != null) {
                RideManager.get().pause(mCollectingRideUri);
            }

            // Check if the ride still exists (it may have been deleted)
            boolean rideExists = RideManager.get().isExistingRide(rideUri);
            Log.d("rideExists=" + rideExists);
            if (!rideExists) {
                stopSelf();
                return;
            }

            // Save the ride as the current one
            RideManager.get().setCurrentRide(rideUri);

            // Now collect for the new current ride
            mCollectingRideUri = rideUri;
            RideManager.get().activate(mCollectingRideUri);

            // Show notification
            Notification notification = createNotification();
            startForeground(NOTIFICATION_ID, notification);

            // Start recording location
            LocationManager.get().addLocationListener(mLocationListener);

            // Start monitoring cadence (if enabled in the prefs)
            if (mPreferences.getBoolean(Constants.PREF_RECORD_CADENCE, Constants.PREF_RECORD_CADENCE_DEFAULT)) {
                CadenceManager.get().addListener(mCadenceListener);
            }

            // Start listening to pref changes (to enable / disable cadence recording accordingly)
            mPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);

            // Start recording heart rate
            HeartRateManager.get().addListener(mHeartRateListener);
        });
    }

    private void stopCollecting(Uri rideUri) {
        Schedulers.io().scheduleDirect(() -> RideManager.get().pause(rideUri));

        // Dismiss notification
        dismissNotification();

        LocationManager.get().removeLocationListener(mLocationListener);
        CadenceManager.get().removeListener(mCadenceListener);
        HeartRateManager.get().removeListener(mHeartRateListener);

        mCollectingRideUri = null;
        stopSelf();
    }

    /*
     * Location listener.
     */

    private LocationListener mLocationListener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            Schedulers.io().scheduleDirect(() -> {
                LogManager.get().add(mCollectingRideUri, location, mLastLocation, mLastCadence, mLastHeartRate);
                mLastLocation = location;
            });
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {}

        @Override
        public void onProviderEnabled(String provider) {}

        @Override
        public void onProviderDisabled(String provider) {}
    };


    /*
     * Cadence listener.
     */

    private CadenceListener mCadenceListener = new CadenceListener() {
        @Override
        public void onCadenceChanged(Float cadence, float[][] rawData) {
            mLastCadence = cadence;
        }
    };


    /*
     * Pref listener.
     */

    protected OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            if (mPreferences.getBoolean(Constants.PREF_RECORD_CADENCE, Constants.PREF_RECORD_CADENCE_DEFAULT)) {
                // Start monitoring cadence
                CadenceManager.get().addListener(mCadenceListener);
            } else {
                // Stop monitoring cadence
                CadenceManager.get().removeListener(mCadenceListener);
            }

            // TODO check for smartwatch preferences and start / stop sending values
        }
    };


    /*
     * Heart rate listener.
     */

    private HeartRateListener mHeartRateListener = new HeartRateListener() {
        @Override
        public void onConnecting() {}

        @Override
        public void onHeartRateChange(int bpm) {
            mLastHeartRate = bpm;
        }

        @Override
        public void onDisconnected() {
            mLastHeartRate = null;
        }

        @Override
        public void onConnected() {}

        @Override
        public void onError() {
            onDisconnected();
        }
    };


    /*
     * Notification.
     */

    private Notification createNotification() {
        NotificationCompat.Builder mainNotifBuilder = new NotificationCompat.Builder(this);
        mainNotifBuilder.setOngoing(true);
        mainNotifBuilder.setSmallIcon(R.drawable.ic_stat_collecting);
        mainNotifBuilder.setTicker(getString(R.string.service_notification_ticker));
        mainNotifBuilder.setContentTitle(getString(R.string.app_name));
        mainNotifBuilder.setContentText(getString(R.string.service_notification_text));
        mainNotifBuilder.setPriority(NotificationCompat.PRIORITY_MAX);

        //        Intent intent = new Intent(this, DisplayActivity.class).setData(mCollectingRideUri);
        //        builder.setContentIntent(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));

        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(this);
        taskStackBuilder.addParentStack(DisplayActivity.class);
        Log.d("mCollectingRideUri=" + mCollectingRideUri);
        Intent intent = new Intent(this, DisplayActivity.class).setData(mCollectingRideUri);
        taskStackBuilder.addNextIntent(intent);
        mainNotifBuilder.setContentIntent(taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT));

        //TODO
        //        builder.addAction(R.drawable.ic_action_stop, getString(R.string.service_notification_action_stop),
        //                PendingIntent.getBroadcast(this, 0, new Intent(ACTION_DISABLE), PendingIntent.FLAG_CANCEL_CURRENT));
        //        builder.addAction(R.drawable.ic_action_logs, getString(R.string.service_notification_action_logs),
        //                PendingIntent.getActivity(this, 0, new Intent(this, LogActivity.class), PendingIntent.FLAG_UPDATE_CURRENT));

        Notification notification = mainNotifBuilder.build();
        return notification;
    }

    private void dismissNotification() {
        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.cancel(NOTIFICATION_ID);
    }


    @Override
    public void onDestroy() {
        // Disconnect smartwatch senders
        if (mAndroidWearSender != null) mAndroidWearSender.stopSending();

        // Unregister pref listener
        PreferenceManager.getDefaultSharedPreferences(LogCollectorService.this).unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
        super.onDestroy();
    }
} public class CompassDisplayFragment extends BaseFragment<DisplayActivity> {
    private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();

    public static CompassDisplayFragment newInstance() {
        return new CompassDisplayFragment();
    }

    private ImageView mImgCompass;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View res = inflater.inflate(R.layout.display_compass, container, false);
        mImgCompass = (ImageView) res.findViewById(R.id.imgCompass);
        return res;
    }

    @Override
    public void onStart() {
        super.onStart();
        if (!isHidden()) CompassManager.get().addListener(mCompassListener);
    }

    @Override
    public void onStop() {
        CompassManager.get().removeListener(mCompassListener);
        super.onStop();
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        Log.d("hidden=" + hidden);
        if (hidden) {
            CompassManager.get().removeListener(mCompassListener);
        } else {
            CompassManager.get().addListener(mCompassListener);
        }
    }

    private CompassListener mCompassListener = new CompassListener() {
        @Override
        public void onCompassChange(float compass) {
            float currentRotation = mImgCompass.getRotation();
            float newRotation = compass * 360f;
            if (newRotation - currentRotation < 180) {
                mImgCompass.animate().setDuration(CompassManager.RATE).setInterpolator(LINEAR_INTERPOLATOR).rotation(newRotation);
            } else {
                mImgCompass.animate().setDuration(CompassManager.RATE).setInterpolator(LINEAR_INTERPOLATOR).rotation(newRotation - 360);
            }
        }
    };
} public class CurrentTimeDisplayFragment extends SimpleDisplayFragment {
    protected static final long REFRESH_RATE = 30 * 1000;
    private Handler mHandler = new Handler();
    private DateFormat mTimeFormat;

    public static CurrentTimeDisplayFragment newInstance() {
        return new CurrentTimeDisplayFragment();
    }

    @Override
    public void onStart() {
        super.onStart();
        mTimeFormat = android.text.format.DateFormat.getTimeFormat(getActivity());
        setTextEnabled(true);
        mHandler.post(mShowTimeRunnable);
    }

    @Override
    public void onStop() {
        mHandler.removeCallbacks(mShowTimeRunnable);
        super.onStop();
    }

    private Runnable mShowTimeRunnable = new Runnable() {
        @Override
        public void run() {
            setText(mTimeFormat.format(new Date()));
            mHandler.postDelayed(mShowTimeRunnable, REFRESH_RATE);
        }
    };
} public class ElapsedTimeDisplayFragment extends SimpleDisplayFragment {
    private Chronometer mChronometer;

    public static ElapsedTimeDisplayFragment newInstance() {
        return new ElapsedTimeDisplayFragment();
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.display_elapsed_time;
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mChronometer = (Chronometer) view.findViewById(R.id.chronometer);
    }

    private void setChronometerDuration(long duration) {
        mChronometer.setBase(SystemClock.elapsedRealtime() - duration);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // Ride updates
        RideManager rideManager = RideManager.get();
        rideManager.addListener(mRideListener);

        Uri rideUri = getRideUri();
        if (rideUri == null) return;


        Single.fromCallable(() -> readRideDurationInfo(rideUri))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(rideDurationInfo -> {
                    if (rideDurationInfo.isActive) {
                        long additionalDuration = System.currentTimeMillis() - rideDurationInfo.activatedDate;
                        setChronometerDuration(rideDurationInfo.duration + additionalDuration);
                        mChronometer.setEnabled(true);
                        mChronometer.start();
                    } else {
                        setChronometerDuration(rideDurationInfo.duration);
                    }
                });
    }

    private static class RideDurationInfo {
        long duration;
        boolean isActive;
        long activatedDate;
    }

    @WorkerThread
    private RideDurationInfo readRideDurationInfo(Uri rideUri) {
        RideDurationInfo result = new RideDurationInfo();
        RideManager rideManager = RideManager.get();
        result.duration = rideManager.getDuration(rideUri);
        result.isActive = rideManager.getState(rideUri) == RideState.ACTIVE;
        result.activatedDate = result.isActive ? rideManager.getActivatedDate(rideUri).getTime() : 0;
        return result;
    }

    @Override
    public void onDestroy() {
        // Ride updates
        RideManager.get().removeListener(mRideListener);
        super.onDestroy();
    }

    private RideListener mRideListener = new RideListener() {
        @Override
        public void onActivated(Uri rideUri) {
            if (!rideUri.equals(getRideUri())) return;

            Single.fromCallable(() -> RideManager.get().getDuration(rideUri))
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(duration -> {
                        setChronometerDuration(duration);
                        mChronometer.start();
                        mChronometer.setEnabled(true);
                    });
        }

        @Override
        public void onPaused(Uri rideUri) {
            if (!rideUri.equals(getRideUri())) return;
            mChronometer.stop();
            mChronometer.setEnabled(false);
        }
    };
} public class HeartRateDisplayFragment extends SimpleDisplayFragment {
    public static HeartRateDisplayFragment newInstance() {
        return new HeartRateDisplayFragment();
    }

    @Override
    public void onStart() {
        super.onStart();
        // HeartRate
        HeartRateManager heartRateManager = HeartRateManager.get();
        heartRateManager.addListener(mHeartRateListener);
        if (heartRateManager.isConnecting()) {
            mHeartRateListener.onConnecting();
        } else if (heartRateManager.isConnected()) {
            mHeartRateListener.onConnected();
        } else {
            mHeartRateListener.onHeartRateChange(heartRateManager.getLastValue());
            mHeartRateListener.onDisconnected();
        }
    }

    @Override
    public void onStop() {
        // HeartRate
        HeartRateManager.get().removeListener(mHeartRateListener);
        super.onStop();
    }

    private HeartRateListener mHeartRateListener = new HeartRateListener() {
        @Override
        public void onConnecting() {}

        @Override
        public void onConnected() {
            setTextEnabled(true);
        }

        @Override
        public void onHeartRateChange(int bpm) {
            setText(UnitUtil.formatHeartRate(bpm));
        }

        @Override
        public void onDisconnected() {
            setText("    -    ");
            setTextEnabled(false);
        }

        @Override
        public void onError() {
            onDisconnected();
        }
    };
} public class SpeedDisplayFragment extends SimpleDisplayFragment {
    public static SpeedDisplayFragment newInstance() {
        return new SpeedDisplayFragment();
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }

    @Override
    public void onStart() {
        super.onStart();
        // GPS status
        LocationManager.get().addStatusListener(mGpsStatusListener);

        // Speed updates
        mSpeedometer.startListening();
    }

    @Override
    public void onStop() {
        // GPS status
        LocationManager.get().removeStatusListener(mGpsStatusListener);

        // Speed updates
        mSpeedometer.stopListening();
        super.onStop();
    }

    private Speedometer mSpeedometer = new Speedometer() {
        @Override
        public void onLocationChanged(Location location) {
            super.onLocationChanged(location);
            setText(UnitUtil.formatSpeed(getSpeed()));
        }
    };

    private StatusListener mGpsStatusListener = this::setTextEnabled;
} public class TotalDistanceDisplayFragment extends LogDisplayFragment {
    public static TotalDistanceDisplayFragment newInstance() {
        return new TotalDistanceDisplayFragment();
    }

    @Override
    protected @Nullable CharSequence queryValue() {
        Uri rideUri = getRideUri();
        if (rideUri == null) return null;
        return UnitUtil.formatDistance((float) LogManager.get().getTotalDistance(rideUri));
    }
} public abstract class HeartRateManager {
    private static final HeartRateManager INSTANCE = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 ? new HeartRateManagerDisabled()
            : new HeartRateManagerJellyBeanMR2();

    public static HeartRateManager get() {
        return INSTANCE;
    }

    public abstract void addListener(HeartRateListener listener);

    public abstract void removeListener(HeartRateListener listener);

    public abstract void setBluetoothDevice(BluetoothDevice bluetoothDevice);

    public abstract boolean isConnected();

    public abstract boolean isConnecting();

    public abstract int getLastValue();

    public abstract void disconnect();
} public class LocationManager {
    public static interface StatusListener {
        void onStatusChanged(boolean active);
    }

    public static interface ActivityRecognitionListener {
        void onActivityRecognized(int activityType, int confidence);
    }

    private static final LocationManager INSTANCE = new LocationManager();

    private static final int INTERVAL_LOC_REQUEST = 1000;
    private static final int ALLOWED_LOC_MISSES = 8;

    protected static final float ACCURACY_THRESHOLD_M = 20;

    private static final int IGNORE_LOCATION_COUNT = 7;

    /**
     * Speeds below this value will be reported as 0 (because of GPS low precision).
     */
    public static final float SPEED_MIN_THRESHOLD_M_S = 2.2f / 3.6f;


    public static LocationManager get() {
        return INSTANCE;
    }

    private final Context mContext;
    protected long mLastFixDate;
    private Handler mHandler;
    private boolean mActive = false;
    private int mIgnoreLocationCount = IGNORE_LOCATION_COUNT;

    private LocationManager() {
        mContext = Application.getApplication();
    }


    /*
     * Location.
     */

    public void addLocationListener(LocationListener listener) {
        mLocationListeners.add(listener);
    }

    public void removeLocationListener(LocationListener listener) {
        mLocationListeners.remove(listener);
    }

    private Listeners<LocationListener> mLocationListeners = new Listeners<LocationListener>() {
        @Override
        protected void onFirstListener() {
            Log.d("First location listener, start location listener");
            startLocationListener();
        }

        @Override
        protected void onNoMoreListeners() {
            Log.d("No more location listeners, stop location listener");
            stopLocationListener();
        }
    };

    private void startLocationListener() {
        Log.d();
        mIgnoreLocationCount = IGNORE_LOCATION_COUNT;
        HandlerUtil.getMainHandler().post(() -> {
            android.location.LocationManager locationManager = (android.location.LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
            locationManager.removeUpdates(mGpsLocationListener);
            locationManager.requestLocationUpdates(android.location.LocationManager.GPS_PROVIDER, INTERVAL_LOC_REQUEST, 0, mGpsLocationListener);
        });
    }

    private void stopLocationListener() {
        Log.d();
        HandlerUtil.getMainHandler().post(() -> {
            android.location.LocationManager locationManager = (android.location.LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
            locationManager.removeUpdates(mGpsLocationListener);
        });
    }

    private android.location.LocationListener mGpsLocationListener = new android.location.LocationListener() {
        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            Log.d("status=" + status);
        }

        @Override
        public void onProviderEnabled(String provider) {
            Log.d("provider=" + provider);
        }

        @Override
        public void onProviderDisabled(String provider) {
            Log.d("provider=" + provider);
        }

        @Override
        public void onLocationChanged(Location location) {
            // For some reason, the time seems to have a 1 second precision.
            // Use the system time instead.
            location.setTime(System.currentTimeMillis());
            mLocationListener.onLocationChanged(location);
        }
    };

    private LocationListener mLocationListener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            Log.d("location=" + location);
            if (location.hasAccuracy() && location.getAccuracy() > ACCURACY_THRESHOLD_M) {
                Log.d("Accuracy above threshold: ignore location");
                return;
            }

            mIgnoreLocationCount--;
            if (mIgnoreLocationCount >= 0) {
                Log.d("Ignore first few locations");
                return;
            }

            // Dispatch to listeners
            mLocationListeners.dispatch(listener -> listener.onLocationChanged(location));
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {}

        @Override
        public void onProviderEnabled(String provider) {}

        @Override
        public void onProviderDisabled(String provider) {}
    };


    /*
     * Gps status.
     */

    public void addStatusListener(StatusListener listener) {
        mStatusListeners.add(listener);
    }

    public void removeStatusListener(StatusListener listener) {
        mStatusListeners.remove(listener);
    }

    private Listeners<StatusListener> mStatusListeners = new Listeners<StatusListener>() {
        @Override
        protected void onFirstListener() {
            Log.d("First status listener, start gps location listener");
            startGpsLocationListener();
        }

        @Override
        protected void onNoMoreListeners() {
            Log.d("No more status listeners, stop gps location listener");
            stopGpsLocationListener();
        }
    };

    private void startGpsLocationListener() {
        Log.d();
        setActive(false);
        HandlerUtil.getMainHandler().post(() -> {
            android.location.LocationManager locationManager = (android.location.LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
            locationManager.removeUpdates(mGpsStatusLocationListener);
            locationManager.requestLocationUpdates(android.location.LocationManager.GPS_PROVIDER, INTERVAL_LOC_REQUEST, 0, mGpsStatusLocationListener);
        });
    }

    private void stopGpsLocationListener() {
        Log.d();
        HandlerUtil.getMainHandler().post(() -> {
            android.location.LocationManager locationManager = (android.location.LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
            locationManager.removeUpdates(mGpsStatusLocationListener);
        });
    }

    private android.location.LocationListener mGpsStatusLocationListener = new android.location.LocationListener() {
        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            Log.d("status=" + status);
        }

        @Override
        public void onProviderEnabled(String provider) {
            Log.d("provider=" + provider);
        }

        @Override
        public void onProviderDisabled(String provider) {
            Log.d("provider=" + provider);
        }

        @Override
        public void onLocationChanged(Location location) {
            mLastFixDate = System.currentTimeMillis();
            // We just received a fix so we're active
            setActive(true);

            // Schedule to check if we're still active
            getHandler().removeCallbacks(mCheckForActiveRunnable);
            getHandler().postDelayed(mCheckForActiveRunnable, INTERVAL_LOC_REQUEST * ALLOWED_LOC_MISSES);
        }
    };

    private Handler getHandler() {
        if (mHandler == null) {
            mHandler = new Handler();
        }
        return mHandler;
    }

    protected Runnable mCheckForActiveRunnable = new Runnable() {
        @Override
        public void run() {
            if (System.currentTimeMillis() - mLastFixDate >= INTERVAL_LOC_REQUEST * ALLOWED_LOC_MISSES) {
                setActive(false);
            }
        }
    };

    protected void setActive(boolean active) {
        if (mActive != active) {
            // Dispatch to listeners
            mStatusListeners.dispatch(listener -> listener.onStatusChanged(active));
        }
        mActive = active;
    }
} public static interface StatusListener {
    void onStatusChanged(boolean active);
} public interface RideListener {
    void onActivated(Uri rideUri);

    void onPaused(Uri rideUri);
} public class RideManager {
    private static final RideManager INSTANCE = new RideManager();

    public static RideManager get() {
        return INSTANCE;
    }

    private final Context mContext;
    private Listeners<RideListener> mListeners = Listeners.newInstance();

    private RideManager() {
        mContext = Application.getApplication();
    }

    @WorkerThread
    public Uri create(String name) {
        RideContentValues values = new RideContentValues();
        values.putUuid(UUID.randomUUID().toString());
        values.putCreatedDate(new Date());
        if (!TextUtils.isEmpty(name)) {
            values.putName(name);
        }
        values.putState(RideState.CREATED);
        values.putDuration(0L);
        values.putDistance(0f);
        return values.insert(mContext);
    }

    @WorkerThread
    public int delete(long[] ids) {
        // First pause any active rides in the list
        pauseRides(ids);

        // Mark rides as deleted
        RideSelection rideSelection = new RideSelection();
        rideSelection.id(ids);
        RideContentValues rideContentValues = new RideContentValues();
        rideContentValues.putState(RideState.DELETED);
        int res = rideContentValues.update(mContext, rideSelection);

        // Delete logs
        LogSelection logSelection = new LogSelection();
        logSelection.rideId(ids);
        logSelection.delete(mContext);

        // If we just deleted the current ride, select another ride to be the current ride (if any).
        Uri currentRideUri = getCurrentRide();
        if (currentRideUri != null) {
            long currentRideId = Long.valueOf(currentRideUri.getLastPathSegment());
            for (long id : ids) {
                if (currentRideId == id) {
                    Uri nextRideUri = getMostRecentRide();
                    setCurrentRide(nextRideUri);
                    break;
                }
            }
        }
        return res;
    }

    @WorkerThread
    public void merge(long[] ids) {
        // First pause any active rides in the list
        pauseRides(ids);

        // Choose the master ride (the one with the earliest creation date)
        String[] projection = {RideColumns._ID};
        RideSelection rideSelection = new RideSelection();
        rideSelection.id(ids);
        rideSelection.orderByCreatedDate();
        ContentResolver contentResolver = mContext.getContentResolver();
        RideCursor rideCursor = rideSelection.query(contentResolver, projection);
        long masterRideId;
        try {
            rideCursor.moveToNext();
            masterRideId = rideCursor.getId();
        } finally {
            rideCursor.close();
        }

        // Calculate the total duration
        projection = new String[] {"sum(" + RideColumns.DURATION + ")"};
        Cursor c = contentResolver.query(RideColumns.CONTENT_URI, projection, rideSelection.sel(), rideSelection.args(), null);
        long totalDuration = 0;
        try {
            if (c.moveToNext()) {
                totalDuration = c.getLong(0);
            }
        } finally {
            c.close();
        }

        // Merge
        for (long mergedRideId : ids) {
            if (mergedRideId == masterRideId) continue;

            // Update logs
            LogSelection logSelection = new LogSelection();
            logSelection.rideId(mergedRideId);
            LogContentValues values = new LogContentValues();
            values.putRideId(masterRideId);
            values.update(contentResolver, logSelection);

            // Delete merged ride
            rideSelection = new RideSelection();
            rideSelection.id(mergedRideId);
            // Do not notify yet
            Uri contentUri = BikeyProvider.notify(RideColumns.CONTENT_URI, false);
            contentResolver.delete(contentUri, rideSelection.sel(), rideSelection.args());
        }

        // Rename master ride
        Uri masterRideUri = ContentUris.withAppendedId(RideColumns.CONTENT_URI, masterRideId);
        String name = getName(masterRideUri);
        if (name == null) {
            name = mContext.getString(R.string.ride_list_mergedRide);
        } else {
            name = mContext.getString(R.string.ride_list_mergedRide_append, name);
        }
        RideContentValues values = new RideContentValues();
        values.putName(name);
        contentResolver.update(masterRideUri, values.values(), null, null);

        // Update master ride total distance
        float distance = LogManager.get().getTotalDistance(masterRideUri);
        updateTotalDistance(masterRideUri, distance);

        // Update master ride total duration
        updateDuration(masterRideUri, totalDuration);
    }

    private void pauseRides(long[] ids) {
        for (long rideId : ids) {
            Uri rideUri = ContentUris.withAppendedId(RideColumns.CONTENT_URI, rideId);
            RideState state = getState(rideUri);
            if (state == RideState.ACTIVE) {
                mContext.startService(new Intent(LogCollectorService.ACTION_STOP_COLLECTING, rideUri, mContext, LogCollectorService.class));
                break;
            }
        }
    }

    @WorkerThread
    public void activate(@NonNull Uri rideUri) {
        // Get first activated date
        Date firstActivatedDate = getFirstActivatedDate(rideUri);

        // Update state
        RideContentValues values = new RideContentValues();
        values.putState(RideState.ACTIVE);
        // Update activated date
        Date now = new Date();
        values.putActivatedDate(now);
        // Update first activated date, only if first time
        if (firstActivatedDate == null) {
            values.putFirstActivatedDate(now);
        }
        mContext.getContentResolver().update(rideUri, values.values(), null, null);

        // Dispatch to listeners
        mListeners.dispatch(listener -> listener.onActivated(rideUri));
    }

    @WorkerThread
    public void updateTotalDistance(@NonNull Uri rideUri, float distance) {
        RideContentValues values = new RideContentValues();
        values.putDistance(distance);
        mContext.getContentResolver().update(rideUri, values.values(), null, null);
    }

    @WorkerThread
    private void updateDuration(@NonNull Uri rideUri, long duration) {
        RideContentValues values = new RideContentValues();
        values.putDuration(duration);
        mContext.getContentResolver().update(rideUri, values.values(), null, null);
    }

    @WorkerThread
    public void updateName(@NonNull Uri rideUri, String name) {
        RideContentValues values = new RideContentValues();
        if (TextUtils.isEmpty(name)) {
            values.putNameNull();
        } else {
            values.putName(name);
        }
        mContext.getContentResolver().update(rideUri, values.values(), null, null);
    }

    @WorkerThread
    public void pause(@NonNull Uri rideUri) {
        // Get current activated date / duration
        String[] projection = {RideColumns.ACTIVATED_DATE, RideColumns.DURATION};
        RideCursor c = new RideCursor(mContext.getContentResolver().query(rideUri, projection, null, null, null));
        try {
            if (!c.moveToNext()) {
                Log.w("Could not pause ride, uri " + rideUri + " not found");
                return;
            }
            long activatedDate = c.getActivatedDate().getTime();
            long duration = c.getDuration();

            // Update duration, state, and reset activated date
            duration += System.currentTimeMillis() - activatedDate;

            RideContentValues values = new RideContentValues();
            values.putState(RideState.PAUSED);
            values.putDuration(duration);
            values.putActivatedDate(0l);
            mContext.getContentResolver().update(rideUri, values.values(), null, null);

            // Dispatch to listeners
            mListeners.dispatch(listener -> listener.onPaused(rideUri));
        } finally {
            c.close();
        }
    }

    /**
     * Queries all the columns for the given ride.
     * Do not forget to call {@link Cursor#close()} on the returned Cursor.
     */
    public RideCursor query(@NonNull Uri rideUri) {
        if (rideUri == null) throw new IllegalArgumentException("null rideUri");
        Cursor c = mContext.getContentResolver().query(rideUri, null, null, null, null);
        if (!c.moveToNext()) {
            c.close();
            throw new IllegalArgumentException(rideUri + " not found");
        }
        return new RideCursor(c);
    }

    @WorkerThread
    @Nullable
    public Uri getCurrentRide() {
        String currentRideUriStr = PreferenceManager.getDefaultSharedPreferences(mContext).getString(Constants.PREF_CURRENT_RIDE_URI, null);
        if (!TextUtils.isEmpty(currentRideUriStr)) {
            Uri currentRideUri = Uri.parse(currentRideUriStr);
            return currentRideUri;
        }
        return null;
    }

    @WorkerThread
    public void setCurrentRide(@NonNull Uri rideUri) {
        PreferenceManager.getDefaultSharedPreferences(mContext).edit().putString(Constants.PREF_CURRENT_RIDE_URI, rideUri.toString()).apply();
    }

    @WorkerThread
    private Uri getMostRecentRide() {
        String[] projection = {RideColumns._ID};
        // Return a ride, prioritizing ACTIVE ones first, then sorting by creation date.
        Cursor c = mContext.getContentResolver().query(RideColumns.CONTENT_URI, projection, null, null,
                RideColumns.STATE + ", " + RideColumns.CREATED_DATE + " DESC");
        try {
            if (!c.moveToNext()) return null;
            long id = c.getLong(0);
            return ContentUris.withAppendedId(RideColumns.CONTENT_URI, id);
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public Date getActivatedDate(@NonNull Uri rideUri) {
        RideCursor c = query(rideUri);
        try {
            return c.getActivatedDate();
        } finally {
            c.close();
        }
    }

    @WorkerThread
    private Date getFirstActivatedDate(@NonNull Uri rideUri) {
        RideCursor c = query(rideUri);
        try {
            return c.getFirstActivatedDate();
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public long getDuration(@NonNull Uri rideUri) {
        RideCursor c = query(rideUri);
        try {
            return c.getDuration();
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public RideState getState(@NonNull Uri rideUri) {
        RideCursor c = query(rideUri);
        try {
            return c.getState();
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public String getDisplayName(@NonNull Uri rideUri) {
        RideCursor c = query(rideUri);
        try {
            String name = c.getName();
            long createdDateLong = c.getCreatedDate().getTime();
            String createdDateTimeStr = DateUtils.formatDateTime(mContext, createdDateLong, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
            if (name == null) {
                return createdDateTimeStr;
            }
            return name + " (" + createdDateTimeStr + ")";
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public String getName(@NonNull Uri rideUri) {
        RideCursor c = query(rideUri);
        try {
            return c.getName();
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public boolean isExistingRide(@NonNull Uri rideUri) {
        Cursor c = mContext.getContentResolver().query(rideUri, null, null, null, null);
        try {
            return c.moveToNext();
        } finally {
            c.close();
        }
    }


    /*
     * Listeners.
     */

    public void addListener(@NonNull RideListener listener) {
        mListeners.add(listener);
    }

    public void removeListener(@NonNull RideListener listener) {
        mListeners.remove(listener);
    }
} public class FragmentCycler {
    private int mContainerResId;
    private List<String> mFragmentTags = new ArrayList<>(10);
    private List<Checkable> mTabs = new ArrayList<>(10);
    private List<Integer> mTitles = new ArrayList<>(10);
    private int mCurrentVisibleIndex = 0;
    private TextView mTxtTitle;
    private Map<String, Boolean> mEnabled = new HashMap<>();
    private long mUpdateTitleDelay;
    private final int mTabColorEnabled;
    private final int mTabColorDisabled;

    public FragmentCycler(int containerResId, TextView txtTitle, long updateTitleDelay, int tabColorEnabled, int tabColorDisabled) {
        mContainerResId = containerResId;
        mTxtTitle = txtTitle;
        mUpdateTitleDelay = updateTitleDelay;
        mTabColorEnabled = tabColorEnabled;
        mTabColorDisabled = tabColorDisabled;
    }

    /**
     * @param tabResId Optional, use 0 for no tab.
     */
    public void add(FragmentActivity activity, Fragment fragment, int tabResId, int titleResId) {
        String tag = getTag(fragment);
        FragmentManager fragmentManager = activity.getSupportFragmentManager();
        Fragment foundFragment = fragmentManager.findFragmentByTag(tag);
        if (foundFragment == null) {
            FragmentTransaction t = fragmentManager.beginTransaction();
            t.add(mContainerResId, fragment, tag);
            t.hide(fragment);
            t.commit();
        } else {
            FragmentTransaction t = fragmentManager.beginTransaction();
            t.hide(foundFragment);
            t.commit();
        }
        mFragmentTags.add(tag);
        View tab = activity.findViewById(tabResId);
        if (tab != null) {
            mTabs.add((Checkable) tab);
            tab.setOnClickListener(mTabOnClickListener);
        }
        mTitles.add(titleResId);
    }

    public void show(FragmentActivity activity) {
        String tag = mFragmentTags.get(mCurrentVisibleIndex);
        FragmentManager fragmentManager = activity.getSupportFragmentManager();
        fragmentManager.executePendingTransactions();
        Fragment fragment = fragmentManager.findFragmentByTag(tag);
        FragmentTransaction t = fragmentManager.beginTransaction();
        t.show(fragment);
        t.commit();
        if (!mTabs.isEmpty()) {
            Checkable checkable = mTabs.get(mCurrentVisibleIndex);
            checkable.setChecked(true);
        }
        updateTitle();
    }

    public void cycle(FragmentActivity activity) {
        // Find the next *enabled* fragment to show
        int newIndex = mCurrentVisibleIndex;
        do {
            newIndex = (newIndex + 1) % mFragmentTags.size();
        } while (!isEnabled(newIndex));
        setCurrentVisibleIndex(activity, newIndex);
    }

    private void setCurrentVisibleIndex(FragmentActivity activity, int newIndex) {
        int previousVisibleIndex = mCurrentVisibleIndex;
        mCurrentVisibleIndex = newIndex;
        String hideTag = mFragmentTags.get(previousVisibleIndex);
        String showTag = mFragmentTags.get(mCurrentVisibleIndex);
        FragmentManager fragmentManager = activity.getSupportFragmentManager();
        Fragment showFragment = fragmentManager.findFragmentByTag(showTag);
        Fragment hideFragment = fragmentManager.findFragmentByTag(hideTag);
        FragmentTransaction t = fragmentManager.beginTransaction();
        t.hide(hideFragment);
        t.show(showFragment);
        t.commit();
        if (!mTabs.isEmpty()) {
            Checkable prevCheckable = mTabs.get(previousVisibleIndex);
            prevCheckable.setChecked(false);
            Checkable curCheckable = mTabs.get(mCurrentVisibleIndex);
            curCheckable.setChecked(true);
        }
        updateTitle();
    }

    private void updateTitle() {
        HandlerUtil.getMainHandler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mTxtTitle.setText(mTitles.get(mCurrentVisibleIndex));
            }
        }, mUpdateTitleDelay);
    }

    private String getTag(Fragment fragment) {
        return getTag(fragment.getClass());
    }

    private String getTag(Class<? extends Fragment> fragmentClass) {
        return fragmentClass.getName();
    }

    private OnClickListener mTabOnClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (!(v instanceof Checkable)) return;
            Checkable checkable = (Checkable) v;
            if (!checkable.isChecked()) checkable.setChecked(true);
            int newIndex = mTabs.indexOf(checkable);
            if (mCurrentVisibleIndex == newIndex) return;
            setCurrentVisibleIndex((FragmentActivity) v.getContext(), newIndex);
        }
    };

    public int getCurrentVisibleIndex() {
        return mCurrentVisibleIndex;
    }

    public void setCurrentVisibleIndex(int currentVisibleIndex) {
        mCurrentVisibleIndex = currentVisibleIndex;
    }

    public void setEnabled(Context context, Class<? extends Fragment> fragmentClass, boolean enabled) {
        String tag = getTag(fragmentClass);
        mEnabled.put(tag, enabled);

        int index = mFragmentTags.indexOf(tag);
        if (!mTabs.isEmpty()) {
            TextView textView = (TextView) mTabs.get(index);
            textView.setTextColor(enabled ? mTabColorEnabled : mTabColorDisabled);
        }
    }

    private boolean isEnabled(int index) {
        Boolean enabled = mEnabled.get(mFragmentTags.get(index));
        if (enabled == null) {
            // Treat no value (default) as enabled.
            enabled = true;
        }
        return enabled;
    }
}
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.PointF;
import android.graphics.drawable.AnimationDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView;
import android.widget.TextView;
import org.jraf.android.bikey.R;
import org.jraf.android.bikey.app.collect.LogCollectorService;
import org.jraf.android.bikey.app.display.fragment.compass.CompassDisplayFragment;
import org.jraf.android.bikey.app.display.fragment.currenttime.CurrentTimeDisplayFragment;
import org.jraf.android.bikey.app.display.fragment.elapsedtime.ElapsedTimeDisplayFragment;
import org.jraf.android.bikey.app.display.fragment.heartrate.HeartRateDisplayFragment;
import org.jraf.android.bikey.app.display.fragment.speed.SpeedDisplayFragment;
import org.jraf.android.bikey.app.display.fragment.totaldistance.TotalDistanceDisplayFragment;
import org.jraf.android.bikey.backend.heartrate.HeartRateManager;
import org.jraf.android.bikey.backend.location.LocationManager;
import org.jraf.android.bikey.backend.location.LocationManager.StatusListener;
import org.jraf.android.bikey.backend.ride.RideListener;
import org.jraf.android.bikey.backend.ride.RideManager;
import org.jraf.android.bikey.common.widget.fragmentcycler.FragmentCycler;
import org.jraf.android.util.app.base.BaseFragmentActivity;
import org.jraf.android.util.log.Log;
import org.jraf.android.util.ui.checkable.CheckableRelativeLayout;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
/*
 * This source is part of the
 *      _____  ___   ____
 *  __ / / _ \/ _ | / __/___  _______ _
 * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
 * \___/_/|_/_/ |_/_/ (_)___/_/  \_, /
 *                              /___/
 * repository.
 * 
 * Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org)
 * 
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jraf.android.bikey.app.display;




public class DisplayActivity extends BaseFragmentActivity {
    private static final long DELAY_HIDE_CONTROLS = 4500;

    private Handler mHandler = new Handler();

    private ImageView mImgGpsStatus;
    private CheckableRelativeLayout mChkRecord;
    private TextView mChkRecordText;
    private Animator mChkRecordTextAnimator;
    private View mConTabsA;
    private View mConTabsB;
    private View mConFragments;
    private TextView mTxtTitle;

    private boolean mNavigationBarHiding = false;
    private Uri mRideUri;
    private FragmentCycler mFragmentCycler;
    private boolean mControlsVisible = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);// TODO only if setting says so

        mRideUri = getIntent().getData();

        setContentView(R.layout.display);

        mChkRecord = (CheckableRelativeLayout) findViewById(R.id.chkRecord);
        mChkRecord.setEnabled(false);
        mChkRecordText = (TextView) findViewById(R.id.chkRecord_text);
        mChkRecordTextAnimator = AnimatorInflater.loadAnimator(this, R.animator.blink);
        mChkRecordTextAnimator.setTarget(mChkRecordText);
        mImgGpsStatus = (ImageView) findViewById(R.id.imgGpsStatus);
        ((AnimationDrawable) mImgGpsStatus.getDrawable()).start();
        findViewById(R.id.vieFragmentCycle).setOnTouchListener(mFragmentCycleOnTouchListener);
        mConTabsA = findViewById(R.id.conTabsA);
        mConTabsB = findViewById(R.id.conTabsB);
        mConFragments = findViewById(R.id.conFragments);
        mTxtTitle = (TextView) findViewById(R.id.txtTitle);

        setupFragments(savedInstanceState == null ? 0 : savedInstanceState.getInt("mFragmentCycler.currentVisibleIndex"));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            setupNavigationBarHiding();
        }
        scheduleHideControls();

        setupFragmentContainer();
    }

    private void setupFragmentContainer() {
        mConFragments.post(() -> {
            PointF shrinkPercents = getShrinkPercents();
            mConFragments.setScaleX(shrinkPercents.x);
            mConFragments.setScaleY(shrinkPercents.y);

            mTxtTitle.setAlpha(0);
        });
    }

    private void toggleRecordingIfActive() {
        Single.fromCallable(() -> RideManager.get().getState(mRideUri))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(rideState -> {
                    switch (rideState) {
                        case CREATED:
                            mChkRecord.setChecked(false, false);
                            mChkRecordText.setText(R.string.display_chkRecord_created);
                            break;

                        case ACTIVE:
                            mChkRecord.setChecked(true, false);
                            mChkRecordText.setText(R.string.display_chkRecord_active);
                            if (!mChkRecordTextAnimator.isStarted()) mChkRecordTextAnimator.start();
                            break;

                        case PAUSED:
                            mChkRecord.setChecked(false, false);
                            mChkRecordText.setText(R.string.display_chkRecord_paused);
                            if (mChkRecordTextAnimator.isStarted()) mChkRecordTextAnimator.cancel();
                            mChkRecordText.setAlpha(1f);

                            break;
                    }

                    mChkRecord.setEnabled(true);
                    mChkRecord.setOnCheckedChangeListener(mRecordingOnCheckedChangeListener);

                });
    }

    @Override
    protected void onStart() {
        super.onStart();

        toggleRecordingIfActive();

        // GPS status
        LocationManager.get().addStatusListener(mGpsStatusListener);

        // Ride
        RideManager.get().addListener(mRideListener);
    }

    @Override
    protected void onStop() {
        // GPS status
        LocationManager.get().removeStatusListener(mGpsStatusListener);

        // Ride
        RideManager.get().removeListener(mRideListener);

        super.onStop();
    }

    private void setupFragments(int currentVisibleIndex) {
        long updateTitleDelay = getResources().getInteger(R.integer.animation_controls_showHide);
        int tabColorEnabled = getResources().getColor(R.color.bright_foreground_dark);
        int tabColorDisabled = getResources().getColor(R.color.bright_foreground_disabled_dark);
        mFragmentCycler = new FragmentCycler(R.id.conFragments, mTxtTitle, updateTitleDelay, tabColorEnabled, tabColorDisabled);
        mFragmentCycler.setCurrentVisibleIndex(currentVisibleIndex);
        mFragmentCycler.add(this, SpeedDisplayFragment.newInstance(), R.id.chkTabSpeed, R.string.display_title_speed);
        mFragmentCycler.add(this, ElapsedTimeDisplayFragment.newInstance(), R.id.chkTabDuration, R.string.display_title_duration);