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);
        mFragmentCycler.add(this, TotalDistanceDisplayFragment.newInstance(), R.id.chkTabDistance, R.string.display_title_distance);
        mFragmentCycler.add(this, HeartRateDisplayFragment.newInstance(), R.id.chkHeartRate, R.string.display_title_heartRate);
        mFragmentCycler.add(this, CompassDisplayFragment.newInstance(), R.id.chkTabCompass, R.string.display_title_compass);
        mFragmentCycler.add(this, CurrentTimeDisplayFragment.newInstance(), R.id.chkTabCurrentTime, R.string.display_title_currentTime);

        HeartRateManager heartRateManager = HeartRateManager.get();
        mFragmentCycler.setEnabled(this, HeartRateDisplayFragment.class, heartRateManager.isConnected() || heartRateManager.isConnecting());

        mFragmentCycler.show(this);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt("mFragmentCycler.currentVisibleIndex", mFragmentCycler.getCurrentVisibleIndex());
        super.onSaveInstanceState(outState);
    }

    private OnTouchListener mFragmentCycleOnTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                mFragmentCycler.cycle(thiz);
            }
            return true;
        }
    };


    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void setupNavigationBarHiding() {
        findViewById(android.R.id.content).setOnSystemUiVisibilityChangeListener(visibility -> {
            Log.d("visibility=" + visibility);
            if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) {
                Log.d("Navigation bar showing");
                if (!isPaused()) mFragmentCycler.cycle(thiz);
                scheduleHideNavigationBar();
                showControls();
                scheduleHideControls();
            }
        });
        scheduleHideNavigationBar();
        mNavigationBarHiding = true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mNavigationBarHiding) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    Log.d("up");
                    scheduleHideNavigationBar();
                    scheduleHideControls();
                    break;

                default:
                    mHandler.removeCallbacks(mHideNavigationBarRunnable);
                    mHandler.removeCallbacks(mHideControlsRunnable);
                    showControls();
                    break;
            }
        }
        return super.dispatchTouchEvent(ev);
    }

    private Runnable mHideNavigationBarRunnable = this::hideNavigationBar;

    private void scheduleHideNavigationBar() {
        mHandler.removeCallbacks(mHideNavigationBarRunnable);
        mHandler.postDelayed(mHideNavigationBarRunnable, DELAY_HIDE_CONTROLS + 1000);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void hideNavigationBar() {
        Log.d();
        findViewById(android.R.id.content).setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
    }

    private OnCheckedChangeListener mRecordingOnCheckedChangeListener = new OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            Log.d("isChecked=" + isChecked);
            if (isChecked) {
                startService(new Intent(LogCollectorService.ACTION_START_COLLECTING, mRideUri, thiz, LogCollectorService.class));
                mChkRecordText.setText(R.string.display_chkRecord_active);
                //                mChkRecordTextAnimator.start();
            } else {
                startService(new Intent(LogCollectorService.ACTION_STOP_COLLECTING, mRideUri, thiz, LogCollectorService.class));
                mChkRecordText.setText(R.string.display_chkRecord_paused);
                //                mChkRecordTextAnimator.cancel();
                //                mChkRecordText.setAlpha(1f);
            }
        }
    };

    private StatusListener mGpsStatusListener = new StatusListener() {
        @Override
        public void onStatusChanged(boolean active) {
            if (active) {
                mImgGpsStatus.setVisibility(View.GONE);
            } else {
                mImgGpsStatus.setVisibility(View.VISIBLE);
            }
        }
    };

    public Uri getRideUri() {
        return mRideUri;
    }


    /*
     * Ride listener.
     */

    private RideListener mRideListener = new RideListener() {
        @Override
        public void onActivated(Uri rideUri) {
            if (!rideUri.equals(mRideUri)) return;
            mChkRecord.setChecked(true, false);
            mChkRecordText.setText(R.string.display_chkRecord_active);
            if (!mChkRecordTextAnimator.isStarted()) mChkRecordTextAnimator.start();
        }

        @Override
        public void onPaused(Uri rideUri) {
            if (!rideUri.equals(mRideUri)) return;
            mChkRecord.setChecked(false, false);
            mChkRecordText.setText(R.string.display_chkRecord_paused);
            if (mChkRecordTextAnimator.isStarted()) mChkRecordTextAnimator.cancel();
            mChkRecordText.setAlpha(1f);
        }
    };


    /*
     * Controls show / hide.
     */

    private void hideControls() {
        mControlsVisible = false;
        int duration = getResources().getInteger(R.integer.animation_controls_showHide);
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
            // Landscape: tab containers slide to left / right
            mConTabsA.animate().alpha(0).translationX(-mConTabsA.getWidth()).setInterpolator(new AccelerateInterpolator()).setDuration(duration);
            mConTabsB.animate().alpha(0).translationX(mConTabsB.getWidth()).setInterpolator(new AccelerateInterpolator()).setDuration(duration);
        } else {
            // Portrait: tab containers slide to top / bottom
            mConTabsA.animate().alpha(0).translationY(-mConTabsA.getHeight()).setInterpolator(new AccelerateInterpolator()).setDuration(duration);
            mConTabsB.animate().alpha(0).translationY(mConTabsB.getHeight()).setInterpolator(new AccelerateInterpolator()).setDuration(duration);
        }

        // 'Uncompress' the main fragment
        mConFragments.animate().scaleX(1f).scaleY(1f).setInterpolator(new AccelerateInterpolator()).setDuration(duration);

        // Record button
        mChkRecord.animate().alpha(0).translationY(-mChkRecord.getHeight()).setInterpolator(new AccelerateInterpolator()).setDuration(duration);

        // Show the title
        mTxtTitle.animate().alpha(1f).setDuration(duration).setStartDelay(duration);
    }

    private PointF getShrinkPercents() {
        float percentX;
        float percentY;
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
            int fragmentWidth = mConFragments.getWidth();
            percentX = (fragmentWidth - 2 * getResources().getDimension(R.dimen.display_tabs_width)) / fragmentWidth;
            // Remove 5% because it looks better
            percentX -= .05f;
            percentY = percentX + (1f - percentX) / 2f;
        } else {
            int fragmentHeight = mConFragments.getHeight();
            percentY = (fragmentHeight - 2 * getResources().getDimension(R.dimen.display_tabs_width)) / fragmentHeight;
            // Remove 5% because it looks better
            percentY -= .05f;
            percentX = percentY + (1f - percentY) / 2f;
        }
        return new PointF(percentX, percentY);
    }

    private void showControls() {
        if (mControlsVisible) return;
        mControlsVisible = true;
        int duration = getResources().getInteger(R.integer.animation_controls_showHide);
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
            // Landscape: tab containers slide from left / right
            mConTabsA.animate().alpha(1).translationX(0).setInterpolator(new DecelerateInterpolator()).setDuration(duration);
            mConTabsB.animate().alpha(1).translationX(0).setInterpolator(new DecelerateInterpolator()).setDuration(duration);
        } else {
            // Portrait: tab containers slide from top / bottom
            mConTabsA.animate().alpha(1).translationY(0).setInterpolator(new DecelerateInterpolator()).setDuration(duration);
            mConTabsB.animate().alpha(1).translationY(0).setInterpolator(new DecelerateInterpolator()).setDuration(duration);
        }

        // 'Compress' the main fragment, to make space for the tab containers
        PointF shrinkPercents = getShrinkPercents();
        mConFragments.animate().scaleX(shrinkPercents.x).scaleY(shrinkPercents.y).setInterpolator(new DecelerateInterpolator()).setDuration(duration);

        // Record button
        mChkRecord.animate().alpha(1).translationY(0).setInterpolator(new DecelerateInterpolator()).setDuration(duration);

        mTxtTitle.animate().alpha(0).setDuration(duration).setStartDelay(0);
    }

    private Runnable mHideControlsRunnable = this::hideControls;

    private void scheduleHideControls() {
        mHandler.removeCallbacks(mHideControlsRunnable);
        mHandler.postDelayed(mHideControlsRunnable, DELAY_HIDE_CONTROLS);
    }
} public class AndroidWearSender extends SmartwatchSender {
    private WearCommHelper mWearCommHelper = WearCommHelper.get();

    @Override
    public void startSending(Context context) {
        super.startSending(context);

        // Propagate unit preferences at this point
        mWearCommHelper.updatePreferences();

        // Inform wearables that there is an ongoing ride
        mWearCommHelper.updateRideOngoing(true);
    }

    @Override
    public void stopSending() {
        super.stopSending();

        // Inform wearables that there are no ongoing rides
        mWearCommHelper.updateRideOngoing(false);

    }

    @Override
    protected void sendValues() {
        Log.d();
        float totalDistance = LogManager.get().getTotalDistance(mActiveRideUri);
        long startDateOffset = mInitialDuration - mActivatedDate;
        float speed = mSpeedometer.getSpeed();
        mWearCommHelper.updateRideValues(startDateOffset, speed, totalDistance, 0); // TODO heart rate
    }
} public interface CadenceListener {
    /**
     * @param cadence The cadence, in revolutions per minute.
     * @param rawData The rotations values that were used to calculate the cadence.
     */
    void onCadenceChanged(Float cadence, float[][] rawData);
} public class CadenceManager {
    private static final CadenceManager INSTANCE = new CadenceManager();

    private static final long BROADCAST_CURRENT_VALUE_RATE_S = 2;
    protected static final long LOG_SIZE_MS = 5 * 1000;

    private static final float SANITY_CHECK_MAX = 170;
    private static final float SANITY_CHECK_MIN = 30;

    private static final float MIN_AMPLITUDE = .3f;

    private static class Entry {
        long timestamp;
        float[] values;

        Entry(float[] values) {
            this.values = values;
            timestamp = System.currentTimeMillis();
        }
    }

    public static CadenceManager get() {
        return INSTANCE;
    }

    private Context mContext;
    private ArrayDeque<Entry> mValues = new ArrayDeque<>(200);
    private ScheduledExecutorService mScheduledExecutorService;
    protected Float mLastValue = -1f;
    private float[][] mLastRawData;

    private Listeners<CadenceListener> mListeners = new Listeners<CadenceListener>() {
        @Override
        protected void onFirstListener() {
            startListening();
        }

        @Override
        protected void onNoMoreListeners() {
            stopListening();
        }
    };


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

    public void addListener(CadenceListener listener) {
        mListeners.add(listener);
    }

    public void removeListener(CadenceListener listener) {
        mListeners.remove(listener);
    }

    protected void startListening() {
        Log.d();
        SensorManager sensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
        sensorManager.registerListener(mRotationSensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR),
                SensorManager.SENSOR_DELAY_NORMAL);

        if (mScheduledExecutorService == null) {
            mScheduledExecutorService = Executors.newScheduledThreadPool(1);
        }
        mScheduledExecutorService.scheduleAtFixedRate(mBroadcastCurrentValueRunnable, BROADCAST_CURRENT_VALUE_RATE_S, BROADCAST_CURRENT_VALUE_RATE_S,
                TimeUnit.SECONDS);
    }

    protected void stopListening() {
        Log.d();
        SensorManager sensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
        sensorManager.unregisterListener(mRotationSensorEventListener);
        if (mScheduledExecutorService != null) {
            mScheduledExecutorService.shutdown();
            mScheduledExecutorService = null;
        }
    }

    private SensorEventListener mRotationSensorEventListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            // Keep only logs for a specific duration (discard old logs)
            while (mValues.size() >= 2 && mValues.peekLast().timestamp - mValues.peekFirst().timestamp >= LOG_SIZE_MS) {
                mValues.removeFirst();
            }
            float[] values = event.values.clone();
            synchronized (mValues) {
                mValues.add(new Entry(values));
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
    };

    private static class ValuesAsArray {
        float[][] values;
        long[] times;
    }

    private ValuesAsArray getValuesAsFloatArray() {
        float[][] values = new float[3][mValues.size()];
        long[] timestamps = new long[mValues.size()];
        int i = 0;

        for (Entry e : mValues) {
            float valX = e.values[0];
            float valY = e.values[1];
            float valZ = e.values[2];

            values[0][i] = valX;
            values[1][i] = valY;
            values[2][i] = valZ;

            timestamps[i] = e.timestamp;

            i++;
        }
        ValuesAsArray res = new ValuesAsArray();
        res.values = values;
        res.times = timestamps;
        return res;
    }

    /**
     * Get the current cadence.
     * 
     * @return The current cadence in revolutions per minute, or {@code null} if the information is not available.
     */
    private Float getCurrentCadence() {
        if (mListeners.size() == 0) throw new IllegalStateException("There must be at least one listener prior to calling getCurrentCadence");
        ValuesAsArray valuesAsArrays;
        long durationMs;
        int len;
        synchronized (mValues) {
            len = mValues.size();
            if (len < 2) return null;
            valuesAsArrays = getValuesAsFloatArray();
            durationMs = mValues.peekLast().timestamp - mValues.peekFirst().timestamp;
        }
        mLastRawData = valuesAsArrays.values;

        float[] vX = valuesAsArrays.values[0];
        float[] vY = valuesAsArrays.values[1];
        float[] vZ = valuesAsArrays.values[2];

        float amplitudeX = MathUtil.getAmplitude(vX);
        float amplitudeY = MathUtil.getAmplitude(vY);
        float amplitudeZ = MathUtil.getAmplitude(vZ);

        // Use the values with the highest amplitude
        float maxAmplitude = MathUtil.getMinMax(amplitudeX, amplitudeY, amplitudeZ)[1];

        if (maxAmplitude < MIN_AMPLITUDE) {
            Log.d("Amplitude to small: returning null");
            return null;
        }

        float[] values;
        String logValues;
        if (maxAmplitude == amplitudeX) {
            values = vX;
            logValues = "x";
        } else if (maxAmplitude == amplitudeY) {
            values = vY;
            logValues = "y";
        } else {
            values = vZ;
            logValues = "z";
        }

        // Average
        float average = MathUtil.getAverage(values);
        ArrayList<Long> periods = new ArrayList<>();
        long lastTime = -1;
        for (int i = 1; i < len; i++) {
            if (values[i - 1] < average && values[i] >= average) {
                // Going up
                if (lastTime != -1) {
                    long duration = valuesAsArrays.times[i] - lastTime;
                    periods.add(duration);
                }
                lastTime = valuesAsArrays.times[i];
            }
        }
        int periodCount = periods.size();
        if (periodCount == 0) {
            Log.d("No periods: returning null");
            return null;
        }

        // Average of the rev per ms for each period
        float revPerMs = 0;
        for (Long t : periods) {
            revPerMs += 1f / t;
        }
        revPerMs /= periodCount;

        float revPerMin = revPerMs * 60000f;

        Log.d("durationMs=" + durationMs + " revPerMin=" + revPerMin + " periods=" + periods + " values=" + logValues + " maxAmplitude=" + maxAmplitude);

        // Sanity checks
        if (revPerMin > SANITY_CHECK_MAX || revPerMin < SANITY_CHECK_MIN) {
            Log.d("Invalid value " + revPerMin + ": returning null");
            return null;
        }

        return revPerMin;
    }

    private Runnable mBroadcastCurrentValueRunnable = new Runnable() {
        @Override
        public void run() {
            Float value = getCurrentCadence();
            if (ObjectUtil.equals(mLastValue, value)) {
                // Skip if the value was the same
                mLastValue = value;
                return;
            }
            mLastValue = value;
            mListeners.dispatch(listener -> listener.onCadenceChanged(value, mLastRawData));
        }
    };
} public interface HeartRateListener {
    void onConnecting();

    void onConnected();

    void onHeartRateChange(int bpm);

    void onDisconnected();

    void onError();
} 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 class LogManager {
    private static final LogManager INSTANCE = new LogManager();

    public static LogManager get() {
        return INSTANCE;
    }

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

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

    @WorkerThread
    public Uri add(@NonNull Uri rideUri, Location location, Location previousLocation, Float cadence, Integer heartRate) {
        // Add a log
        LogContentValues values = new LogContentValues();
        long rideId = ContentUris.parseId(rideUri);
        values.putRideId(rideId);
        values.putRecordedDate(location.getTime());
        values.putLat(location.getLatitude());
        values.putLon(location.getLongitude());
        values.putEle(location.getAltitude());
        if (previousLocation != null) {
            LocationPair locationPair = new LocationPair(previousLocation, location);
            float speed = locationPair.getSpeed();
            if (speed < LocationManager.SPEED_MIN_THRESHOLD_M_S) {
                Log.d("Speed under threshold, not logging it");
            } else {
                values.putLogDuration(locationPair.getDuration());
                values.putLogDistance(locationPair.getDistance());
                values.putSpeed(speed);
            }
        }
        values.putCadence(cadence);
        values.putHeartRate(heartRate);

        Uri res = values.insert(mContext);

        // Update total distance for ride
        float totalDistance = getTotalDistance(rideUri);
        RideManager.get().updateTotalDistance(rideUri, totalDistance);

        // Dispatch to listeners
        mListeners.dispatch(listener -> listener.onLogAdded(rideUri));
        return res;
    }

    @WorkerThread
    public float getTotalDistance(@NonNull Uri rideUri) {
        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"sum(" + LogColumns.LOG_DISTANCE + ")"};
        LogSelection where = new LogSelection();
        where.rideId(rideId);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return 0;
            return c.getFloat(0);
        } finally {
            c.close();
        }
    }

    /**
     * Note: the top 10% points are discarded to account for imprecise values.
     */
    @WorkerThread
    public float getAverageMovingSpeed(@NonNull Uri rideUri) {
        // First get the max
        float max = getMaxSpeed(rideUri);

        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"sum(" + LogColumns.LOG_DISTANCE + ")/sum(" + LogColumns.LOG_DURATION + ")*1000"};
        LogSelection where = new LogSelection();
        where.rideId(rideId).and().speedGt(LocationManager.SPEED_MIN_THRESHOLD_M_S).and().speedLtEq(max);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return 0;
            return c.getFloat(0);
        } finally {
            c.close();
        }
    }

    /**
     * Note: the top 10% points are discarded to account for imprecise values.
     */
    @WorkerThread
    public Float getAverageCadence(@NonNull Uri rideUri) {
        // First get the min and max
        float min = getMinCadence(rideUri);
        float max = getMaxCadence(rideUri);

        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"avg(" + LogColumns.CADENCE + ")"};
        LogSelection where = new LogSelection();
        where.rideId(rideId).and().cadenceGtEq(min).and().cadenceLtEq(max);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return null;
            if (c.isNull(0)) return null;
            return c.getFloat(0);
        } finally {
            c.close();
        }
    }

    /**
     * Note: the top 10% points are discarded to account for imprecise values.
     */
    @WorkerThread
    public Float getAverageHeartRate(@NonNull Uri rideUri) {
        // First get the min and max
        float min = getMinHeartRate(rideUri);
        float max = getMaxHeartRate(rideUri);

        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"avg(" + LogColumns.HEART_RATE + ")"};
        LogSelection where = new LogSelection();
        where.rideId(rideId).and().heartRateGtEq((int) min).and().heartRateLtEq((int) max);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return null;
            if (c.isNull(0)) return null;
            return c.getFloat(0);
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public Long getMovingDuration(@NonNull Uri rideUri) {
        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"sum(" + LogColumns.LOG_DURATION + ")"};
        LogSelection where = new LogSelection();
        where.rideId(rideId).and().speedGt(LocationManager.SPEED_MIN_THRESHOLD_M_S);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return null;
            if (c.isNull(0)) return null;
            return c.getLong(0);
        } finally {
            c.close();
        }
    }

    /**
     * Note: the top 10% points are discarded to account for imprecise values.
     */
    @WorkerThread
    private float getMax(@NonNull Uri rideUri, @NonNull String column) {
        // Get the point count to discard the top 10% values
        Integer count = getLogCount(rideUri);
        if (count == null) return 0;

        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {column};
        LogSelection where = new LogSelection()
                .rideId(rideId).and().addRaw(column + " IS NOT NULL")
                .orderBy(column, true)
                .limit(count / 10);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToLast()) return 0;
            return c.getFloat(0);
        } finally {
            c.close();
        }
    }

    /**
     * Note: the bottom 10% points are discarded to account for imprecise values.
     */
    @WorkerThread
    private float getMin(@NonNull Uri rideUri, @NonNull String column) {
        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {column};
        LogSelection where = new LogSelection()
                .rideId(rideId).and().addRaw(column + " IS NOT NULL")
                .orderBy(column);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToFirst()) return 0;
            int count = c.getCount();
            // Discard the first 10%
            int index = count / 10;
            c.moveToPosition(index);
            return c.getFloat(0);
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public float getMaxSpeed(@NonNull Uri rideUri) {
        return getMax(rideUri, LogColumns.SPEED);
    }

    @WorkerThread
    public float getMaxCadence(@NonNull Uri rideUri) {
        return getMax(rideUri, LogColumns.CADENCE);
    }

    private float getMinCadence(@NonNull Uri rideUri) {
        return getMin(rideUri, LogColumns.CADENCE);
    }

    @WorkerThread
    public float getMaxHeartRate(@NonNull Uri rideUri) {
        return getMax(rideUri, LogColumns.HEART_RATE);
    }

    @WorkerThread
    public float getMinHeartRate(@NonNull Uri rideUri) {
        return getMin(rideUri, LogColumns.HEART_RATE);
    }


    @WorkerThread
    public Long getFirstLogDate(@NonNull Uri rideUri) {
        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"min(" + LogColumns.RECORDED_DATE + ")"};
        LogSelection where = new LogSelection();
        where.rideId(rideId);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return null;
            if (c.isNull(0)) return null;
            return c.getLong(0);
        } finally {
            c.close();
        }
    }

    @WorkerThread
    public Long getLastLogDate(@NonNull Uri rideUri) {
        long rideId = ContentUris.parseId(rideUri);
        String[] projection = {"max(" + LogColumns.RECORDED_DATE + ")"};
        LogSelection where = new LogSelection();
        where.rideId(rideId);
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return null;
            if (c.isNull(0)) return null;
            return c.getLong(0);
        } finally {
            c.close();
        }
    }

    private Integer getLogCount(@NonNull Uri rideUri) {
        String[] projection = {"count(*)"};
        long rideId = ContentUris.parseId(rideUri);
        LogSelection where = new LogSelection();
        where.rideId(rideId);
        int count;
        Cursor c = where.query(mContext, projection);
        try {
            if (!c.moveToNext()) return null;
            count = c.getInt(0);
        } finally {
            c.close();
        }
        return count;
    }

    @WorkerThread
    public List<LatLng> getLatLngArray(@NonNull Uri rideUri, int max) {
        // Get the point count to determine the ratio to apply to not get more than max values
        Integer count = getLogCount(rideUri);
        if (count == null) return null;
        int ratio = count / max;
        if (ratio == 0) ratio = 1;

        ArrayList<LatLng> res = new ArrayList<>(max);
        // Get the values
        String[] projection = new String[] {LogColumns.LAT, LogColumns.LON};
        LogSelection where = new LogSelection();
        // Get at most max rows by applying a modulo on the id
        long rideId = ContentUris.parseId(rideUri);
        where.rideId(rideId).and().addRaw(LogColumns._ID + "%" + ratio + "=0");
        LogCursor cursor = where.query(mContext, projection);
        try {
            while (cursor.moveToNext()) {
                res.add(new LatLng(cursor.getLat(), cursor.getLon()));
            }
        } finally {
            cursor.close();
        }
        return res;
    }

    @WorkerThread
    public List<Float> getSpeedArray(@NonNull Uri rideUri, int max) {
        // Get the point count to determine the ratio to apply to not get more than max values
        Integer count = getLogCount(rideUri);
        if (count == null) return null;
        int ratio = count / max;
        if (ratio == 0) ratio = 1;

        ArrayList<Float> res = new ArrayList<>(max);
        // Get the values
        String[] projection = new String[] {LogColumns.SPEED};
        LogSelection where = new LogSelection();
        // Get at most max rows by applying a modulo on the id
        long rideId = ContentUris.parseId(rideUri);
        where.rideId(rideId).and().speedNot((Float) null).and().addRaw(LogColumns._ID + "%" + ratio + "=0");
        LogCursor cursor = where.query(mContext, projection);
        try {
            while (cursor.moveToNext()) {
                res.add(cursor.getSpeed());
            }
        } finally {
            cursor.close();
        }
        return res;
    }

    @WorkerThread
    public List<Float> getCadenceArray(@NonNull Uri rideUri, int max) {
        // Get the point count to determine the ratio to apply to not get more than max values
        Integer count = getLogCount(rideUri);
        if (count == null) return null;
        int ratio = count / max;
        if (ratio == 0) ratio = 1;

        ArrayList<Float> res = new ArrayList<>(max);
        // Get the values
        String[] projection = new String[] {LogColumns.CADENCE};
        LogSelection where = new LogSelection();
        // Get at most max rows by applying a modulo on the id
        long rideId = ContentUris.parseId(rideUri);
        where.rideId(rideId).and().cadenceNot((Float) null).and().addRaw(LogColumns._ID + "%" + ratio + "=0");
        LogCursor cursor = where.query(mContext, projection);
        try {
            while (cursor.moveToNext()) {
                res.add(cursor.getCadence());
            }
        } finally {
            cursor.close();
        }
        return res;
    }

    @WorkerThread
    public List<Float> getHeartRateArray(@NonNull Uri rideUri, int max) {
        // Get the point count to determine the ratio to apply to not get more than max values
        Integer count = getLogCount(rideUri);
        if (count == null) return null;
        int ratio = count / max;
        if (ratio == 0) ratio = 1;

        ArrayList<Float> res = new ArrayList<>(max);
        // Get the values
        String[] projection = new String[] {LogColumns.HEART_RATE};
        LogSelection where = new LogSelection();
        // Get at most max rows by applying a modulo on the id
        long rideId = ContentUris.parseId(rideUri);
        where.rideId(rideId).and().heartRateNot((Integer) null).and().addRaw(LogColumns._ID + "%" + ratio + "=0");
        LogCursor cursor = where.query(mContext, projection);
        try {
            while (cursor.moveToNext()) {
                res.add(Float.valueOf(cursor.getHeartRate()));
            }
        } finally {
            cursor.close();
        }
        return res;
    }


    /*
     * Listeners.
     */

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

    public void removeListener(@NonNull LogListener listener) {
        mListeners.remove(listener);
    }

} 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 Constants {

    public static final String TAG = "bikey/";

    public static final String PREF_UNITS = "PREF_UNITS";
    public static final String PREF_UNITS_METRIC = "PREF_UNITS_METRIC";
    public static final String PREF_UNITS_IMPERIAL = "PREF_UNITS_IMPERIAL";
    public static final String PREF_UNITS_DEFAULT = PREF_UNITS_IMPERIAL;
    public static final String PREF_CATEGORY_GENERAL= "PREF_CATEGORY_GENERAL";
    public static final String PREF_CATEGORY_HEART_RATE = "PREF_CATEGORY_HEART_RATE";
    public static final String PREF_HEART_RATE_SCAN = "PREF_HEART_RATE_SCAN";
    public static final String PREF_IMPORT = "PREF_IMPORT";
    public static final String PREF_EXPORT = "PREF_EXPORT";
    public static final String SYNC_WITH_GOOGLE_DRIVE = "SYNC_WITH_GOOGLE_DRIVE";

    public static final String PREF_LISTEN_TO_HEADSET_BUTTON = "PREF_LISTEN_TO_HEADSET_BUTTON";
    public static final boolean PREF_LISTEN_TO_HEADSET_BUTTON_DEFAULT = false;

    public static final String PREF_RECORD_CADENCE = "PREF_RECORD_CADENCE";
    public static final boolean PREF_RECORD_CADENCE_DEFAULT = false;

    public static final String PREF_CURRENT_RIDE_URI = "PREF_CURRENT_RIDE_URI";

    public static final String PREF_RIDE_MAP_TYPE = "PREF_RIDE_MAP_TYPE";
    public static final String PREF_RIDE_MAP_TYPE_NORMAL = "PREF_RIDE_MAP_TYPE_NORMAL";
    public static final String PREF_RIDE_MAP_TYPE_SATELLITE = "PREF_RIDE_MAP_TYPE_SATELLITE";
    public static final String PREF_RIDE_MAP_TYPE_TERRAIN = "PREF_RIDE_MAP_TYPE_TERRAIN";

    public static final String PREF_ANDROID_WEAR = "PREF_ANDROID_WEAR";
    public static final boolean PREF_ANDROID_WEAR_DEFAULT = false;
}
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.location.LocationListener;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import io.reactivex.schedulers.Schedulers;
import org.jraf.android.bikey.R;
import org.jraf.android.bikey.app.display.DisplayActivity;
import org.jraf.android.bikey.app.smartwatchsender.AndroidWearSender;
import org.jraf.android.bikey.backend.cadence.CadenceListener;
import org.jraf.android.bikey.backend.cadence.CadenceManager;
import org.jraf.android.bikey.backend.heartrate.HeartRateListener;
import org.jraf.android.bikey.backend.heartrate.HeartRateManager;
import org.jraf.android.bikey.backend.location.LocationManager;
import org.jraf.android.bikey.backend.log.LogManager;
import org.jraf.android.bikey.backend.ride.RideManager;
import org.jraf.android.bikey.common.Constants;
import org.jraf.android.util.log.Log;
import org.jraf.android.util.string.StringUtil;
/*
 * 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.collect;




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);