javaandroidkotlinchronometer

Keep the chronometer always running without a service


I'm using a chronometer and it needs to run persistently until stopped.

The code I'm using below works well and the chronometer keeps running, however, when the device restarts, I get negative values.

SharedPreferences sharedPreferences = getSharedPreferences("chronometer", MODE_PRIVATE);

    //Check if the start time was saved before
    long elapsedTime = sharedPreferences.getLong("elapsed_time", 0);
    
    if (elapsedTime != 0) {
        //It means the chronometer was running before, so we set the time from SharedPreferences
        chronometer.setBase(elapsedTime);
        chronometer.start();
    }

    //Code below starts the chronometer, after the user manually starts it

    Button startButton = findViewById(R.id.btnStart);
    startButton.setOnClickListener(v -> {
        //Save the starting time
        long elapsedRealtime = SystemClock.elapsedRealtime();
        sharedPreferences.edit().putLong("elapsed_time", elapsedRealtime).apply();

        chronometer.setBase(SystemClock.elapsedRealtime());
        chronometer.start();
    });

Why does the chronometer display negative values after a re-boot & how can I sort it out?


Solution

  • From Android Documentation

    Chronometer

    Class that implements a simple timer.

    You can give it a start time in the SystemClock#elapsedRealtime timebase, and it counts up from that.

    The timebase of this class is based on SystemClock.elapsedRealtime().

    elapsedRealtime

    public static long elapsedRealtime()

    Returns milliseconds since boot, including time spent in sleep.

    Back to your questions:

    Why does the chronometer display negative values after a re-boot?

    Here is an example to demonstrate this scenario.

    Assume your phone is running about 10 minutes from last booting, at that time users open the activity, the elapsedRealtime() method will return 600000L (10 mins = 10 * 60 * 1000 milliseconds). When users click on the Start button, you set that time for Chronometer as basetime and save 600000L to SharePreferences using elapsed_time key.

    At this time users press the BACK key to exit the app and restart the phone.

    The phone is kinda slow, so it took 2 mins from booting. Users open the activity again. At this time.

    Because the value you set as basetime of Chronometer is ahead of the elapsedRealtime(), so the Chronometer will display -08:00 and start counting up from that value.

    How can I sort it out?

    You can save the elapsed time of Chronometer and the elapsed time from the last time users leave the activity until the next time users open the activity. Then using these two values to calculate the basetime for Chronometer.

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">
    
        <Chronometer
            android:id="@+id/chronometer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20dp"
            android:textColor="#F00"
            android:textSize="24sp" />
    
        <Button
            android:id="@+id/btnStart"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Start" />
    </LinearLayout>
    

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
    
        private static final String KEY_CHRONOMETER_ELAPSED_TIME = "chronometerElapsedTime";
        private static final String KEY_CHRONOMETER_STOPPED_TIME = "chronometerStoppedTime";
    
        private Chronometer chronometer;
        private SharedPreferences prefs;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            chronometer = findViewById(R.id.chronometer);
    
            // Code below starts the chronometer, after the user manually starts it
            Button startButton = findViewById(R.id.btnStart);
            startButton.setOnClickListener(v -> {
                setElapsedTime(-1);
                setStoppedTime(-1);
                chronometer.setBase(SystemClock.elapsedRealtime());
                chronometer.start();
            });
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            prefs = getSharedPreferences("chronometer", MODE_PRIVATE);
            if (prefs.contains(KEY_CHRONOMETER_ELAPSED_TIME)
                    && prefs.contains(KEY_CHRONOMETER_STOPPED_TIME)) {
                long chronometerElapsedTime = prefs.getLong(KEY_CHRONOMETER_ELAPSED_TIME, -1);
                long chronometerStoppedTime = prefs.getLong(KEY_CHRONOMETER_STOPPED_TIME, -1);
                if (chronometerElapsedTime != -1 && chronometerStoppedTime != -1) {
                    long now = System.currentTimeMillis();
                    long elapsedTimeFromLastStop = now - chronometerStoppedTime; // Including restart time
    
                    long elapsedRealTime = SystemClock.elapsedRealtime();
                    long base = elapsedRealTime - (chronometerElapsedTime + elapsedTimeFromLastStop);
    
                    chronometer.setBase(base);
                    chronometer.start();
                }
            }
        }
    
        @Override
        protected void onStop() {
            setElapsedTime(getChronometerTimeMs());
            setStoppedTime(System.currentTimeMillis());
            super.onStop();
        }
    
        private void setElapsedTime(long elapsedTimeMs) {
            prefs.edit().putLong(KEY_CHRONOMETER_ELAPSED_TIME, elapsedTimeMs).apply();
        }
    
        private void setStoppedTime(long stoppedTimeMs) {
            prefs.edit().putLong(KEY_CHRONOMETER_STOPPED_TIME, stoppedTimeMs).apply();
        }
    
        private long getChronometerTimeMs() {
            long chronometerTimeMs = 0;
    
            // Regex for HH:MM:SS or MM:SS
            String regex = "([0-1]?\\d|2[0-3])(?::([0-5]?\\d))?(?::([0-5]?\\d))?";
    
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(chronometer.getText());
            if (matcher.find()) {
                boolean isHHMMSSFormat = matcher.groupCount() == 4;
                if (isHHMMSSFormat) {
                    int hour = Integer.valueOf(matcher.group(1));
                    int minute = Integer.valueOf(matcher.group(2));
                    int second = Integer.valueOf(matcher.group(3));
                    chronometerTimeMs = (hour * DateUtils.HOUR_IN_MILLIS)
                            + (minute * DateUtils.MINUTE_IN_MILLIS)
                            + (second * DateUtils.SECOND_IN_MILLIS);
                } else {
                    int minute = Integer.valueOf(matcher.group(1));
                    int second = Integer.valueOf(matcher.group(2));
                    chronometerTimeMs = (minute * DateUtils.MINUTE_IN_MILLIS)
                            + (second * DateUtils.SECOND_IN_MILLIS);
                }
            }
    
            return chronometerTimeMs;
        }
    }