From 21d914949834d2acbcabfbc2a8f52eb9e81754af Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Mon, 24 Oct 2022 14:45:21 +1300 Subject: [PATCH 1/7] Quit trying to move timer logic into AlarmModule I just can't figure out how to make the stop button and delete intents work. --- EditSet.tsx | 5 + Page.tsx | 20 +-- Routes.tsx | 4 + TimerPage.tsx | 76 +++++++++ .../src/main/java/com/massive/AlarmModule.kt | 153 +++++++++++++++++- .../src/main/java/com/massive/StopTimer.kt | 36 ++++- db.ts | 3 + drawer-param-list.ts | 1 + settings.ts | 1 + 9 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 TimerPage.tsx diff --git a/EditSet.tsx b/EditSet.tsx index 7c1046f..e965521 100644 --- a/EditSet.tsx +++ b/EditSet.tsx @@ -7,6 +7,7 @@ import {useSnackbar} from './MassiveSnack'; import Set from './set'; import {addSet, getSet, updateSet} from './set.service'; import SetForm from './SetForm'; +import {updateSettings} from './settings.service'; import StackHeader from './StackHeader'; import {useSettings} from './use-settings'; @@ -22,11 +23,15 @@ export default function EditSet() { if (!settings.alarm) return; const {minutes, seconds} = await getSet(name); const milliseconds = (minutes ?? 3) * 60 * 1000 + (seconds ?? 0) * 1000; + console.log(`startTimer:`, `Starting timer in ${minutes}:${seconds}`); NativeModules.AlarmModule.timer( milliseconds, !!settings.vibrate, settings.sound, ); + const nextAlarm = new Date(); + nextAlarm.setTime(nextAlarm.getTime() + milliseconds); + updateSettings({...settings, nextAlarm: nextAlarm.toISOString()}); }, [settings], ); diff --git a/Page.tsx b/Page.tsx index c945fc4..7651ed4 100644 --- a/Page.tsx +++ b/Page.tsx @@ -12,18 +12,20 @@ export default function Page({ }: { children: JSX.Element | JSX.Element[]; onAdd?: () => void; - search: string; - setSearch: (value: string) => void; + search?: string; + setSearch?: (value: string) => void; }) { return ( - + {typeof search === 'string' && setSearch && ( + + )} {children} {onAdd && } diff --git a/Routes.tsx b/Routes.tsx index 7bad039..a070d3a 100644 --- a/Routes.tsx +++ b/Routes.tsx @@ -1,4 +1,5 @@ import {createDrawerNavigator} from '@react-navigation/drawer'; +import {useNavigation} from '@react-navigation/native'; import React from 'react'; import {IconButton} from 'react-native-paper'; import BestPage from './BestPage'; @@ -7,6 +8,7 @@ import HomePage from './HomePage'; import PlanPage from './PlanPage'; import Route from './route'; import SettingsPage from './SettingsPage'; +import TimerPage from './TimerPage'; import useDark from './use-dark'; import WorkoutsPage from './WorkoutsPage'; @@ -14,12 +16,14 @@ const Drawer = createDrawerNavigator(); export default function Routes() { const dark = useDark(); + const navigation = useNavigation(); const routes: Route[] = [ {name: 'Home', component: HomePage, icon: 'home'}, {name: 'Plans', component: PlanPage, icon: 'event'}, {name: 'Best', component: BestPage, icon: 'insights'}, {name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'}, + {name: 'Timer', component: TimerPage, icon: 'access-time'}, {name: 'Settings', component: SettingsPage, icon: 'settings'}, ]; diff --git a/TimerPage.tsx b/TimerPage.tsx new file mode 100644 index 0000000..35d7c67 --- /dev/null +++ b/TimerPage.tsx @@ -0,0 +1,76 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback, useState} from 'react'; +import {NativeModules, View} from 'react-native'; +import {Button, Subheading, Title} from 'react-native-paper'; +import DrawerHeader from './DrawerHeader'; +import MassiveFab from './MassiveFab'; +import Page from './Page'; +import {getSettings, updateSettings} from './settings.service'; +import {useSettings} from './use-settings'; + +export default function TimerPage() { + const [remaining, setRemaining] = useState(0); + const {settings} = useSettings(); + + const minutes = Math.floor(remaining / 1000 / 60); + const seconds = Math.floor((remaining / 1000) % 60); + let interval = 0; + + const tick = useCallback(() => { + let newRemaining = 0; + getSettings().then(gotSettings => { + if (!gotSettings.nextAlarm) return; + const date = new Date(gotSettings.nextAlarm); + newRemaining = date.getTime() - new Date().getTime(); + if (newRemaining < 0) setRemaining(0); + else setRemaining(newRemaining); + }); + interval = setInterval(() => { + console.log({newRemaining}); + newRemaining -= 1000; + if (newRemaining > 0) return setRemaining(newRemaining); + clearInterval(interval); + setRemaining(0); + }, 1000); + return () => clearInterval(interval); + }, []); + + useFocusEffect(tick); + + const stop = () => { + NativeModules.AlarmModule.stop(); + clearInterval(interval); + setRemaining(0); + updateSettings({...settings, nextAlarm: undefined}); + }; + + const add = async () => { + if (!settings.nextAlarm) return; + const date = new Date(settings.nextAlarm); + date.setTime(date.getTime() + 1000 * 60); + NativeModules.AlarmModule.add(date, settings.vibrate, settings.sound); + await updateSettings({...settings, nextAlarm: date.toISOString()}); + tick(); + }; + + return ( + <> + + + + Remaining + + {minutes}:{seconds} + + + + + + + ); +} diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index d2a30d0..f353afb 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -1,29 +1,55 @@ package com.massive import android.annotation.SuppressLint +import android.app.* import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import android.os.CountDownTimer import android.os.PowerManager import android.provider.Settings import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import com.facebook.react.bridge.ActivityEventListener +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Callback import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter +import kotlin.math.floor class AlarmModule internal constructor(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { + var countdownTimer: CountDownTimer? = null + override fun getName(): String { return "AlarmModule" } + private val mActivityEventListener: ActivityEventListener = + object : BaseActivityEventListener() { + override fun onActivityResult( + activity: Activity?, + requestCode: Int, + resultCode: Int, + data: Intent? + ) { + Log.d("AlarmModule", "onActivityResult") + } + } + + init { + reactApplicationContext.addActivityEventListener(mActivityEventListener) + } + @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { @@ -50,11 +76,19 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : @ReactMethod fun timer(milliseconds: Int, vibrate: Boolean, sound: String?) { Log.d("AlarmModule", "Queue alarm for $milliseconds delay") - val intent = Intent(reactApplicationContext, TimerService::class.java) - intent.putExtra("milliseconds", milliseconds) - intent.putExtra("vibrate", vibrate) - intent.putExtra("sound", sound) - reactApplicationContext.startService(intent) + val intent = Intent(reactApplicationContext, AlarmModule::class.java) + currentActivity?.startActivityForResult(intent, 0) + val manager = getManager() + manager.cancel(NOTIFICATION_ID_DONE) + reactApplicationContext.stopService( + Intent( + reactApplicationContext, + AlarmService::class.java + ) + ) + countdownTimer?.cancel() + countdownTimer = getTimer(milliseconds, vibrate, sound) + countdownTimer?.start() } @RequiresApi(Build.VERSION_CODES.M) @@ -87,4 +121,113 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : ).show() } } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getTimer(endMs: Int, vibrate: Boolean, sound: String?): CountDownTimer { + val builder = getBuilder() + return object : CountDownTimer(endMs.toLong(), 1000) { + @RequiresApi(Build.VERSION_CODES.O) + override fun onTick(current: Long) { + val seconds = floor((current / 1000).toDouble() % 60) + .toInt().toString().padStart(2, '0') + val minutes = floor((current / 1000).toDouble() / 60) + .toInt().toString().padStart(2, '0') + builder.setContentText("$minutes:$seconds") + .setAutoCancel(false) + .setDefaults(0) + .setProgress(endMs, current.toInt(), false) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .priority = NotificationCompat.PRIORITY_LOW + val manager = getManager() + manager.notify(NOTIFICATION_ID_PENDING, builder.build()) + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onFinish() { + val context = reactApplicationContext + val finishIntent = Intent(context, StopAlarm::class.java) + val finishPending = + PendingIntent.getActivity( + context, + 0, + finishIntent, + PendingIntent.FLAG_IMMUTABLE + ) + val fullIntent = Intent(context, TimerDone::class.java) + val fullPending = + PendingIntent.getActivity( + context, + 0, + fullIntent, + PendingIntent.FLAG_IMMUTABLE + ) + builder.setContentText("Timer finished.") + .setProgress(0, 0, false) + .setAutoCancel(true) + .setOngoing(true) + .setFullScreenIntent(fullPending, true) + .setContentIntent(finishPending) + .setChannelId(CHANNEL_ID_DONE) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .priority = NotificationCompat.PRIORITY_HIGH + val manager = getManager() + manager.notify(NOTIFICATION_ID_DONE, builder.build()) + manager.cancel(NOTIFICATION_ID_PENDING) + val alarmIntent = Intent(context, AlarmService::class.java) + alarmIntent.putExtra("vibrate", vibrate) + alarmIntent.putExtra("sound", sound) + context.startService(alarmIntent) + } + } + } + + @SuppressLint("UnspecifiedImmutableFlag") + @RequiresApi(Build.VERSION_CODES.M) + private fun getBuilder(): NotificationCompat.Builder { + val context = reactApplicationContext + val contentIntent = Intent(context, MainActivity::class.java) + val pendingContent = + PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) + val stopIntent = Intent(context, StopTimer::class.java) + val pendingStop = + PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE) + return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) + .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24) + .setContentTitle("Resting") + .setContentIntent(pendingContent) + .addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop) + .setDeleteIntent(pendingStop) + } + + @RequiresApi(Build.VERSION_CODES.O) + fun getManager(): NotificationManager { + val alarmsChannel = NotificationChannel( + CHANNEL_ID_DONE, + CHANNEL_ID_DONE, + NotificationManager.IMPORTANCE_HIGH + ) + alarmsChannel.description = "Alarms for rest timers." + alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val notificationManager = reactApplicationContext.getSystemService( + NotificationManager::class.java + ) + notificationManager.createNotificationChannel(alarmsChannel) + val timersChannel = + NotificationChannel( + CHANNEL_ID_PENDING, + CHANNEL_ID_PENDING, + NotificationManager.IMPORTANCE_LOW + ) + timersChannel.setSound(null, null) + timersChannel.description = "Progress on rest timers." + notificationManager.createNotificationChannel(timersChannel) + return notificationManager + } + + companion object { + const val CHANNEL_ID_PENDING = "Timer" + const val CHANNEL_ID_DONE = "Alarm" + const val NOTIFICATION_ID_PENDING = 1 + const val NOTIFICATION_ID_DONE = 2 + } } diff --git a/android/app/src/main/java/com/massive/StopTimer.kt b/android/app/src/main/java/com/massive/StopTimer.kt index a65333b..1c02edf 100644 --- a/android/app/src/main/java/com/massive/StopTimer.kt +++ b/android/app/src/main/java/com/massive/StopTimer.kt @@ -1,18 +1,52 @@ package com.massive +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.app.Service import android.content.Intent +import android.os.Build import android.os.IBinder import android.util.Log +import androidx.annotation.RequiresApi +import com.facebook.react.bridge.ReactApplicationContext class StopTimer : Service() { + @RequiresApi(Build.VERSION_CODES.O) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - applicationContext.stopService(Intent(applicationContext, TimerService::class.java)) + Log.d("StopTimer", "onStartCommand") applicationContext.stopService(Intent(applicationContext, AlarmService::class.java)) + val manager = getManager(); + manager.cancel(AlarmModule.NOTIFICATION_ID_DONE) + manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING) + val mod = AlarmModule(applicationContext as ReactApplicationContext?) + Log.d("StopTimer", "countdownTimer=${mod.countdownTimer}") return super.onStartCommand(intent, flags, startId) } override fun onBind(p0: Intent?): IBinder? { return null } + + + @RequiresApi(Build.VERSION_CODES.O) + private fun getManager(): NotificationManager { + val alarmsChannel = NotificationChannel( + AlarmModule.CHANNEL_ID_DONE, + AlarmModule.CHANNEL_ID_DONE, + NotificationManager.IMPORTANCE_HIGH + ) + alarmsChannel.description = "Alarms for rest timers." + alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val notificationManager = applicationContext.getSystemService( + NotificationManager::class.java + ) + notificationManager.createNotificationChannel(alarmsChannel) + val timersChannel = + NotificationChannel(AlarmModule.CHANNEL_ID_PENDING, AlarmModule.CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW) + timersChannel.setSound(null, null) + timersChannel.description = "Progress on rest timers." + notificationManager.createNotificationChannel(timersChannel) + return notificationManager + } } \ No newline at end of file diff --git a/db.ts b/db.ts index bb576bb..bed371e 100644 --- a/db.ts +++ b/db.ts @@ -117,6 +117,9 @@ const migrations = [ ` ALTER TABLE settings ADD COLUMN showSets BOOLEAN DEFAULT 1 `, + ` + CREATE INDEX sets_created ON sets(created) + `, ]; export let db: SQLiteDatabase; diff --git a/drawer-param-list.ts b/drawer-param-list.ts index e590f4c..8fea9ed 100644 --- a/drawer-param-list.ts +++ b/drawer-param-list.ts @@ -4,4 +4,5 @@ export type DrawerParamList = { Best: {}; Plans: {}; Workouts: {}; + Timer: {}; }; diff --git a/settings.ts b/settings.ts index 6a422e6..d16fdea 100644 --- a/settings.ts +++ b/settings.ts @@ -11,4 +11,5 @@ export default interface Settings { showDate: number; theme: 'system' | 'dark' | 'light'; showSets: number; + nextAlarm?: string; } From 46dcfb96bf796cbe9b4ec3e4bd2633829e486ab9 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Fri, 28 Oct 2022 16:48:29 +1300 Subject: [PATCH 2/7] Add broadcast receiver to AlarmModule --- .../src/main/java/com/massive/AlarmModule.kt | 98 +++++++------------ 1 file changed, 35 insertions(+), 63 deletions(-) diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index f353afb..fa26818 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -3,8 +3,10 @@ package com.massive import android.annotation.SuppressLint import android.app.* import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.net.Uri import android.os.Build import android.os.CountDownTimer @@ -14,14 +16,10 @@ import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import com.facebook.react.bridge.ActivityEventListener -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Callback import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import kotlin.math.floor @@ -34,20 +32,15 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : return "AlarmModule" } - private val mActivityEventListener: ActivityEventListener = - object : BaseActivityEventListener() { - override fun onActivityResult( - activity: Activity?, - requestCode: Int, - resultCode: Int, - data: Intent? - ) { - Log.d("AlarmModule", "onActivityResult") - } + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Toast.makeText(reactApplicationContext, "called from test receiver", Toast.LENGTH_SHORT) + .show() } + } init { - reactApplicationContext.addActivityEventListener(mActivityEventListener) + reactApplicationContext.registerReceiver(broadcastReceiver, IntentFilter(STOP_BROADCAST)) } @RequiresApi(api = Build.VERSION_CODES.O) @@ -82,8 +75,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : manager.cancel(NOTIFICATION_ID_DONE) reactApplicationContext.stopService( Intent( - reactApplicationContext, - AlarmService::class.java + reactApplicationContext, AlarmService::class.java ) ) countdownTimer?.cancel() @@ -95,8 +87,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : @ReactMethod fun ignoringBattery(callback: Callback) { val packageName = reactApplicationContext.packageName - val pm = - reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager + val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { callback.invoke(pm.isIgnoringBatteryOptimizations(packageName)) } else { @@ -128,16 +119,14 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : return object : CountDownTimer(endMs.toLong(), 1000) { @RequiresApi(Build.VERSION_CODES.O) override fun onTick(current: Long) { - val seconds = floor((current / 1000).toDouble() % 60) - .toInt().toString().padStart(2, '0') - val minutes = floor((current / 1000).toDouble() / 60) - .toInt().toString().padStart(2, '0') - builder.setContentText("$minutes:$seconds") - .setAutoCancel(false) - .setDefaults(0) + val seconds = + floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0') + val minutes = + floor((current / 1000).toDouble() / 60).toInt().toString().padStart(2, '0') + builder.setContentText("$minutes:$seconds").setAutoCancel(false).setDefaults(0) .setProgress(endMs, current.toInt(), false) - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .priority = NotificationCompat.PRIORITY_LOW + .setCategory(NotificationCompat.CATEGORY_PROGRESS).priority = + NotificationCompat.PRIORITY_LOW val manager = getManager() manager.notify(NOTIFICATION_ID_PENDING, builder.build()) } @@ -146,30 +135,18 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : override fun onFinish() { val context = reactApplicationContext val finishIntent = Intent(context, StopAlarm::class.java) - val finishPending = - PendingIntent.getActivity( - context, - 0, - finishIntent, - PendingIntent.FLAG_IMMUTABLE - ) + val finishPending = PendingIntent.getActivity( + context, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE + ) val fullIntent = Intent(context, TimerDone::class.java) - val fullPending = - PendingIntent.getActivity( - context, - 0, - fullIntent, - PendingIntent.FLAG_IMMUTABLE - ) - builder.setContentText("Timer finished.") - .setProgress(0, 0, false) - .setAutoCancel(true) - .setOngoing(true) - .setFullScreenIntent(fullPending, true) - .setContentIntent(finishPending) - .setChannelId(CHANNEL_ID_DONE) - .setCategory(NotificationCompat.CATEGORY_ALARM) - .priority = NotificationCompat.PRIORITY_HIGH + val fullPending = PendingIntent.getActivity( + context, 0, fullIntent, PendingIntent.FLAG_IMMUTABLE + ) + builder.setContentText("Timer finished.").setProgress(0, 0, false) + .setAutoCancel(true).setOngoing(true).setFullScreenIntent(fullPending, true) + .setContentIntent(finishPending).setChannelId(CHANNEL_ID_DONE) + .setCategory(NotificationCompat.CATEGORY_ALARM).priority = + NotificationCompat.PRIORITY_HIGH val manager = getManager() manager.notify(NOTIFICATION_ID_DONE, builder.build()) manager.cancel(NOTIFICATION_ID_PENDING) @@ -188,12 +165,11 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : val contentIntent = Intent(context, MainActivity::class.java) val pendingContent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) - val stopIntent = Intent(context, StopTimer::class.java) + val stopIntent = Intent(STOP_BROADCAST) val pendingStop = PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) - .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24) - .setContentTitle("Resting") + .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting") .setContentIntent(pendingContent) .addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop) .setDeleteIntent(pendingStop) @@ -202,9 +178,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : @RequiresApi(Build.VERSION_CODES.O) fun getManager(): NotificationManager { val alarmsChannel = NotificationChannel( - CHANNEL_ID_DONE, - CHANNEL_ID_DONE, - NotificationManager.IMPORTANCE_HIGH + CHANNEL_ID_DONE, CHANNEL_ID_DONE, NotificationManager.IMPORTANCE_HIGH ) alarmsChannel.description = "Alarms for rest timers." alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC @@ -212,12 +186,9 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : NotificationManager::class.java ) notificationManager.createNotificationChannel(alarmsChannel) - val timersChannel = - NotificationChannel( - CHANNEL_ID_PENDING, - CHANNEL_ID_PENDING, - NotificationManager.IMPORTANCE_LOW - ) + val timersChannel = NotificationChannel( + CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW + ) timersChannel.setSound(null, null) timersChannel.description = "Progress on rest timers." notificationManager.createNotificationChannel(timersChannel) @@ -225,6 +196,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : } companion object { + const val STOP_BROADCAST = "stop-timer-event" const val CHANNEL_ID_PENDING = "Timer" const val CHANNEL_ID_DONE = "Alarm" const val NOTIFICATION_ID_PENDING = 1 From 1c58dc2db1ea13ea0ff4e8b99679f6296144d3c0 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Fri, 28 Oct 2022 17:22:26 +1300 Subject: [PATCH 3/7] Local broadcast receiver is not running on stop intent --- .../app/src/main/java/com/massive/AlarmModule.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index 46bd6c6..341b411 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -23,7 +23,7 @@ import com.facebook.react.bridge.ReactMethod import kotlin.math.floor -class AlarmModule internal constructor(context: ReactApplicationContext?) : +class AlarmModule constructor(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { var countdownTimer: CountDownTimer? = null @@ -32,17 +32,14 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : return "AlarmModule" } - private val broadcastReceiver = object : BroadcastReceiver() { + private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { + Log.d("AlarmModule", "Received broadcast intent") Toast.makeText(reactApplicationContext, "called from test receiver", Toast.LENGTH_SHORT) .show() } } - init { - reactApplicationContext.registerReceiver(broadcastReceiver, IntentFilter(STOP_BROADCAST)) - } - @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { @@ -69,8 +66,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : @ReactMethod fun timer(milliseconds: Int, vibrate: Boolean, sound: String?, noSound: Boolean = false) { Log.d("AlarmModule", "Queue alarm for $milliseconds delay") - val intent = Intent(reactApplicationContext, AlarmModule::class.java) - currentActivity?.startActivityForResult(intent, 0) + reactApplicationContext.registerReceiver(receiver, IntentFilter(STOP_BROADCAST)) val manager = getManager() manager.cancel(NOTIFICATION_ID_DONE) reactApplicationContext.stopService( @@ -166,8 +162,9 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : val pendingContent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) val stopIntent = Intent(STOP_BROADCAST) + stopIntent.flags =Intent.FLAG_ACTIVITY_NEW_TASK val pendingStop = - PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_MUTABLE) return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting") .setContentIntent(pendingContent) From 5441aa164b2d22e8c19c3bbc32533b61037080c5 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Fri, 28 Oct 2022 17:31:10 +1300 Subject: [PATCH 4/7] Move registerReceiver to no avail --- android/app/src/main/java/com/massive/AlarmModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index 341b411..289f3bd 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -66,7 +66,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) : @ReactMethod fun timer(milliseconds: Int, vibrate: Boolean, sound: String?, noSound: Boolean = false) { Log.d("AlarmModule", "Queue alarm for $milliseconds delay") - reactApplicationContext.registerReceiver(receiver, IntentFilter(STOP_BROADCAST)) val manager = getManager() manager.cancel(NOTIFICATION_ID_DONE) reactApplicationContext.stopService( @@ -161,6 +160,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) : val contentIntent = Intent(context, MainActivity::class.java) val pendingContent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) + reactApplicationContext.registerReceiver(receiver, IntentFilter(STOP_BROADCAST)) val stopIntent = Intent(STOP_BROADCAST) stopIntent.flags =Intent.FLAG_ACTIVITY_NEW_TASK val pendingStop = From f52b1437f2f9bb2d5488f569f9dd2d97487b8847 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Thu, 3 Nov 2022 19:09:50 +1300 Subject: [PATCH 5/7] Fix edit set crashing on fresh installs --- SetForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SetForm.tsx b/SetForm.tsx index f6606c2..9e59787 100644 --- a/SetForm.tsx +++ b/SetForm.tsx @@ -21,14 +21,14 @@ export default function SetForm({ settings: Settings }) { const [name, setName] = useState(set.name) - const [reps, setReps] = useState(set.reps.toString()) - const [weight, setWeight] = useState(set.weight.toString()) + const [reps, setReps] = useState(set.reps?.toString()) + const [weight, setWeight] = useState(set.weight?.toString()) const [newImage, setNewImage] = useState(set.image) const [unit, setUnit] = useState(set.unit) const [showRemove, setShowRemove] = useState(false) const [selection, setSelection] = useState({ start: 0, - end: set.reps.toString().length, + end: set.reps?.toString().length, }) const [removeImage, setRemoveImage] = useState(false) const weightRef = useRef(null) From 4a95ed050c2f1f84f0a19c53548effa0f1a6a041 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Thu, 3 Nov 2022 19:21:19 +1300 Subject: [PATCH 6/7] Fix adding new set on fresh installs --- SetList.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/SetList.tsx b/SetList.tsx index e2f7216..ee54080 100644 --- a/SetList.tsx +++ b/SetList.tsx @@ -3,7 +3,7 @@ import { useFocusEffect, useNavigation, } from '@react-navigation/native' -import {useCallback, useEffect, useState} from 'react' +import {useCallback, useState} from 'react' import {FlatList} from 'react-native' import {List} from 'react-native-paper' import {Like} from 'typeorm' @@ -34,7 +34,9 @@ export default function SetList() { order: {created: 'DESC'}, }) console.log(`${SetList.name}.refresh:`, {newSets}) - setSet(newSets[0]) + const newSet = newSets[0] + delete newSet.id + setSet(newSet) if (newSets.length === 0) return setSets([]) setSets(newSets) setOffset(0) @@ -80,8 +82,18 @@ export default function SetList() { const onAdd = useCallback(async () => { console.log(`${SetList.name}.onAdd`, {set}) const [{now}] = await getNow() - const newSet: GymSet = set || new GymSet() - delete newSet.id + const newSet: GymSet = set || { + created: now, + hidden: false, + image: '', + minutes: 3, + seconds: 30, + name: '', + reps: 0, + sets: 0, + unit: 'kg', + weight: 0, + } newSet.created = now navigation.navigate('EditSet', {set: newSet}) }, [navigation, set]) From fcce1ad9ef82c017dc9d5c41496218a563a9327a Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Thu, 3 Nov 2022 20:04:15 +1300 Subject: [PATCH 7/7] Add native events to communicate the running timer Closes #99 --- App.tsx | 30 ++++--- Routes.tsx | 2 + TimerPage.tsx | 83 ++++++++----------- .../src/main/java/com/massive/AlarmModule.kt | 77 ++++++++++++----- drawer-param-list.ts | 1 + 5 files changed, 109 insertions(+), 84 deletions(-) diff --git a/App.tsx b/App.tsx index 8b62db0..3b5b2e9 100644 --- a/App.tsx +++ b/App.tsx @@ -50,18 +50,24 @@ const App = () => { ) useEffect(() => { - DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => { - console.log(`${Routes.name}.toast:`, {value}) - setSnackbar(value) - }) - if (AppDataSource.isInitialized) return setInitialized(true) - AppDataSource.initialize().then(async () => { - const settings = await settingsRepo.findOne({where: {}}) - console.log(`${App.name}.useEffect:`, {gotSettings: settings}) - setTheme(settings.theme) - setColor(settings.color) - setInitialized(true) - }) + const description = DeviceEventEmitter.addListener( + TOAST, + ({value}: {value: string}) => { + console.log(`${Routes.name}.toast:`, {value}) + setSnackbar(value) + }, + ) + if (AppDataSource.isInitialized) setInitialized(true) + else { + AppDataSource.initialize().then(async () => { + const settings = await settingsRepo.findOne({where: {}}) + console.log(`${App.name}.useEffect:`, {gotSettings: settings}) + setTheme(settings.theme) + setColor(settings.color) + setInitialized(true) + }) + } + return description.remove }, []) const paperTheme = useMemo(() => { diff --git a/Routes.tsx b/Routes.tsx index 23b2060..b8c1cac 100644 --- a/Routes.tsx +++ b/Routes.tsx @@ -7,6 +7,7 @@ import HomePage from './HomePage' import PlanPage from './PlanPage' import Route from './route' import SettingsPage from './SettingsPage' +import TimerPage from './TimerPage' import useDark from './use-dark' import WorkoutsPage from './WorkoutsPage' @@ -21,6 +22,7 @@ export default function Routes() { {name: 'Plans', component: PlanPage, icon: 'event'}, {name: 'Best', component: BestPage, icon: 'insights'}, {name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'}, + {name: 'Timer', component: TimerPage, icon: 'access-time'}, {name: 'Settings', component: SettingsPage, icon: 'settings'}, ], [], diff --git a/TimerPage.tsx b/TimerPage.tsx index 35d7c67..b07ca36 100644 --- a/TimerPage.tsx +++ b/TimerPage.tsx @@ -1,62 +1,45 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useState} from 'react'; -import {NativeModules, View} from 'react-native'; -import {Button, Subheading, Title} from 'react-native-paper'; -import DrawerHeader from './DrawerHeader'; -import MassiveFab from './MassiveFab'; -import Page from './Page'; -import {getSettings, updateSettings} from './settings.service'; -import {useSettings} from './use-settings'; +import React, {useEffect, useState} from 'react' +import {NativeEventEmitter, NativeModules, View} from 'react-native' +import {Button, Subheading, Title} from 'react-native-paper' +import {PADDING} from './constants' +import {settingsRepo} from './db' +import DrawerHeader from './DrawerHeader' +import MassiveFab from './MassiveFab' +import Settings from './settings' + +interface TickEvent { + minutes: string + seconds: string +} export default function TimerPage() { - const [remaining, setRemaining] = useState(0); - const {settings} = useSettings(); + const [minutes, setMinutes] = useState('00') + const [seconds, setSeconds] = useState('00') + const [settings, setSettings] = useState() - const minutes = Math.floor(remaining / 1000 / 60); - const seconds = Math.floor((remaining / 1000) % 60); - let interval = 0; - - const tick = useCallback(() => { - let newRemaining = 0; - getSettings().then(gotSettings => { - if (!gotSettings.nextAlarm) return; - const date = new Date(gotSettings.nextAlarm); - newRemaining = date.getTime() - new Date().getTime(); - if (newRemaining < 0) setRemaining(0); - else setRemaining(newRemaining); - }); - interval = setInterval(() => { - console.log({newRemaining}); - newRemaining -= 1000; - if (newRemaining > 0) return setRemaining(newRemaining); - clearInterval(interval); - setRemaining(0); - }, 1000); - return () => clearInterval(interval); - }, []); - - useFocusEffect(tick); + useEffect(() => { + settingsRepo.findOne({where: {}}).then(setSettings) + const emitter = new NativeEventEmitter() + const listener = emitter.addListener('tick', (event: TickEvent) => { + console.log(`${TimerPage.name}.tick:`, {event}) + setMinutes(event.minutes) + setSeconds(event.seconds) + }) + return listener.remove + }, []) const stop = () => { - NativeModules.AlarmModule.stop(); - clearInterval(interval); - setRemaining(0); - updateSettings({...settings, nextAlarm: undefined}); - }; + NativeModules.AlarmModule.stop() + } const add = async () => { - if (!settings.nextAlarm) return; - const date = new Date(settings.nextAlarm); - date.setTime(date.getTime() + 1000 * 60); - NativeModules.AlarmModule.add(date, settings.vibrate, settings.sound); - await updateSettings({...settings, nextAlarm: date.toISOString()}); - tick(); - }; + NativeModules.AlarmModule.add(settings.vibrate, settings.sound) + } return ( <> - + - + - ); + ) } diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index 289f3bd..99c4c35 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -16,10 +16,8 @@ import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import com.facebook.react.bridge.Callback -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule import kotlin.math.floor @@ -27,39 +25,63 @@ class AlarmModule constructor(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { var countdownTimer: CountDownTimer? = null + var currentMs: Long = 0 + var running = false override fun getName(): String { return "AlarmModule" } - private val receiver = object : BroadcastReceiver() { + private val stopReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) override fun onReceive(context: Context?, intent: Intent?) { Log.d("AlarmModule", "Received broadcast intent") - Toast.makeText(reactApplicationContext, "called from test receiver", Toast.LENGTH_SHORT) - .show() + stop() } } + init { + reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) + } + + override fun onCatalystInstanceDestroy() { + reactApplicationContext.unregisterReceiver(stopReceiver) + super.onCatalystInstanceDestroy() + } + @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod - fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { + fun add(vibrate: Boolean, sound: String?) { Log.d("AlarmModule", "Add 1 min to alarm.") - val addIntent = Intent(reactApplicationContext, TimerService::class.java) - addIntent.action = "add" - addIntent.putExtra("vibrate", vibrate) - addIntent.putExtra("sound", sound) - addIntent.data = Uri.parse("$milliseconds") - reactApplicationContext.startService(addIntent) + countdownTimer?.cancel() + val newMs = if (running) currentMs.toInt().plus(60000) else 60000 + countdownTimer = getTimer(newMs, vibrate, sound) + countdownTimer?.start() + running = true } @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun stop() { Log.d("AlarmModule", "Stop alarm.") - val timerIntent = Intent(reactApplicationContext, TimerService::class.java) - reactApplicationContext.stopService(timerIntent) - val alarmIntent = Intent(reactApplicationContext, AlarmService::class.java) - reactApplicationContext.stopService(alarmIntent) + countdownTimer?.cancel() + running = false + reactApplicationContext?.stopService( + Intent( + reactApplicationContext, + AlarmService::class.java + ) + ) + val manager = getManager() + manager.cancel(NOTIFICATION_ID_DONE) + manager.cancel(NOTIFICATION_ID_PENDING) + val params = Arguments.createMap().apply { + putString("minutes", "00") + putString("seconds", "00") + } + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("tick", params) } @RequiresApi(api = Build.VERSION_CODES.O) @@ -76,6 +98,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) : countdownTimer?.cancel() countdownTimer = getTimer(milliseconds, vibrate, sound) countdownTimer?.start() + running = true } @RequiresApi(Build.VERSION_CODES.M) @@ -114,6 +137,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) : return object : CountDownTimer(endMs.toLong(), 1000) { @RequiresApi(Build.VERSION_CODES.O) override fun onTick(current: Long) { + currentMs = current val seconds = floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0') val minutes = @@ -124,6 +148,13 @@ class AlarmModule constructor(context: ReactApplicationContext?) : NotificationCompat.PRIORITY_LOW val manager = getManager() manager.notify(NOTIFICATION_ID_PENDING, builder.build()) + val params = Arguments.createMap().apply { + putString("minutes", minutes) + putString("seconds", seconds) + } + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("tick", params) } @RequiresApi(Build.VERSION_CODES.O) @@ -149,6 +180,9 @@ class AlarmModule constructor(context: ReactApplicationContext?) : alarmIntent.putExtra("vibrate", vibrate) alarmIntent.putExtra("sound", sound) context.startService(alarmIntent) + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("finish", Arguments.createMap()) } } } @@ -160,11 +194,10 @@ class AlarmModule constructor(context: ReactApplicationContext?) : val contentIntent = Intent(context, MainActivity::class.java) val pendingContent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) - reactApplicationContext.registerReceiver(receiver, IntentFilter(STOP_BROADCAST)) - val stopIntent = Intent(STOP_BROADCAST) - stopIntent.flags =Intent.FLAG_ACTIVITY_NEW_TASK + val stopBroadcast = Intent(STOP_BROADCAST) + stopBroadcast.setPackage(reactApplicationContext.packageName) val pendingStop = - PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_MUTABLE) + PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting") .setContentIntent(pendingContent) diff --git a/drawer-param-list.ts b/drawer-param-list.ts index a7e8ac5..68cae78 100644 --- a/drawer-param-list.ts +++ b/drawer-param-list.ts @@ -4,4 +4,5 @@ export type DrawerParamList = { Best: {} Plans: {} Workouts: {} + Timer: {} }