Compare commits

...

5 Commits

Author SHA1 Message Date
Brandon Presley 617fca0094 Fix tsconfig.json - 2.25 🚀 2024-02-17 19:11:02 +13:00
Brandon Presley eea6c96e8e Make separate channel for finished notifications
Still aren't showing on lock screen, or waking the
device sadly. Ive got to finish reading https://github.com/giorgosneokleous93/fullscreenintentexample/tree/main/app/src/main/java/com/giorgosneokleous/fullscreenintentexample
2024-02-17 19:01:43 +13:00
Brandon Presley 9e3f2fea78 Auto request battery optimizations are off
If the user reinstalls the app, and then imports their
database, they might end up with timers on but
battery optimizations on as well.
2024-02-17 17:42:19 +13:00
Brandon Presley a0dc62e761 Remove all JS side Timers
This is the result of me fixing the background timers.
Previously our code just used a CountdownTimer
not even in a service, just immediately in the
@ReactMethod. This would in certain scenarios stop
running. Even with battery optimizations turned off.

The reason why all the JS side timers had to be removed
is because we were relying on RCTDeviceEventEmitter
which I don't know how to use from within a Service.
See my stackoverflow ticket here: https://stackoverflow.com/questions/74204339/sending-react-native-android-events-to-javascript-from-a-service

Closes #212, #196
2024-02-17 17:27:42 +13:00
Brandon Presley 47cfaa4b67 Fix dismissing alarm and add +1 minute button 2024-02-16 17:47:26 +13:00
15 changed files with 264 additions and 624 deletions

View File

@ -17,7 +17,6 @@ import FatalError from "./FatalError";
import { AppDataSource } from "./data-source";
import { settingsRepo } from "./db";
import { ThemeContext } from "./use-theme";
import TimerProgress from "./TimerProgress";
export const CombinedDefaultTheme = {
...NavigationDefaultTheme,
@ -119,7 +118,6 @@ const App = () => {
</NavigationContainer>
<AppSnack textColor={paperTheme.colors.background} />
<TimerProgress />
</PaperProvider>
);
};

View File

@ -8,7 +8,6 @@ import InsightsPage from "./InsightsPage";
import PlanList from "./PlanList";
import SetList from "./SetList";
import SettingsPage from "./SettingsPage";
import TimerPage from "./TimerPage";
import WeightList from "./WeightList";
const Drawer = createDrawerNavigator<DrawerParams>();
@ -55,11 +54,6 @@ export default function AppDrawer({
drawerIcon: () => <IconButton icon="chart-bell-curve-cumulative" />,
}}
/>
<Drawer.Screen
name="Timer"
component={TimerPage}
options={{ drawerIcon: () => <IconButton icon="timer-outline" /> }}
/>
<Drawer.Screen
name="Weight"
component={WeightList}

View File

@ -6,7 +6,6 @@ import { FlatList, NativeModules } from "react-native";
import DocumentPicker from "react-native-document-picker";
import { Dirs, FileSystem } from "react-native-file-access";
import { Button } from "react-native-paper";
import { check, PERMISSIONS, request, RESULTS } from "react-native-permissions";
import AppInput from "./AppInput";
import ConfirmDialog from "./ConfirmDialog";
import { PADDING } from "./constants";

View File

@ -1,68 +0,0 @@
import { useFocusEffect } from "@react-navigation/native";
import React, { useCallback, useState } from "react";
import { NativeModules, View } from "react-native";
import { FAB, Text, useTheme } from "react-native-paper";
import AppFab from "./AppFab";
import DrawerHeader from "./DrawerHeader";
import { settingsRepo } from "./db";
import Settings from "./settings";
import useTimer from "./use-timer";
export interface TickEvent {
minutes: string;
seconds: string;
}
export default function TimerPage() {
const { minutes, seconds, update } = useTimer();
const [settings, setSettings] = useState<Settings>();
const { colors } = useTheme();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [])
);
const stop = () => {
NativeModules.AlarmModule.stop();
update();
};
const add = async () => {
console.log(`${TimerPage.name}.add:`, settings);
NativeModules.AlarmModule.add();
update();
};
return (
<>
<DrawerHeader name="Timer" />
<View
style={{
flex: 1,
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 70, position: "absolute" }}>
{minutes}:{seconds}
</Text>
</View>
<FAB
icon="plus"
onPress={add}
style={{
position: "absolute",
left: 20,
bottom: 20,
backgroundColor: colors.primary,
}}
color={colors.background}
/>
<AppFab icon="stop" onPress={stop} />
</>
);
}

