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
36
EditSet.tsx
36
EditSet.tsx
|
@ -3,31 +3,32 @@ import {StyleSheet, Text, View} from 'react-native';
|
||||||
import {Button, Modal, Portal, TextInput} from 'react-native-paper';
|
import {Button, Modal, Portal, TextInput} from 'react-native-paper';
|
||||||
import {getDb} from './db';
|
import {getDb} from './db';
|
||||||
import Set from './set';
|
import Set from './set';
|
||||||
|
import {format} from 'date-fns';
|
||||||
|
|
||||||
export default function EditSet({
|
export default function EditSet({
|
||||||
id,
|
id,
|
||||||
onSave,
|
onSave,
|
||||||
show,
|
show,
|
||||||
setShow,
|
setShow,
|
||||||
onRemove,
|
clearId,
|
||||||
}: {
|
}: {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
clearId: () => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
setShow: (visible: boolean) => void;
|
setShow: (visible: boolean) => void;
|
||||||
onRemove: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [reps, setReps] = useState('');
|
const [reps, setReps] = useState('');
|
||||||
const [weight, setWeight] = useState('');
|
const [weight, setWeight] = useState('');
|
||||||
const [unit, setUnit] = 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 weightRef = useRef<any>(null);
|
||||||
const repsRef = useRef<any>(null);
|
const repsRef = useRef<any>(null);
|
||||||
const unitRef = useRef<any>(null);
|
const unitRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return setCreated(new Date(new Date().toUTCString()));
|
||||||
getDb().then(async db => {
|
getDb().then(async db => {
|
||||||
const [result] = await db.executeSql(`SELECT * FROM sets WHERE id = ?`, [
|
const [result] = await db.executeSql(`SELECT * FROM sets WHERE id = ?`, [
|
||||||
id,
|
id,
|
||||||
|
@ -59,21 +60,13 @@ export default function EditSet({
|
||||||
onSave();
|
onSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = async () => {
|
|
||||||
if (!id) return;
|
|
||||||
const db = await getDb();
|
|
||||||
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [id]);
|
|
||||||
setShow(false);
|
|
||||||
onRemove();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Modal
|
<Modal
|
||||||
visible={show}
|
visible={show}
|
||||||
contentContainerStyle={styles.modal}
|
contentContainerStyle={styles.modal}
|
||||||
onDismiss={() => setShow(false)}>
|
onDismiss={() => setShow(false)}>
|
||||||
<Text style={styles.title}>Add a set</Text>
|
<Text style={styles.title}>{id ? `Edit "${name}"` : 'Add a set'}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.text}
|
style={styles.text}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
@ -108,12 +101,7 @@ export default function EditSet({
|
||||||
ref={unitRef}
|
ref={unitRef}
|
||||||
onSubmitEditing={save}
|
onSubmitEditing={save}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<Text style={styles.text}>{format(created, 'PPPP p')}</Text>
|
||||||
style={styles.text}
|
|
||||||
label="Created"
|
|
||||||
disabled
|
|
||||||
value={created.toLocaleString()}
|
|
||||||
/>
|
|
||||||
<View style={styles.bottom}>
|
<View style={styles.bottom}>
|
||||||
<Button mode="contained" icon="save" onPress={save}>
|
<Button mode="contained" icon="save" onPress={save}>
|
||||||
Save
|
Save
|
||||||
|
@ -121,13 +109,11 @@ export default function EditSet({
|
||||||
<Button icon="close" onPress={() => setShow(false)}>
|
<Button icon="close" onPress={() => setShow(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{id && (
|
||||||
style={{alignSelf: 'flex-end'}}
|
<Button icon="copy" onPress={clearId}>
|
||||||
icon="trash"
|
Duplicate
|
||||||
onPress={remove}
|
|
||||||
disabled={!id}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
@ -11,6 +11,7 @@ export default function Exercises({
|
||||||
}: NativeStackScreenProps<RootStackParamList, 'Exercises'>) {
|
}: NativeStackScreenProps<RootStackParamList, 'Exercises'>) {
|
||||||
const [exercises, setExercises] = useState<Exercise[]>([]);
|
const [exercises, setExercises] = useState<Exercise[]>([]);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [refreshing, setRefresing] = useState(false);
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
@ -41,7 +42,16 @@ export default function Exercises({
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
30
Home.tsx
30
Home.tsx
|
@ -1,4 +1,5 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import {useNavigation} from '@react-navigation/native';
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {
|
import {
|
||||||
FlatList,
|
FlatList,
|
||||||
|
@ -8,7 +9,7 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import {AnimatedFAB, Searchbar} from 'react-native-paper';
|
import {AnimatedFAB, Searchbar} from 'react-native-paper';
|
||||||
import {getDb} from './db';
|
import {getSets} from './db';
|
||||||
import EditSet from './EditSet';
|
import EditSet from './EditSet';
|
||||||
|
|
||||||
import Set from './set';
|
import Set from './set';
|
||||||
|
@ -22,12 +23,13 @@ export default function Home() {
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [refreshing, setRefresing] = useState(false);
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const db = await getDb();
|
setRefresing(true);
|
||||||
const [result] = await db.executeSql(
|
const [result] = await getSets({search, limit, offset: 0}).finally(() =>
|
||||||
`SELECT * from sets WHERE name LIKE ? ORDER BY created DESC LIMIT ? OFFSET ?`,
|
setRefresing(false),
|
||||||
[`%${search}%`, limit, 0],
|
|
||||||
);
|
);
|
||||||
if (!result) return setSets([]);
|
if (!result) return setSets([]);
|
||||||
setSets(result.rows.raw());
|
setSets(result.rows.raw());
|
||||||
|
@ -38,6 +40,8 @@ export default function Home() {
|
||||||
refresh();
|
refresh();
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
|
useEffect(() => navigation.addListener('focus', refresh), [navigation]);
|
||||||
|
|
||||||
const renderItem = ({item}: {item: Set}) => (
|
const renderItem = ({item}: {item: Set}) => (
|
||||||
<SetItem
|
<SetItem
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -59,15 +63,13 @@ export default function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
|
setRefresing(true);
|
||||||
const newOffset = offset + limit;
|
const newOffset = offset + limit;
|
||||||
const db = await getDb();
|
const [result] = await getSets({search, limit, offset: newOffset}).finally(
|
||||||
const [result] = await db.executeSql(
|
() => setRefresing(false),
|
||||||
`SELECT * from sets WHERE name LIKE ? LIMIT ? OFFSET ?`,
|
|
||||||
[`%${search}%`, limit, newOffset],
|
|
||||||
);
|
);
|
||||||
if (result.rows.length === 0) return;
|
if (result.rows.length === 0) return;
|
||||||
if (!sets) return;
|
if (!sets) return;
|
||||||
if (sets.filter(set => set.id === result.rows.item(0).id)) return;
|
|
||||||
setSets([...sets, ...result.rows.raw()]);
|
setSets([...sets, ...result.rows.raw()]);
|
||||||
setOffset(newOffset);
|
setOffset(newOffset);
|
||||||
};
|
};
|
||||||
|
@ -76,19 +78,21 @@ export default function Home() {
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
||||||
<FlatList
|
<FlatList
|
||||||
style={{height: '100%'}}
|
style={{height: '90%'}}
|
||||||
data={sets}
|
data={sets}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={set => set.id.toString()}
|
keyExtractor={set => set.id.toString()}
|
||||||
onScrollEndDrag={next}
|
onEndReached={next}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={refresh}
|
||||||
/>
|
/>
|
||||||
<View style={styles.bottom}>
|
<View style={styles.bottom}>
|
||||||
<EditSet
|
<EditSet
|
||||||
|
clearId={() => setId(undefined)}
|
||||||
id={id}
|
id={id}
|
||||||
show={showEdit}
|
show={showEdit}
|
||||||
setShow={setShowEdit}
|
setShow={setShowEdit}
|
||||||
onSave={save}
|
onSave={save}
|
||||||
onRemove={refresh}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</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 {NativeStackScreenProps} from '@react-navigation/native-stack';
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {NativeModules, StyleSheet, Text, View} from 'react-native';
|
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 {RootStackParamList} from './App';
|
||||||
import {getDb} from './db';
|
import {getDb} from './db';
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export default function Settings({
|
||||||
const [minutes, setMinutes] = useState<string>('');
|
const [minutes, setMinutes] = useState<string>('');
|
||||||
const [seconds, setSeconds] = useState<string>('');
|
const [seconds, setSeconds] = useState<string>('');
|
||||||
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(true);
|
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(true);
|
||||||
|
const [snackbar, setSnackbar] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -28,6 +29,8 @@ export default function Settings({
|
||||||
}, [minutes, seconds, alarmEnabled]);
|
}, [minutes, seconds, alarmEnabled]);
|
||||||
|
|
||||||
const clear = async () => {
|
const clear = async () => {
|
||||||
|
setSnackbar('Deleting all data...');
|
||||||
|
setTimeout(() => setSnackbar(''), 5000);
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
await db.executeSql(`DELETE FROM sets`);
|
await db.executeSql(`DELETE FROM sets`);
|
||||||
};
|
};
|
||||||
|
@ -36,6 +39,10 @@ export default function Settings({
|
||||||
NativeModules.ExportModule.sets();
|
NativeModules.ExportModule.sets();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importSets = () => {
|
||||||
|
NativeModules.ImportModule.sets();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -64,7 +71,13 @@ export default function Settings({
|
||||||
style={{alignSelf: 'flex-start'}}
|
style={{alignSelf: 'flex-start'}}
|
||||||
icon="arrow-down"
|
icon="arrow-down"
|
||||||
onPress={exportSets}>
|
onPress={exportSets}>
|
||||||
Download
|
Export
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{alignSelf: 'flex-start'}}
|
||||||
|
icon="arrow-up"
|
||||||
|
onPress={importSets}>
|
||||||
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
style={{alignSelf: 'flex-start', marginTop: 'auto'}}
|
style={{alignSelf: 'flex-start', marginTop: 'auto'}}
|
||||||
|
@ -72,6 +85,13 @@ export default function Settings({
|
||||||
onPress={clear}>
|
onPress={clear}>
|
||||||
Delete all data
|
Delete all data
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
visible={!!snackbar}
|
||||||
|
onDismiss={() => setSnackbar('')}
|
||||||
|
action={{label: 'Close', onPress: () => setSnackbar('')}}>
|
||||||
|
{snackbar}
|
||||||
|
</Snackbar>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,12 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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=".ExportActivity" />
|
||||||
|
<activity android:exported="true" android:process=":remote" android:name=".ImportActivity" />
|
||||||
<service android:name=".StopTimer" android:exported="true" android:process=":remote" />
|
<service android:name=".StopTimer" android:exported="true" android:process=":remote" />
|
||||||
<service android:name=".AlarmService" android:exported="true" />
|
<service android:name=".AlarmService" android:exported="true" />
|
||||||
<service android:name=".TimerService" android:exported="true" />
|
<service android:name=".TimerService" android:exported="true" />
|
||||||
|
<receiver android:name=".TimerBroadcast" android:exported="true" android:process=":remote"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -17,6 +17,10 @@ class AlarmService : Service(), OnPreparedListener {
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
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 = MediaPlayer.create(applicationContext, R.raw.argon)
|
||||||
mediaPlayer?.start()
|
mediaPlayer?.start()
|
||||||
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
|
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
|
||||||
|
|
|
@ -2,21 +2,13 @@ package com.massive
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,10 +16,8 @@ class ExportActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d("ExportActivity", "Started ExportActivity.")
|
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)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
type = "text/csv"
|
type = "text/csv"
|
||||||
putExtra(Intent.EXTRA_TITLE, "sets.csv")
|
putExtra(Intent.EXTRA_TITLE, "sets.csv")
|
||||||
}
|
}
|
||||||
|
@ -61,7 +51,6 @@ class ExportActivity : Activity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@ -111,13 +100,6 @@ class ExportActivity : Activity() {
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
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
|
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.ReactContextBaseJavaModule
|
||||||
import com.facebook.react.bridge.ReactMethod
|
import com.facebook.react.bridge.ReactMethod
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileReader
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
@ -40,7 +41,7 @@ class ExportModule internal constructor(context: ReactApplicationContext?) :
|
||||||
val file = File(dir, "sets-$formatted.csv")
|
val file = File(dir, "sets-$formatted.csv")
|
||||||
file.createNewFile()
|
file.createNewFile()
|
||||||
val writer = FileWriter(file)
|
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
|
val db = MassiveHelper(reactApplicationContext).readableDatabase
|
||||||
db.use {
|
db.use {
|
||||||
with(it.query("sets", null, null, null, null, null, null)) {
|
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 weight = getInt(getColumnIndex("weight"))
|
||||||
val created = getString(getColumnIndex("created"))
|
val created = getString(getColumnIndex("created"))
|
||||||
val unit = getString(getColumnIndex("unit"))
|
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()
|
val modules: MutableList<NativeModule> = ArrayList()
|
||||||
modules.add(AlarmModule(reactContext))
|
modules.add(AlarmModule(reactContext))
|
||||||
modules.add(ExportModule(reactContext))
|
modules.add(ExportModule(reactContext))
|
||||||
|
modules.add(ImportModule(reactContext))
|
||||||
return modules
|
return modules
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@ import androidx.annotation.RequiresApi
|
||||||
import com.massive.AlarmService
|
import com.massive.AlarmService
|
||||||
import com.massive.MainActivity
|
import com.massive.MainActivity
|
||||||
|
|
||||||
class AlarmActivity : Activity() {
|
class StopAlarm : Activity() {
|
||||||
@RequiresApi(Build.VERSION_CODES.O_MR1)
|
@RequiresApi(Build.VERSION_CODES.O_MR1)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Log.d("AlarmActivity", "Call to AlarmActivity")
|
Log.d("AlarmActivity", "Call to AlarmActivity")
|
|
@ -3,6 +3,7 @@ package com.massive
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
class StopTimer : Service() {
|
class StopTimer : Service() {
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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
|
package com.massive
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -10,68 +7,30 @@ import android.os.CountDownTimer
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import kotlin.math.floor
|
|
||||||
|
|
||||||
class TimerService : Service() {
|
class TimerService : Service() {
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private var countdownTimer: CountDownTimer? = null
|
||||||
private lateinit var countdownTimer: CountDownTimer
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.d("TimerService", "Started timer service.")
|
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")
|
val endMs = intent!!.extras!!.getInt("milliseconds")
|
||||||
notificationManager = NotificationManagerCompat.from(applicationContext)
|
countdownTimer?.cancel()
|
||||||
countdownTimer = object : CountDownTimer(endMs.toLong(), 1000) {
|
countdownTimer = object : CountDownTimer(endMs.toLong(), 1000) {
|
||||||
override fun onTick(currentMs: Long) {
|
override fun onTick(currentMs: Long) {
|
||||||
val seconds = floor((currentMs / 1000).toDouble() % 60)
|
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
|
||||||
.toInt().toString().padStart(2, '0')
|
broadcastIntent.putExtra("endMs", endMs)
|
||||||
val minutes = floor((currentMs / 1000).toDouble() / 60)
|
broadcastIntent.putExtra("currentMs", currentMs)
|
||||||
.toInt().toString().padStart(2, '0')
|
broadcastIntent.action = "tick"
|
||||||
builder.setContentText("$minutes:$seconds")
|
sendBroadcast(broadcastIntent)
|
||||||
.setAutoCancel(false)
|
|
||||||
.setDefaults(0)
|
|
||||||
.setProgress(endMs, currentMs.toInt(), false)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
|
||||||
.priority = NotificationCompat.PRIORITY_LOW
|
|
||||||
notificationManager.notify(ALARM_ID, builder.build())
|
|
||||||
}
|
}
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
builder.setContentText("Timer finished.")
|
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
|
||||||
.clearActions()
|
broadcastIntent.action = "finish"
|
||||||
.setAutoCancel(true)
|
sendBroadcast(broadcastIntent)
|
||||||
.setOngoing(false)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
|
||||||
.priority = NotificationCompat.PRIORITY_HIGH
|
|
||||||
notificationManager.notify(ALARM_ID, builder.build())
|
|
||||||
applicationContext.startService(Intent(applicationContext, AlarmService::class.java))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
countdownTimer!!.start()
|
||||||
countdownTimer.start()
|
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,13 +39,11 @@ class TimerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
countdownTimer.cancel()
|
Log.d("TimerService", "Destroying...")
|
||||||
notificationManager.cancel(ALARM_ID)
|
countdownTimer?.cancel()
|
||||||
|
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
|
||||||
|
broadcastIntent.action = "stop"
|
||||||
|
sendBroadcast(broadcastIntent)
|
||||||
super.onDestroy()
|
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));
|
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",
|
"@react-navigation/native-stack": "^6.6.2",
|
||||||
"@types/react-native-sqlite-storage": "^5.0.2",
|
"@types/react-native-sqlite-storage": "^5.0.2",
|
||||||
"@types/react-native-vector-icons": "^6.4.11",
|
"@types/react-native-vector-icons": "^6.4.11",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
"react-devtools": "^4.24.7",
|
"react-devtools": "^4.24.7",
|
||||||
"react-native": "0.69.1",
|
"react-native": "0.69.1",
|
||||||
|
"react-native-document-picker": "^8.1.1",
|
||||||
"react-native-gesture-handler": "^2.5.0",
|
"react-native-gesture-handler": "^2.5.0",
|
||||||
"react-native-pager-view": "^5.4.24",
|
"react-native-pager-view": "^5.4.24",
|
||||||
"react-native-paper": "^4.12.2",
|
"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-mimetype "^2.3.0"
|
||||||
whatwg-url "^8.0.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:
|
dayjs@^1.8.15:
|
||||||
version "1.11.3"
|
version "1.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
|
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"
|
jscodeshift "^0.13.1"
|
||||||
nullthrows "^1.1.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:
|
react-native-gesture-handler@^2.5.0:
|
||||||
version "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"
|
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