From 7b0929bef4a5420b54f6db6203ccb7cf252bf4cd Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Sat, 9 Jul 2022 19:39:11 +1200 Subject: [PATCH] Use react-native libraries for Export and Import --- SettingsPage.tsx | 102 ++++++++++++++---- .../src/main/java/com/massive/ExportModule.kt | 97 ----------------- .../main/java/com/massive/MassivePackage.kt | 3 +- package.json | 4 + yarn.lock | 22 ++++ 5 files changed, 109 insertions(+), 119 deletions(-) delete mode 100644 android/app/src/main/java/com/massive/ExportModule.kt diff --git a/SettingsPage.tsx b/SettingsPage.tsx index 25f607a..79efee6 100644 --- a/SettingsPage.tsx +++ b/SettingsPage.tsx @@ -1,9 +1,18 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import React, {useContext, useEffect, useState} from 'react'; -import {NativeModules, StyleSheet, Text, View} from 'react-native'; +import React, {useCallback, useContext, useEffect, useState} from 'react'; +import { + NativeModules, + PermissionsAndroid, + StyleSheet, + Text, + 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 BatteryDialog from './BatteryDialog'; +import Set from './set'; +import DocumentPicker from 'react-native-document-picker'; const {getItem, setItem} = AsyncStorage; @@ -14,38 +23,91 @@ export default function SettingsPage() { const [snackbar, setSnackbar] = useState(''); const [showBattery, setShowBattery] = useState(false); const [ignoring, setIgnoring] = useState(false); + const [timeoutId, setTimeoutId] = useState(0); const db = useContext(DatabaseContext); - const refresh = async () => { + const refresh = useCallback(async () => { setMinutes((await getItem('minutes')) || ''); setSeconds((await getItem('seconds')) || ''); setAlarmEnabled((await getItem('alarmEnabled')) === 'true'); NativeModules.AlarmModule.ignoringBatteryOptimizations(setIgnoring); - }; + }, [setIgnoring]); useEffect(() => { refresh(); - }, []); + }, [refresh]); - const clear = async () => { - setSnackbar('Deleting all data...'); - setTimeout(() => setSnackbar(''), 5000); + const toast = useCallback( + (message: string) => { + setSnackbar(message); + clearTimeout(timeoutId); + setTimeoutId(setTimeout(() => setSnackbar(''), 3000)); + }, + [setSnackbar, timeoutId, setTimeoutId], + ); + + const clear = useCallback(async () => { await db.executeSql(`DELETE FROM sets`); - }; + toast('All data has been deleted!'); + }, [db, toast]); - const exportSets = () => { - NativeModules.ExportModule.sets(); - }; + 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 importSets = () => { - NativeModules.ImportModule.sets(); - }; + 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 changeAlarmEnabled = (enabled: boolean) => { - setAlarmEnabled(enabled); - if (enabled && !ignoring) setShowBattery(true); - setItem('alarmEnabled', enabled ? 'true' : 'false'); - }; + 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) + .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); + if (enabled && !ignoring) setShowBattery(true); + setItem('alarmEnabled', enabled ? 'true' : 'false'); + }, + [alarmEnabled, setShowBattery], + ); return ( diff --git a/android/app/src/main/java/com/massive/ExportModule.kt b/android/app/src/main/java/com/massive/ExportModule.kt deleted file mode 100644 index 63404fe..0000000 --- a/android/app/src/main/java/com/massive/ExportModule.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.massive - -import android.annotation.SuppressLint -import android.app.DownloadManager -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.DocumentsContract -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -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 - -class ExportModule internal constructor(context: ReactApplicationContext?) : - ReactContextBaseJavaModule(context) { - override fun getName(): String { - return "ExportModule" - } - - @RequiresApi(Build.VERSION_CODES.O) - @SuppressLint("Recycle", "Range") - @ReactMethod(isBlockingSynchronousMethod = true) - fun sets(): String { - Log.d("ExportModule", "Exporting sets...") - val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - val current = LocalDateTime.now() - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val formatted = current.format(formatter) - val file = File(dir, "sets-$formatted.csv") - file.createNewFile() - val writer = FileWriter(file) - 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)) { - 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")) - writer.appendLine("$id,$name,$reps,$weight,$created,$unit") - } - } - } - writer.flush() - writer.close() - sendNotification() - return file.path - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun sendNotification() { - val notificationManager = NotificationManagerCompat.from(reactApplicationContext) - 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." - notificationManager.createNotificationChannel(channel) - } - - val contentIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); - val pendingContent = - PendingIntent.getActivity( - reactApplicationContext, - 0, - contentIntent, - PendingIntent.FLAG_IMMUTABLE - ) - val builder = NotificationCompat.Builder(reactApplicationContext, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_baseline_arrow_downward_24) - .setContentTitle("Downloaded sets") - .setContentIntent(pendingContent) - .setAutoCancel(true) - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } - - companion object { - private const val CHANNEL_ID = "Exports" - private const val NOTIFICATION_ID = 2 - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/massive/MassivePackage.kt b/android/app/src/main/java/com/massive/MassivePackage.kt index ee27580..a49f6db 100644 --- a/android/app/src/main/java/com/massive/MassivePackage.kt +++ b/android/app/src/main/java/com/massive/MassivePackage.kt @@ -17,8 +17,7 @@ class MassivePackage : ReactPackage { ): List { val modules: MutableList = ArrayList() modules.add(AlarmModule(reactContext)) - modules.add(ExportModule(reactContext)) modules.add(ImportModule(reactContext)) return modules } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 67ce71d..6d9cd15 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@react-navigation/native": "^6.0.10", "@react-navigation/native-stack": "^6.6.2", "@types/d3-shape": "^3.1.0", + "@types/react-native-base64": "^0.2.0", "@types/react-native-sqlite-storage": "^5.0.2", "@types/react-native-svg-charts": "^5.0.12", "@types/react-native-vector-icons": "^6.4.11", @@ -24,6 +25,9 @@ "react": "18.0.0", "react-devtools": "^4.24.7", "react-native": "0.69.1", + "react-native-base64": "^0.2.1", + "react-native-document-picker": "^8.1.1", + "react-native-file-access": "^2.4.3", "react-native-gesture-handler": "^2.5.0", "react-native-linear-gradient": "^2.6.2", "react-native-modal-datetime-picker": "^13.1.2", diff --git a/yarn.lock b/yarn.lock index a4884cf..8874c60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1870,6 +1870,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== +"@types/react-native-base64@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@types/react-native-base64/-/react-native-base64-0.2.0.tgz#c934e7c3cd549d4613bbfa7929a6845ab07a20af" + integrity sha512-5kUtS7Kf8Xl4zEwbqLITEuQReQTby4UOGfnsEeBFJEVmUfT+ygOv/Qmv0v6El0iV1eDhXS+/0i7CGR9d3/nRSA== + "@types/react-native-sqlite-storage@^5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@types/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.2.tgz#eefcc9ea6ff73043bb2945023fa8ee721683b61b" @@ -6841,6 +6846,11 @@ react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-native-base64@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/react-native-base64/-/react-native-base64-0.2.1.tgz#3d0e73a649c4c0129f7b7695d3912456aebae847" + integrity sha512-eHgt/MA8y5ZF0aHfZ1aTPcIkDWxza9AaEk4GcpIX+ZYfZ04RcaNahO+527KR7J44/mD3efYfM23O2C1N44ByWA== + react-native-codegen@^0.69.1: version "0.69.1" resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.69.1.tgz#3632be2f24464e6fad8dd11a25d1b6f3bc2c7d0b" @@ -6851,6 +6861,18 @@ 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-file-access@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/react-native-file-access/-/react-native-file-access-2.4.3.tgz#83c632ee3e6a403662e7c92f10de0d88539c42ba" + integrity sha512-9f/z5dUSZgl1js+7jl43vkUrfProNuWo6rNRXV2AXdm1dckokYjeai/Mj6x+XMDyaBtRYztNKNqwZJhe5kHrNA== + 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"