Merge branch 'alarm-module'

This commit is contained in:
Brandon Presley 2022-11-03 20:04:50 +13:00
commit 84b369d54b
6 changed files with 283 additions and 37 deletions

30
App.tsx
View File

@ -50,18 +50,24 @@ const App = () => {
)
useEffect(() => {
DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => {
console.log(`${Routes.name}.toast:`, {value})
setSnackbar(value)
})
if (AppDataSource.isInitialized) return setInitialized(true)
AppDataSource.initialize().then(async () => {
const settings = await settingsRepo.findOne({where: {}})
console.log(`${App.name}.useEffect:`, {gotSettings: settings})
setTheme(settings.theme)
setColor(settings.color)
setInitialized(true)
})
const description = DeviceEventEmitter.addListener(
TOAST,
({value}: {value: string}) => {
console.log(`${Routes.name}.toast:`, {value})
setSnackbar(value)
},
)
if (AppDataSource.isInitialized) setInitialized(true)
else {
AppDataSource.initialize().then(async () => {
const settings = await settingsRepo.findOne({where: {}})
console.log(`${App.name}.useEffect:`, {gotSettings: settings})
setTheme(settings.theme)
setColor(settings.color)
setInitialized(true)
})
}
return description.remove
}, [])
const paperTheme = useMemo(() => {

View File

@ -7,6 +7,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'
@ -21,6 +22,7 @@ export default function Routes() {
{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'},
],
[],

59
TimerPage.tsx Normal file
View File

@ -0,0 +1,59 @@
import React, {useEffect, useState} from 'react'
import {NativeEventEmitter, NativeModules, View} from 'react-native'
import {Button, Subheading, Title} from 'react-native-paper'
import {PADDING} from './constants'
import {settingsRepo} from './db'
import DrawerHeader from './DrawerHeader'
import MassiveFab from './MassiveFab'
import Settings from './settings'
interface TickEvent {
minutes: string
seconds: string
}
export default function TimerPage() {
const [minutes, setMinutes] = useState('00')
const [seconds, setSeconds] = useState('00')
const [settings, setSettings] = useState<Settings>()
useEffect(() => {
settingsRepo.findOne({where: {}}).then(setSettings)
const emitter = new NativeEventEmitter()
const listener = emitter.addListener('tick', (event: TickEvent) => {
console.log(`${TimerPage.name}.tick:`, {event})
setMinutes(event.minutes)
setSeconds(event.seconds)
})
return listener.remove
}, [])
const stop = () => {
NativeModules.AlarmModule.stop()
}
const add = async () => {
NativeModules.AlarmModule.add(settings.vibrate, settings.sound)
}
return (
<>
<DrawerHeader name="Timer" />
<View style={{flexGrow: 1, padding: PADDING}}>
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
<Title>Remaining</Title>
<Subheading>
{minutes}:{seconds}
</Subheading>
<Button onPress={add}>Add 1 min</Button>
</View>
</View>
<MassiveFab icon="stop" onPress={stop} />
</>
)
}

View File

@ -1,69 +1,111 @@
package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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 com.facebook.react.bridge.Callback
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import androidx.core.app.NotificationCompat
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.math.floor
class AlarmModule internal constructor(context: ReactApplicationContext?) :
class AlarmModule constructor(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context) {
var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
var running = false
override fun getName(): String {
return "AlarmModule"
}
private val stopReceiver = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received broadcast intent")
stop()
}
}
init {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
}
override fun onCatalystInstanceDestroy() {
reactApplicationContext.unregisterReceiver(stopReceiver)
super.onCatalystInstanceDestroy()
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod
fun add(milliseconds: Int, vibrate: Boolean, sound: String?) {
fun add(vibrate: Boolean, sound: String?) {
Log.d("AlarmModule", "Add 1 min to alarm.")
val addIntent = Intent(reactApplicationContext, TimerService::class.java)
addIntent.action = "add"
addIntent.putExtra("vibrate", vibrate)
addIntent.putExtra("sound", sound)
addIntent.data = Uri.parse("$milliseconds")
reactApplicationContext.startService(addIntent)
countdownTimer?.cancel()
val newMs = if (running) currentMs.toInt().plus(60000) else 60000
countdownTimer = getTimer(newMs, vibrate, sound)
countdownTimer?.start()
running = true
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod
fun stop() {
Log.d("AlarmModule", "Stop alarm.")
val timerIntent = Intent(reactApplicationContext, TimerService::class.java)
reactApplicationContext.stopService(timerIntent)
val alarmIntent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.stopService(alarmIntent)
countdownTimer?.cancel()
running = false
reactApplicationContext?.stopService(
Intent(
reactApplicationContext,
AlarmService::class.java
)
)
val manager = getManager()
manager.cancel(NOTIFICATION_ID_DONE)
manager.cancel(NOTIFICATION_ID_PENDING)
val params = Arguments.createMap().apply {
putString("minutes", "00")
putString("seconds", "00")
}
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("tick", params)
}
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod
fun timer(milliseconds: Int, vibrate: Boolean, sound: String?, noSound: Boolean = false) {
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)
intent.putExtra("noSound", noSound)
reactApplicationContext.startService(intent)
val manager = getManager()
manager.cancel(NOTIFICATION_ID_DONE)
reactApplicationContext.stopService(
Intent(
reactApplicationContext, AlarmService::class.java
)
)
countdownTimer?.cancel()
countdownTimer = getTimer(milliseconds, vibrate, sound)
countdownTimer?.start()
running = true
}
@RequiresApi(Build.VERSION_CODES.M)
@ReactMethod
fun ignoringBattery(callback: Callback) {
val packageName = reactApplicationContext.packageName
val pm =
reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
callback.invoke(pm.isIgnoringBatteryOptimizations(packageName))
} else {
@ -88,4 +130,106 @@ 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) {
currentMs = current
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())
val params = Arguments.createMap().apply {
putString("minutes", minutes)
putString("seconds", seconds)
}
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("tick", params)
}
@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)
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("finish", Arguments.createMap())
}
}
}
@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 stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(reactApplicationContext.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, 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 STOP_BROADCAST = "stop-timer-event"
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
}
}

View File

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