Move timer into notifications bar

This commit is contained in:
Brandon Presley 2022-07-05 15:33:42 +12:00
parent 47af169ca6
commit 6581b32afe
13 changed files with 137 additions and 178 deletions

View File

@ -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 (
<>
<Portal>
<Modal
visible={show}
style={styles.center}
onDismiss={() => setShow(false)}>
<Text style={[styles.center, styles.title]}>Resting</Text>
<Text style={styles.center}>
{minutes}:{seconds}
</Text>
<View style={{flexDirection: 'row'}}>
<Button icon="close" onPress={() => setShow(false)}>
Close
</Button>
<Button mode="contained" icon="stop" onPress={stop}>
Stop
</Button>
</View>
</Modal>
</Portal>
<Button icon="time" onPress={() => setShow(true)}>
Time left
</Button>
</>
);
}
const styles = StyleSheet.create({
center: {
alignItems: 'center',
alignSelf: 'center',
marginBottom: 10,
},
title: {
fontSize: 18,
},
});

View File

@ -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}
/>
<View style={styles.bottom}>
<Alarm />
<EditSet
id={id}
show={showEdit}

View File

@ -56,8 +56,11 @@ export default function Settings({
value={alarmEnabled}
onValueChange={setAlarmEnabled}
/>
<Button style={{alignSelf: 'flex-start'}} icon="trash" onPress={clear}>
Clear sets
<Button
style={{alignSelf: 'flex-start', marginTop: 'auto'}}
icon="trash"
onPress={clear}>
Delete all data
</Button>
</View>
);
@ -66,6 +69,7 @@ export default function Settings({
const styles = StyleSheet.create({
container: {
padding: 10,
flex: 1,
},
text: {
marginBottom: 10,

View File

@ -29,7 +29,8 @@
</intent-filter>
</activity>
<activity android:exported="true" android:process=":remote" android:name=".AlarmActivity" />
<receiver android:exported="true" android:process=":remote" android:name=".MyBroadcastReceiver" />
<service android:name=".StopTimer" android:exported="true" android:process=":remote" />
<service android:name=".AlarmService" android:exported="true" />
<service android:name=".TimerService" android:exported="true" />
</application>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,22l-0.01,-6L14,12l3.99,-4.01L18,2H6v6l4,4l-4,3.99V22H18zM8,7.5V4h8v3.5l-4,4L8,7.5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/>
</vector>

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">massive</string>
<string name="app_name">Massive</string>
</resources>

View File

@ -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",

View File

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