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