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(() => { useEffect(() => {
DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => { const description = DeviceEventEmitter.addListener(
console.log(`${Routes.name}.toast:`, {value}) TOAST,
setSnackbar(value) ({value}: {value: string}) => {
}) console.log(`${Routes.name}.toast:`, {value})
if (AppDataSource.isInitialized) return setInitialized(true) setSnackbar(value)
AppDataSource.initialize().then(async () => { },
const settings = await settingsRepo.findOne({where: {}}) )
console.log(`${App.name}.useEffect:`, {gotSettings: settings}) if (AppDataSource.isInitialized) setInitialized(true)
setTheme(settings.theme) else {
setColor(settings.color) AppDataSource.initialize().then(async () => {
setInitialized(true) 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(() => { const paperTheme = useMemo(() => {

View File

@ -7,6 +7,7 @@ import HomePage from './HomePage'
import PlanPage from './PlanPage' import PlanPage from './PlanPage'
import Route from './route' import Route from './route'
import SettingsPage from './SettingsPage' import SettingsPage from './SettingsPage'
import TimerPage from './TimerPage'
import useDark from './use-dark' import useDark from './use-dark'
import WorkoutsPage from './WorkoutsPage' import WorkoutsPage from './WorkoutsPage'
@ -21,6 +22,7 @@ export default function Routes() {
{name: 'Plans', component: PlanPage, icon: 'event'}, {name: 'Plans', component: PlanPage, icon: 'event'},
{name: 'Best', component: BestPage, icon: 'insights'}, {name: 'Best', component: BestPage, icon: 'insights'},
{name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'}, {name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'},
{name: 'Timer', component: TimerPage, icon: 'access-time'},
{name: 'Settings', component: SettingsPage, icon: 'settings'}, {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 package com.massive
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.*
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.CountDownTimer
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.facebook.react.bridge.Callback import androidx.core.app.NotificationCompat
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.*
import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.bridge.ReactMethod import kotlin.math.floor
class AlarmModule internal constructor(context: ReactApplicationContext?) : class AlarmModule constructor(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context) { ReactContextBaseJavaModule(context) {
var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
var running = false
override fun getName(): String { override fun getName(): String {
return "AlarmModule" 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) @RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { fun add(vibrate: Boolean, sound: String?) {
Log.d("AlarmModule", "Add 1 min to alarm.") Log.d("AlarmModule", "Add 1 min to alarm.")
val addIntent = Intent(reactApplicationContext, TimerService::class.java) countdownTimer?.cancel()
addIntent.action = "add" val newMs = if (running) currentMs.toInt().plus(60000) else 60000
addIntent.putExtra("vibrate", vibrate) countdownTimer = getTimer(newMs, vibrate, sound)
addIntent.putExtra("sound", sound) countdownTimer?.start()
addIntent.data = Uri.parse("$milliseconds") running = true
reactApplicationContext.startService(addIntent)
} }
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun stop() { fun stop() {
Log.d("AlarmModule", "Stop alarm.") Log.d("AlarmModule", "Stop alarm.")
val timerIntent = Intent(reactApplicationContext, TimerService::class.java) countdownTimer?.cancel()
reactApplicationContext.stopService(timerIntent) running = false
val alarmIntent = Intent(reactApplicationContext, AlarmService::class.java) reactApplicationContext?.stopService(
reactApplicationContext.stopService(alarmIntent) 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) @RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun timer(milliseconds: Int, vibrate: Boolean, sound: String?, noSound: Boolean = false) { fun timer(milliseconds: Int, vibrate: Boolean, sound: String?, noSound: Boolean = false) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay") Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val intent = Intent(reactApplicationContext, TimerService::class.java) val manager = getManager()
intent.putExtra("milliseconds", milliseconds) manager.cancel(NOTIFICATION_ID_DONE)
intent.putExtra("vibrate", vibrate) reactApplicationContext.stopService(
intent.putExtra("sound", sound) Intent(
intent.putExtra("noSound", noSound) reactApplicationContext, AlarmService::class.java
reactApplicationContext.startService(intent) )
)
countdownTimer?.cancel()
countdownTimer = getTimer(milliseconds, vibrate, sound)
countdownTimer?.start()
running = true
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
@ReactMethod @ReactMethod
fun ignoringBattery(callback: Callback) { fun ignoringBattery(callback: Callback) {
val packageName = reactApplicationContext.packageName val packageName = reactApplicationContext.packageName
val pm = val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
callback.invoke(pm.isIgnoringBatteryOptimizations(packageName)) callback.invoke(pm.isIgnoringBatteryOptimizations(packageName))
} else { } else {
@ -88,4 +130,106 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) :
).show() ).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 package com.massive
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import com.facebook.react.bridge.ReactApplicationContext
class StopTimer : Service() { class StopTimer : Service() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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)) 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) return super.onStartCommand(intent, flags, startId)
} }
override fun onBind(p0: Intent?): IBinder? { override fun onBind(p0: Intent?): IBinder? {
return null 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: {} Best: {}
Plans: {} Plans: {}
Workouts: {} Workouts: {}
Timer: {}
} }