View File

@ -1,31 +0,0 @@
import { useEffect, useState } from "react";
import { ProgressBar } from "react-native-paper";
import { TickEvent } from "./TimerPage";
import { emitter } from "./emitter";
export default function TimerProgress() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const description = emitter.addListener(
"tick",
({ minutes, seconds }: TickEvent) => {
setProgress((Number(minutes) * 60 + Number(seconds)) / 210);
}
);
return description.remove;
}, []);
if (progress === 0) return null;
return (
<ProgressBar
style={{
position: "absolute",
bottom: 0,
height: 5,
}}
progress={progress}
/>
);
}

View File

@ -87,8 +87,8 @@ android {
applicationId "com.massive"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 36239
versionName "2.24"
versionCode 36240
versionName "2.25"
}
signingConfigs {
release {

View File

@ -11,43 +11,40 @@
android:name="android.permission.ACCESS_NETWORK_STATE"
tools:node="remove" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove"/>
<uses-permission
android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:name=".MainApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".TimerDone"
android:exported="false">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".TimerDone"
android:exported="false">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".StopAlarm"
android:exported="true"
android:process=":remote" />
<service
android:name=".AlarmService"
android:exported="false" />
<activity
android:name=".StopAlarm"
android:exported="true"
android:process=":remote" />
<service
android:name=".TimerService"

View File

@ -1,217 +1,25 @@
package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.CountDownTimer
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.annotation.RequiresApi
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.math.floor
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@RequiresApi(Build.VERSION_CODES.O)
class AlarmModule(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context), LifecycleEventListener {
private var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
private var running = false
private var currentDescription = ""
ReactContextBaseJavaModule(context) {
override fun getName(): String {
return "AlarmModule"
}
private val stopReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received stop broadcast intent")
stop()
}
}
private val addReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
add()
}
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
}
else {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
}
}
override fun onHostDestroy() {
reactApplicationContext.unregisterReceiver(stopReceiver)
reactApplicationContext.unregisterReceiver(addReceiver)
}
@ReactMethod
fun add() {
Log.d("AlarmModule", "Add 1 min to alarm.")
countdownTimer?.cancel()
val newMs = if (running) currentMs.toInt().plus(60000) else 60000
countdownTimer = getTimer(newMs)
countdownTimer?.start()
running = true
val manager = getManager()
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.stopService(intent)
}
@ReactMethod(isBlockingSynchronousMethod = true)
fun getCurrent(): Int {
Log.d("AlarmModule", "currentMs=$currentMs")
if (running) return currentMs.toInt()
return 0
}
@ReactMethod
fun stop() {
Log.d("AlarmModule", "Stop alarm.")
countdownTimer?.cancel()
running = false
val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext?.stopService(intent)
val manager = getManager()
manager.cancel(AlarmService.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)
}
@ReactMethod
fun timer(milliseconds: Int, description: String) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val intent = Intent(reactApplicationContext, TimerService::class.java)
intent.putExtra("milliseconds", milliseconds)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
reactApplicationContext.startForegroundService(intent)
}
else {
reactApplicationContext.startService(intent)
}
}
private fun getTimer(
endMs: Int,
): CountDownTimer {
val builder = getBuilder()
return object : CountDownTimer(endMs.toLong(), 1000) {
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)
}
override fun onFinish() {
val context = reactApplicationContext
val intent = Intent(context, AlarmService::class.java)
context.startService(intent)
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(
"tick",
Arguments.createMap().apply {
putString("minutes", "00")
putString("seconds", "00")
}
)
}
}
}
@SuppressLint("UnspecifiedImmutableFlag")
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 addBroadcast = Intent(ADD_BROADCAST).apply { setPackage(context.packageName) }
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(context.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(currentDescription)
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
private fun getManager(): NotificationManager {
val notificationManager =
reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
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 ADD_BROADCAST = "add-timer-event"
const val CHANNEL_ID_PENDING = "Timer"
const val NOTIFICATION_ID_PENDING = 1
}
override fun onHostResume() {
TODO("Not yet implemented")
}
override fun onHostPause() {
TODO("Not yet implemented")
intent.putExtra("description", description)
reactApplicationContext.startForegroundService(intent)
}
}

View File

