Skip to content
This repository has been archived by the owner on Jun 15, 2022. It is now read-only.

Commit

Permalink
Functioning backup export for iOS and Android
Browse files Browse the repository at this point in the history
  • Loading branch information
moughxyz committed Jan 5, 2019
1 parent f5a01ff commit 272d6d7
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 50 deletions.
2 changes: 2 additions & 0 deletions android/app/build.gradle
Expand Up @@ -159,6 +159,8 @@ android {
}

dependencies {
compile project(':react-native-file-viewer')
compile project(':react-native-fs')
compile project(':react-native-gesture-handler')

implementation fileTree(dir: "libs", include: ["*.jar"])
Expand Down
Binary file modified android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf
Binary file not shown.
Binary file modified android/app/src/main/assets/fonts/Octicons.ttf
Binary file not shown.
Expand Up @@ -9,6 +9,8 @@
import android.view.WindowManager;

import com.facebook.react.ReactApplication;
import com.vinzscam.reactnativefileviewer.RNFileViewerPackage;
import com.rnfs.RNFSPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
Expand Down Expand Up @@ -44,6 +46,8 @@ public boolean getUseDeveloperSupport() {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNFileViewerPackage(),
new RNFSPackage(),
new RNGestureHandlerPackage(),
BugsnagReactNative.getPackage(),
new KeychainPackage(),
Expand Down
4 changes: 4 additions & 0 deletions android/settings.gradle
@@ -1,4 +1,8 @@
rootProject.name = 'StandardNotes'
include ':react-native-file-viewer'
project(':react-native-file-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-file-viewer/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':app'

include ':react-native-gesture-handler'
Expand Down
104 changes: 103 additions & 1 deletion ios/StandardNotes.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ios/StandardNotes/Info.plist
Expand Up @@ -40,7 +40,7 @@
<key>NSFaceIDUsageDescription</key>
<string>Face ID is required to unlock your notes.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<string/>
<key>UIAppFonts</key>
<array>
<string>Entypo.ttf</string>
Expand Down
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -19,6 +19,8 @@
"moment": "^2.23.0",
"react": "16.6.3",
"react-native": "0.57.8",
"react-native-file-viewer": "^1.0.10",
"react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.0.12",
"react-native-keychain": "^1.2.1",
"react-native-store-review": "^0.1.3",
Expand Down
2 changes: 1 addition & 1 deletion src/containers/account/OptionsSection.js
Expand Up @@ -75,7 +75,7 @@ export default class OptionsSection extends Component {
disabled={this.state.loadingExport}
leftAligned={true}
options={this.exportOptions()}
title={this.state.loadingExport ? "Preparing Data..." : "Export Data"}
title={this.state.loadingExport ? "Processing..." : "Export Data"}
onPress={this.onExportPress}
/>

Expand Down
123 changes: 123 additions & 0 deletions src/lib/BackupsManager.js
@@ -0,0 +1,123 @@
import { Share } from 'react-native';
import Storage from '@SFJS/storageManager'
import Auth from '@SFJS/authManager'
import KeysManager from '@Lib/keysManager'
import AlertManager from "@SFJS/alertManager";
import UserPrefsManager from '@Lib/userPrefsManager'
import ModelManager from '@SFJS/modelManager'
import ApplicationState from "@Lib/ApplicationState"
import RNFS from 'react-native-fs';
import FileViewer from 'react-native-file-viewer';
const base64 = require('base-64');

export default class BackupsManager {

static instance = null;
static get() {
if(this.instance == null) {
this.instance = new BackupsManager();
}
return this.instance;
}

/*
On iOS, we can use Share to share a file of arbitrary length.
This doesn't work on Android however. Seems to have a very low limit.
For Android, we'll use RNFS to save the file to disk, then FileViewer to
ask the user what application they would like to open the file with.
For .txt files, not many applications handle it. So, we'll want to notify the user
the path the file was saved to.
*/

async export(encrypted) {
var auth_params = await Auth.get().getAuthParams();
var keys = encrypted ? KeysManager.get().activeKeys() : null;

var items = [];

for(var item of ModelManager.get().allItems) {
var itemParams = new SFItemParams(item, keys, auth_params);
var params = await itemParams.paramsForExportFile();
items.push(params);
}

if(items.length == 0) {
Alert.alert('No Data', "You don't have any notes yet.");
return false;
}

var data = {items: items}

if(keys) {
var authParams = KeysManager.get().activeAuthParams();
// auth params are only needed when encrypted with a standard file key
data["auth_params"] = authParams;
}

var jsonString = JSON.stringify(data, null, 2 /* pretty print */);
let modifier = encrypted ? "Encrypted" : "Decrypted";
let filename = `Standard Notes ${modifier} Backup - ${this._formattedDate()}.txt`;

if(ApplicationState.isIOS) {
return this._exportIOS(filename, jsonString);
} else {
let filepath = await this._exportAndroid(filename, jsonString);
return this._showFileSavePromptAndroid(filepath);
}
}

async _exportIOS(filename, data) {
return new Promise((resolve, reject) => {
ApplicationState.get().performActionWithoutStateChangeImpact(async () => {
Share.share({
title: filename,
message: data,
}).then((result) => {
resolve(result != Share.dismissedAction);
}).catch((error) => {
resolve(false);
})
})
})
}

async _exportAndroid(filename, data) {
let filepath = `${RNFS.DocumentDirectoryPath}/${filename}`;
return RNFS.writeFile(filepath, data).then(() => {
return filepath;
})
}

async _openFileAndroid(filepath) {
return FileViewer.open(filepath).then(() => {
// success
return true;
}).catch(error => {
console.log("Error opening file", error);
return false;
});
}

async _showFileSavePromptAndroid(filepath) {
return AlertManager.get().confirm({
title: "Backup Saved",
text: `Your backup file has been saved to your local disk at this location:\n\n${filepath}`,
cancelButtonText: "Done",
confirmButtonText: "Open File",
onConfirm: () => {
this._openFileAndroid(filepath);
}
}).then(() => {
return true;
}).catch(() => {
// Did Cancel, still success
return true;
})
}

/* Utils */

_formattedDate() {
return new Date().getTime();
}
}
5 changes: 2 additions & 3 deletions src/lib/sfjs/alertManager.js
Expand Up @@ -12,11 +12,11 @@ export default class AlertManager extends SFAlertManager {
return this.instance;
}

async confirm({title, text, confirmButtonText = "OK", onConfirm, onCancel} = {}) {
async confirm({title, text, confirmButtonText = "OK", cancelButtonText = "Cancel", onConfirm, onCancel} = {}) {
return new Promise((resolve, reject) => {
// On iOS, confirm should go first. On Android, cancel should go first.
let buttons = [
{text: 'Cancel', onPress: () => {
{text: cancelButtonText, onPress: () => {
reject();
onCancel && onCancel();
}},
Expand All @@ -28,5 +28,4 @@ export default class AlertManager extends SFAlertManager {
Alert.alert(title, text, buttons, { cancelable: true })
})
}

}
49 changes: 5 additions & 44 deletions src/screens/Settings.js
Expand Up @@ -2,14 +2,17 @@ import React, { Component } from 'react';
import {ScrollView, View, Alert, Keyboard, Linking, Platform, Share, NativeModules} from 'react-native';

import Sync from '../lib/sfjs/syncManager'
import ModelManager from '../lib/sfjs/modelManager'
import ModelManager from '@SFJS/modelManager'
import AlertManager from '../lib/sfjs/alertManager'
import SF from '@SFJS/sfjs'

import Auth from '../lib/sfjs/authManager'
import KeysManager from '@Lib/keysManager'
import UserPrefsManager from '../lib/userPrefsManager'
import OptionsState from "@Lib/OptionsState"
import ApplicationState from "@Lib/ApplicationState"
import StyleKit from "@Style/StyleKit"
import BackupsManager from "@Lib/BackupsManager"

import SectionHeader from "../components/SectionHeader";
import ButtonCell from "../components/ButtonCell";
Expand All @@ -25,10 +28,6 @@ import PasscodeSection from "../containers/account/PasscodeSection"
import EncryptionSection from "../containers/account/EncryptionSection"
import CompanySection from "../containers/account/CompanySection"
import LockedView from "../containers/LockedView";
import ApplicationState from "@Lib/ApplicationState"
import StyleKit from "../style/StyleKit"

var base64 = require('base-64');

export default class Settings extends Abstract {

Expand Down Expand Up @@ -269,51 +268,13 @@ export default class Settings extends Abstract {

onExportPress = async (encrypted, callback) => {
this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManageBackups, async () => {
let customCallback = (success) => {
BackupsManager.get().export(encrypted, callback).then((success) => {
if(success) {
var date = new Date();
this.setState({lastExportDate: date});
UserPrefsManager.get().setLastExportDate(date);
}
callback();
}
var auth_params = await Auth.get().getAuthParams();
var keys = encrypted ? KeysManager.get().activeKeys() : null;

var items = [];

for(var item of ModelManager.get().allItems) {
var itemParams = new SFItemParams(item, keys, auth_params);
var params = await itemParams.paramsForExportFile();
items.push(params);
}

if(items.length == 0) {
Alert.alert('No Data', "You don't have any notes yet.");
customCallback();
return;
}

var data = {items: items}

if(keys) {
var authParams = KeysManager.get().activeAuthParams();
// auth params are only needed when encrypted with a standard file key
data["auth_params"] = authParams;
}

var jsonString = JSON.stringify(data, null, 2 /* pretty print */);

var calledCallback = false;

ApplicationState.get().performActionWithoutStateChangeImpact(() => {
Share.share({
title: encrypted ? "SN-Encrypted-Backup" : 'SN-Decrypted-Backup',
message: jsonString,
}).then((event) => {
console.log("Result", event);
customCallback(event != Share.dismissedAction);
})
})
});
}
Expand Down

0 comments on commit 272d6d7

Please sign in to comment.