From 6581b32afed58588184c84563c8cfacab307d619 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Tue, 5 Jul 2022 15:33:42 +1200 Subject: [PATCH] Move timer into notifications bar --- Alarm.tsx | 78 ---------------- Home.tsx | 7 +- Settings.tsx | 8 +- android/app/src/main/AndroidManifest.xml | 3 +- .../src/main/java/com/massive/AlarmModule.kt | 38 ++------ .../java/com/massive/MyBroadcastReceiver.kt | 51 ---------- .../src/main/java/com/massive/StopTimer.kt | 16 ++++ .../src/main/java/com/massive/TimerService.kt | 92 +++++++++++++++++++ .../ic_baseline_hourglass_bottom_24.xml | 5 + .../main/res/drawable/ic_baseline_stop_24.xml | 5 + android/app/src/main/res/values/strings.xml | 2 +- package.json | 1 - yarn.lock | 9 -- 13 files changed, 137 insertions(+), 178 deletions(-) delete mode 100644 Alarm.tsx delete mode 100644 android/app/src/main/java/com/massive/MyBroadcastReceiver.kt create mode 100644 android/app/src/main/java/com/massive/StopTimer.kt create mode 100644 android/app/src/main/java/com/massive/TimerService.kt create mode 100644 android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_stop_24.xml diff --git a/Alarm.tsx b/Alarm.tsx deleted file mode 100644 index fed04f5..0000000 --- a/Alarm.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import React, {useEffect, useState} from 'react'; -import {NativeModules, StyleSheet, Text, View} from 'react-native'; -import {Button, Modal, Portal} from 'react-native-paper'; - -export default function Alarm() { - const [show, setShow] = useState(false); - const [seconds, setSeconds] = useState(0); - const [minutes, setMinutes] = useState(0); - - let intervalId: number; - - useEffect(() => { - if (!show) return; - (async () => { - const next = await AsyncStorage.getItem('nextAlarm'); - if (!next) return; - const milliseconds = new Date(next).getTime() - new Date().getTime(); - if (milliseconds <= 0) return; - let secondsLeft = milliseconds / 1000; - setSeconds(Math.floor(secondsLeft % 60)); - setMinutes(Math.floor(secondsLeft / 60)); - intervalId = setInterval(() => { - secondsLeft--; - if (secondsLeft <= 0) return clearInterval(intervalId); - setSeconds(Math.floor(secondsLeft % 60)); - setMinutes(Math.floor(secondsLeft / 60)); - }, 1000); - })(); - return () => clearInterval(intervalId); - }, [show]); - - const stop = async () => { - NativeModules.AlarmModule.stop(); - clearInterval(intervalId); - setSeconds(0); - setMinutes(0); - await AsyncStorage.setItem('nextAlarm', ''); - }; - - return ( - <> - - setShow(false)}> - Resting - - {minutes}:{seconds} - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - center: { - alignItems: 'center', - alignSelf: 'center', - marginBottom: 10, - }, - title: { - fontSize: 18, - }, -}); diff --git a/Home.tsx b/Home.tsx index 51399ba..eeff3a2 100644 --- a/Home.tsx +++ b/Home.tsx @@ -7,8 +7,7 @@ import { StyleSheet, View, } from 'react-native'; -import {AnimatedFAB, Button, Searchbar} from 'react-native-paper'; -import Alarm from './Alarm'; +import {AnimatedFAB, Searchbar} from 'react-native-paper'; import {getDb} from './db'; import EditSet from './EditSet'; @@ -56,10 +55,7 @@ export default function Home() { const minutes = await AsyncStorage.getItem('minutes'); const seconds = await AsyncStorage.getItem('seconds'); const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000; - const when = new Date(); - when.setTime(when.getTime() + milliseconds); NativeModules.AlarmModule.timer(milliseconds); - await AsyncStorage.setItem('nextAlarm', when.toISOString()); }; const next = async () => { @@ -87,7 +83,6 @@ export default function Home() { onScrollEndDrag={next} /> - - ); @@ -66,6 +69,7 @@ export default function Settings({ const styles = StyleSheet.create({ container: { padding: 10, + flex: 1, }, text: { marginBottom: 10, diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 83a4564..8504aa5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,7 +29,8 @@ - + + diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index f2e4da2..6b341bb 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -1,23 +1,16 @@ package com.massive -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import android.app.PendingIntent -import android.app.AlarmManager -import androidx.annotation.RequiresApi -import com.facebook.react.bridge.ReactMethod import android.content.Intent -import com.massive.MyBroadcastReceiver -import android.app.AlarmManager.AlarmClockInfo -import android.content.Context import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod -// replace com.your-app-name with your app’s name class AlarmModule internal constructor(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { - private var pendingIntent: PendingIntent? = null - private var alarmManager: AlarmManager? = null + override fun getName(): String { return "AlarmModule" } @@ -26,21 +19,8 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) : @ReactMethod fun timer(milliseconds: Int) { Log.d("AlarmModule", "Queue alarm for $milliseconds delay") - val intent = Intent(reactApplicationContext, MyBroadcastReceiver::class.java) - pendingIntent = PendingIntent.getBroadcast( - reactApplicationContext, 69, intent, PendingIntent.FLAG_IMMUTABLE - ) - alarmManager = - reactApplicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val info = AlarmClockInfo(System.currentTimeMillis() + milliseconds, pendingIntent) - alarmManager!!.setAlarmClock(info, pendingIntent) - + val intent = Intent(reactApplicationContext, TimerService::class.java) + intent.putExtra("milliseconds", milliseconds) + reactApplicationContext.startService(intent) } - - @ReactMethod - fun stop() { - Log.d("AlarmModule", "Request to stop timer.") - alarmManager?.cancel(pendingIntent) - reactApplicationContext.stopService(Intent(reactApplicationContext, AlarmService::class.java)) - } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/massive/MyBroadcastReceiver.kt b/android/app/src/main/java/com/massive/MyBroadcastReceiver.kt deleted file mode 100644 index b309d1c..0000000 --- a/android/app/src/main/java/com/massive/MyBroadcastReceiver.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.massive - -import androidx.annotation.RequiresApi -import android.content.Intent -import android.app.NotificationManager -import android.app.NotificationChannel -import com.massive.MyBroadcastReceiver -import com.massive.AlarmService -import com.massive.AlarmActivity -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.os.Build -import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat - -class MyBroadcastReceiver : BroadcastReceiver() { - @RequiresApi(api = Build.VERSION_CODES.M) - override fun onReceive(context: Context, intent: Intent) { - Log.d("MyBroadcastReceiver", "Received intent for BroadcastReceiver.") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val importance = NotificationManager.IMPORTANCE_HIGH - val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance) - channel.description = "Alarms for rest timings." - val notificationManager = context.getSystemService( - NotificationManager::class.java - ) - notificationManager.createNotificationChannel(channel) - } - context.startService(Intent(context, AlarmService::class.java)) - val contentIntent = Intent(context.applicationContext, AlarmActivity::class.java) - val pendingContent = - PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) - val builder = NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_baseline_timer_24) - .setContentTitle("Rest") - .setContentText("Break times over!") - .setContentIntent(pendingContent) - .setAutoCancel(true) - .setCategory(NotificationCompat.CATEGORY_ALARM) - .setPriority(NotificationCompat.PRIORITY_HIGH) - val notificationManager = NotificationManagerCompat.from(context) - notificationManager.notify(ALARM_ID, builder.build()) - } - - companion object { - private const val CHANNEL_ID = "MassiveAlarm" - private const val ALARM_ID = 1 - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/massive/StopTimer.kt b/android/app/src/main/java/com/massive/StopTimer.kt new file mode 100644 index 0000000..92811a8 --- /dev/null +++ b/android/app/src/main/java/com/massive/StopTimer.kt @@ -0,0 +1,16 @@ +package com.massive + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class StopTimer : Service() { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + applicationContext.stopService(Intent(applicationContext, TimerService::class.java)) + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(p0: Intent?): IBinder? { + return null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/massive/TimerService.kt b/android/app/src/main/java/com/massive/TimerService.kt new file mode 100644 index 0000000..d6ed1c4 --- /dev/null +++ b/android/app/src/main/java/com/massive/TimerService.kt @@ -0,0 +1,92 @@ +package com.massive + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.CountDownTimer +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import kotlin.math.floor + +class TimerService : Service() { + private lateinit var notificationManager: NotificationManagerCompat + private lateinit var countdownTimer: CountDownTimer + + @RequiresApi(Build.VERSION_CODES.M) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d("TimerService", "Started timer service.") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance) + channel.description = "Alarms for rest timings." + val notificationManager = applicationContext.getSystemService( + NotificationManager::class.java + ) + notificationManager.createNotificationChannel(channel) + } + + val contentIntent = Intent(applicationContext, MainActivity::class.java) + val pendingContent = + PendingIntent.getActivity(applicationContext, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) + val actionIntent = Intent(applicationContext, StopTimer::class.java) + val pendingAction = + PendingIntent.getService(applicationContext, 0, actionIntent, PendingIntent.FLAG_IMMUTABLE) + val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24) + .setContentTitle("Resting") + .setContentIntent(pendingContent) + .addAction(R.drawable.ic_baseline_stop_24, "STOP", pendingAction) + + val endMs = intent!!.extras!!.getInt("milliseconds") + notificationManager = NotificationManagerCompat.from(applicationContext) + countdownTimer = object : CountDownTimer(endMs.toLong(), 1000) { + override fun onTick(currentMs: Long) { + val seconds = floor((currentMs / 1000).toDouble() % 60) + .toInt().toString().padStart(2, '0') + val minutes = floor((currentMs / 1000).toDouble() / 60) + .toInt().toString().padStart(2, '0') + builder.setContentText("$minutes:$seconds") + .setAutoCancel(false) + .setDefaults(0) + .setProgress(endMs, currentMs.toInt(), false) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .priority = NotificationCompat.PRIORITY_LOW + notificationManager.notify(ALARM_ID, builder.build()) + } + override fun onFinish() { + builder.setContentText("Timer finished.") + .clearActions() + .setAutoCancel(true) + .setOngoing(false) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .priority = NotificationCompat.PRIORITY_HIGH + notificationManager.notify(ALARM_ID, builder.build()) + applicationContext.startService(Intent(applicationContext, AlarmService::class.java)) + } + } + + countdownTimer.start() + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(p0: Intent?): IBinder? { + return null + } + + override fun onDestroy() { + countdownTimer.cancel() + notificationManager.cancel(ALARM_ID) + super.onDestroy() + } + + companion object { + private const val CHANNEL_ID = "MassiveAlarm" + private const val ALARM_ID = 1 + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml b/android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml new file mode 100644 index 0000000..e2dc75e --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_stop_24.xml b/android/app/src/main/res/drawable/ic_baseline_stop_24.xml new file mode 100644 index 0000000..19bcbee --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_stop_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 7dc2df2..bc186d5 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - massive + Massive diff --git a/package.json b/package.json index e487e22..076c64a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "dependencies": { "@babel/preset-env": "^7.1.6", "@react-native-async-storage/async-storage": "^1.17.7", - "@react-navigation/bottom-tabs": "^6.3.1", "@react-navigation/material-top-tabs": "^6.2.1", "@react-navigation/native": "^6.0.10", "@react-navigation/native-stack": "^6.6.2", diff --git a/yarn.lock b/yarn.lock index 8a3e00a..790fc8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1618,15 +1618,6 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@react-navigation/bottom-tabs@^6.3.1": - version "6.3.1" - resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.3.1.tgz#1552ccdb789b6c9fc05af0877f8f3a50ab28870c" - integrity sha512-sL9F4WMhhR6I9bE7bpsPVHnK1cN9doaFHAuy5YmD+Sw6OyO0TAmNgQFx4xZWqboA5ZwSkN0tWcRCr6wGXaRRag== - dependencies: - "@react-navigation/elements" "^1.3.3" - color "^3.1.3" - warn-once "^0.1.0" - "@react-navigation/core@^6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.2.1.tgz#90459f9afd25b71a9471b0706ebea2cdd2534fc4"