diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/StopwatchFragment.kt b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/StopwatchFragment.kt index 750d9fe..d26f357 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/StopwatchFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/StopwatchFragment.kt @@ -4,45 +4,25 @@ import android.graphics.Bitmap import android.graphics.Color import android.graphics.Matrix import android.os.Bundle -import android.os.Handler -import android.os.SystemClock import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.simplemobiletools.clock.R import com.simplemobiletools.clock.activities.SimpleActivity import com.simplemobiletools.clock.adapters.StopwatchAdapter +import com.simplemobiletools.clock.extensions.config import com.simplemobiletools.clock.extensions.formatStopwatchTime import com.simplemobiletools.clock.helpers.SORT_BY_LAP import com.simplemobiletools.clock.helpers.SORT_BY_LAP_TIME import com.simplemobiletools.clock.helpers.SORT_BY_TOTAL_TIME +import com.simplemobiletools.clock.helpers.Stopwatch import com.simplemobiletools.clock.models.Lap import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.SORT_DESCENDING import kotlinx.android.synthetic.main.fragment_stopwatch.view.* class StopwatchFragment : Fragment() { - private val UPDATE_INTERVAL = 10L - private val WAS_RUNNING = "was_running" - private val TOTAL_TICKS = "total_ticks" - private val CURRENT_TICKS = "current_ticks" - private val LAP_TICKS = "lap_ticks" - private val CURRENT_LAP = "current_lap" - private val LAPS = "laps" - private val SORTING = "sorting" - - private val updateHandler = Handler() - private var uptimeAtStart = 0L - private var totalTicks = 0 - private var currentTicks = 0 // ticks that reset at pause - private var lapTicks = 0 - private var currentLap = 1 - private var isRunning = false - private var sorting = SORT_BY_LAP or SORT_DESCENDING - private var laps = ArrayList() private var storedTextColor = 0 @@ -51,6 +31,8 @@ class StopwatchFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { storeStateVariables() + val sorting = requireContext().config.stopwatchLapsSort + Lap.sorting = sorting view = (inflater.inflate(R.layout.fragment_stopwatch, container, false) as ViewGroup).apply { stopwatch_time.setOnClickListener { togglePlayPause() @@ -78,20 +60,7 @@ class StopwatchFragment : Fragment() { stopwatch_lap.setOnClickListener { stopwatch_sorting_indicators_holder.beVisible() - if (laps.isEmpty()) { - val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL) - laps.add(0, lap) - lapTicks = 0 - } else { - laps.first().apply { - lapTime = lapTicks * UPDATE_INTERVAL - totalTime = totalTicks * UPDATE_INTERVAL - } - } - - val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL) - laps.add(0, lap) - lapTicks = 0 + Stopwatch.lap() updateLaps() } @@ -100,11 +69,10 @@ class StopwatchFragment : Fragment() { changeSorting(it) } } - Lap.sorting = sorting stopwatch_list.adapter = stopwatchAdapter } - updateSortingIndicators() + updateSortingIndicators(sorting) return view } @@ -116,64 +84,25 @@ class StopwatchFragment : Fragment() { if (storedTextColor != configTextColor) { stopwatchAdapter.updateTextColor(configTextColor) } + + Stopwatch.addUpdateListener(updateListener) + updateLaps() + view.stopwatch_sorting_indicators_holder.beVisibleIf(Stopwatch.laps.isNotEmpty()) + if (Stopwatch.laps.isNotEmpty()) { + updateSorting(Lap.sorting) + } } override fun onPause() { super.onPause() storeStateVariables() - } - - override fun onDestroy() { - super.onDestroy() - if (isRunning && activity?.isChangingConfigurations == false) { - context?.toast(R.string.stopwatch_stopped) - } - isRunning = false - updateHandler.removeCallbacks(updateRunnable) + Stopwatch.removeUpdateListener(updateListener) } private fun storeStateVariables() { storedTextColor = requireContext().getProperTextColor() } - override fun onSaveInstanceState(outState: Bundle) { - outState.apply { - putBoolean(WAS_RUNNING, isRunning) - putInt(TOTAL_TICKS, totalTicks) - putInt(CURRENT_TICKS, currentTicks) - putInt(LAP_TICKS, lapTicks) - putInt(CURRENT_LAP, currentLap) - putInt(SORTING, sorting) - putString(LAPS, Gson().toJson(laps)) - super.onSaveInstanceState(this) - } - } - - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - savedInstanceState?.apply { - isRunning = getBoolean(WAS_RUNNING, false) - totalTicks = getInt(TOTAL_TICKS, 0) - currentTicks = getInt(CURRENT_TICKS, 0) - lapTicks = getInt(LAP_TICKS, 0) - currentLap = getInt(CURRENT_LAP, 0) - sorting = getInt(SORTING, SORT_BY_LAP or SORT_DESCENDING) - - val lapsToken = object : TypeToken>() {}.type - laps = Gson().fromJson(getString(LAPS), lapsToken) - - if (laps.isNotEmpty()) { - view.stopwatch_sorting_indicators_holder.beVisibleIf(laps.isNotEmpty()) - updateSorting() - } - - if (isRunning) { - uptimeAtStart = SystemClock.uptimeMillis() - currentTicks * UPDATE_INTERVAL - updateStopwatchState(false) - } - } - } - private fun setupViews() { val properPrimaryColor = requireContext().getProperPrimaryColor() view.apply { @@ -181,60 +110,30 @@ class StopwatchFragment : Fragment() { stopwatch_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, properPrimaryColor) stopwatch_reset.applyColorFilter(requireContext().getProperTextColor()) } - - updateIcons() - updateDisplayedText() } - private fun updateIcons() { - val drawableId = if (isRunning) R.drawable.ic_pause_vector else R.drawable.ic_play_vector + private fun updateIcons(state: Stopwatch.State) { + val drawableId = if (state == Stopwatch.State.RUNNING) R.drawable.ic_pause_vector else R.drawable.ic_play_vector val iconColor = if (requireContext().getProperPrimaryColor() == Color.WHITE) Color.BLACK else Color.WHITE view.stopwatch_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor)) } private fun togglePlayPause() { - isRunning = !isRunning - updateStopwatchState(true) + Stopwatch.toggle(true) } - private fun updateStopwatchState(setUptimeAtStart: Boolean) { - updateIcons() - view.stopwatch_lap.beVisibleIf(isRunning) - if (isRunning) { - updateHandler.post(updateRunnable) - view.stopwatch_reset.beVisible() - if (setUptimeAtStart) { - uptimeAtStart = SystemClock.uptimeMillis() - } - } else { - val prevSessionsMS = (totalTicks - currentTicks) * UPDATE_INTERVAL - val totalDuration = SystemClock.uptimeMillis() - uptimeAtStart + prevSessionsMS - updateHandler.removeCallbacksAndMessages(null) - view.stopwatch_time.text = totalDuration.formatStopwatchTime(true) - currentTicks = 0 - totalTicks-- - } - } - - private fun updateDisplayedText() { - view.stopwatch_time.text = (totalTicks * UPDATE_INTERVAL).formatStopwatchTime(false) - if (currentLap > 1) { - stopwatchAdapter.updateLastField(lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL) + private fun updateDisplayedText(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean) { + view.stopwatch_time.text = totalTime.formatStopwatchTime(useLongerMSFormat) + if (Stopwatch.laps.isNotEmpty() && lapTime != -1L) { + stopwatchAdapter.updateLastField(lapTime, totalTime) } } private fun resetStopwatch() { - updateHandler.removeCallbacksAndMessages(null) - isRunning = false - currentTicks = 0 - totalTicks = 0 - currentLap = 1 - lapTicks = 0 - laps.clear() - updateIcons() - stopwatchAdapter.updateItems(laps) + Stopwatch.reset() + updateLaps() view.apply { stopwatch_reset.beGone() stopwatch_lap.beGone() @@ -244,21 +143,22 @@ class StopwatchFragment : Fragment() { } private fun changeSorting(clickedValue: Int) { - sorting = if (sorting and clickedValue != 0) { - sorting.flipBit(SORT_DESCENDING) + val sorting = if (Lap.sorting and clickedValue != 0) { + Lap.sorting.flipBit(SORT_DESCENDING) } else { clickedValue or SORT_DESCENDING } - updateSorting() + updateSorting(sorting) } - private fun updateSorting() { - updateSortingIndicators() + private fun updateSorting(sorting: Int) { + updateSortingIndicators(sorting) Lap.sorting = sorting + requireContext().config.stopwatchLapsSort = sorting updateLaps() } - private fun updateSortingIndicators() { + private fun updateSortingIndicators(sorting: Int) { var bitmap = requireContext().resources.getColoredBitmap(R.drawable.ic_sorting_triangle_vector, requireContext().getProperPrimaryColor()) view.apply { stopwatch_sorting_indicator_1.beInvisibleIf(sorting and SORT_BY_LAP == 0) @@ -281,20 +181,18 @@ class StopwatchFragment : Fragment() { } private fun updateLaps() { - stopwatchAdapter.updateItems(laps) + stopwatchAdapter.updateItems(Stopwatch.laps) } - private val updateRunnable = object : Runnable { - override fun run() { - if (isRunning) { - if (totalTicks % 10 == 0) { - updateDisplayedText() - } - totalTicks++ - currentTicks++ - lapTicks++ - updateHandler.postAtTime(this, uptimeAtStart + currentTicks * UPDATE_INTERVAL) - } + private val updateListener = object : Stopwatch.UpdateListener { + override fun onUpdate(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean) { + updateDisplayedText(totalTime, lapTime, useLongerMSFormat) + } + + override fun onStateChanged(state: Stopwatch.State) { + updateIcons(state) + view.stopwatch_lap.beVisibleIf(state == Stopwatch.State.RUNNING) + view.stopwatch_reset.beVisibleIf(state != Stopwatch.State.STOPPED) } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt index 94064c8..a5a1270 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt @@ -10,6 +10,7 @@ import com.simplemobiletools.clock.models.TimerState import com.simplemobiletools.commons.extensions.getDefaultAlarmSound import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle import com.simplemobiletools.commons.helpers.BaseConfig +import com.simplemobiletools.commons.helpers.SORT_DESCENDING class Config(context: Context) : BaseConfig(context) { companion object { @@ -81,4 +82,8 @@ class Config(context: Context) : BaseConfig(context) { var timerChannelId: String? get() = prefs.getString(TIMER_CHANNEL_ID, null) set(id) = prefs.edit().putString(TIMER_CHANNEL_ID, id).apply() + + var stopwatchLapsSort: Int + get() = prefs.getInt(STOPWATCH_LAPS_SORT_BY, SORT_BY_LAP or SORT_DESCENDING) + set(stopwatchLapsSort) = prefs.edit().putInt(STOPWATCH_LAPS_SORT_BY, stopwatchLapsSort).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt index 308e723..9ae1035 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt @@ -22,6 +22,7 @@ const val ALARM_LAST_CONFIG = "alarm_last_config" const val TIMER_LAST_CONFIG = "timer_last_config" const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually" const val ALARMS_SORT_BY = "alarms_sort_by" +const val STOPWATCH_LAPS_SORT_BY = "stopwatch_laps_sort_by" const val TABS_COUNT = 4 const val EDITED_TIME_ZONE_SEPARATOR = ":" diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Stopwatch.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Stopwatch.kt new file mode 100644 index 0000000..405d779 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Stopwatch.kt @@ -0,0 +1,130 @@ +package com.simplemobiletools.clock.helpers + +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import com.simplemobiletools.clock.models.Lap +import java.util.concurrent.CopyOnWriteArraySet + +private const val UPDATE_INTERVAL = 10L + +object Stopwatch { + + private val updateHandler = Handler(Looper.getMainLooper()) + private var uptimeAtStart = 0L + private var totalTicks = 0 + private var currentTicks = 0 // ticks that reset at pause + private var lapTicks = 0 + private var currentLap = 1 + val laps = ArrayList() + var state = State.STOPPED + private set(value) { + field = value + for (listener in updateListeners) { + listener.onStateChanged(value) + } + } + private var updateListeners = CopyOnWriteArraySet() + + fun reset() { + updateHandler.removeCallbacksAndMessages(null) + state = State.STOPPED + currentTicks = 0 + totalTicks = 0 + currentLap = 1 + lapTicks = 0 + laps.clear() + } + + fun toggle(setUptimeAtStart: Boolean) { + if (state != State.RUNNING) { + state = State.RUNNING + updateHandler.post(updateRunnable) + if (setUptimeAtStart) { + uptimeAtStart = SystemClock.uptimeMillis() + } + } else { + state = State.PAUSED + val prevSessionsMS = (totalTicks - currentTicks) * UPDATE_INTERVAL + val totalDuration = SystemClock.uptimeMillis() - uptimeAtStart + prevSessionsMS + updateHandler.removeCallbacksAndMessages(null) + currentTicks = 0 + totalTicks-- + for (listener in updateListeners) { + listener.onUpdate(totalDuration, -1, true) + } + } + } + + fun lap() { + if (laps.isEmpty()) { + val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL) + laps.add(0, lap) + lapTicks = 0 + } else { + laps.first().apply { + lapTime = lapTicks * UPDATE_INTERVAL + totalTime = totalTicks * UPDATE_INTERVAL + } + } + + val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL) + laps.add(0, lap) + lapTicks = 0 + } + + /** + * Add a update listener to the stopwatch. The listener gets the current state + * immediately after adding. To avoid memory leaks the listener should be removed + * from the stopwatch. + * @param updateListener the listener + */ + fun addUpdateListener(updateListener: UpdateListener) { + updateListeners.add(updateListener) + updateListener.onUpdate( + totalTicks * UPDATE_INTERVAL, + lapTicks * UPDATE_INTERVAL, + state != State.STOPPED + ) + updateListener.onStateChanged(state) + } + + /** + * Remove the listener from the stopwatch + * @param updateListener the listener + */ + fun removeUpdateListener(updateListener: UpdateListener) { + updateListeners.remove(updateListener) + } + + private val updateRunnable = object : Runnable { + override fun run() { + if (state == State.RUNNING) { + if (totalTicks % 10 == 0) { + for (listener in updateListeners) { + listener.onUpdate( + totalTicks * UPDATE_INTERVAL, + lapTicks * UPDATE_INTERVAL, + false + ) + } + } + totalTicks++ + currentTicks++ + lapTicks++ + updateHandler.postAtTime(this, uptimeAtStart + currentTicks * UPDATE_INTERVAL) + } + } + } + + enum class State { + RUNNING, + PAUSED, + STOPPED + } + + interface UpdateListener { + fun onUpdate(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean) + fun onStateChanged(state: State) + } +}