Add native events to communicate the running timer

Closes #99
This commit is contained in:
Brandon Presley 2022-11-03 20:04:15 +13:00
parent 4a95ed050c
commit fcce1ad9ef
5 changed files with 109 additions and 84 deletions

30
App.tsx
View File

@ -50,18 +50,24 @@ const App = () => {
) )
useEffect(() => { useEffect(() => {
DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => { const description = DeviceEventEmitter.addListener(
console.log(`${Routes.name}.toast:`, {value}) TOAST,
setSnackbar(value) ({value}: {value: string}) => {
}) console.log(`${Routes.name}.toast:`, {value})
if (AppDataSource.isInitialized) return setInitialized(true) setSnackbar(value)
AppDataSource.initialize().then(async () => { },
const settings = await settingsRepo.findOne({where: {}}) )
console.log(`${App.name}.useEffect:`, {gotSettings: settings}) if (AppDataSource.isInitialized) setInitialized(true)
setTheme(settings.theme) else {
setColor(settings.color) AppDataSource.initialize().then(async () => {
setInitialized(true) 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(() => { const paperTheme = useMemo(() => {

View File

@ -7,6 +7,7 @@ import HomePage from './HomePage'
import PlanPage from './PlanPage' import PlanPage from './PlanPage'
import Route from './route' import Route from './route'
import SettingsPage from './SettingsPage' import SettingsPage from './SettingsPage'
import TimerPage from './TimerPage'
import useDark from './use-dark' import useDark from './use-dark'
import WorkoutsPage from './WorkoutsPage' import WorkoutsPage from './WorkoutsPage'
@ -21,6 +22,7 @@ export default function Routes() {
{name: 'Plans', component: PlanPage, icon: 'event'}, {name: 'Plans', component: PlanPage, icon: 'event'},
{name: 'Best', component: BestPage, icon: 'insights'}, {name: 'Best', component: BestPage, icon: 'insights'},
{name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'}, {name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'},
{name: 'Timer', component: TimerPage, icon: 'access-time'},
{name: 'Settings', component: SettingsPage, icon: 'settings'}, {name: 'Settings', component: SettingsPage, icon: 'settings'},
], ],
[], [],

View File

@ -1,62 +1,45 @@
import {useFocusEffect} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'
import React, {useCallback, useState} from 'react'; import {NativeEventEmitter, NativeModules, View} from 'react-native'
import {NativeModules, View} from 'react-native'; import {Button, Subheading, Title} from 'react-native-paper'
import {Button, Subheading, Title} from 'react-native-paper'; import {PADDING} from './constants'
import DrawerHeader from './DrawerHeader'; import {settingsRepo} from './db'
import MassiveFab from './MassiveFab'; import DrawerHeader from './DrawerHeader'
import Page from './Page'; import MassiveFab from './MassiveFab'
import {getSettings, updateSettings} from './settings.service'; import Settings from './settings'
import {useSettings} from './use-settings';
interface TickEvent {
minutes: string
seconds: string
}
export default function TimerPage() { export default function TimerPage() {
const [remaining, setRemaining] = useState(0); const [minutes, setMinutes] = useState('00')
const {settings} = useSettings(); const [seconds, setSeconds] = useState('00')
const [settings, setSettings] = useState<Settings>()
const minutes = Math.floor(remaining / 1000 / 60); useEffect(() => {
const seconds = Math.floor((remaining / 1000) % 60); settingsRepo.findOne({where: {}}).then(setSettings)
let interval = 0; const emitter = new NativeEventEmitter()
const listener = emitter.addListener('tick', (event: TickEvent) => {
const tick = useCallback(() => { console.log(`${TimerPage.name}.tick:`, {event})
let newRemaining = 0; setMinutes(event.minutes)
getSettings().then(gotSettings => { setSeconds(event.seconds)
if (!gotSettings.nextAlarm) return; })
const date = new Date(gotSettings.nextAlarm); return listener.remove
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 = () => { const stop = () => {
NativeModules.AlarmModule.stop(); NativeModules.AlarmModule.stop()
clearInterval(interval); }
setRemaining(0);
updateSettings({...settings, nextAlarm: undefined});
};
const add = async () => { const add = async () => {
if (!settings.nextAlarm) return; NativeModules.AlarmModule.add(settings.vibrate, settings.sound)
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 ( return (
<> <>
<DrawerHeader name="Timer" /> <DrawerHeader name="Timer" />
<Page> <View style={{flexGrow: 1, padding: PADDING}}>
<View <View
style={{ style={{
flex: 1, flex: 1,
@ -69,8 +52,8 @@ export default function TimerPage() {
</Subheading> </Subheading>
<Button onPress={add}>Add 1 min</Button> <Button onPress={add}>Add 1 min</Button>
</View> </View>
</Page> </View>
<MassiveFab icon="stop" onPress={stop} /> <MassiveFab icon="stop" onPress={stop} />
</> </>
); )
} }

View File

@ -16,10 +16,8 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.facebook.react.bridge.Callback import com.facebook.react.bridge.*
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import kotlin.math.floor import kotlin.math.floor
@ -27,39 +25,63 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context) { ReactContextBaseJavaModule(context) {
var countdownTimer: CountDownTimer? = null var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
var running = false
override fun getName(): String { override fun getName(): String {
return "AlarmModule" return "AlarmModule"
} }
private val receiver = object : BroadcastReceiver() { private val stopReceiver = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received broadcast intent") Log.d("AlarmModule", "Received broadcast intent")
Toast.makeText(reactApplicationContext, "called from test receiver", Toast.LENGTH_SHORT) stop()
.show()
} }
} }
init {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
}
override fun onCatalystInstanceDestroy() {
reactApplicationContext.unregisterReceiver(stopReceiver)
super.onCatalystInstanceDestroy()
}
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { fun add(vibrate: Boolean, sound: String?) {
Log.d("AlarmModule", "Add 1 min to alarm.") Log.d("AlarmModule", "Add 1 min to alarm.")
val addIntent = Intent(reactApplicationContext, TimerService::class.java) countdownTimer?.cancel()
addIntent.action = "add" val newMs = if (running) currentMs.toInt().plus(60000) else 60000
addIntent.putExtra("vibrate", vibrate) countdownTimer = getTimer(newMs, vibrate, sound)
addIntent.putExtra("sound", sound) countdownTimer?.start()
addIntent.data = Uri.parse("$milliseconds") running = true
reactApplicationContext.startService(addIntent)
} }
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun stop() { fun stop() {
Log.d("AlarmModule", "Stop alarm.") Log.d("AlarmModule", "Stop alarm.")
val timerIntent = Intent(reactApplicationContext, TimerService::class.java) countdownTimer?.cancel()
reactApplicationContext.stopService(timerIntent) running = false
val alarmIntent = Intent(reactApplicationContext, AlarmService::class.java) reactApplicationContext?.stopService(
reactApplicationContext.stopService(alarmIntent) 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) @RequiresApi(api = Build.VERSION_CODES.O)
@ -76,6 +98,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
countdownTimer?.cancel() countdownTimer?.cancel()
countdownTimer = getTimer(milliseconds, vibrate, sound) countdownTimer = getTimer(milliseconds, vibrate, sound)
countdownTimer?.start() countdownTimer?.start()
running = true
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
@ -114,6 +137,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
return object : CountDownTimer(endMs.toLong(), 1000) { return object : CountDownTimer(endMs.toLong(), 1000) {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
override fun onTick(current: Long) { override fun onTick(current: Long) {
currentMs = current
val seconds = val seconds =
floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0') floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0')
val minutes = val minutes =
@ -124,6 +148,13 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
NotificationCompat.PRIORITY_LOW NotificationCompat.PRIORITY_LOW
val manager = getManager() val manager = getManager()
manager.notify(NOTIFICATION_ID_PENDING, builder.build()) 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) @RequiresApi(Build.VERSION_CODES.O)
@ -149,6 +180,9 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
alarmIntent.putExtra("vibrate", vibrate) alarmIntent.putExtra("vibrate", vibrate)
alarmIntent.putExtra("sound", sound) alarmIntent.putExtra("sound", sound)
context.startService(alarmIntent) 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 contentIntent = Intent(context, MainActivity::class.java)
val pendingContent = val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
reactApplicationContext.registerReceiver(receiver, IntentFilter(STOP_BROADCAST)) val stopBroadcast = Intent(STOP_BROADCAST)
val stopIntent = Intent(STOP_BROADCAST) stopBroadcast.setPackage(reactApplicationContext.packageName)
stopIntent.flags =Intent.FLAG_ACTIVITY_NEW_TASK
val pendingStop = 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) 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) .setContentIntent(pendingContent)

View File

@ -4,4 +4,5 @@ export type DrawerParamList = {
Best: {} Best: {}
Plans: {} Plans: {}
Workouts: {} Workouts: {}
Timer: {}
} }