AOSP/DeskClock - Add Kotlin files for Stopwatch data files
Test: manual - Ran the following on Sargo phone. Tested the stopwatch
interactions (ie. start, pause, reset, laps, share).
$ source build/envsetup.sh
$ lunch aosp_sargo-userdebug
$ make DeskClockKotlin -j
$ adb install out/target/product/sargo/product/app/DeskClockKotlin/DeskClockKotlin.apk
BUG: 157255731
Change-Id: Ia3ac367d2ba43fe0f08386a3f099878fd3af72d4
diff --git a/Android.bp b/Android.bp
index 6918211..4b93880 100644
--- a/Android.bp
+++ b/Android.bp
@@ -53,7 +53,13 @@
"src/**/deskclock/data/CityModel.java",
"src/**/deskclock/data/CustomRingtone.java",
"src/**/deskclock/data/CustomRingtoneDAO.java",
+ "src/**/deskclock/data/Lap.java",
"src/**/deskclock/data/RingtoneModel.java",
+ "src/**/deskclock/data/Stopwatch.java",
+ "src/**/deskclock/data/StopwatchDAO.java",
+ "src/**/deskclock/data/StopwatchListener.java",
+ "src/**/deskclock/data/StopwatchModel.java",
+ "src/**/deskclock/data/StopwatchNotificationBuilder.java",
"src/**/deskclock/events/*.java",
"src/**/deskclock/provider/*.java",
"src/**/deskclock/settings/*.java",
diff --git a/src/com/android/deskclock/data/Lap.kt b/src/com/android/deskclock/data/Lap.kt
new file mode 100644
index 0000000..bb109ed
--- /dev/null
+++ b/src/com/android/deskclock/data/Lap.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+/**
+ * A read-only domain object representing a stopwatch lap.
+ */
+data class Lap(
+ /** The 1-based position of the lap. */
+ val lapNumber: Int,
+ /** Elapsed time in ms since the lap was last started. */
+ val lapTime: Long,
+ /** Elapsed time in ms accumulated for all laps up to and including this one. */
+ val accumulatedTime: Long
+)
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.kt b/src/com/android/deskclock/data/Stopwatch.kt
new file mode 100644
index 0000000..2953670
--- /dev/null
+++ b/src/com/android/deskclock/data/Stopwatch.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import com.android.deskclock.Utils
+
+import kotlin.math.max
+
+/**
+ * A read-only domain object representing a stopwatch.
+ */
+class Stopwatch internal constructor(
+ /** Current state of this stopwatch. */
+ val state: State,
+ /** Elapsed time in ms the stopwatch was last started; [.UNUSED] if not running. */
+ val lastStartTime: Long,
+ /** The time since epoch at which the stopwatch was last started. */
+ val lastWallClockTime: Long,
+ /** Elapsed time in ms this stopwatch has accumulated while running. */
+ val accumulatedTime: Long
+) {
+
+ enum class State {
+ RESET, RUNNING, PAUSED
+ }
+
+ val isReset: Boolean
+ get() = state == State.RESET
+
+ val isPaused: Boolean
+ get() = state == State.PAUSED
+
+ val isRunning: Boolean
+ get() = state == State.RUNNING
+
+ /**
+ * @return the total amount of time accumulated up to this moment
+ */
+ val totalTime: Long
+ get() {
+ if (state != State.RUNNING) {
+ return accumulatedTime
+ }
+
+ // In practice, "now" can be any value due to device reboots. When the real-time clock
+ // is reset, there is no more guarantee that "now" falls after the last start time. To
+ // ensure the stopwatch is monotonically increasing, normalize negative time segments to
+ // 0
+ val timeSinceStart = Utils.now() - lastStartTime
+ return accumulatedTime + max(0, timeSinceStart)
+ }
+
+ /**
+ * @return a copy of this stopwatch that is running
+ */
+ fun start(): Stopwatch {
+ return if (state == State.RUNNING) {
+ this
+ } else {
+ Stopwatch(State.RUNNING, Utils.now(), Utils.wallClock(), totalTime)
+ }
+ }
+
+ /**
+ * @return a copy of this stopwatch that is paused
+ */
+ fun pause(): Stopwatch {
+ return if (state != State.RUNNING) {
+ this
+ } else {
+ Stopwatch(State.PAUSED, UNUSED, UNUSED, totalTime)
+ }
+ }
+
+ /**
+ * @return a copy of this stopwatch that is reset
+ */
+ fun reset(): Stopwatch = RESET_STOPWATCH
+
+ /**
+ * @return this Stopwatch if it is not running or an updated version based on wallclock time.
+ * The internals of the stopwatch are updated using the wallclock time which is durable
+ * across reboots.
+ */
+ fun updateAfterReboot(): Stopwatch {
+ if (state != State.RUNNING) {
+ return this
+ }
+ val timeSinceBoot = Utils.now()
+ val wallClockTime = Utils.wallClock()
+ // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
+ // update the recorded times and proceed with no change in accumulated time.
+ val delta = max(0, wallClockTime - lastWallClockTime)
+ return Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta)
+ }
+
+ /**
+ * @return this Stopwatch if it is not running or an updated version based on the realtime.
+ * The internals of the stopwatch are updated using the realtime clock which is accurate
+ * across wallclock time adjustments.
+ */
+ fun updateAfterTimeSet(): Stopwatch {
+ if (state != State.RUNNING) {
+ return this
+ }
+ val timeSinceBoot = Utils.now()
+ val wallClockTime = Utils.wallClock()
+ val delta = timeSinceBoot - lastStartTime
+ return if (delta < 0) {
+ // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
+ // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
+ // updateAfterReboot() can successfully correct the data at a later time.
+ this
+ } else {
+ Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta)
+ }
+ }
+
+ companion object {
+ const val UNUSED = Long.MIN_VALUE
+
+ /** The single, immutable instance of a reset stopwatch. */
+ private val RESET_STOPWATCH = Stopwatch(State.RESET, UNUSED, UNUSED, 0)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.kt b/src/com/android/deskclock/data/StopwatchDAO.kt
new file mode 100644
index 0000000..5e049fe
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchDAO.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.SharedPreferences
+
+/**
+ * This class encapsulates the transfer of data between [Stopwatch] and [Lap] domain
+ * objects and their permanent storage in [SharedPreferences].
+ */
+internal object StopwatchDAO {
+ /** Key to a preference that stores the state of the stopwatch. */
+ private const val STATE = "sw_state"
+
+ /** Key to a preference that stores the last start time of the stopwatch. */
+ private const val LAST_START_TIME = "sw_start_time"
+
+ /** Key to a preference that stores the epoch time when the stopwatch last started. */
+ private const val LAST_WALL_CLOCK_TIME = "sw_wall_clock_time"
+
+ /** Key to a preference that stores the accumulated elapsed time of the stopwatch. */
+ private const val ACCUMULATED_TIME = "sw_accum_time"
+
+ /** Prefix for a key to a preference that stores the number of recorded laps. */
+ private const val LAP_COUNT = "sw_lap_num"
+
+ /** Prefix for a key to a preference that stores accumulated time at the end of a lap. */
+ private const val LAP_ACCUMULATED_TIME = "sw_lap_time_"
+
+ /**
+ * @return the stopwatch from permanent storage or a reset stopwatch if none exists
+ */
+ fun getStopwatch(prefs: SharedPreferences): Stopwatch {
+ val stateIndex: Int = prefs.getInt(STATE, Stopwatch.State.RESET.ordinal)
+ val state = Stopwatch.State.values()[stateIndex]
+ val lastStartTime: Long = prefs.getLong(LAST_START_TIME, Stopwatch.UNUSED)
+ val lastWallClockTime: Long = prefs.getLong(LAST_WALL_CLOCK_TIME, Stopwatch.UNUSED)
+ val accumulatedTime: Long = prefs.getLong(ACCUMULATED_TIME, 0)
+ var s = Stopwatch(state, lastStartTime, lastWallClockTime, accumulatedTime)
+
+ // If the stopwatch reports an illegal (negative) amount of time, remove the bad data.
+ if (s.totalTime < 0) {
+ s = s.reset()
+ setStopwatch(prefs, s)
+ }
+ return s
+ }
+
+ /**
+ * @param stopwatch the last state of the stopwatch
+ */
+ fun setStopwatch(prefs: SharedPreferences, stopwatch: Stopwatch) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+
+ if (stopwatch.isReset) {
+ editor.remove(STATE)
+ .remove(LAST_START_TIME)
+ .remove(LAST_WALL_CLOCK_TIME)
+ .remove(ACCUMULATED_TIME)
+ } else {
+ editor.putInt(STATE, stopwatch.state.ordinal)
+ .putLong(LAST_START_TIME, stopwatch.lastStartTime)
+ .putLong(LAST_WALL_CLOCK_TIME, stopwatch.lastWallClockTime)
+ .putLong(ACCUMULATED_TIME, stopwatch.accumulatedTime)
+ }
+
+ editor.apply()
+ }
+
+ /**
+ * @return a list of recorded laps for the stopwatch
+ */
+ fun getLaps(prefs: SharedPreferences): MutableList<Lap> {
+ // Prepare the container to be filled with laps.
+ val lapCount: Int = prefs.getInt(LAP_COUNT, 0)
+ val laps: MutableList<Lap> = mutableListOf()
+
+ var prevAccumulatedTime: Long = 0
+
+ // Lap numbers are 1-based and so the are corresponding shared preference keys.
+ for (lapNumber in 1..lapCount) {
+ // Look up the accumulated time for the lap.
+ val lapAccumulatedTimeKey = LAP_ACCUMULATED_TIME + lapNumber
+ val accumulatedTime: Long = prefs.getLong(lapAccumulatedTimeKey, 0)
+
+ // Lap time is the delta between accumulated time of this lap and prior lap.
+ val lapTime = accumulatedTime - prevAccumulatedTime
+
+ // Create the lap instance from the data.
+ laps.add(Lap(lapNumber, lapTime, accumulatedTime))
+
+ // Update the accumulated time of the previous lap.
+ prevAccumulatedTime = accumulatedTime
+ }
+
+ // Laps are stored in the order they were recorded; display order is the reverse.
+ laps.reverse()
+
+ return laps
+ }
+
+ /**
+ * @param newLapCount the number of laps including the new lap
+ * @param accumulatedTime the amount of time accumulate by the stopwatch at the end of the lap
+ */
+ fun addLap(prefs: SharedPreferences, newLapCount: Int, accumulatedTime: Long) {
+ prefs.edit()
+ .putInt(LAP_COUNT, newLapCount)
+ .putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
+ .apply()
+ }
+
+ /**
+ * Remove the recorded laps for the stopwatch
+ */
+ fun clearLaps(prefs: SharedPreferences) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+
+ val lapCount: Int = prefs.getInt(LAP_COUNT, 0)
+ for (lapNumber in 1..lapCount) {
+ editor.remove(LAP_ACCUMULATED_TIME + lapNumber)
+ }
+ editor.remove(LAP_COUNT)
+
+ editor.apply()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchListener.kt b/src/com/android/deskclock/data/StopwatchListener.kt
new file mode 100644
index 0000000..334662f
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchListener.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+/**
+ * The interface through which interested parties are notified of changes to the stopwatch or laps.
+ */
+interface StopwatchListener {
+ /**
+ * @param before the stopwatch state before the update
+ * @param after the stopwatch state after the update
+ */
+ fun stopwatchUpdated(before: Stopwatch, after: Stopwatch)
+
+ /**
+ * @param lap the lap that was added
+ */
+ fun lapAdded(lap: Lap)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchModel.kt b/src/com/android/deskclock/data/StopwatchModel.kt
new file mode 100644
index 0000000..c3e022f
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchModel.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.app.Notification
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationManagerCompat
+
+import kotlin.math.max
+
+/**
+ * All [Stopwatch] data is accessed via this model.
+ */
+internal class StopwatchModel(
+ private val mContext: Context,
+ private val mPrefs: SharedPreferences,
+ /** The model from which notification data are fetched. */
+ private val mNotificationModel: NotificationModel
+) {
+
+ /** Used to create and destroy system notifications related to the stopwatch. */
+ private val mNotificationManager = NotificationManagerCompat.from(mContext)
+
+ /** Update stopwatch notification when locale changes. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /** The listeners to notify when the stopwatch or its laps change. */
+ private val mStopwatchListeners: MutableList<StopwatchListener> = mutableListOf()
+
+ /** Delegate that builds platform-specific stopwatch notifications. */
+ private val mNotificationBuilder = StopwatchNotificationBuilder()
+
+ /** The current state of the stopwatch. */
+ private var mStopwatch: Stopwatch? = null
+
+ /** A mutable copy of the recorded stopwatch laps. */
+ private var mLaps: MutableList<Lap>? = null
+
+ init {
+ // Update stopwatch notification when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+ }
+
+ /**
+ * @param stopwatchListener to be notified when stopwatch changes or laps are added
+ */
+ fun addStopwatchListener(stopwatchListener: StopwatchListener) {
+ mStopwatchListeners.add(stopwatchListener)
+ }
+
+ /**
+ * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
+ */
+ fun removeStopwatchListener(stopwatchListener: StopwatchListener) {
+ mStopwatchListeners.remove(stopwatchListener)
+ }
+
+ /**
+ * @return the current state of the stopwatch
+ */
+ val stopwatch: Stopwatch
+ get() {
+ if (mStopwatch == null) {
+ mStopwatch = StopwatchDAO.getStopwatch(mPrefs)
+ }
+
+ return mStopwatch!!
+ }
+
+ /**
+ * @param stopwatch the new state of the stopwatch
+ */
+ fun setStopwatch(stopwatch: Stopwatch): Stopwatch {
+ val before = this.stopwatch
+ if (before != stopwatch) {
+ StopwatchDAO.setStopwatch(mPrefs, stopwatch)
+ mStopwatch = stopwatch
+
+ // Refresh the stopwatch notification to reflect the latest stopwatch state.
+ if (!mNotificationModel.isApplicationInForeground) {
+ updateNotification()
+ }
+
+ // Resetting the stopwatch implicitly clears the recorded laps.
+ if (stopwatch.isReset) {
+ clearLaps()
+ }
+
+ // Notify listeners of the stopwatch change.
+ for (stopwatchListener in mStopwatchListeners) {
+ stopwatchListener.stopwatchUpdated(before, stopwatch)
+ }
+ }
+
+ return stopwatch
+ }
+
+ /**
+ * @return the laps recorded for this stopwatch
+ */
+ val laps: List<Lap>
+ get() = mutableLaps
+
+ /**
+ * @return a newly recorded lap completed now; `null` if no more laps can be added
+ */
+ fun addLap(): Lap? {
+ if (!mStopwatch!!.isRunning || !canAddMoreLaps()) {
+ return null
+ }
+
+ val totalTime = stopwatch.totalTime
+ val laps: MutableList<Lap> = mutableLaps
+
+ val lapNumber = laps.size + 1
+ StopwatchDAO.addLap(mPrefs, lapNumber, totalTime)
+
+ val prevAccumulatedTime = if (laps.isEmpty()) 0 else laps[0].accumulatedTime
+ val lapTime = totalTime - prevAccumulatedTime
+
+ val lap = Lap(lapNumber, lapTime, totalTime)
+ laps.add(0, lap)
+
+ // Refresh the stopwatch notification to reflect the latest stopwatch state.
+ if (!mNotificationModel.isApplicationInForeground) {
+ updateNotification()
+ }
+
+ // Notify listeners of the new lap.
+ for (stopwatchListener in mStopwatchListeners) {
+ stopwatchListener.lapAdded(lap)
+ }
+
+ return lap
+ }
+
+ /**
+ * Clears the laps recorded for this stopwatch.
+ */
+ @VisibleForTesting
+ fun clearLaps() {
+ StopwatchDAO.clearLaps(mPrefs)
+ mutableLaps.clear()
+ }
+
+ /**
+ * @return `true` iff more laps can be recorded
+ */
+ fun canAddMoreLaps(): Boolean = laps.size < 98
+
+ /**
+ * @return the longest lap time of all recorded laps and the current lap
+ */
+ val longestLapTime: Long
+ get() {
+ var maxLapTime: Long = 0
+
+ val laps = laps
+ if (laps.isNotEmpty()) {
+ // Compute the maximum lap time across all recorded laps.
+ for (lap in laps) {
+ maxLapTime = max(maxLapTime, lap.lapTime)
+ }
+
+ // Compare with the maximum lap time for the current lap.
+ val stopwatch = stopwatch
+ val currentLapTime = stopwatch.totalTime - laps[0].accumulatedTime
+ maxLapTime = max(maxLapTime, currentLapTime)
+ }
+
+ return maxLapTime
+ }
+
+ /**
+ * In practice, `time` can be any value due to device reboots. When the real-time clock is
+ * reset, there is no more guarantee that this time falls after the last recorded lap.
+ *
+ * @param time a point in time expected, but not required, to be after the end of the prior lap
+ * @return the elapsed time between the given `time` and the end of the prior lap;
+ * negative elapsed times are normalized to `0`
+ */
+ fun getCurrentLapTime(time: Long): Long {
+ val previousLap = laps[0]
+ val currentLapTime = time - previousLap.accumulatedTime
+ return max(0, currentLapTime)
+ }
+
+ /**
+ * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
+ */
+ fun updateNotification() {
+ val stopwatch = stopwatch
+
+ // Notification should be hidden if the stopwatch has no time or the app is open.
+ if (stopwatch.isReset || mNotificationModel.isApplicationInForeground) {
+ mNotificationManager.cancel(mNotificationModel.stopwatchNotificationId)
+ return
+ }
+
+ // Otherwise build and post a notification reflecting the latest stopwatch state.
+ val notification: Notification =
+ mNotificationBuilder.build(mContext, mNotificationModel, stopwatch)
+ mNotificationBuilder.buildChannel(mContext, mNotificationManager)
+ mNotificationManager.notify(mNotificationModel.stopwatchNotificationId, notification)
+ }
+
+ private val mutableLaps: MutableList<Lap>
+ get() {
+ if (mLaps == null) {
+ mLaps = StopwatchDAO.getLaps(mPrefs)
+ }
+
+ return mLaps!!
+ }
+
+ /**
+ * Update the stopwatch notification in response to a locale change.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ updateNotification()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt b/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt
new file mode 100644
index 0000000..7671b05
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.SystemClock
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.RemoteViews
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.Action
+import androidx.core.app.NotificationCompat.Builder
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.events.Events
+import com.android.deskclock.stopwatch.StopwatchService
+
+/**
+ * Builds notification to reflect the latest state of the stopwatch and recorded laps.
+ */
+internal class StopwatchNotificationBuilder {
+ fun buildChannel(context: Context, notificationManager: NotificationManagerCompat) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ STOPWATCH_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ fun build(context: Context, nm: NotificationModel, stopwatch: Stopwatch?): Notification {
+ @StringRes val eventLabel: Int = R.string.label_notification
+
+ // Intent to load the app when the notification is tapped.
+ val showApp: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_SHOW_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ val pendingShowApp: PendingIntent = PendingIntent.getService(context, 0, showApp,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+ // Compute some values required below.
+ val running = stopwatch!!.isRunning
+ val pname: String = context.getPackageName()
+ val res: Resources = context.getResources()
+ val base: Long = SystemClock.elapsedRealtime() - stopwatch.totalTime
+
+ val content = RemoteViews(pname, R.layout.chronometer_notif_content)
+ content.setChronometer(R.id.chronometer, base, null, running)
+
+ val actions: MutableList<Action> = ArrayList<Action>(2)
+
+ if (running) {
+ // Left button: Pause
+ val pause: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_pause_24dp
+ val title1: CharSequence = res.getText(R.string.sw_pause_button)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, pause)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right button: Add Lap
+ if (DataModel.getDataModel().canAddMoreLaps()) {
+ val lap: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_LAP_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon2: Int = R.drawable.ic_sw_lap_24dp
+ val title2: CharSequence = res.getText(R.string.sw_lap_button)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, lap)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+ }
+
+ // Show the current lap number if any laps have been recorded.
+ val lapCount = DataModel.getDataModel().laps.size
+ if (lapCount > 0) {
+ val lapNumber = lapCount + 1
+ val lap: String = res.getString(R.string.sw_notification_lap_number, lapNumber)
+ content.setTextViewText(R.id.state, lap)
+ content.setViewVisibility(R.id.state, VISIBLE)
+ } else {
+ content.setViewVisibility(R.id.state, GONE)
+ }
+ } else {
+ // Left button: Start
+ val start: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_START_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_start_24dp
+ val title1: CharSequence = res.getText(R.string.sw_start_button)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, start)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right button: Reset (dismisses notification and resets stopwatch)
+ val reset: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_RESET_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon2: Int = R.drawable.ic_reset_24dp
+ val title2: CharSequence = res.getText(R.string.sw_reset_button)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, reset)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+
+ // Indicate the stopwatch is paused.
+ content.setTextViewText(R.id.state, res.getString(R.string.swn_paused))
+ content.setViewVisibility(R.id.state, VISIBLE)
+ }
+ val notification: Builder = Builder(
+ context, STOPWATCH_NOTIFICATION_CHANNEL_ID)
+ .setLocalOnly(true)
+ .setOngoing(running)
+ .setCustomContentView(content)
+ .setContentIntent(pendingShowApp)
+ .setAutoCancel(stopwatch.isPaused)
+ .setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
+ .setSmallIcon(R.drawable.stat_notify_stopwatch)
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+ if (Utils.isNOrLater()) {
+ notification.setGroup(nm.stopwatchNotificationGroupKey)
+ }
+
+ for (action in actions) {
+ notification.addAction(action)
+ }
+
+ return notification.build()
+ }
+
+ companion object {
+ /**
+ * Notification channel containing all stopwatch notifications.
+ */
+ private const val STOPWATCH_NOTIFICATION_CHANNEL_ID = "StopwatchNotification"
+ }
+}
\ No newline at end of file