├── .gitignore
├── .github
├── FUNDING.yml
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE
│ ├── documentation-issue.md
│ ├── feature_request.md
│ └── bug_report.md
├── src
├── android
│ ├── backup_rules_lte_api_30.xml
│ ├── backup_rules_gte_api_31.xml
│ ├── BackupAgentHelper.java
│ └── CloudSettingsPlugin.java
└── ios
│ ├── CloudSettingsPlugin.h
│ └── CloudSettingsPlugin.m
├── package.json
├── scripts
└── android
│ └── test_cloud_backup.sh
├── cordova-plugin-cloud-settings.d.ts
├── plugin.xml
├── www
├── ios
│ └── cloudsettings.js
└── android
│ └── cloudsettings.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ dpa99c ]
4 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZRD3W47HQ3EMJ&source=url
5 | patreon: dpa99c
6 | ko_fi: davealden
--------------------------------------------------------------------------------
/src/android/backup_rules_lte_api_30.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/android/backup_rules_gte_api_31.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/ios/CloudSettingsPlugin.h:
--------------------------------------------------------------------------------
1 | //
2 | // CloudSettingsPlugin.m
3 | // Cordova Cloud Settings
4 | // Copyright (c) by Dave Alden 2018
5 | #import
6 | #import
7 |
8 | @interface CloudSettingsPlugin : CDVPlugin
9 |
10 | @property (nonatomic) BOOL debugEnabled;
11 |
12 | // Plugin API
13 | -(void)enableDebug:(CDVInvokedUrlCommand*)command;
14 | -(void)save:(CDVInvokedUrlCommand *)command;
15 | -(void)load:(CDVInvokedUrlCommand *)command;
16 | -(void)exists:(CDVInvokedUrlCommand *)command;
17 | @end
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cordova-plugin-cloud-settings",
3 | "version": "2.0.1",
4 | "description": "A Cordova plugin for Android & iOS to persist user settings in cloud storage across devices and installs.",
5 | "author": "Dave Alden",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/dpa99c/cordova-plugin-cloud-settings.git"
10 | },
11 | "cordova": {
12 | "id": "cordova-plugin-cloud-settings",
13 | "platforms": [
14 | "android",
15 | "ios"
16 | ]
17 | },
18 | "issue": "https://github.com/dpa99c/cordova-plugin-cloud-settings/issues",
19 | "keywords": [
20 | "ecosystem:cordova",
21 | "cordova",
22 | "cordova-android",
23 | "cordova-ios",
24 | "phonegap",
25 | "backup",
26 | "cloud",
27 | "settings",
28 | "icloud"
29 | ],
30 | "dependencies": {
31 | "xml2js": "^0.4.9"
32 | },
33 | "devDependencies": {},
34 | "types": "./cordova-plugin-cloud-settings.d.ts"
35 | }
36 |
--------------------------------------------------------------------------------
/scripts/android/test_cloud_backup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 | : "${1?"Usage: $0 package name"}"
3 |
4 | # Initialize and create a backup
5 | adb shell bmgr enable true
6 | adb shell bmgr transport com.android.localtransport/.LocalTransport | grep -q "Selected transport" || (echo "Error: error selecting local transport"; exit 1)
7 | adb shell settings put secure backup_local_transport_parameters 'is_encrypted=true'
8 | adb shell bmgr backupnow "$1" | grep -F "Package $1 with result: Success" || (echo "Backup failed"; exit 1)
9 |
10 | # Uninstall and reinstall the app to clear the data and trigger a restore
11 | apk_path_list=$(adb shell pm path "$1")
12 | OIFS=$IFS
13 | IFS=$'\n'
14 | apk_number=0
15 | for apk_line in $apk_path_list
16 | do
17 | (( ++apk_number ))
18 | apk_path=${apk_line:8:1000}
19 | adb pull "$apk_path" "myapk${apk_number}.apk"
20 | done
21 | IFS=$OIFS
22 | adb shell pm uninstall --user 0 "$1"
23 | apks=$(seq -f 'myapk%.f.apk' 1 $apk_number)
24 | adb install-multiple -t --user 0 $apks
25 |
26 | # Clean up
27 | adb shell bmgr transport com.google.android.gms/.backup.BackupTransportService
28 | rm $apks
29 |
30 | echo "Done"
31 |
32 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## PR Type
2 | What kind of change does this PR introduce?
3 |
4 |
5 | - [ ] Bugfix
6 | - [ ] Feature
7 | - [ ] Code style update (formatting, local variables)
8 | - [ ] Refactoring (no functional changes, no api changes)
9 | - [ ] Documentation changes
10 | - [ ] Other... Please describe:
11 |
12 |
13 |
14 | ## PR Checklist
15 | For bug fixes / features, please check if your PR fulfills the following requirements:
16 |
17 | - [ ] Testing has been carried out for the changes have been added
18 | - [ ] Regression testing has been carried out for existing functionality
19 | - [ ] Docs have been added / updated
20 |
21 | ## What is the purpose of this PR?
22 |
23 |
24 |
25 | ## Does this PR introduce a breaking change?
26 | - [ ] Yes
27 | - [ ] No
28 |
29 |
30 |
31 | ## What testing has been done on the changes in the PR?
32 |
33 |
34 | ## What testing has been done on existing functionality?
35 |
36 |
37 | ## Other information
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation-issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation issue
3 | about: Describe an issue with the documentation
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
16 |
17 |
18 | # Documentation issue
19 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
15 |
16 |
17 | # Feature request
18 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/android/BackupAgentHelper.java:
--------------------------------------------------------------------------------
1 | package cordova.plugin.cloudsettings;
2 |
3 | import android.app.backup.BackupDataInput;
4 | import android.app.backup.BackupDataOutput;
5 | import android.app.backup.FileBackupHelper;
6 | import android.os.ParcelFileDescriptor;
7 | import android.util.Log;
8 |
9 | import java.io.IOException;
10 |
11 | public class BackupAgentHelper extends android.app.backup.BackupAgentHelper {
12 | static final String FILE_NAME = "cloudsettings.json";
13 | static final String FILES_BACKUP_KEY = "data_file";
14 |
15 | @Override
16 | public void onCreate() {
17 | CloudSettingsPlugin.d("Created");
18 | FileBackupHelper helper = new FileBackupHelper(this, FILE_NAME);
19 | addHelper(FILES_BACKUP_KEY, helper);
20 | }
21 |
22 | @Override
23 | public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
24 |
25 | synchronized (CloudSettingsPlugin.sDataLock) {
26 | try {
27 | CloudSettingsPlugin.d("Backup invoked: " + data.toString());
28 | super.onBackup(oldState, data, newState);
29 | } catch (Exception e) {
30 | CloudSettingsPlugin.handleException(e, "when backup invoked");
31 | }
32 | }
33 | }
34 |
35 | @Override
36 | public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
37 | synchronized (CloudSettingsPlugin.sDataLock) {
38 | try {
39 | CloudSettingsPlugin.d("Restore invoked: " + data.toString());
40 | super.onRestore(data, appVersionCode, newState);
41 | } catch (Exception e) {
42 | CloudSettingsPlugin.handleException(e, "when restore invoked");
43 | }
44 | }
45 | }
46 |
47 | @Override
48 | public void onRestoreFinished(){
49 | synchronized (CloudSettingsPlugin.sDataLock) {
50 | try {
51 | CloudSettingsPlugin.onRestore();
52 | } catch (Exception e) {
53 | CloudSettingsPlugin.handleException(e, "when restore finished invoked");
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/cordova-plugin-cloud-settings.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for cordova-plugin-cloud-settings
2 | // Project: https://github.com/dpa99c/cordova-diagnostic-plugin
3 | // Definitions by: Dave Alden
4 |
5 | ///
6 |
7 | /**
8 | * Provides a mechanism to store key/value app settings in the form of a JSON structure which will persist in cloud storage so if the user re-installs the app or installs it on a different device, the settings will be restored and available in the new installation.
9 | */
10 | interface CloudSettings {
11 |
12 | /**
13 | * Outputs verbose log messages from the native plugin components to the JS console.
14 | * @param {function} successCallback - callback function to invoke when debug mode has been enabled
15 | */
16 | enableDebug: (
17 | successCallback: () => void,
18 | ) => void;
19 |
20 | /**
21 | * Indicates if any stored cloud settings currently exist for the current user.
22 | * @param {function} successCallback - callback function to invoke with the result.
23 | * Will be passed a boolean flag which indicates whether an store settings exist for the user.
24 | */
25 | exists: (
26 | successCallback: (available: boolean) => void,
27 | ) => void;
28 |
29 | /**
30 | * Saves the settings to cloud backup.
31 | * @param {object} settings - a JSON structure representing the user settings to save to cloud backup.
32 | * @param {function} successCallback - (optional) callback function to invoke on successfuly saving settings and scheduling for backup.
33 | Will be passed a single object argument which contains the saved settings as a JSON object.
34 | * @param {function} errorCallback - (optional) callback function to invoke on failure to save settings or schedule for backup.
35 | Will be passed a single string argument which contains a description of the error.
36 | * @param {boolean} overwrite - (optional) if true, existing settings will be replaced rather than updated. Defaults to false.
37 | - If false, existing settings will be merged with the new settings passed to this function.
38 | */
39 | save: (
40 | settings: object,
41 | successCallback: (savedSettings: object) => void,
42 | errorCallback: (error: string) => void,
43 | overwrite?: boolean
44 | ) => void;
45 |
46 | /**
47 | * Loads the current settings.
48 | * @param {settings
49 | * @param {function} successCallback - (optional) callback function to invoke on successfuly loading settings.
50 | Will be passed a single object argument which contains the current settings as a JSON object.
51 | * {function} errorCallback - (optional) callback function to invoke on failure to load settings.
52 | Will be passed a single string argument which contains a description of the error.
53 | */
54 | load: (
55 | successCallback: (savedSettings: object) => void,
56 | errorCallback: (error: string) => void
57 | ) => void;
58 |
59 | /**
60 | * Registers a function which will be called if/when settings on the device have been updated from the cloud.
61 | * @param {function} successCallback - callback function to invoke when device settings have been updated from the cloud.
62 | */
63 | onRestore: (
64 | successCallback: () => void,
65 | ) => void;
66 |
67 | }
68 |
69 | interface CordovaPlugin {
70 | cloudsettings: CloudSettings
71 | }
72 |
--------------------------------------------------------------------------------
/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | cordova-plugin-cloud-settings
8 | A Cordova plugin for Android & iOS to persist user settings in cloud storage across devices and installs.
9 | MIT
10 | cordova,android,ios,backup,cloud,settings,icloud
11 |
12 | Dave Alden
13 |
14 |
15 |
16 |
17 |
18 | https://github.com/dpa99c/cordova-plugin-cloud-settings.git
19 | https://github.com/dpa99c/cordova-plugin-cloud-settings/issues
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | $(TeamIdentifierPrefix)$(CFBundleIdentifier)
55 |
56 |
57 |
58 | $(TeamIdentifierPrefix)$(CFBundleIdentifier)
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/www/ios/cloudsettings.js:
--------------------------------------------------------------------------------
1 | var onRestoreFn = function(){};
2 |
3 | var merge = function () {
4 | var destination = {},
5 | sources = [].slice.call(arguments, 0);
6 | sources.forEach(function (source) {
7 | var prop;
8 | for (prop in source) {
9 | if (prop in destination && Array.isArray(destination[prop])) {
10 |
11 | // Concat Arrays
12 | destination[prop] = destination[prop].concat(source[prop]);
13 |
14 | } else if (prop in destination && typeof destination[prop] === "object") {
15 |
16 | // Merge Objects
17 | destination[prop] = merge(destination[prop], source[prop]);
18 |
19 | } else {
20 |
21 | // Set new values
22 | destination[prop] = source[prop];
23 |
24 | }
25 | }
26 | });
27 | return destination;
28 | };
29 |
30 | var fail = function (onError, operation, error) {
31 | if(typeof error === "object"){
32 | error = JSON.stringify(error);
33 | }
34 | var msg = "CloudSettingsPlugin ERROR " + operation + ": " + error;
35 | if (onError){
36 | onError(msg);
37 | }else{
38 | console.error(msg);
39 | }
40 | };
41 |
42 | var cloudsettings = {};
43 |
44 | cloudsettings.enableDebug = function(onSuccess) {
45 | return cordova.exec(onSuccess,
46 | null,
47 | 'CloudSettingsPlugin',
48 | 'enableDebug',
49 | []);
50 | };
51 |
52 | cloudsettings.load = function(onSuccess, onError){
53 | cordova.exec(function(sData){
54 | try{
55 | var oData = JSON.parse(sData);
56 | }catch(e){
57 | return fail(onError, "parsing stored settings to JSON", e.message);
58 | }
59 | try{
60 | onSuccess(oData);
61 | }catch(e){
62 | return fail(onError, "calling success callback", e.message);
63 | }
64 | }, fail.bind(this, onError, "loading stored settings"), 'CloudSettingsPlugin', 'load', []);
65 | };
66 |
67 | cloudsettings.save = function(settings, onSuccess, onError, overwrite){
68 | if(typeof settings !== "object" || typeof settings.length !== "undefined") throw "settings must be a key/value object!";
69 |
70 | var doSave = function(){
71 | settings.timestamp = (new Date()).valueOf();
72 | try{
73 | var data = JSON.stringify(settings);
74 | }catch(e){
75 | return fail(onError, "convert settings to JSON", e.message);
76 | }
77 | cordova.exec(function(){
78 | try{
79 | onSuccess(settings);
80 | }catch(e){
81 | return fail(onError, "calling success callback", e.message);
82 | }
83 | }, fail.bind(this, onError, "saving settings"), 'CloudSettingsPlugin', 'save', [data]);
84 | };
85 |
86 | if(overwrite){
87 | doSave();
88 | }else{
89 | cloudsettings.exists(function(exists){
90 | if(exists){
91 | // Load stored settings and merge them with new settings
92 | cloudsettings.load(function(stored){
93 | settings = merge(stored, settings);
94 | doSave();
95 | }, onError);
96 | }else{
97 | doSave();
98 | }
99 | });
100 | }
101 |
102 | };
103 |
104 | cloudsettings.exists = function(onSuccess){
105 | cordova.exec(onSuccess, null, 'CloudSettingsPlugin', 'exists', []);
106 | };
107 |
108 | cloudsettings.onRestore = function(fn){
109 | onRestoreFn = fn;
110 | };
111 |
112 | cloudsettings._onRestore = function(){
113 | onRestoreFn();
114 | };
115 |
116 | module.exports = cloudsettings;
117 |
118 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a problem
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
15 |
16 |
17 |
18 | # Bug report
19 |
20 |
21 | **CHECKLIST**
22 | - [ ] I have read the [issue reporting guidelines](README#reporting-issues)
23 |
24 | - [ ] I confirm this is a suspected bug or issue that will affect other users
25 |
26 |
27 | - [ ] I have reproduced the issue using the example projector provided the necessary information to reproduce the issue.
28 |
29 |
30 | - [ ] I have read the documentation thoroughly and it does not help solve my issue.
31 |
32 |
33 | - [ ] I have checked that no similar issues (open or closed) already exist.
34 |
35 |
36 | **Current behavior:**
37 |
38 |
39 |
40 |
44 |
45 | **Expected behavior:**
46 |
47 |
48 | **Steps to reproduce:**
49 |
50 |
51 | **Screenshots**
52 |
53 |
54 | **Environment information**
55 |
56 | - Cordova CLI version
57 | - `cordova -v`
58 | - Cordova platform version
59 | - `cordova platform ls`
60 | - Plugins & versions installed in project (including this plugin)
61 | - `cordova plugin ls`
62 | - Dev machine OS and version, e.g.
63 | - OSX
64 | - `sw_vers`
65 | - Windows 10
66 | - `winver`
67 |
68 | _Runtime issue_
69 | - Device details
70 | - _e.g. iPhone X, Samsung Galaxy S8, iPhone X Simulator, Pixel XL Emulator_
71 | - OS details
72 | - _e.g. iOS 12.2, Android 9.0_
73 |
74 | _Android build issue:_
75 | - Node JS version
76 | - `node -v`
77 | - Gradle version
78 | - `ls platforms/android/.gradle`
79 | - Target Android SDK version
80 | - `android:targetSdkVersion` in `AndroidManifest.xml`
81 | - Android SDK details
82 | - `sdkmanager --list | sed -e '/Available Packages/q'`
83 |
84 | _iOS build issue:_
85 | - Node JS version
86 | - `node -v`
87 | - XCode version
88 |
89 |
90 | **Related code:**
91 | ```
92 | insert any relevant code here such as plugin API calls / input parameters
93 | ```
94 |
95 | **Console output**
96 |
97 | console output
98 |
99 | ```
100 |
101 | // Paste any relevant JS/native console output here
102 |
103 | ```
104 |
105 |
106 |
107 | **Other information:**
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/www/android/cloudsettings.js:
--------------------------------------------------------------------------------
1 | var FILE_NAME = "cloudsettings.json";
2 | var dirPath, filePath;
3 |
4 | var onRestoreFn = function(){};
5 |
6 | var merge = function () {
7 | var destination = {},
8 | sources = [].slice.call(arguments, 0);
9 | sources.forEach(function (source) {
10 | var prop;
11 | for (prop in source) {
12 | if (prop in destination && Array.isArray(destination[prop])) {
13 |
14 | // Concat Arrays
15 | destination[prop] = destination[prop].concat(source[prop]);
16 |
17 | } else if (prop in destination && typeof destination[prop] === "object") {
18 |
19 | // Merge Objects
20 | destination[prop] = merge(destination[prop], source[prop]);
21 |
22 | } else {
23 |
24 | // Set new values
25 | destination[prop] = source[prop];
26 |
27 | }
28 | }
29 | });
30 | return destination;
31 | };
32 |
33 | var resolveFilepath = function(){
34 | if(filePath) return;
35 | dirPath = cordova.file.dataDirectory;
36 | filePath = dirPath + FILE_NAME;
37 | };
38 |
39 | var getFileReader = function() {
40 | const fileReader = new FileReader();
41 | const zoneOriginalInstance = fileReader["__zone_symbol__originalInstance"];
42 | return zoneOriginalInstance || fileReader;
43 | };
44 |
45 | var fail = function (onError, operation, error) {
46 | if(typeof error === "object"){
47 | error = JSON.stringify(error);
48 | }
49 | var msg = "CloudSettingsPlugin ERROR " + operation + ": " + error;
50 | if (onError){
51 | onError(msg);
52 | }else{
53 | console.error(msg);
54 | }
55 | };
56 |
57 | var cloudsettings = {};
58 |
59 | cloudsettings.enableDebug = function(onSuccess) {
60 | return cordova.exec(onSuccess,
61 | null,
62 | 'CloudSettingsPlugin',
63 | 'enableDebug',
64 | []);
65 | };
66 |
67 | cloudsettings.load = function(onSuccess, onError){
68 | resolveFilepath();
69 |
70 | window.resolveLocalFileSystemURL(dirPath, function (dirEntry) {
71 | dirEntry.getFile(FILE_NAME, {
72 | create: false,
73 | exclusive: false
74 | }, function (fileEntry) {
75 | fileEntry.file(function (file) {
76 | var reader = new getFileReader();
77 | reader.onloadend = function() {
78 | try{
79 | var data = JSON.parse(this.result);
80 | }catch(e){
81 | return fail(onError, "parsing file contents to JSON", e.message);
82 | }
83 | try{
84 | onSuccess(data);
85 | }catch(e){
86 | return fail(onError, "calling success callback", e.message);
87 | }
88 | };
89 | reader.readAsText(file);
90 | }, fail.bind(this, onError, "getting file handle"));
91 | }, fail.bind(this, onError, "getting file entry"));
92 | }, fail.bind(this, onError, "resolving storage directory"));
93 | };
94 |
95 | cloudsettings.save = function(settings, onSuccess, onError, overwrite){
96 | if(typeof settings !== "object" || typeof settings.length !== "undefined") throw "settings must be a key/value object!";
97 |
98 | resolveFilepath();
99 |
100 | var doSave = function(){
101 | settings.timestamp = (new Date()).valueOf();
102 | try{
103 | var data = JSON.stringify(settings);
104 | }catch(e){
105 | return fail(onError, "converting settings to JSON", e.message);
106 | }
107 |
108 | window.resolveLocalFileSystemURL(dirPath, function (dirEntry) {
109 | dirEntry.getFile(FILE_NAME, {
110 | create: true,
111 | exclusive: false
112 | }, function (fileEntry) {
113 | fileEntry.createWriter(function (writer) {
114 | writer.onwriteend = function (evt) {
115 | cordova.exec(function(){
116 | try{
117 | onSuccess(settings);
118 | }catch(e){
119 | fail(onError, "calling success callback",e.message);
120 | }
121 | }, fail.bind(this, onError, "requesting backup"), 'CloudSettingsPlugin', 'saveBackup', []);
122 |
123 | };
124 | writer.write(data);
125 | }, fail.bind(this, onError, "creating file writer"));
126 | }, fail.bind(this, onError, "getting file entry"));
127 | }, fail.bind(this, onError, "resolving storage directory"));
128 | };
129 |
130 | if(overwrite){
131 | doSave();
132 | }else{
133 | cloudsettings.exists(function(exists){
134 | if(exists){
135 | // Load stored settings and merge them with new settings
136 | cloudsettings.load(function(stored){
137 | settings = merge(stored, settings);
138 | doSave();
139 | }, onError);
140 | }else{
141 | doSave();
142 | }
143 | });
144 | }
145 | };
146 |
147 | cloudsettings.exists = function(onSuccess){
148 | resolveFilepath();
149 | window.resolveLocalFileSystemURL(filePath, function() {
150 | onSuccess(true);
151 | }, function(){
152 | onSuccess(false);
153 | });
154 | };
155 |
156 | cloudsettings.onRestore = function(fn){
157 | onRestoreFn = fn;
158 | };
159 |
160 | cloudsettings._onRestore = function(){
161 | onRestoreFn();
162 | };
163 |
164 | module.exports = cloudsettings;
165 |
166 |
--------------------------------------------------------------------------------
/src/android/CloudSettingsPlugin.java:
--------------------------------------------------------------------------------
1 | package cordova.plugin.cloudsettings;
2 |
3 | import android.app.Activity;
4 | import android.app.backup.BackupManager;
5 | import android.util.Log;
6 |
7 | import org.apache.cordova.CallbackContext;
8 | import org.apache.cordova.CordovaInterface;
9 | import org.apache.cordova.CordovaPlugin;
10 | import org.apache.cordova.CordovaWebView;
11 | import org.apache.cordova.PluginResult;
12 | import org.json.JSONArray;
13 | import org.json.JSONException;
14 |
15 |
16 | public class CloudSettingsPlugin extends CordovaPlugin {
17 |
18 | static final String LOG_TAG = "CloudSettingsPlugin";
19 | static final String LOG_TAG_JS = "CloudSettingsPlugin[native]";
20 | static final Object sDataLock = new Object();
21 | static String javascriptNamespace = "cordova.plugin.cloudsettings";
22 |
23 | protected boolean debugEnabled = false;
24 |
25 | public static CloudSettingsPlugin instance = null;
26 | static CordovaWebView webView;
27 | static BackupManager bm;
28 |
29 | /**
30 | * Sets the context of the Command. This can then be used to do things like
31 | * get file paths associated with the Activity.
32 | *
33 | * @param cordova The context of the main Activity.
34 | * @param webView The CordovaWebView Cordova is running in.
35 | */
36 | @Override
37 | public void initialize(CordovaInterface cordova, CordovaWebView webView) {
38 | super.initialize(cordova, webView);
39 | bm = new BackupManager(cordova.getActivity().getApplicationContext());
40 | instance = this;
41 | this.webView = webView;
42 | }
43 |
44 | public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
45 | boolean success = true;
46 | try {
47 | if (action.equals("enableDebug")) {
48 | setDebug(true, callbackContext);
49 | }else if (action.equals("saveBackup")) {
50 | saveBackup(args, callbackContext);
51 | }else if (action.equals("saveBackup")) {
52 | saveBackup(args, callbackContext);
53 | }else {
54 | handleError("Invalid action: " + action);
55 | success = false;
56 | }
57 | } catch (Exception e) {
58 | handleException(e);
59 | success = false;
60 | }
61 | return success;
62 | }
63 |
64 | protected void setDebug(boolean enabled, CallbackContext callbackContext) {
65 | debugEnabled = enabled;
66 | d("debug: " + String.valueOf(enabled));
67 | sendPluginResultOk(callbackContext);
68 | }
69 |
70 | protected void saveBackup(JSONArray args, CallbackContext callbackContext) throws JSONException {
71 | d("Requesting Backup");
72 | bm.dataChanged();
73 | sendPluginResultOk(callbackContext);
74 | }
75 |
76 | protected static void handleException(Exception e, String description) {
77 | handleError("EXCEPTION: " + description + ": " + e.getMessage());
78 | }
79 |
80 | protected static void handleException(Exception e) {
81 | handleError("EXCEPTION: " + e.getMessage());
82 | }
83 |
84 | protected static void handleError(String error) {
85 | e(error);
86 | }
87 |
88 | protected static void executeGlobalJavascript(final String jsString) {
89 | instance.getActivity().runOnUiThread(new Runnable() {
90 | @Override
91 | public void run() {
92 | try {
93 | webView.loadUrl("javascript:" + jsString);
94 | } catch (Exception e) {
95 | instance.handleException(e);
96 | }
97 | }
98 | });
99 | }
100 |
101 | protected static void jsCallback(String name) {
102 | String jsStatement = String.format(javascriptNamespace + "[\"%s\"]();", name);
103 | executeGlobalJavascript(jsStatement);
104 | }
105 |
106 | protected static String jsQuoteEscape(String js) {
107 | js = js.replace("\"", "\\\"");
108 | return "\"" + js + "\"";
109 | }
110 |
111 | protected Activity getActivity() {
112 | return this.cordova.getActivity();
113 | }
114 |
115 | protected void sendPluginResultOk(CallbackContext callbackContext) {
116 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
117 | }
118 |
119 | protected void sendPluginResultError(String errorMessage, CallbackContext callbackContext) {
120 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, errorMessage));
121 | }
122 |
123 | protected static void onRestore() {
124 | if(instance != null){
125 | jsCallback("_onRestore");
126 | }
127 | }
128 |
129 | protected static void d(String message) {
130 | Log.d(LOG_TAG, message);
131 | if (instance != null && instance.debugEnabled) {
132 | message = LOG_TAG_JS + ": " + message;
133 | message = instance.jsQuoteEscape(message);
134 | instance.executeGlobalJavascript("console.log("+message+")");
135 | }
136 | }
137 |
138 | protected static void i(String message) {
139 | Log.i(LOG_TAG, message);
140 | if (instance != null && instance.debugEnabled) {
141 | message = LOG_TAG_JS + ": " + message;
142 | message = instance.jsQuoteEscape(message);
143 | instance.executeGlobalJavascript("console.info("+message+")");
144 | }
145 | }
146 |
147 | protected static void w(String message) {
148 | Log.w(LOG_TAG, message);
149 | if (instance != null && instance.debugEnabled) {
150 | message = LOG_TAG_JS + ": " + message;
151 | message = instance.jsQuoteEscape(message);
152 | instance.executeGlobalJavascript("console.warn("+message+")");
153 | }
154 | }
155 |
156 | protected static void e(String message) {
157 | Log.e(LOG_TAG, message);
158 | if (instance != null && instance.debugEnabled) {
159 | message = LOG_TAG_JS + ": " + message;
160 | message = instance.jsQuoteEscape(message);
161 | instance.executeGlobalJavascript("console.error("+message+")");
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/ios/CloudSettingsPlugin.m:
--------------------------------------------------------------------------------
1 | //
2 | // CloudSettingsPlugin.m
3 | // Cordova Cloud Settings
4 | // Copyright (c) by Dave Alden 2018
5 |
6 | #import "CloudSettingsPlugin.h"
7 |
8 | @interface CloudSettingsPlugin (private)
9 | - (void) cloudNotification:(NSNotification *)receivedNotification;
10 | - (void) sendPluginResult: (CDVPluginResult*)result :(CDVInvokedUrlCommand*)command;
11 | - (void) sendPluginResultBool: (BOOL)result :(CDVInvokedUrlCommand*)command;
12 | - (void) sendPluginResultString: (NSString*)result :(CDVInvokedUrlCommand*)command;
13 | - (void) sendPluginSuccess: (CDVInvokedUrlCommand*)command;
14 | - (void) sendPluginError: (NSString*) errorMessage :(CDVInvokedUrlCommand*)command;
15 | - (void) handlePluginException: (NSException*) exception :(CDVInvokedUrlCommand*)command;
16 | - (void)executeGlobalJavascript: (NSString*)jsString;
17 | - (NSString*) arrayToJsonString:(NSArray*)inputArray;
18 | - (NSString*) objectToJsonString:(NSDictionary*)inputObject;
19 | - (NSArray*) jsonStringToArray:(NSString*)jsonStr;
20 | - (NSDictionary*) jsonStringToDictionary:(NSString*)jsonStr;
21 | - (bool)isNull: (NSString*)str;
22 | - (void)jsCallback: (NSString*)name;
23 | - (void)jsCallbackWithArguments: (NSString*)name : (NSString*)arguments;
24 | - (void)d: (NSString*)msg;
25 | - (void)i: (NSString*)msg;
26 | - (void)w: (NSString*)msg;
27 | - (void)e: (NSString*)msg;
28 | - (NSString*)escapeDoubleQuotes: (NSString*)str;
29 | @end
30 |
31 | @implementation CloudSettingsPlugin
32 |
33 | static NSString*const LOG_TAG = @"CloudSettingsPlugin[native]";
34 | static NSString*const KEY = @"settings";
35 |
36 | static NSString*const javascriptNamespace = @"cordova.plugin.cloudsettings";
37 |
38 | /********************************/
39 | #pragma mark - Plugin API
40 | /********************************/
41 |
42 | -(void)enableDebug:(CDVInvokedUrlCommand*)command{
43 | self.debugEnabled = true;
44 | [self d:@"Debug enabled"];
45 | [self sendPluginSuccess:command];
46 | }
47 |
48 | -(void)save:(CDVInvokedUrlCommand *)command
49 | {
50 | [self.commandDelegate runInBackground:^{
51 | @try {
52 | NSString* sNewData = [command.arguments objectAtIndex:0];
53 |
54 | // Store new settings values
55 | [[NSUbiquitousKeyValueStore defaultStore] setString:sNewData forKey:KEY];
56 |
57 | // sync memory values to disk (in preparation for next iCloud sync)
58 | BOOL success = [[NSUbiquitousKeyValueStore defaultStore] synchronize];
59 | if (success){
60 | [self sendPluginSuccess:command];
61 | }else{
62 | [self sendPluginError:@"synchronize failed" :command];
63 | }
64 | }@catch (NSException *exception) {
65 | [self handlePluginException:exception :command];
66 | }
67 | }];
68 | }
69 |
70 |
71 | -(void)load:(CDVInvokedUrlCommand *)command
72 | {
73 | [self.commandDelegate runInBackground:^{
74 | @try {
75 | NSString* sStoredData = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:KEY];
76 | [self sendPluginResultString:sStoredData :command];
77 | }@catch (NSException *exception) {
78 | [self handlePluginException:exception :command];
79 | }
80 | }];
81 | }
82 |
83 | -(void)exists:(CDVInvokedUrlCommand *)command
84 | {
85 | [self.commandDelegate runInBackground:^{
86 | @try {
87 | NSString* sStoredData = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:KEY];
88 | if(sStoredData != nil){
89 | [self sendPluginResultBool:TRUE :command];
90 | }else{
91 | [self sendPluginResultBool:FALSE :command];
92 | }
93 | }@catch (NSException *exception) {
94 | [self handlePluginException:exception :command];
95 | }
96 | }];
97 | }
98 |
99 | - (void)cloudNotification:(NSNotification *)receivedNotification
100 | {
101 | @try {
102 | int cause=[[[receivedNotification userInfo] valueForKey:NSUbiquitousKeyValueStoreChangeReasonKey] intValue];
103 | NSString* msg = @"unknown notification";
104 | switch(cause) {
105 | case NSUbiquitousKeyValueStoreQuotaViolationChange:
106 | msg = @"storage quota exceeded";
107 | [self e:msg];
108 | break;
109 | case NSUbiquitousKeyValueStoreInitialSyncChange:
110 | msg = @"initial sync notification";
111 | [self d:msg];
112 | break;
113 | case NSUbiquitousKeyValueStoreServerChange:
114 | msg = @"change sync notification";
115 | [self d:msg];
116 | break;
117 | }
118 | [self d:[NSString stringWithFormat:@"iCloud notification received: %@", msg]];
119 | [self jsCallbackWithArguments:@"'_onRestore'" :[NSString stringWithFormat:@"'%@'", msg]];
120 | }@catch (NSException *exception) {
121 | [self e:exception.reason];
122 | }
123 | }
124 |
125 | /********************************/
126 | #pragma mark - Internal functions
127 | /********************************/
128 |
129 | - (void)pluginInitialize {
130 | @try {
131 | [super pluginInitialize];
132 | self.debugEnabled = false;
133 | [[NSNotificationCenter defaultCenter] addObserver:self
134 | selector:@selector(cloudNotification:)
135 | name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
136 | object:[NSUbiquitousKeyValueStore defaultStore]];
137 | }@catch (NSException *exception) {
138 | [self e:exception.reason];
139 | }
140 | }
141 |
142 | /********************************/
143 | #pragma mark - Send results
144 | /********************************/
145 |
146 | - (void) sendPluginResult: (CDVPluginResult*)result :(CDVInvokedUrlCommand*)command
147 | {
148 | [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
149 | }
150 |
151 | - (void) sendPluginResultBool: (BOOL)result :(CDVInvokedUrlCommand*)command
152 | {
153 | CDVPluginResult* pluginResult;
154 | if(result) {
155 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:1];
156 | } else {
157 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:0];
158 | }
159 | [self sendPluginResult:pluginResult :command];
160 | }
161 |
162 | - (void) sendPluginResultString: (NSString*)result :(CDVInvokedUrlCommand*)command
163 | {
164 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:result];
165 | [self sendPluginResult:pluginResult :command];
166 | }
167 |
168 | - (void) sendPluginSuccess: (CDVInvokedUrlCommand*)command
169 | {
170 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
171 | [self sendPluginResult:pluginResult :command];
172 | }
173 |
174 | - (void) sendPluginError: (NSString*) errorMessage :(CDVInvokedUrlCommand*)command
175 | {
176 | [self e:errorMessage];
177 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:errorMessage];
178 | [self sendPluginResult:pluginResult :command];
179 | }
180 |
181 | - (void) handlePluginException: (NSException*) exception :(CDVInvokedUrlCommand*)command
182 | {
183 | [self e:[NSString stringWithFormat:@"EXCEPTION: %@", exception.reason]];
184 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:exception.reason];
185 | [self sendPluginResult:pluginResult :command];
186 | }
187 |
188 | - (void)executeGlobalJavascript: (NSString*)jsString
189 | {
190 | [self.commandDelegate evalJs:jsString];
191 | }
192 |
193 | - (NSString*) arrayToJsonString:(NSArray*)inputArray
194 | {
195 | NSError* error;
196 | NSData* jsonData = [NSJSONSerialization dataWithJSONObject:inputArray options:NSJSONWritingPrettyPrinted error:&error];
197 | NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
198 | return jsonString;
199 | }
200 |
201 | - (NSString*) objectToJsonString:(NSDictionary*)inputObject
202 | {
203 | NSError* error;
204 | NSData* jsonData = [NSJSONSerialization dataWithJSONObject:inputObject options:NSJSONWritingPrettyPrinted error:&error];
205 | NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
206 | return jsonString;
207 | }
208 |
209 | - (NSArray*) jsonStringToArray:(NSString*)jsonStr
210 | {
211 | NSError* error = nil;
212 | NSArray* array = [NSJSONSerialization JSONObjectWithData:[jsonStr dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error];
213 | if (error != nil){
214 | array = nil;
215 | }
216 | return array;
217 | }
218 |
219 | - (NSDictionary*) jsonStringToDictionary:(NSString*)jsonStr
220 | {
221 | return (NSDictionary*) [self jsonStringToArray:jsonStr];
222 | }
223 |
224 | - (bool)isNull: (NSString*)str
225 | {
226 | return str == nil || str == (id)[NSNull null] || str.length == 0 || [str isEqual: @""];
227 | }
228 |
229 | - (void)jsCallback: (NSString*)name
230 | {
231 | NSString* jsString = [NSString stringWithFormat:@"%@[%@]", javascriptNamespace, name];
232 | [self executeGlobalJavascript:jsString];
233 | }
234 |
235 | - (void)jsCallbackWithArguments: (NSString*)name : (NSString*)arguments
236 | {
237 | NSString* jsString = [NSString stringWithFormat:@"%@[%@](%@)", javascriptNamespace, name, [self escapeDoubleQuotes:arguments]];
238 | [self executeGlobalJavascript:jsString];
239 | }
240 |
241 | /********************************/
242 | #pragma mark - utility functions
243 | /********************************/
244 |
245 | - (void)d: (NSString*)msg
246 | {
247 | if(self.debugEnabled){
248 | NSLog(@"%@ DEBUG: %@", LOG_TAG, msg);
249 | NSString* jsString = [NSString stringWithFormat:@"console.log(\"%@: %@\")", LOG_TAG, [self escapeDoubleQuotes:msg]];
250 | [self executeGlobalJavascript:jsString];
251 | }
252 | }
253 |
254 | - (void)i: (NSString*)msg
255 | {
256 | if(self.debugEnabled){
257 | NSLog(@"%@ INFO: %@", LOG_TAG, msg);
258 | NSString* jsString = [NSString stringWithFormat:@"console.info(\"%@: %@\")", LOG_TAG, [self escapeDoubleQuotes:msg]];
259 | [self executeGlobalJavascript:jsString];
260 | }
261 | }
262 |
263 | - (void)w: (NSString*)msg
264 | {
265 | if(self.debugEnabled){
266 | NSLog(@"%@ WARN: %@", LOG_TAG, msg);
267 | NSString* jsString = [NSString stringWithFormat:@"console.warn(\"%@: %@\")", LOG_TAG, [self escapeDoubleQuotes:msg]];
268 | [self executeGlobalJavascript:jsString];
269 | }
270 | }
271 |
272 | - (void)e: (NSString*)msg
273 | {
274 | NSLog(@"%@ ERROR: %@", LOG_TAG, msg);
275 | if(self.debugEnabled){
276 | NSString* jsString = [NSString stringWithFormat:@"console.error(\"%@: %@\")", LOG_TAG, [self escapeDoubleQuotes:msg]];
277 | [self executeGlobalJavascript:jsString];
278 | }
279 | }
280 |
281 | - (NSString*)escapeDoubleQuotes: (NSString*)str
282 | {
283 | NSString *result =[str stringByReplacingOccurrencesOfString: @"\"" withString: @"\\\""];
284 | return result;
285 | }
286 |
287 | @end
288 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Cordova Cloud Settings plugin [](https://www.npmjs.com/package/cordova-plugin-cloud-settings)
2 | =====================================================================================================================
3 |
4 | A Cordova plugin for Android & iOS to persist user settings in cloud storage across devices and installs.
5 |
6 |
7 |
8 |
9 | **Table of Contents**
10 |
11 | - [Summary](#summary)
12 | - [Android](#android)
13 | - [iOS](#ios)
14 | - [Installation](#installation)
15 | - [Install the plugin](#install-the-plugin)
16 | - [Usage lifecycle](#usage-lifecycle)
17 | - [API](#api)
18 | - [`exists()`](#exists)
19 | - [Parameters](#parameters)
20 | - [`save()`](#save)
21 | - [Parameters](#parameters-1)
22 | - [`load()`](#load)
23 | - [Parameters](#parameters-2)
24 | - [`onRestore()`](#onrestore)
25 | - [Parameters](#parameters-3)
26 | - [`enableDebug()`](#enabledebug)
27 | - [Parameters](#parameters-4)
28 | - [Testing](#testing)
29 | - [Testing Android](#testing-android)
30 | - [Test backup & restore (automatic)](#test-backup--restore-automatic)
31 | - [Test backup (manual)](#test-backup-manual)
32 | - [Android 7 and above](#android-7-and-above)
33 | - [Android 6](#android-6)
34 | - [Test restore (manual)](#test-restore-manual)
35 | - [Wipe backup data](#wipe-backup-data)
36 | - [Testing iOS](#testing-ios)
37 | - [Test backup](#test-backup)
38 | - [Test restore](#test-restore)
39 | - [Example project](#example-project)
40 | - [Use in GDPR-compliant analytics](#use-in-gdpr-compliant-analytics)
41 | - [GDPR background](#gdpr-background)
42 | - [Impact on user tracking in analytics](#impact-on-user-tracking-in-analytics)
43 | - [Benefits of using this plugin](#benefits-of-using-this-plugin)
44 | - [Authors](#authors)
45 | - [Licence](#licence)
46 |
47 |
48 |
49 | # Summary
50 | This plugin provides a mechanism to store key/value app settings in the form of a JSON structure which will persist in cloud storage so if the user re-installs the app or installs it on a different device, the settings will be restored and available in the new installation.
51 |
52 | Key features:
53 | - Settings are stored using the free native cloud storage solution provided by the platform.
54 | - Out-of-the-box cloud storage with no additional SDKs required.
55 | - No user authentication is required so you can read and store settings immediately without asking user to log in.
56 | - This makes the plugin useful for [GDPR-compliant cross-installation user tracking](#use-in-gdpr-compliant-analytics).
57 | - So stored settings are immediately available to new installs on app startup (no user log in required).
58 |
59 |
60 | Note:
61 | - Settings **cannot** be shared between Android and iOS installations.
62 |
63 | ## Android
64 | The plugin uses [Android's Data Backup service](http://developer.android.com/guide/topics/data/backup.html) to store settings in a file.
65 | - Supports Android 6.0+ (API level 23) and above
66 |
67 | ## iOS
68 |
69 | The plugin uses [iCloud](https://support.apple.com/en-gb/HT207428) to store the settings, specifically the [NSUbiquitousKeyValueStore class](https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore) to store settings in a native K/V store
70 |
71 | Note:
72 | - Supports iOS v5.0 and above
73 | - The amount of storage space available is 1MB per user per app.
74 | - You need to enable iCloud for your App Identifier in the [Apple Member Centre](https://developer.apple.com/membercenter/index.action).
75 |
76 |
77 | # Installation
78 |
79 | The plugin is published to npm as [cordova-plugin-cloud-settings](https://www.npmjs.org/package/cordova-plugin-cloud-settings).
80 |
81 | ## Install the plugin
82 |
83 | ```sh
84 | cordova plugin add cordova-plugin-cloud-settings
85 | ```
86 |
87 | # Usage lifecycle
88 |
89 | A typical lifecycle is as follows:
90 | - User installs your app for the first time
91 | - App starts, calls `exists()`, sees it has no existing settings
92 | - Users uses your app, generates settings: app calls `save()` to backup settings to cloud
93 | - Further use, further backups...
94 | - User downloads your app onto a new device
95 | - App starts, calls `exists()`, sees it has existing settings
96 | - App calls `load()` to access existing settings
97 | - User continues where they left off
98 |
99 | # API
100 |
101 | The plugin's JS API is under the global namespace `cordova.plugin.cloudsettings`.
102 |
103 | ## `exists()`
104 |
105 | `cordova.plugin.cloudsettings.exists(successCallback);`
106 |
107 | Indicates if any stored cloud settings currently exist for the current user.
108 |
109 | ### Parameters
110 | - {function} successCallback - callback function to invoke with the result.
111 | Will be passed a boolean flag which indicates whether an store settings exist for the user.
112 |
113 | ```javascript
114 | cordova.plugin.cloudsettings.exists(function(exists){
115 | if(exists){
116 | console.log("Saved settings exist for the current user");
117 | }else{
118 | console.log("Saved settings do not exist for the current user");
119 | }
120 | });
121 | ```
122 |
123 | ## `save()`
124 |
125 | `cordova.plugin.cloudsettings.save(settings, [successCallback], [errorCallback], [overwrite]);`
126 |
127 | Saves the settings to cloud backup.
128 |
129 | Notes:
130 | - you can pass a deep JSON structure and the plugin will store/merge it with an existing one
131 | - but make sure the structure will [stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) (i.e. no functions, circular references, etc.)
132 | - the cloud backup may not happen immediately on Android or iOS but will occur when the next scheduled backup takes place.
133 |
134 | ### Parameters
135 | - {object} settings - a JSON structure representing the user settings to save to cloud backup.
136 | - {function} successCallback - (optional) callback function to invoke on successfuly saving settings and scheduling for backup.
137 | Will be passed a single object argument which contains the saved settings as a JSON object.
138 | - {function} errorCallback - (optional) callback function to invoke on failure to save settings or schedule for backup.
139 | Will be passed a single string argument which contains a description of the error.
140 | - {boolean} overwrite - (optional) if true, existing settings will be replaced rather than updated. Defaults to false.
141 | - If false, existing settings will be merged with the new settings passed to this function.
142 |
143 | ```javascript
144 | var settings = {
145 | user: {
146 | id: 1678,
147 | name: 'Fred',
148 | preferences: {
149 | mute: true,
150 | locale: 'en_GB'
151 | }
152 | }
153 | }
154 |
155 | cordova.plugin.cloudsettings.save(settings, function(savedSettings){
156 | console.log("Settings successfully saved at " + (new Date(savedSettings.timestamp)).toISOString());
157 | }, function(error){
158 | console.error("Failed to save settings: " + error);
159 | }, false);
160 | ```
161 |
162 | ## `load()`
163 |
164 | `cordova.plugin.cloudsettings.load(successCallback, [errorCallback]);`
165 |
166 | Loads the current settings.
167 |
168 | Note: the settings are loaded locally off disk rather than directly from cloud backup so are not guaranteed to be the latest settings in cloud backup.
169 | If you require conflict resolution of local vs cloud settings, you should save a timestamp when loading/saving your settings and use this to resolve any conflicts.
170 |
171 | ### Parameters
172 | - {function} successCallback - (optional) callback function to invoke on successfuly loading settings.
173 | Will be passed a single object argument which contains the current settings as a JSON object.
174 | - {function} errorCallback - (optional) callback function to invoke on failure to load settings.
175 | Will be passed a single string argument which contains a description of the error.
176 |
177 | ```javascript
178 | cordova.plugin.cloudsettings.load(function(settings){
179 | console.log("Successfully loaded settings: " + console.log(JSON.stringify(settings)));
180 | }, function(error){
181 | console.error("Failed to load settings: " + error);
182 | });
183 | ```
184 |
185 | ## `onRestore()`
186 |
187 | `cordova.plugin.cloudsettings.onRestore(successCallback);`
188 |
189 | Registers a function which will be called if/when settings on the device have been updated from the cloud.
190 |
191 | The purpose of this is to notify your app if current settings have changed due to being updated from the cloud while your app is running.
192 | This may occur, for example, if the user has two devices with your app installed; changing settings on one device will cause them to be synched to the other device.
193 |
194 | When you call `save()`, a `timestamp` key is added to the stored settings object which indicates when the settings were saved.
195 | If necessary, this can be used for conflict resolution.
196 |
197 | ### Parameters
198 | - {function} successCallback - callback function to invoke when device settings have been updated from the cloud.
199 |
200 | ```javascript
201 | cordova.plugin.cloudsettings.onRestore(function(){
202 | console.log("Settings have been updated from the cloud");
203 | });
204 | ```
205 |
206 | ## `enableDebug()`
207 |
208 | `cordova.plugin.cloudsettings.enableDebug(successCallback);`
209 |
210 | Outputs verbose log messages from the native plugin components to the JS console.
211 |
212 | ### Parameters
213 | - {function} successCallback - callback function to invoke when debug mode has been enabled
214 |
215 | ```javascript
216 | cordova.plugin.cloudsettings.enableDebug(function(){
217 | console.log("Debug mode enabled");
218 | });
219 | ```
220 |
221 | # Testing
222 |
223 | ## Testing Android
224 |
225 | ### Test backup & restore (automatic)
226 | To automatically test backup and restore of your app, run `scripts/android/test_cloud_backup.sh` in this repo with the package name of your app. This will initialise and create a backup, then uninstall/reinstall the app to trigger a restore.
227 |
228 | ```bash
229 | $ scripts/android/test_cloud_backup.sh
230 | ```
231 |
232 | ### Test backup (manual)
233 | To test backup of settings you need to manually invoke the backup manager (as instructed in [the Android documentation](https://developer.android.com/guide/topics/data/testingbackup)) to force backing up of the updated values:
234 |
235 | First make sure the backup manager is enabled and setup for verbose logging:
236 |
237 | ```bash
238 | $ adb shell bmgr enabled
239 | $ adb shell setprop log.tag.GmsBackupTransport VERBOSE
240 | $ adb shell setprop log.tag.BackupXmlParserLogging VERBOSE
241 | ```
242 |
243 | The method of testing the backup then depends on the version of Android running of the target device:
244 |
245 | #### Android 7 and above
246 |
247 | Run the following command to perform a backup:
248 |
249 | ```bash
250 | $ adb shell bmgr backupnow
251 | ```
252 |
253 | #### Android 6
254 |
255 | * Run the following command:
256 | ```bash
257 | $ adb shell bmgr backup @pm@ && adb shell bmgr run
258 | ```
259 |
260 | * Wait until the command in the previous step finishes by monitoring `adb logcat` for the following output:
261 | ```
262 | I/BackupManagerService: Backup pass finished.
263 | ```
264 |
265 | * Run the following command to perform a backup:
266 | ```bash
267 | $ adb shell bmgr fullbackup
268 | ```
269 |
270 | ### Test restore (manual)
271 |
272 | To manually initiate a restore, run the following command:
273 | ```bash
274 | $ adb shell bmgr restore
275 | ```
276 |
277 | * To look up backup tokens run `adb shell dumpsys backup`.
278 | * The token is the hexidecimal string following the labels `Ancestral:` and `Current:`
279 | * The ancestral token refers to the backup dataset that was used to restore the device when it was initially setup (with the device-setup wizard).
280 | * The current token refers to the device's current backup dataset (the dataset that the device is currently sending its backup data to).
281 | * You can use a regex to filter the output for just your app ID, for example if your app package ID is `io.cordova.plugin.cloudsettings.test`:
282 | ```bash
283 | $ adb shell dumpsys backup | grep -P '^\S+\: | \: io\.cordova\.plugin\.cloudsettings\.test'
284 | ```
285 |
286 | You also can test automatic restore for your app by uninstalling and reinstalling your app either with adb or through the Google Play Store app.
287 |
288 | ### Wipe backup data
289 | To wipe the backup data for your app:
290 |
291 | ```bash
292 | $ adb shell bmgr list transports
293 | # note the one with an * next to it, it is the transport protocol for your backup
294 | $ adb shell bmgr wipe [* transport-protocol]
295 | ```
296 |
297 | ## Testing iOS
298 |
299 | ### Test backup
300 |
301 | iCloud backups happen periodically, hence saving data via the plugin will write it to disk, but it may not be synced to iCloud immediately.
302 |
303 | To force an iCloud backup immediately (on iOS 11):
304 |
305 | - Open the Settings app
306 | - Select: Accounts & Passwords > iCloud > iCloud Backup > Back Up Now
307 |
308 | ### Test restore
309 |
310 | To test restoring, uninstall then re-install the app to sync the data back from iCloud.
311 |
312 | You can also install the app on 2 iOS devices. Saving data on one should trigger an call to the `onRestore()` callback on the other.
313 |
314 | # Example project
315 |
316 | An example project illustrating/validating use of this plugin can be found here: [https://github.com/dpa99c/cordova-plugin-cloud-settings-test](https://github.com/dpa99c/cordova-plugin-cloud-settings-test)
317 |
318 | # Use in GDPR-compliant analytics
319 |
320 | ## GDPR background
321 | - The EU's [General Data Protection Regulation (GDPR)](https://www.eugdpr.org/) is effective from 25 May 2018.
322 | - It introduces strict controls regarding the use of personal data by apps and websites.
323 | - GDPR distinguishes 3 types of data and associated obligations ([reference](https://iapp.org/media/pdf/resource_center/PA_WP2-Anonymous-pseudonymous-comparison.pdf)):
324 | - Personally Identifiable Information (PII)
325 | - directly identifies an individual
326 | - e.g. unencrypted device ID
327 | - fully obligated under GDPR
328 | - Pseudonmyized data
329 | - indirectly identifies an individual
330 | - e.g. an encypted [one-way hash](https://support.google.com/analytics/answer/6366371?hl=en&ref_topic=2919631) of customer ID
331 | - partially obligated under GDPR
332 | - Anonymous data
333 | - no direct connection to an individual
334 | - e.g. an randomly-generated GUID
335 | - no obligation under GDPR
336 |
337 |
338 | ## Impact on user tracking in analytics
339 | - It can be [useful to track a user ID across app installs](https://support.google.com/analytics/answer/3123663)
340 | - However, GDPR applies to any personal information sent to analytics platforms such as Google Analytics or Firebase.
341 | - If you use PII or pseudonmyized data (such as for the user ID), you are obligated by GDPR to provide the user a mechanism to:
342 | - opt-in to analytics before sending any data (implicit consent is no longer acceptable under GDPR)
343 | - opt-out at a later date if they opt-in (e.g. via an app setting)
344 | - request retrieval or removal their analytics data
345 |
346 | ## Benefits of using this plugin
347 | - This plugin can be used to stored a randomly-generated user ID which persists across app installs and devices.
348 | - Passing this to an analytics service means that user can be (anonymously) tracked across app installs and devices.
349 | - Because the GUID is random and has no association with the user's personal identity, it is classed as **anonymous data** under GDPR and therefore is not obligated.
350 | - This means you don't have to offer opt-in/opt-out and are not obliged to provide retrieval or removal of their analytics data.
351 | - This of course only applies if you aren't sending any other PII or pseudonymous data to analytics.
352 |
353 | # Authors
354 |
355 | - [Dave Alden](https://github.com/dpa99c)
356 |
357 | Major code contributors:
358 | - [Daniel Jackson](https://github.com/cloakedninjas)
359 | - [Alex Drel](https://github.com/alexdrel)
360 |
361 | Based on the plugins:
362 | - https://github.com/cloakedninjas/cordova-plugin-backup
363 | - https://github.com/alexdrel/cordova-plugin-icloudkv
364 | - https://github.com/jcesarmobile/FilePicker-Phonegap-iOS-Plugin
365 |
366 | # Licence
367 |
368 | The MIT License
369 |
370 | Copyright (c) 2018-21, Dave Alden (Working Edge Ltd.)
371 |
372 | Permission is hereby granted, free of charge, to any person obtaining a copy
373 | of this software and associated documentation files (the "Software"), to deal
374 | in the Software without restriction, including without limitation the rights
375 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
376 | copies of the Software, and to permit persons to whom the Software is
377 | furnished to do so, subject to the following conditions:
378 |
379 | The above copyright notice and this permission notice shall be included in
380 | all copies or substantial portions of the Software.
381 |
382 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
383 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
384 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
385 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
386 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
387 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
388 | THE SOFTWARE.
389 |
--------------------------------------------------------------------------------