Compare commits

...

12 Commits

Author SHA1 Message Date
Brandon Presley d513b147bc Add ability to add 1 minute to timer 2022-07-19 17:03:43 +12:00
Brandon Presley 9e790e7e1d Fix error when adding set at the end of a plan 2022-07-19 17:02:50 +12:00
Brandon Presley 839d872c1c Shorten method names in AlarmModule 2022-07-19 16:38:58 +12:00
Brandon Presley 65f6eaff57 Remove TimerBroadcast
This was added briefly because the timer was
pausing when the app was in the background (sometimes).
I read somewhere using a BroadcastReceiver prevents the timer
from being slept by Android, which turned out to be false (on a Pixel
    4).
The actual solution was disabling battery optimizations, so this
broadcast receiver is now redundant.
2022-07-19 16:34:49 +12:00
Brandon Presley 0159956ee8 Merge branch 'no-broadcast' 2022-07-19 16:27:24 +12:00
Brandon Presley c0b6ba8606 Move uploading/downloading/deleting into the top bar 2022-07-19 16:24:16 +12:00
Brandon Presley 42c2502e12 Upgrade yarn version 2022-07-19 16:24:05 +12:00
Brandon Presley f2e5192002 Organize SettingsPage imports 2022-07-19 14:26:36 +12:00
Brandon Presley 0cb8d7b962 Stop alarm when pressing stop on notification 2022-07-19 14:26:11 +12:00
Brandon Presley c965064e57 Use best as default for EditSet 2022-07-19 14:26:00 +12:00
Brandon Presley d591d2e453 Remove redundant code from db.ts 2022-07-18 14:10:32 +12:00
Brandon Presley c387ee7e61 Run timer in TimerService instead of Broadcast 2022-07-16 16:12:28 +12:00
16 changed files with 12453 additions and 8892 deletions

1
.gitignore vendored
View File

@ -64,3 +64,4 @@ buck-out/
deploy.sh
README.pdf
.yarn

3
.yarnrc.yml Normal file
View File

@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.1.cjs

143
DrawerMenu.tsx Normal file
View File

