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:
parent
3e09f38ef0
commit
e4ed53c358
38
EditSet.tsx
38
EditSet.tsx
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
30
Home.tsx
30
Home.tsx
|
@ -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>
|
||||
|
||||
|
|
24
Settings.tsx
24
Settings.tsx
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
59
android/app/src/main/java/com/massive/ImportActivity.kt
Normal file
59
android/app/src/main/java/com/massive/ImportActivity.kt
Normal 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
|
||||
}
|
||||
}
|
20
android/app/src/main/java/com/massive/ImportModule.kt
Normal file
20
android/app/src/main/java/com/massive/ImportModule.kt
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
|
@ -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 {
|
||||
|
|
90
android/app/src/main/java/com/massive/TimerBroadcast.kt
Normal file
90
android/app/src/main/java/com/massive/TimerBroadcast.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
17
db.ts
|
@ -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]));
|
||||
|
|
|
@ -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",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user