Add sets exporting

This commit is contained in:
Brandon Presley 2022-07-06 00:06:16 +12:00
parent 76e6ffbc25
commit 3e09f38ef0
5 changed files with 163 additions and 19 deletions

View File

@ -6,7 +6,7 @@ import {
NavigationContainer, NavigationContainer,
} from '@react-navigation/native'; } from '@react-navigation/native';
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import {NativeModules, StatusBar, useColorScheme} from 'react-native'; import {StatusBar, useColorScheme} from 'react-native';
import {setupSchema} from './db'; import {setupSchema} from './db';
import Exercises from './Exercises'; import Exercises from './Exercises';
import Home from './Home'; import Home from './Home';
@ -28,7 +28,6 @@ const App = () => {
AsyncStorage.getItem('minutes').then(async minutes => { AsyncStorage.getItem('minutes').then(async minutes => {
if (!minutes) await AsyncStorage.setItem('minutes', '3'); if (!minutes) await AsyncStorage.setItem('minutes', '3');
}); });
console.log(NativeModules.ExportModule.sets());
}, []); }, []);
return ( return (

View File

@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; 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 {StyleSheet, Text, View} from 'react-native'; import {NativeModules, StyleSheet, Text, View} from 'react-native';
import {Button, Switch, TextInput} from 'react-native-paper'; import {Button, Switch, TextInput} from 'react-native-paper';
import {RootStackParamList} from './App'; import {RootStackParamList} from './App';
import {getDb} from './db'; import {getDb} from './db';
@ -32,6 +32,10 @@ export default function Settings({
await db.executeSql(`DELETE FROM sets`); await db.executeSql(`DELETE FROM sets`);
}; };
const exportSets = () => {
NativeModules.ExportModule.sets();
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
<TextInput <TextInput
@ -56,6 +60,12 @@ export default function Settings({
value={alarmEnabled} value={alarmEnabled}
onValueChange={setAlarmEnabled} onValueChange={setAlarmEnabled}
/> />
<Button
style={{alignSelf: 'flex-start'}}
icon="arrow-down"
onPress={exportSets}>
Download
</Button>
<Button <Button
style={{alignSelf: 'flex-start', marginTop: 'auto'}} style={{alignSelf: 'flex-start', marginTop: 'auto'}}
icon="trash" icon="trash"

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@ -30,6 +31,7 @@
</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=".AlarmActivity" />
<activity android:exported="true" android:process=":remote" android:name=".ExportActivity" />
<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" />

View File

@ -0,0 +1,123 @@
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.*
class ExportActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ExportActivity", "Started ExportActivity.")
val intent = Intent(Intent.ACTION_CREATE_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")
}
startActivityForResult(intent, CREATE_FILE, null)
}
@SuppressLint("Range")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d("ExportActivity", "Got activity result: requestCode=$requestCode,resultCode=$resultCode")
data?.data?.also { uri ->
contentResolver.openFileDescriptor(uri, "w")?.use { fd ->
FileWriter(fd.fileDescriptor).use { fw ->
Log.d("ExportActivity", "Got file writer: $fw")
fw.write("id,name,reps,weight,created,unit\n")
val db = MassiveHelper(applicationContext).readableDatabase
db.use {
with(it.query("sets", null, null, null, null, null, null)) {
while (moveToNext()) {
val id = getInt(getColumnIndex("id"))
val name = getString(getColumnIndex("name"))
val reps = getInt(getColumnIndex("reps"))
val weight = getInt(getColumnIndex("weight"))
val created = getString(getColumnIndex("created"))
val unit = getString(getColumnIndex("unit"))
fw.appendLine("$id,$name,$reps,$weight,$created,$unit\n")
}
}
}
fw.flush()
fw.close()
}
}
}
}
@Throws(IOException::class)
fun getFile(context: Context, uri: Uri): File? {
val destinationFilename =
File(context.filesDir.path + File.separatorChar + queryName(context, uri))
try {
context.contentResolver.openInputStream(uri).use { ins ->
if (ins != null) {
createFileFromStream(
ins,
destinationFilename
)
}
}
} catch (ex: Exception) {
Log.e("Save File", ex.message!!)
ex.printStackTrace()
}
return destinationFilename
}
private fun createFileFromStream(ins: InputStream, destination: File?) {
try {
FileOutputStream(destination).use { os ->
val buffer = ByteArray(4096)
var length: Int
while (ins.read(buffer).also { length = it } > 0) {
os.write(buffer, 0, length)
}
os.flush()
}
} catch (ex: Exception) {
Log.e("Save File", ex.message!!)
ex.printStackTrace()
}
}
private fun queryName(context: Context, uri: Uri): String {
val returnCursor: Cursor = context.contentResolver.query(uri, null, null, null, null)!!
val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name: String = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
companion object {
private const val CHANNEL_ID = "Exports"
private const val NOTIFICATION_ID = 2
// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2
// Request code for creating a PDF document.
const val CREATE_FILE = 1
}
}

View File

@ -1,10 +1,12 @@
package com.massive package com.massive
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.DownloadManager
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
@ -15,8 +17,8 @@ import androidx.core.app.NotificationManagerCompat
import com.facebook.react.bridge.ReactApplicationContext 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 okhttp3.internal.notify
import java.io.File import java.io.File
import java.io.FileWriter
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -35,14 +37,13 @@ class ExportModule internal constructor(context: ReactApplicationContext?) :
val current = LocalDateTime.now() val current = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val formatted = current.format(formatter) val formatted = current.format(formatter)
val sets = File(dir, "sets$formatted.csv") val file = File(dir, "sets-$formatted.csv")
sets.createNewFile() file.createNewFile()
sets.setWritable(true) val writer = FileWriter(file)
sets.setReadable(true) writer.write("id,name,reps,weight,created,unit\n")
sets.writeText("id,name,reps,weight,created,unit\n")
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)) {
while (moveToNext()) { while (moveToNext()) {
val id = getInt(getColumnIndex("id")) val id = getInt(getColumnIndex("id"))
val name = getString(getColumnIndex("name")) val name = getString(getColumnIndex("name"))
@ -50,33 +51,42 @@ 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"))
sets.appendText("$id,$name,$reps,$weight,$created,$unit\n") writer.appendLine("$id,$name,$reps,$weight,$created,$unit\n")
} }
} }
} }
writer.flush()
writer.close()
sendNotification()
return file.path
}
@RequiresApi(Build.VERSION_CODES.M)
private fun sendNotification() {
val notificationManager = NotificationManagerCompat.from(reactApplicationContext) val notificationManager = NotificationManagerCompat.from(reactApplicationContext)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_LOW val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance) val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_ID, importance)
channel.description = "Alarms for rest timings." channel.description = "Alarms for rest timings."
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
val contentIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { val contentIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/csv"
}
val pendingContent = val pendingContent =
PendingIntent.getActivity(reactApplicationContext, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(
reactApplicationContext,
0,
contentIntent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(reactApplicationContext, CHANNEL_ID) val builder = NotificationCompat.Builder(reactApplicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_arrow_downward_24) .setSmallIcon(R.drawable.ic_baseline_arrow_downward_24)
.setContentTitle("Downloaded sets") .setContentTitle("Downloaded sets")
.setContentIntent(pendingContent) .setContentIntent(pendingContent)
.setAutoCancel(true) .setAutoCancel(true)
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
return sets.absolutePath
} }
companion object { companion object {