@ -1,158 +0,0 @@
package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener
import android.net.Uri
import android.os.*
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
class AlarmService : Service(), OnPreparedListener {
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private fun getBuilder(): NotificationCompat.Builder {
val context = applicationContext
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(AlarmModule.ADD_BROADCAST).apply {
setPackage(context.packageName)
}
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, AlarmModule.CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
.setSound(null)
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
@SuppressLint("Range")
private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate, duration FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
var duration = cursor.getLong(cursor.getColumnIndex("duration"))
if (duration.toInt() == 0) duration = 300
cursor.close()
return Settings(sound, noSound, vibrate, duration)
}
private fun playSound(settings: Settings) {
if (settings.noSound) return
if (settings.sound == null) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, Uri.parse(settings.sound))
prepare()
start()
setOnCompletionListener { vibrator?.cancel() }
}
}
}
private fun doNotify(): Notification {
val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val alarmsChannel = NotificationChannel(
CHANNEL_ID_DONE,
CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH
)
alarmsChannel.description = "Alarms for rest timers."
alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
alarmsChannel.setSound(null, null)
manager.createNotificationChannel(alarmsChannel)
}
val builder = getBuilder()
val context = applicationContext
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 notification = builder.build()
manager.notify(NOTIFICATION_ID_DONE, notification)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
return notification
}
@RequiresApi(Build.VERSION_CODES.O)
@SuppressLint("Recycle")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
doNotify()
val settings = getSettings()
playSound(settings)
if (!settings.vibrate) return START_STICKY
val pattern = longArrayOf(0, settings.duration, 1000, settings.duration, 1000, settings.duration)
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
getSystemService(VIBRATOR_SERVICE) as Vibrator
}
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, -1))
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ vibrator!!.cancel() }, 10000)
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onPrepared(player: MediaPlayer) {
player.start()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
}
companion object {
const val CHANNEL_ID_DONE = "Alarm"
const val NOTIFICATION_ID_DONE = 2
}
}

View File

@ -1,17 +1,11 @@
package com.massive
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.WindowManager
import androidx.annotation.RequiresApi
import com.massive.AlarmService
import com.massive.MainActivity
class StopAlarm : Activity() {
@RequiresApi(Build.VERSION_CODES.O_MR1)
@ -19,7 +13,7 @@ class StopAlarm : Activity() {
Log.d("AlarmActivity", "Call to AlarmActivity")
super.onCreate(savedInstanceState)
val context = applicationContext
context.stopService(Intent(context, AlarmService::class.java))
context.stopService(Intent(context, TimerService::class.java))
savedInstanceState.apply { setShowWhenLocked(true) }
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

View File

@ -1,8 +1,5 @@
package com.massive
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
@ -10,49 +7,24 @@ import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
class TimerDone : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_timer_done)
Log.d("TimerDone", "Rendered.")
}
@RequiresApi(Build.VERSION_CODES.O)
@Suppress("UNUSED_PARAMETER")
fun stop(view: View) {
Log.d("TimerDone", "Stopping...")
applicationContext.stopService(Intent(applicationContext, AlarmService::class.java))
val manager = getManager()
manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
applicationContext.stopService(Intent(applicationContext, TimerService::class.java))
val manager = NotificationManagerCompat.from(this)
manager.cancel(TimerService.ONGOING_ID)
val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext.startActivity(intent)
}
@RequiresApi(Build.VERSION_CODES.O)
fun getManager(): NotificationManager {
val alarmsChannel = NotificationChannel(
AlarmService.CHANNEL_ID_DONE,
AlarmService.CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Alarms for rest timers."
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
val timersChannel = NotificationChannel(
AlarmModule.CHANNEL_ID_PENDING,
AlarmModule.CHANNEL_ID_PENDING,
NotificationManager.IMPORTANCE_LOW
).apply {
setSound(null, null)
description = "Progress on rest timers."
}
val notificationManager = applicationContext.getSystemService(
NotificationManager::class.java
)
notificationManager.createNotificationChannel(alarmsChannel)
notificationManager.createNotificationChannel(timersChannel)
return notificationManager
}
}

View File

