Move logic of TimerService into TimerBroadcast

This was supposed to solve the timer stopping sometimes
when the application was in the background.
It was actually stopping because of battery optimizations.
This commit is contained in:
Brandon Presley 2022-07-06 16:27:36 +12:00
parent 3e09f38ef0
commit e4ed53c358
19 changed files with 297 additions and 124 deletions

View File

@ -3,31 +3,32 @@ import {StyleSheet, Text, View} from 'react-native';
import {Button, Modal, Portal, TextInput} from 'react-native-paper';
import {getDb} from './db';
import Set from './set';
import {format} from 'date-fns';
export default function EditSet({
id,
onSave,
show,
setShow,
onRemove,
clearId,
}: {
id?: number;
clearId: () => void;
onSave: () => void;
show: boolean;
setShow: (visible: boolean) => void;
onRemove: () => void;
}) {
const [name, setName] = useState('');
const [reps, setReps] = useState('');
const [weight, setWeight] = useState('');
const [unit, setUnit] = useState('');
const [created, setCreated] = useState(new Date());
const [created, setCreated] = useState(new Date(new Date().toUTCString()));
const weightRef = useRef<any>(null);
const repsRef = useRef<any>(null);
const unitRef = useRef<any>(null);
useEffect(() => {
if (!id) return;
if (!id) return setCreated(new Date(new Date().toUTCString()));
getDb().then(async db => {
const [result] = await db.executeSql(`SELECT * FROM sets WHERE id = ?`, [
id,
@ -59,21 +60,13 @@ export default function EditSet({
onSave();
};
const remove = async () => {
if (!id) return;
const db = await getDb();
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [id]);
setShow(false);
onRemove();
};
return (
<Portal>
<Modal
visible={show}
contentContainerStyle={styles.modal}
onDismiss={() => setShow(false)}>
<Text style={styles.title}>Add a set</Text>
<Text style={styles.title}>{id ? `Edit "${name}"` : 'Add a set'}</Text>
<TextInput
style={styles.text}
autoFocus
@ -108,12 +101,7 @@ export default function EditSet({
ref={unitRef}
onSubmitEditing={save}
/>
<TextInput
style={styles.text}
label="Created"
disabled
value={created.toLocaleString()}
/>
<Text style={styles.text}>{format(created, 'PPPP p')}</Text>
<View style={styles.bottom}>
<Button mode="contained" icon="save" onPress={save}>
Save
@ -121,13 +109,11 @@ export default function EditSet({
<Button icon="close" onPress={() => setShow(false)}>
Cancel
</Button>
<Button
style={{alignSelf: 'flex-end'}}
icon="trash"
onPress={remove}
disabled={!id}>
Delete
</Button>
{id && (
<Button icon="copy" onPress={clearId}>
Duplicate
</Button>
)}
</View>
</Modal>
</Portal>

View File

@ -11,6 +11,7 @@ export default function Exercises({
}: NativeStackScreenProps<RootStackParamList, 'Exercises'>) {
const [exercises, setExercises] = useState<Exercise[]>([]);
const [search, setSearch] = useState('');
const [refreshing, setRefresing] = useState(false);
const refresh = async () => {
const db = await getDb();
@ -41,7 +42,16 @@ export default function Exercises({
return (
<View style={styles.container}>
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
<FlatList renderItem={renderItem} data={exercises} />
<FlatList
refreshing={refreshing}
onRefresh={async () => {
setRefresing(true);
await refresh();
setRefresing(false);
}}
renderItem={renderItem}
data={exercises}
/>
</View>
);
}

View File

@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
import {
FlatList,
@ -8,7 +9,7 @@ import {
View,
} from 'react-native';
import {AnimatedFAB, Searchbar} from 'react-native-paper';
import {getDb} from './db';
import {getSets} from './db';
import EditSet from './EditSet';
import Set from './set';
@ -22,12 +23,13 @@ export default function Home() {
const [offset, setOffset] = useState(0);
const [showEdit, setShowEdit] = useState(false);
const [search, setSearch] = useState('');
const [refreshing, setRefresing] = useState(false);
const navigation = useNavigation();
const refresh = async () => {
const db = await getDb();
const [result] = await db.executeSql(
`SELECT * from sets WHERE name LIKE ? ORDER BY created DESC LIMIT ? OFFSET ?`,
[`%${search}%`, limit, 0],
setRefresing(true);
const [result] = await getSets({search, limit, offset: 0}).finally(() =>
setRefresing(false),
);
if (!result) return setSets([]);
setSets(result.rows.raw());
@ -38,6 +40,8 @@ export default function Home() {
refresh();
}, [search]);
useEffect(() => navigation.addListener('focus', refresh), [navigation]);
const renderItem = ({item}: {item: Set}) => (
<SetItem
item={item}
@ -59,15 +63,13 @@ export default function Home() {
};
const next = async () => {
setRefresing(true);
const newOffset = offset + limit;
const db = await getDb();
const [result] = await db.executeSql(
`SELECT * from sets WHERE name LIKE ? LIMIT ? OFFSET ?`,
[`%${search}%`, limit, newOffset],
const [result] = await getSets({search, limit, offset: newOffset}).finally(
() => setRefresing(false),
);
if (result.rows.length === 0) return;
if (!sets) return;
if (sets.filter(set => set.id === result.rows.item(0).id)) return;
setSets([...sets, ...result.rows.raw()]);
setOffset(newOffset);
};
@ -76,19 +78,21 @@ export default function Home() {
<SafeAreaView style={styles.container}>
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
<FlatList
style={{height: '100%'}}
style={{height: '90%'}}
data={sets}
renderItem={renderItem}
keyExtractor={set => set.id.toString()}
onScrollEndDrag={next}
onEndReached={next}
refreshing={refreshing}
onRefresh={refresh}
/>
<View style={styles.bottom}>
<EditSet
clearId={() => setId(undefined)}
id={id}
show={showEdit}
setShow={setShowEdit}
onSave={save}
onRemove={refresh}
/>
</View>

View File

@ -2,7 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import {NativeStackScreenProps} from '@react-navigation/native-stack';
import React, {useEffect, useState} from 'react';
import {NativeModules, StyleSheet, Text, View} from 'react-native';
import {Button, Switch, TextInput} from 'react-native-paper';
import {Button, Snackbar, Switch, TextInput} from 'react-native-paper';
import {RootStackParamList} from './App';
import {getDb} from './db';
@ -12,6 +12,7 @@ export default function Settings({
const [minutes, setMinutes] = useState<string>('');
const [seconds, setSeconds] = useState<string>('');
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(true);
const [snackbar, setSnackbar] = useState('');
useEffect(() => {
(async () => {
@ -28,6 +29,8 @@ export default function Settings({
}, [minutes, seconds, alarmEnabled]);
const clear = async () => {
setSnackbar('Deleting all data...');
setTimeout(() => setSnackbar(''), 5000);
const db = await getDb();
await db.executeSql(`DELETE FROM sets`);
};
@ -36,6 +39,10 @@ export default function Settings({
NativeModules.ExportModule.sets();
};
const importSets = () => {
NativeModules.ImportModule.sets();
};
return (
<View style={styles.container}>
<TextInput
@ -64,7 +71,13 @@ export default function Settings({
style={{alignSelf: 'flex-start'}}
icon="arrow-down"
onPress={exportSets}>
Download
Export
</Button>
<Button
style={{alignSelf: 'flex-start'}}
icon="arrow-up"
onPress={importSets}>
Import
</Button>
<Button
style={{alignSelf: 'flex-start', marginTop: 'auto'}}
@ -72,6 +85,13 @@ export default function Settings({
onPress={clear}>
Delete all data
</Button>
<Snackbar
visible={!!snackbar}
onDismiss={() => setSnackbar('')}
action={{label: 'Close', onPress: () => setSnackbar('')}}>
{snackbar}
</Snackbar>
</View>
);
}

View File

@ -30,10 +30,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:exported="true" android:process=":remote" android:name=".AlarmActivity" />
<activity android:exported="true" android:process=":remote" android:name=".StopAlarm" />
<activity android:exported="true" android:process=":remote" android:name=".ExportActivity" />
<activity android:exported="true" android:process=":remote" android:name=".ImportActivity" />
<service android:name=".StopTimer" android:exported="true" android:process=":remote" />
<service android:name=".AlarmService" android:exported="true" />
<service android:name=".TimerService" android:exported="true" />
<receiver android:name=".TimerBroadcast" android:exported="true" android:process=":remote"/>
</application>
</manifest>

View File

@ -17,6 +17,10 @@ class AlarmService : Service(), OnPreparedListener {
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == "stop") {
onDestroy()
return START_STICKY
}
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }

View File

@ -2,21 +2,13 @@ package com.massive
import android.annotation.SuppressLint
import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import java.io.*
@ -24,10 +16,8 @@ class ExportActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ExportActivity", "Started ExportActivity.")
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = "text/csv"
putExtra(Intent.EXTRA_TITLE, "sets.csv")
}
@ -61,7 +51,6 @@ class ExportActivity : Activity() {
}
}
}
}
@Throws(IOException::class)
@ -111,13 +100,6 @@ class ExportActivity : Activity() {
companion object {
private const val CHANNEL_ID = "Exports"
private const val NOTIFICATION_ID = 2
// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2
// Request code for creating a PDF document.
const val CREATE_FILE = 1
}
}

View File

@ -18,6 +18,7 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@ -40,7 +41,7 @@ class ExportModule internal constructor(context: ReactApplicationContext?) :
val file = File(dir, "sets-$formatted.csv")
file.createNewFile()
val writer = FileWriter(file)
writer.write("id,name,reps,weight,created,unit\n")
writer.appendLine("id,name,reps,weight,created,unit")
val db = MassiveHelper(reactApplicationContext).readableDatabase
db.use {
with(it.query("sets", null, null, null, null, null, null)) {
@ -51,7 +52,7 @@ class ExportModule internal constructor(context: ReactApplicationContext?) :
val weight = getInt(getColumnIndex("weight"))
val created = getString(getColumnIndex("created"))
val unit = getString(getColumnIndex("unit"))
writer.appendLine("$id,$name,$reps,$weight,$created,$unit\n")
writer.appendLine("$id,$name,$reps,$weight,$created,$unit")
}
}
}

View File

@ -0,0 +1,59 @@
package com.massive
import android.annotation.SuppressLint
import android.app.*
import android.content.ContentValues
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import java.io.*
class ImportActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ImportActivity", "Started ImportActivity.")
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
startActivityForResult(intent, OPEN_FILE, null)
}
@RequiresApi(Build.VERSION_CODES.M)
@SuppressLint("Range")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d("ImportActivity", "Got activity result: requestCode=$requestCode,resultCode=$resultCode")
val db = MassiveHelper(applicationContext).readableDatabase
data?.data?.also { uri ->
contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
reader.readLine()
var line: String? = reader.readLine()
while (line != null) {
Log.d("ImportActivity", "line: $line")
val split = line.split(",")
if (split.isEmpty()) continue
val set = ContentValues().apply {
put("name", split[1])
put("reps", split[2])
put("weight", split[3])
put("created", split[4])
put("unit", split[5])
}
db.insert("sets", null, set)
line = reader.readLine()
}
}
}
}
val mainIntent = Intent(applicationContext, MainActivity::class.java)
mainIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext.startActivity(mainIntent)
}
companion object {
const val OPEN_FILE = 1
}
}

View File

@ -0,0 +1,20 @@
package com.massive
import android.content.Intent
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
class ImportModule internal constructor(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context) {
override fun getName(): String {
return "ImportModule"
}
@ReactMethod()
fun sets() {
val intent = Intent(reactApplicationContext, ImportActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
reactApplicationContext.startActivity(intent)
}
}

View File

@ -18,6 +18,7 @@ class MassivePackage : ReactPackage {
val modules: MutableList<NativeModule> = ArrayList()
modules.add(AlarmModule(reactContext))
modules.add(ExportModule(reactContext))
modules.add(ImportModule(reactContext))
return modules
}
}

View File

@ -13,7 +13,7 @@ import androidx.annotation.RequiresApi
import com.massive.AlarmService
import com.massive.MainActivity
class AlarmActivity : Activity() {
class StopAlarm : Activity() {
@RequiresApi(Build.VERSION_CODES.O_MR1)
override fun onCreate(savedInstanceState: Bundle?) {
Log.d("AlarmActivity", "Call to AlarmActivity")

View File

@ -3,6 +3,7 @@ package com.massive
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
class StopTimer : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

View File

@ -0,0 +1,90 @@
package com.massive
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import kotlin.math.floor
class TimerBroadcast : BroadcastReceiver() {
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onReceive(context: Context, intent: Intent) {
val notificationManager = getManager(context)
val builder = getBuilder(context)
if (intent.action == "tick") {
val endMs = intent.extras!!.getInt("endMs")
val currentMs = intent.extras!!.getLong("currentMs")
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(NOTIFICATION_ID, builder.build())
}
else if (intent.action === "finish") {
Log.d("TimerBroadcast", "Finishing...")
val finishIntent = Intent(context, StopAlarm::class.java)
val finishPending =
PendingIntent.getActivity(context, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE)
builder.setContentText("Timer finished.")
.clearActions()
.setAutoCancel(true)
.setOngoing(false)
.setContentIntent(finishPending)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.priority = NotificationCompat.PRIORITY_HIGH
notificationManager.notify(NOTIFICATION_ID, builder.build())
context.startService(Intent(context, AlarmService::class.java))
}
else if (intent.action === "stop") {
notificationManager.cancel(NOTIFICATION_ID)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getBuilder(context: Context): NotificationCompat.Builder {
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val actionIntent = Intent(context, StopTimer::class.java)
val pendingAction =
PendingIntent.getService(context, 0, actionIntent, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24)
.setContentTitle("Resting")
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "STOP", pendingAction)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getManager(context: Context): NotificationManager {
val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_ID, importance
)
channel.description = "Alarms for rest timings."
val notificationManager = context.getSystemService(
NotificationManager::class.java
)
notificationManager.createNotificationChannel(channel)
return notificationManager
}
companion object {
private const val CHANNEL_ID = "MassiveTimer"
private const val NOTIFICATION_ID = 1
}
}

View File

@ -1,8 +1,5 @@
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
@ -10,68 +7,30 @@ 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
private var countdownTimer: CountDownTimer? = null
@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?.cancel()
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())
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
broadcastIntent.putExtra("endMs", endMs)
broadcastIntent.putExtra("currentMs", currentMs)
broadcastIntent.action = "tick"
sendBroadcast(broadcastIntent)
}
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))
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
broadcastIntent.action = "finish"
sendBroadcast(broadcastIntent)
}
}
countdownTimer.start()
countdownTimer!!.start()
return super.onStartCommand(intent, flags, startId)
}
@ -80,13 +39,11 @@ class TimerService : Service() {
}
override fun onDestroy() {
countdownTimer.cancel()
notificationManager.cancel(ALARM_ID)
Log.d("TimerService", "Destroying...")
countdownTimer?.cancel()
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
broadcastIntent.action = "stop"
sendBroadcast(broadcastIntent)
super.onDestroy()
}
companion object {
private const val CHANNEL_ID = "Alarms"
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="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
</vector>

17
db.ts
View File

@ -15,3 +15,20 @@ const schema = `
`;
export const setupSchema = () => getDb().then(db => db.executeSql(schema));
const select = `
SELECT * from sets
WHERE name LIKE ?
ORDER BY created DESC
LIMIT ? OFFSET ?
`;
export const getSets = ({
search,
limit,
offset,
}: {
search: string;
limit: number;
offset: number;
}) => getDb().then(db => db.executeSql(select, [`%${search}%`, limit, offset]));

View File

@ -17,9 +17,11 @@
"@react-navigation/native-stack": "^6.6.2",
"@types/react-native-sqlite-storage": "^5.0.2",
"@types/react-native-vector-icons": "^6.4.11",
"date-fns": "^2.28.0",
"react": "18.0.0",
"react-devtools": "^4.24.7",
"react-native": "0.69.1",
"react-native-document-picker": "^8.1.1",
"react-native-gesture-handler": "^2.5.0",
"react-native-pager-view": "^5.4.24",
"react-native-paper": "^4.12.2",

View File

@ -3035,6 +3035,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.28.0:
version "2.28.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
dayjs@^1.8.15:
version "1.11.3"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
@ -6632,6 +6637,13 @@ react-native-codegen@^0.69.1:
jscodeshift "^0.13.1"
nullthrows "^1.1.1"
react-native-document-picker@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-8.1.1.tgz#642bbe25752cc428b96416318f8dc07cef29ee10"
integrity sha512-mH0oghd7ndgU9/1meVJdqts1sAkOfUQW1qbrqTTsvR5f2K9r0BAj/X02dve5IBMOMZvlGd7qWrNVuIFg5AUXWg==
dependencies:
invariant "^2.2.4"
react-native-gesture-handler@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.5.0.tgz#61385583570ed0a45a9ed142425e35f8fe8274fb"