├── .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 [![Latest Stable Version](https://img.shields.io/npm/v/cordova-plugin-cloud-settings.svg)](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 | --------------------------------------------------------------------------------