@ -3,24 +3,35 @@ package com.massive
import android.Manifest
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.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
import android.os.*
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
@RequiresApi(Build.VERSION_CODES.O)
class TimerService : Service() {
private lateinit var timerHandler: Handler
private var timerRunnable: Runnable? = null
private var timeLeftInSeconds: Int = 0
private var timeTotalInSeconds: Int = 0
private var notificationId = 1
private var secondsLeft: Int = 0
private var secondsTotal: Int = 0
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private var currentDescription = ""
private val stopReceiver =
object : BroadcastReceiver() {
@ -30,34 +41,54 @@ class TimerService : Service() {
}
}
private val addReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
secondsLeft += 60;
secondsTotal += 60;
mediaPlayer?.stop()
vibrator?.cancel()
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate() {
super.onCreate()
timerHandler = Handler(Looper.getMainLooper())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST),
Context.RECEIVER_NOT_EXPORTED)
}
else {
applicationContext.registerReceiver(
stopReceiver, IntentFilter(STOP_BROADCAST),
Context.RECEIVER_NOT_EXPORTED
)
applicationContext.registerReceiver(
addReceiver, IntentFilter(ADD_BROADCAST),
Context.RECEIVER_NOT_EXPORTED
)
} else {
applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
applicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
timeLeftInSeconds = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000
startForeground(notificationId, createNotification(timeLeftInSeconds))
Log.d("TimerService", "onStartCommand seconds=$timeLeftInSeconds")
timeTotalInSeconds = timeLeftInSeconds
secondsLeft = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000
currentDescription = intent?.getStringExtra("description").toString()
secondsTotal = secondsLeft
startForeground(ONGOING_ID, getProgress(secondsLeft).build())
battery()
Log.d("TimerService", "onStartCommand seconds=$secondsLeft")
timerRunnable = object : Runnable {
override fun run() {
if (timeLeftInSeconds > 0) {
timeLeftInSeconds--
updateNotification(timeLeftInSeconds)
if (secondsLeft > 0) {
secondsLeft--
updateNotification(secondsLeft)
timerHandler.postDelayed(this, 1000)
} else {
startAlarmService()
stopSelf()
val settings = getSettings()
vibrate(settings)
playSound(settings)
notifyFinished()
}
}
}
@ -69,41 +100,120 @@ class TimerService : Service() {
super.onDestroy()
timerHandler.removeCallbacks(timerRunnable!!)
applicationContext.unregisterReceiver(stopReceiver)
applicationContext.unregisterReceiver(addReceiver)
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotification(timeLeftInSeconds: Int): Notification {
val notificationTitle = "Timer"
@SuppressLint("BatteryLife")
fun battery() {
val powerManager =
applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
val ignoring =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
powerManager.isIgnoringBatteryOptimizations(
applicationContext.packageName
)
else true
if (ignoring) return
val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:" + applicationContext.packageName)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
try {
applicationContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
applicationContext,
"Requests to ignore battery optimizations are disabled on your device.",
Toast.LENGTH_LONG
).show()
}
}
@SuppressLint("Range")
private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate, duration FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
var duration = cursor.getLong(cursor.getColumnIndex("duration"))
if (duration.toInt() == 0) duration = 300
cursor.close()
return Settings(sound, noSound, vibrate, duration)
}
private fun playSound(settings: Settings) {
if (settings.noSound) return
if (settings.sound == null) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, Uri.parse(settings.sound))
prepare()
start()
setOnCompletionListener { vibrator?.cancel() }
}
}
}
private fun getProgress(timeLeftInSeconds: Int): NotificationCompat.Builder {
val notificationText = formatTime(timeLeftInSeconds)
val notificationChannelId = "timer_channel"
val notificationIntent = Intent(this, TimerService::class.java)
val pendingIntent = PendingIntent.getActivity(
val notificationIntent = Intent(this, MainActivity::class.java)
val contentPending = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(applicationContext.packageName)
val pendingStop =
PendingIntent.getBroadcast(applicationContext, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
val stopPending =
PendingIntent.getBroadcast(
applicationContext,
0,
stopBroadcast,
PendingIntent.FLAG_IMMUTABLE
)
val addBroadcast =
Intent(ADD_BROADCAST).apply { setPackage(applicationContext.packageName) }
val addPending =
PendingIntent.getBroadcast(
applicationContext,
0,
addBroadcast,
PendingIntent.FLAG_MUTABLE
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setContentTitle(notificationTitle)
.setContentTitle(currentDescription)
.setContentText(notificationText)
.setSmallIcon(R.drawable.ic_baseline_timer_24)
.setProgress(timeTotalInSeconds, timeLeftInSeconds, false)
.setContentIntent(pendingIntent)
.setProgress(secondsTotal, timeLeftInSeconds, false)
.setContentIntent(contentPending)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setDeleteIntent(pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.setDeleteIntent(stopPending)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", stopPending)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", addPending)
val notificationManager = NotificationManagerCompat.from(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -115,27 +225,92 @@ class TimerService : Service() {
notificationManager.createNotificationChannel(channel)
}
return notificationBuilder.build()
return notificationBuilder
}
private fun updateNotification(timeLeftInSeconds: Int) {
private fun vibrate(settings: Settings) {
if (!settings.vibrate) return
val pattern =
longArrayOf(0, settings.duration, 1000, settings.duration, 1000, settings.duration)
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
getSystemService(VIBRATOR_SERVICE) as Vibrator
}
vibrator!!.vibrate(VibrationEffect.createWaveform(pattern, -1))
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ vibrator!!.cancel() }, 10000)
}
private fun notifyFinished() {
val channelId = "finished_channel"
val notificationManager = NotificationManagerCompat.from(this)
val notification = createNotification(timeLeftInSeconds)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(channelId, "Timer Finished Channel", NotificationManager.IMPORTANCE_HIGH)
channel.setSound(null, null)
channel.setBypassDnd(true)
channel.enableVibration(false)
channel.description = "Plays an alarm when a rest timer completes."
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
notificationManager.createNotificationChannel(channel)
}
val fullIntent = Intent(applicationContext, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
applicationContext, 0, fullIntent, PendingIntent.FLAG_MUTABLE
)
val finishIntent = Intent(applicationContext, StopAlarm::class.java)
val finishPending = PendingIntent.getActivity(
applicationContext, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(applicationContext.packageName)
val pendingStop =
PendingIntent.getBroadcast(
applicationContext,
0,
stopBroadcast,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(this, channelId)
.setContentTitle("Timer finished")
.setContentText(currentDescription)
.setSmallIcon(R.drawable.ic_baseline_timer_24)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(finishPending)
.setFullScreenIntent(fullPending, true)
.setAutoCancel(true)
.setDeleteIntent(pendingStop)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return
}
notificationManager.notify(notificationId, notification)
notificationManager.notify(FINISHED_ID, builder.build())
}
private fun updateNotification(seconds: Int) {
val notificationManager = NotificationManagerCompat.from(this)
val notification = getProgress(seconds)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(ONGOING_ID, notification.build())
}
private fun formatTime(timeInSeconds: Int): String {
@ -144,12 +319,10 @@ class TimerService : Service() {
return String.format("%02d:%02d", minutes, seconds)
}
private fun startAlarmService() {
val intent = Intent(applicationContext, AlarmService::class.java)
applicationContext.startService(intent)
}
companion object {
const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event"
const val ONGOING_ID = 1
const val FINISHED_ID = 1
}
}

View File

@ -1,6 +1,6 @@
{
"name": "massive",
"version": "2.24",
"version": "2.25",
"private": true,
"license": "GPL-3.0-only",
"scripts": {
@ -72,4 +72,4 @@
"engines": {
"node": ">=16"
}
}
}

View File

@ -5,8 +5,8 @@
"es2019"
],
"jsx": "react-native",
"module": "CommonJS",
"moduleResolution": "node",
"module": "Node16",
"moduleResolution": "Node16",
"types": [
"react-native",
"jest",

View File

@ -1,38 +0,0 @@
import { useFocusEffect } from "@react-navigation/native";
import { useCallback, useState } from "react";
import { NativeModules } from "react-native";
import { emitter } from "./emitter";
import { TickEvent } from "./TimerPage";
export default function useTimer() {
const [minutes, setMinutes] = useState("00");
const [seconds, setSeconds] = useState("00");
const update = () => {
const current: number = NativeModules.AlarmModule.getCurrent();
setMinutes(
Math.floor(current / 1000 / 60)
.toString()
.padStart(2, "0")
);
setSeconds(
Math.floor((current / 1000) % 60)
.toString()
.padStart(2, "0")
);
};
useFocusEffect(
useCallback(() => {
update();
const listener = emitter.addListener("tick", (event: TickEvent) => {
console.log(`${useTimer.name}.tick:`, { event });
setMinutes(event.minutes);
setSeconds(event.seconds);
});
return listener.remove;
}, [])
);
return { minutes, seconds, update };
}