@ -0,0 +1,143 @@
import {NavigationProp, useNavigation} from '@react-navigation/native';
import React, {useCallback, useContext, useState} from 'react';
import {ToastAndroid} from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import {FileSystem} from 'react-native-file-access';
import {Divider, IconButton, Menu} from 'react-native-paper';
import {DatabaseContext, DrawerParamList} from './App';
import ConfirmDialog from './ConfirmDialog';
import {write} from './file';
import {Plan} from './plan';
import Set from './set';
const setFields = 'id,name,reps,weight,created,unit';
const planFields = 'id,days,workouts';
export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
const [showMenu, setShowMenu] = useState(false);
const [showRemove, setShowRemove] = useState(false);
const db = useContext(DatabaseContext);
const {reset} = useNavigation<NavigationProp<DrawerParamList>>();
const exportSets = useCallback(async () => {
const [result] = await db.executeSql('SELECT * FROM sets');
if (result.rows.length === 0) return;
const sets: Set[] = result.rows.raw();
const data = [setFields]
.concat(
sets.map(
set =>
`${set.id},${set.name},${set.reps},${set.weight},${set.created},${set.unit}`,
),
)
.join('\n');
console.log(`${DrawerMenu.name}.exportSets`, {length: sets.length});
await write('sets.csv', data);
}, [db]);
const exportPlans = useCallback(async () => {
const [result] = await db.executeSql('SELECT * FROM plans');
if (result.rows.length === 0) return;
const sets: Plan[] = result.rows.raw();
const data = [planFields]
.concat(sets.map(set => `"${set.id}","${set.days}","${set.workouts}"`))
.join('\n');
console.log(`${DrawerMenu.name}.exportPlans`, {length: sets.length});
await write('plans.csv', data);
}, [db]);
const download = useCallback(async () => {
setShowMenu(false);
if (name === 'Home') exportSets();
else if (name === 'Plans') exportPlans();
}, [name, exportSets, exportPlans]);
const uploadSets = useCallback(async () => {
const result = await DocumentPicker.pickSingle();
const file = await FileSystem.readFile(result.uri);
console.log(`${DrawerMenu.name}.${uploadSets.name}:`, file.length);
const lines = file.split('\n');
if (lines[0] != setFields)
return ToastAndroid.show('Invalid csv.', ToastAndroid.SHORT);
const values = lines
.slice(1)
.filter(line => line)
.map(set => {
const cells = set.split(',');
return `('${cells[1]}',${cells[2]},${cells[3]},'${cells[4]}','${cells[5]}')`;
})
.join(',');
await db.executeSql(
`INSERT INTO sets(name,reps,weight,created,unit) VALUES ${values}`,
);
ToastAndroid.show('Data imported.', ToastAndroid.SHORT);
reset({index: 0, routes: [{name}]});
}, [db, reset, name]);
const uploadPlans = useCallback(async () => {
const result = await DocumentPicker.pickSingle();
const file = await FileSystem.readFile(result.uri);
console.log(`${DrawerMenu.name}.uploadPlans:`, file.length);
const lines = file.split('\n');
if (lines[0] != planFields)
return ToastAndroid.show('Invalid csv.', ToastAndroid.SHORT);
const values = file
.split('\n')
.slice(1)
.filter(line => line)
.map(set => {
const cells = set.split('","').map(cell => cell.replace(/"/g, ''));
return `('${cells[1]}','${cells[2]}')`;
})
.join(',');
await db.executeSql(`INSERT INTO plans(days,workouts) VALUES ${values}`);
ToastAndroid.show('Data imported.', ToastAndroid.SHORT);
}, [db]);
const upload = useCallback(async () => {
setShowMenu(false);
if (name === 'Home') await uploadSets();
else if (name === 'Plans') await uploadPlans();
reset({index: 0, routes: [{name}]});
}, [name, uploadPlans, uploadSets, reset]);
const remove = useCallback(async () => {
setShowMenu(false);
setShowRemove(false);
if (name === 'Home') await db.executeSql(`DELETE FROM sets`);
else if (name === 'Plans') await db.executeSql(`DELETE FROM plans`);
ToastAndroid.show('All data has been deleted.', ToastAndroid.SHORT);
reset({index: 0, routes: [{name}]});
}, [db, reset, name]);
if (name === 'Home' || name === 'Plans')
return (
<Menu
visible={showMenu}
onDismiss={() => setShowMenu(false)}
anchor={
<IconButton
onPress={() => setShowMenu(true)}
icon="ellipsis-vertical"
/>
}>
<Menu.Item icon="arrow-down" onPress={download} title="Download" />
<Menu.Item icon="arrow-up" onPress={upload} title="Upload" />
<Divider />
<Menu.Item
icon="trash"
onPress={() => setShowRemove(true)}
title="Delete"
/>
<ConfirmDialog
title="Delete all data"
show={showRemove}
setShow={setShowRemove}
onOk={remove}>
This irreversibly deletes all data from the app. Are you sure?
</ConfirmDialog>
</Menu>
);
return null;
}

View File

@ -55,28 +55,62 @@ export default function EditSet() {
return result.rows.raw();
}, [db]);
const getBest = useCallback(
async (query: string): Promise<Set> => {
const bestWeight = `
SELECT name, reps, unit, MAX(weight) AS weight
FROM sets
WHERE name = ?
GROUP BY name;
`;
const bestReps = `
SELECT name, MAX(reps) as reps, unit, weight
FROM sets
WHERE name = ?
AND weight = ?
GROUP BY name;
`;
const [weightResult] = await db.executeSql(bestWeight, [query]);
if (!weightResult.rows.length)
return {
weight: 0,
name: '',
reps: 0,
created: new Date().toISOString(),
id: 0,
};
const [repsResult] = await db.executeSql(bestReps, [
query,
weightResult.rows.item(0).weight,
]);
return repsResult.rows.item(0);
},
[db],
);
const predict = useCallback(async () => {
if ((await AsyncStorage.getItem('predictiveSets')) === 'false') return;
const todaysPlan = await getTodaysPlan();
if (todaysPlan.length === 0) return;
const todaysSets = await getTodaysSets();
const todaysWorkouts = todaysPlan[0].workouts.split(',');
if (todaysSets.length === 0) return setName(todaysWorkouts[0]);
const count = todaysSets.filter(
set => set.name === todaysSets[0].name,
).length;
const maxSets = await AsyncStorage.getItem('maxSets');
if (count < Number(maxSets)) {
setName(todaysSets[0].name);
setReps(todaysSets[0].reps.toString());
setWeight(todaysSets[0].weight.toString());
return setUnit(todaysSets[0].unit);
let nextWorkout = todaysWorkouts[0];
if (todaysSets.length > 0) {
const count = todaysSets.filter(
set => set.name === todaysSets[0].name,
).length;
const maxSets = await AsyncStorage.getItem('maxSets');
nextWorkout = todaysSets[0].name;
if (count >= Number(maxSets))
nextWorkout =
todaysWorkouts[todaysWorkouts.indexOf(todaysSets[0].name!) + 1];
}
const nextWorkout =
todaysWorkouts[todaysWorkouts.indexOf(todaysSets[0].name!) + 1];
if (!nextWorkout) return;
setName(nextWorkout);
}, [getTodaysSets, getTodaysPlan]);
const best = await getBest(nextWorkout);
setName(best.name);
setReps(best.reps.toString());
setWeight(best.weight.toString());
setUnit(best.unit);
}, [getTodaysSets, getTodaysPlan, getBest]);
useEffect(() => {
if (params.set.id) return;

View File

@ -25,6 +25,7 @@ export default function HomePage() {
<Stack.Screen name="Sets" component={SetList} />
<Stack.Screen
name="EditSet"
options={{headerRight: () => <IconButton icon="clock" />}}
component={EditSet}
listeners={{
beforeRemove: () => {

View File

@ -4,6 +4,7 @@ import {IconButton} from 'react-native-paper';
import {SQLiteDatabase} from 'react-native-sqlite-storage';
import {DatabaseContext, Drawer, DrawerParamList} from './App';
import BestPage from './BestPage';
import DrawerMenu from './DrawerMenu';
import HomePage from './HomePage';
import PlanPage from './PlanPage';
import SettingsPage from './SettingsPage';
@ -36,6 +37,7 @@ export default function Routes({db}: {db: SQLiteDatabase | null}) {
name={route.name}
component={route.component}
options={{
headerRight: () => <DrawerMenu name={route.name} />,
drawerIcon: ({focused}) => (
<IconButton
icon={focused ? route.icon : `${route.icon}-outline`}

View File

@ -1,17 +1,13 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {
NativeModules,
PermissionsAndroid,
StyleSheet,
Text,
ToastAndroid,
View,
} from 'react-native';
import {Dirs, FileSystem} from 'react-native-file-access';
import {Button, Snackbar, Switch, TextInput} from 'react-native-paper';
import {DatabaseContext} from './App';
import Set from './set';
import DocumentPicker from 'react-native-document-picker';
import {TextInput} from 'react-native-paper';
import ConfirmDialog from './ConfirmDialog';
import MassiveSwitch from './MassiveSwitch';
@ -23,12 +19,8 @@ export default function SettingsPage() {
const [seconds, setSeconds] = useState<string>('');
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(false);
const [predictiveSets, setPredictiveSets] = useState<boolean>(false);
const [snackbar, setSnackbar] = useState('');
const [showBattery, setShowBattery] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [ignoring, setIgnoring] = useState(false);
const [timeoutId, setTimeoutId] = useState(0);
const db = useContext(DatabaseContext);
const refresh = useCallback(async () => {
setMinutes((await getItem('minutes')) || '');
@ -36,77 +28,13 @@ export default function SettingsPage() {
setAlarmEnabled((await getItem('alarmEnabled')) === 'true');
setPredictiveSets((await getItem('predictiveSets')) === 'true');
setMaxSets((await getItem('maxSets')) || '');
NativeModules.AlarmModule.ignoringBatteryOptimizations(setIgnoring);
NativeModules.AlarmModule.ignoringBattery(setIgnoring);
}, []);
useEffect(() => {
refresh();
}, [refresh]);
const toast = useCallback(
(message: string, timeout = 3000) => {
setSnackbar(message);
clearTimeout(timeoutId);
setTimeoutId(setTimeout(() => setSnackbar(''), timeout));
},
[setSnackbar, timeoutId, setTimeoutId],
);
const clear = useCallback(async () => {
setShowDelete(false);
await db.executeSql(`DELETE FROM sets`);
toast('All data has been deleted!');
}, [db, toast]);
const exportSets = useCallback(async () => {
const fileName = 'sets.csv';
const filePath = `${Dirs.DocumentDir}/${fileName}`;
const [result] = await db.executeSql('SELECT * FROM sets');
if (result.rows.length === 0) return;
const sets: Set[] = result.rows.raw();
const data = ['id,name,reps,weight,created,unit']
.concat(
sets.map(
set =>
`${set.id},${set.name},${set.reps},${set.weight},${set.created},${set.unit}`,
),
)
.join('\n');
console.log('SettingsPage.exportSets', {length: sets.length});
const permission = async () => {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
};
const granted = await permission();
if (granted) {
await FileSystem.writeFile(filePath, data);
if (!FileSystem.exists(filePath)) return;
await FileSystem.cpExternal(filePath, fileName, 'downloads');
}
toast('Exported data. Check your downloads folder.');
}, [db, toast]);
const importSets = useCallback(async () => {
const result = await DocumentPicker.pickSingle();
const file = await FileSystem.readFile(result.uri);
console.log(`${SettingsPage.name}.${importSets.name}:`, file.length);
const values = file
.split('\n')
.slice(1)
.filter(line => line)
.map(set => {
const cells = set.split(',');
return `('${cells[1]}',${cells[2]},${cells[3]},'${cells[4]}','${cells[5]}')`;
})
.join(',');
await db.executeSql(
`INSERT INTO sets(name,reps,weight,created,unit) VALUES ${values}`,
);
toast('Data imported.');
}, [db, toast]);
const changeAlarmEnabled = useCallback(
(enabled: boolean) => {
setAlarmEnabled(enabled);
@ -120,9 +48,12 @@ export default function SettingsPage() {
(enabled: boolean) => {
setPredictiveSets(enabled);
setItem('predictiveSets', enabled ? 'true' : 'false');
toast('Predictive sets guess whats next based on todays plan.', 10000);
ToastAndroid.show(
'Predictive sets guess whats next based on todays plan.',
ToastAndroid.LONG,
);
},
[setPredictiveSets, toast],
[setPredictiveSets],
);
return (
@ -174,7 +105,7 @@ export default function SettingsPage() {
show={showBattery}
setShow={setShowBattery}
onOk={() => {
NativeModules.AlarmModule.openBatteryOptimizations();
NativeModules.AlarmModule.openSettings();
setShowBattery(false);
}}>
Disable battery optimizations for Massive to use rest timers.
@ -186,42 +117,6 @@ export default function SettingsPage() {
value={predictiveSets}
onValueChange={changePredictive}
/>
<Button
style={{alignSelf: 'flex-start'}}
icon="arrow-down"
onPress={exportSets}>
Export
</Button>
<Button
style={{alignSelf: 'flex-start'}}
icon="arrow-up"
onPress={importSets}>
Import
</Button>
<Button
style={{alignSelf: 'flex-start'}}
icon="trash"
onPress={() => setShowDelete(true)}>
Delete all data
</Button>
<ConfirmDialog
title="Delete all data"
show={showDelete}
setShow={setShowDelete}
onOk={clear}>
This irreversibly deletes all data from the app. Are you sure?
</ConfirmDialog>
<Snackbar
visible={!!snackbar}
onDismiss={() => setSnackbar('')}
action={{label: 'Close', onPress: () => setSnackbar('')}}>
{snackbar}
</Snackbar>
</View>
);
}

View File

@ -35,6 +35,5 @@
<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

@ -32,7 +32,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) :
@RequiresApi(Build.VERSION_CODES.M)
@ReactMethod
fun ignoringBatteryOptimizations(callback: Callback) {
fun ignoringBattery(callback: Callback) {
val packageName = reactApplicationContext.packageName
val pm =
reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
@ -45,7 +45,7 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) :
@RequiresApi(Build.VERSION_CODES.M)
@ReactMethod
fun openBatteryOptimizations() {
fun openSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:" + reactApplicationContext.packageName)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

View File

@ -8,6 +8,7 @@ import android.util.Log
class StopTimer : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
applicationContext.stopService(Intent(applicationContext, TimerService::class.java))
applicationContext.stopService(Intent(applicationContext, AlarmService::class.java))
return super.onStartCommand(intent, flags, startId)
}

View File

@ -1,89 +0,0 @@
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.")
.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,39 +1,73 @@
package com.massive
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.CountDownTimer
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import kotlin.math.floor
class TimerService : Service() {
private var notificationManager: NotificationManager? = null
private var endMs: Int? = null
private var countdownTimer: CountDownTimer? = null
@RequiresApi(Build.VERSION_CODES.M)
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("TimerService", "Started timer service.")
val endMs = intent!!.extras!!.getInt("milliseconds")
countdownTimer?.cancel()
countdownTimer = object : CountDownTimer(endMs.toLong(), 1000) {
override fun onTick(currentMs: Long) {
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
broadcastIntent.putExtra("endMs", endMs)
broadcastIntent.putExtra("currentMs", currentMs)
broadcastIntent.action = "tick"
sendBroadcast(broadcastIntent)
}
override fun onFinish() {
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
broadcastIntent.action = "finish"
sendBroadcast(broadcastIntent)
}
if (intent?.action == "add") {
endMs = endMs?.plus(60000)
applicationContext.stopService(Intent(applicationContext, AlarmService::class.java))
}
else {
endMs = intent!!.extras!!.getInt("milliseconds")
}
notificationManager = getManager(applicationContext)
val builder = getBuilder(applicationContext)
countdownTimer?.cancel()
countdownTimer = getTimer(builder, notificationManager!!)
countdownTimer!!.start()
return super.onStartCommand(intent, flags, startId)
}
private fun getTimer(builder: NotificationCompat.Builder, notificationManager: NotificationManager): CountDownTimer {
return 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(NOTIFICATION_ID, builder.build())
}
override fun onFinish() {
val finishIntent = Intent(applicationContext, StopAlarm::class.java)
val finishPending =
PendingIntent.getActivity(applicationContext, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE)
builder.setContentText("Timer finished.")
.setAutoCancel(true)
.setOngoing(false)
.setContentIntent(finishPending)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.priority = NotificationCompat.PRIORITY_HIGH
notificationManager.notify(NOTIFICATION_ID, builder.build())
applicationContext.startService(Intent(applicationContext, AlarmService::class.java))
}
}
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
@ -41,9 +75,46 @@ class TimerService : Service() {
override fun onDestroy() {
Log.d("TimerService", "Destroying...")
countdownTimer?.cancel()
val broadcastIntent = Intent(applicationContext, TimerBroadcast::class.java)
broadcastIntent.action = "stop"
sendBroadcast(broadcastIntent)
notificationManager?.cancel(NOTIFICATION_ID)
super.onDestroy()
}
@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 stopIntent = Intent(context, StopTimer::class.java)
val pendingStop =
PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
val addIntent = Intent(context, TimerService::class.java)
addIntent.action = "add"
val pendingAdd = PendingIntent.getService(context, 0, addIntent, 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", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
}
@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
}
}

8
db.ts
View File

@ -21,11 +21,3 @@ export const createPlans = `
workouts TEXT NOT NULL
);
`;
const selectProgress = `
SELECT count(*) as count from sets
WHERE created LIKE ?
AND name = ?
`;
export const getProgress = ({created, name}: {created: string; name: string}) =>
getDb().then(db => db.executeSql(selectProgress, [`%${created}%`, name]));

18
file.ts Normal file
View File

@ -0,0 +1,18 @@
import {PermissionsAndroid, ToastAndroid} from 'react-native';
import {Dirs, FileSystem} from 'react-native-file-access';
export const write = async (name: string, data: string) => {
const filePath = `${Dirs.DocumentDir}/${name}`;
const permission = async () => {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
};
const granted = await permission();
if (!granted) return;
await FileSystem.writeFile(filePath, data);
if (!FileSystem.exists(filePath)) return;
await FileSystem.cpExternal(filePath, name, 'downloads');
ToastAndroid.show(`Saved "${name}". Check downloads`, ToastAndroid.LONG);
};

View File

@ -78,5 +78,6 @@
"json",
"node"
]
}
},
"packageManager": "yarn@3.2.1"
}

20773
yarn.lock

File diff suppressed because it is too large Load Diff