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 {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],
);

View File

@ -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 (
<View style={styles.container}>
<Searchbar
placeholder="Search"
value={search}
onChangeText={setSearch}
icon="search"
clearIcon="clear"
/>
{typeof search === 'string' && setSearch && (
<Searchbar
placeholder="Search"
value={search}
onChangeText={setSearch}
icon="search"
clearIcon="clear"
/>
)}
{children}
{onAdd && <MassiveFab onPress={onAdd} />}
</View>

View File

@ -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<DrawerParamList>();
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'},
];

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
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
}
}

View File

@ -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
}
}

3
db.ts
View File

@ -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;

View File

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

View File

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