Quit trying to move timer logic into AlarmModule

I just can't figure out how to make the stop button
and delete intents work.
This commit is contained in:
Brandon Presley 2022-10-24 14:45:21 +13:00
parent c9125575cc
commit 21d9149498
9 changed files with 284 additions and 15 deletions

View File

@ -7,6 +7,7 @@ import {useSnackbar} from './MassiveSnack';
import Set from './set'; import Set from './set';
import {addSet, getSet, updateSet} from './set.service'; import {addSet, getSet, updateSet} from './set.service';
import SetForm from './SetForm'; import SetForm from './SetForm';
import {updateSettings} from './settings.service';
import StackHeader from './StackHeader'; import StackHeader from './StackHeader';
import {useSettings} from './use-settings'; import {useSettings} from './use-settings';
@ -22,11 +23,15 @@ export default function EditSet() {
if (!settings.alarm) return; if (!settings.alarm) return;
const {minutes, seconds} = await getSet(name); const {minutes, seconds} = await getSet(name);
const milliseconds = (minutes ?? 3) * 60 * 1000 + (seconds ?? 0) * 1000; const milliseconds = (minutes ?? 3) * 60 * 1000 + (seconds ?? 0) * 1000;
console.log(`startTimer:`, `Starting timer in ${minutes}:${seconds}`);
NativeModules.AlarmModule.timer( NativeModules.AlarmModule.timer(
milliseconds, milliseconds,
!!settings.vibrate, !!settings.vibrate,
settings.sound, settings.sound,
); );
const nextAlarm = new Date();
nextAlarm.setTime(nextAlarm.getTime() + milliseconds);
updateSettings({...settings, nextAlarm: nextAlarm.toISOString()});
}, },
[settings], [settings],
); );

View File

@ -12,18 +12,20 @@ export default function Page({
}: { }: {
children: JSX.Element | JSX.Element[]; children: JSX.Element | JSX.Element[];
onAdd?: () => void; onAdd?: () => void;
search: string; search?: string;
setSearch: (value: string) => void; setSearch?: (value: string) => void;
}) { }) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Searchbar {typeof search === 'string' && setSearch && (
placeholder="Search" <Searchbar
value={search} placeholder="Search"
onChangeText={setSearch} value={search}
icon="search" onChangeText={setSearch}
clearIcon="clear" icon="search"
/> clearIcon="clear"
/>
)}
{children} {children}
{onAdd && <MassiveFab onPress={onAdd} />} {onAdd && <MassiveFab onPress={onAdd} />}
</View> </View>

View File

@ -1,4 +1,5 @@
import {createDrawerNavigator} from '@react-navigation/drawer'; import {createDrawerNavigator} from '@react-navigation/drawer';
import {useNavigation} from '@react-navigation/native';
import React from 'react'; import React from 'react';
import {IconButton} from 'react-native-paper'; import {IconButton} from 'react-native-paper';
import BestPage from './BestPage'; import BestPage from './BestPage';
@ -7,6 +8,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';
@ -14,12 +16,14 @@ const Drawer = createDrawerNavigator<DrawerParamList>();
export default function Routes() { export default function Routes() {
const dark = useDark(); const dark = useDark();
const navigation = useNavigation();
const routes: Route[] = [ const routes: Route[] = [
{name: 'Home', component: HomePage, icon: 'home'}, {name: 'Home', component: HomePage, icon: 'home'},
{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'},
]; ];

76
TimerPage.tsx Normal file
View File

@ -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 (
<>
<DrawerHeader name="Timer" />
<Page>
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
<Title>Remaining</Title>
<Subheading>
{minutes}:{seconds}
</Subheading>
<Button onPress={add}>Add 1 min</Button>
</View>
</Page>
<MassiveFab icon="stop" onPress={stop} />
</>
);
}

View File

@ -1,29 +1,55 @@
package com.massive package com.massive
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.*
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.CountDownTimer
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.util.Log 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 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.Callback
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReactMethod
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import kotlin.math.floor
class AlarmModule internal constructor(context: ReactApplicationContext?) : class AlarmModule internal constructor(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context) { ReactContextBaseJavaModule(context) {
var countdownTimer: CountDownTimer? = null
override fun getName(): String { override fun getName(): String {
return "AlarmModule" 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) @RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { fun add(milliseconds: Int, vibrate: Boolean, sound: String?) {
@ -50,11 +76,19 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) :
@ReactMethod @ReactMethod
fun timer(milliseconds: Int, vibrate: Boolean, sound: String?) { fun timer(milliseconds: Int, vibrate: Boolean, sound: String?) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay") Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val intent = Intent(reactApplicationContext, TimerService::class.java) val intent = Intent(reactApplicationContext, AlarmModule::class.java)
intent.putExtra("milliseconds", milliseconds) currentActivity?.startActivityForResult(intent, 0)
intent.putExtra("vibrate", vibrate) val manager = getManager()
intent.putExtra("sound", sound) manager.cancel(NOTIFICATION_ID_DONE)
reactApplicationContext.startService(intent) reactApplicationContext.stopService(
Intent(
reactApplicationContext,
AlarmService::class.java
)
)
countdownTimer?.cancel()
countdownTimer = getTimer(milliseconds, vibrate, sound)
countdownTimer?.start()
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
@ -87,4 +121,113 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) :
).show() ).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
}
} }

View File

@ -1,18 +1,52 @@
package com.massive package com.massive
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import com.facebook.react.bridge.ReactApplicationContext
class StopTimer : Service() { class StopTimer : Service() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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)) 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) return super.onStartCommand(intent, flags, startId)
} }
override fun onBind(p0: Intent?): IBinder? { override fun onBind(p0: Intent?): IBinder? {
return null 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
}
} }

3
db.ts
View File

@ -117,6 +117,9 @@ const migrations = [
` `
ALTER TABLE settings ADD COLUMN showSets BOOLEAN DEFAULT 1 ALTER TABLE settings ADD COLUMN showSets BOOLEAN DEFAULT 1
`, `,
`
CREATE INDEX sets_created ON sets(created)
`,
]; ];
export let db: SQLiteDatabase; export let db: SQLiteDatabase;

View File

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

View File

@ -11,4 +11,5 @@ export default interface Settings {
showDate: number; showDate: number;
theme: 'system' | 'dark' | 'light'; theme: 'system' | 'dark' | 'light';
showSets: number; showSets: number;
nextAlarm?: string;
} }