├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── .travis.yml ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── app-settings.json ├── appveyor.yml ├── bin ├── after_plugin_install.js ├── after_prepare.js ├── before_plugin_install.js ├── before_plugin_uninstall.js ├── build-app-settings.js ├── lib │ ├── android.js │ ├── filesystem.js │ ├── ios.js │ ├── mappings.js │ ├── path-parse.js │ └── settings.js ├── package.json ├── spec │ ├── android.spec.js │ ├── helpers │ │ └── common.js │ ├── ios.spec.js │ └── support │ │ └── jasmine.json ├── test-server.js └── travis-emu.sh ├── circle.yml ├── package.json ├── plugin.xml ├── src ├── android │ ├── AppPreferences.java │ ├── AppPreferencesActivity.template │ ├── PreferencesActivity.java │ ├── libs │ │ └── android-support-v4.jar │ └── xml │ │ ├── pref_data_sync.xml │ │ ├── pref_general.xml │ │ ├── pref_headers.xml │ │ └── pref_notification.xml ├── blackberry10 │ └── platform.js ├── browser │ └── platform.js ├── ios │ ├── AppPreferences.h │ └── AppPreferences.m ├── osx │ ├── AppPreferences.h │ └── AppPreferences.m ├── platform-android-emulator.patch ├── test.js ├── test.patch ├── windows8 │ ├── apppreferences.html │ └── platform.js └── wp │ └── AppPreferences.cs └── www ├── apppreferences.js └── task └── AppPreferences.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.csproj.user 2 | *.suo 3 | *.cache 4 | Thumbs.db 5 | *.DS_Store 6 | 7 | *.bak 8 | *.cache 9 | *.log 10 | *.swp 11 | *.user 12 | 13 | 14 | .DS_Store 15 | ._* 16 | Device-Debug/src 17 | Device-Release/src 18 | Simulator-Debug/src 19 | 20 | # Xcode 21 | build/ 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | *.xcworkspace 31 | !default.xcworkspace 32 | xcuserdata 33 | profile 34 | *.moved-aside 35 | DerivedData 36 | .idea/ 37 | 38 | bin/node_modules 39 | *.*~ 40 | .*~ 41 | 42 | platforms 43 | app-preferences-app/ 44 | node_modules/ 45 | cordova-lib/ 46 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | # - install 3 | - script 4 | - cleanup 5 | 6 | test: 7 | stage: script 8 | variables: 9 | NODE_ENV: 'development' 10 | script: 11 | - xcrun simctl list 12 | - instruments -s devices 13 | - export PATH=$(pwd)/node_modules/.bin:${PATH} 14 | - export NODE_PATH=$(pwd)/node_modules:$(pwd)/node_modules/cordova/node_modules:$NODE_PATH 15 | - (if [ ! -d node_modules ] ; then npm install ; fi) 16 | - (if [ -d app-preferences-app ] ; then rm -rf app-preferences-app ; fi) 17 | - cordova create app-preferences-app 18 | - echo running jasmine test 19 | - cd bin 20 | - pwd 21 | - jasmine 22 | - cd .. 23 | # let's create cordova app, add and remove plugin a few times 24 | - echo test plugin within cordova app 25 | - cd app-preferences-app 26 | - cordova platform add ios android 27 | - cordova plugin add cordova-plugin-device 28 | - cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 29 | - (if [ ! -f app-settings.json ]; then exit 0; fi) 30 | - cp plugins/cordova-plugin-app-preferences/src/test.js www/js/apppreferences-test.js 31 | - patch -p0 -i plugins/cordova-plugin-app-preferences/src/test.patch 32 | - cordova prepare 33 | - echo plugin must be ok without platforms installed 34 | - cordova platform rm android 35 | - cordova prepare 36 | - cordova platform rm ios 37 | - cordova -d platform add android 38 | - echo patch cordova android emulator 39 | - (patch -p0 -i ../src/platform-android-emulator.patch || exit 0) 40 | - echo check for UIWebView 41 | - cordova platform add ios 42 | - cordova prepare 43 | - (cordova build --emulator ios > /dev/null) 44 | - node ../bin/test-server.js ios 45 | - (android list avd && cordova build --emulator android > /dev/null || exit 0) 46 | - (android list avd && node ../bin/test-server.js android circleci-android22 || exit 0) 47 | 48 | cleanup: 49 | stage: cleanup 50 | script: 51 | - echo ok 52 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | app-preferences-app/ 2 | node_modules/ 3 | cordova-lib/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | # xcode 6 still "Invalid Device State" 3 | osx_image: xcode7 4 | node_js: 5 | - "6" 6 | before_install: 7 | - xcrun simctl list 8 | - instruments -s devices 9 | # - export EMU_TARGET=$(cordova emulate ios --list | grep iPhone-5s | cut -f 2 | head -n 1) 10 | # - export ANDROID_HOME=$PWD/android-sdk-macosx 11 | # - export ANDROID_SDK=$ANDROID_HOME 12 | # - export PATH=$(pwd)/node_modules/.bin:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} 13 | - export PATH=$(pwd)/node_modules/.bin:${PATH} 14 | - export NODE_PATH=$(pwd)/node_modules:$(pwd)/node_modules/cordova/node_modules:$NODE_PATH 15 | # TODO: install android sdk 16 | install: 17 | - (if [ ! -d node_modules ] ; then npm install ; fi) 18 | # xcode version 0.8.0 is not compatible with node 4.x and later 19 | # - find node_modules -type d -name xcode 20 | # - find node_modules -type d -name xcode | grep node_modules/xcode | xargs rm -rf 21 | # - npm install xcode 22 | # - (if [ ! -d cordova-lib/cordova-lib ] ; then git clone https://github.com/apla/cordova-lib -b patch-1 ; fi) 23 | # - npm install ./cordova-lib/cordova-lib 24 | # - rm -rf node_modules/cordova/node_modules/cordova-lib 25 | - (if [ -d app-preferences-app ] ; then rm -rf app-preferences-app ; fi) 26 | # - pwd 27 | # - ls -la 28 | # - ls -la node_modules 29 | # - ls -la node_modules/.bin 30 | - cordova create app-preferences-app 31 | script: 32 | # testing basic functionality of preference generator 33 | - echo running jasmine test 34 | - cd bin 35 | - pwd 36 | - jasmine 37 | - cd .. 38 | # let's create cordova app, add and remove plugin a few times 39 | - echo test plugin within cordova app 40 | - cd app-preferences-app 41 | # - cordova platform add ios@3.9.2 android 42 | - cordova platform add ios android 43 | - cordova plugin add cordova-plugin-device 44 | - cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 45 | - (if [ ! -f app-settings.json ]; then exit 0; fi) 46 | - cp plugins/cordova-plugin-app-preferences/src/test.js www/js/apppreferences-test.js 47 | - patch -p0 -i plugins/cordova-plugin-app-preferences/src/test.patch 48 | - cordova prepare 49 | # - (cordova build --emulator ios > /dev/null) 50 | # - node ../bin/test-server.js ios 51 | - echo plugin must be ok without platforms installed 52 | - cordova platform rm android 53 | - cordova prepare 54 | - cordova platform rm ios 55 | - cordova -d platform add android 56 | - echo patch cordova android emulator 57 | - (patch -p0 -i ../src/platform-android-emulator.patch || exit 0) 58 | - echo check for UIWebView 59 | - cordova platform add ios 60 | - cordova prepare 61 | - (cordova build --emulator ios > /dev/null) 62 | - node ../bin/test-server.js ios 63 | - (android list avd && cordova build --emulator android > /dev/null || exit 0) 64 | - (android list avd && node ../bin/test-server.js android circleci-android22 || exit 0) 65 | - cordova platform add browser 66 | - cordova prepare 67 | - cordova build --emulator browser 68 | - node ../bin/test-server.js browser 69 | - echo plugin can be uninstalled 70 | - cordova plugin rm cordova-plugin-app-preferences 71 | - cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 72 | - cordova prepare 73 | - echo plugin must not rewrite existing Settings.bundle generated by external tool 74 | - rm platforms/ios/Settings.bundle/.me.apla.apppreferences 75 | - (! cordova prepare) 76 | - touch platforms/ios/Settings.bundle/.me.apla.apppreferences 77 | - cordova prepare 78 | - echo plugin must cleanup after remove 79 | - cordova plugin rm cordova-plugin-app-preferences 80 | - (if [ ! -d platforms/ios/Settings.bundle ]; then exit 0 ; fi) 81 | - cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 82 | - cordova prepare 83 | - echo plugin must not rewrite existing Settings.bundle generated by external tool 84 | - rm platforms/ios/Settings.bundle/.me.apla.apppreferences 85 | - cordova plugin rm cordova-plugin-app-preferences 86 | - (if [ -f platforms/ios/Settings.bundle/Root.plist ]; then exit 0 ; fi) 87 | - cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 88 | - cordova plugin add cordova-plugin-console 89 | - touch platforms/ios/Settings.bundle/.me.apla.apppreferences 90 | # prepare and launch ios simulator to test preferences access on ios 91 | - cordova -d prepare 92 | after_script: 93 | - cd .. 94 | - echo rm -rf node_modules 95 | - echo rm -rf app-preferences-app 96 | 97 | 98 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue: 2 | _____ 3 | 4 | 5 | Please specify your environment 6 | 7 | Plugin version: 8 | - [ ] released version: _____ 9 | - [ ] repository master 10 | 11 | Toolchain: 12 | - [ ] Cordova cli 13 | - [ ] Phonegap cli 14 | - [ ] Phonegap cloud 15 | - [ ] Ionic 16 | - [ ] Other: _____ 17 | 18 | Platforms affected: 19 | - [ ] Android 20 | - [ ] iOS/macOS 21 | - [ ] LocalStorage fallback for browser and blackberry 22 | - [ ] Windows and Windows Phone 8.1 and later 23 | - [ ] Windows Phone 8 and earlier (deprecated) 24 | 25 | What the scope of your problem: 26 | - [ ] General functionality (store/fetch/remove/clearAll) 27 | - [ ] Suites 28 | - [ ] Cloud synchronization and events 29 | - [ ] Preferences pane generation and display 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Application preferences plugin for Cordova 3+ 2 | ----------------------- 3 | 4 | Why you should use this plugin? 5 | 6 | * Cordova + Promise interface out of the box 7 | * Supports many platforms (Android, iOS/macOS, Windows and local storage fallback) 8 | * Have tests 9 | (iOS: [![iOS and browser status](https://travis-ci.org/apla/me.apla.cordova.app-preferences.svg)](https://travis-ci.org/apla/me.apla.cordova.app-preferences), 10 | Android: [![Android status](https://circleci.com/gh/apla/me.apla.cordova.app-preferences.svg?&style=shield&circle-token=f3e5e46c1a698c62f0450bf1d25a3694d4f714c6)](https://circleci.com/gh/apla/me.apla.cordova.app-preferences), 11 | Windows: [![Windows status](https://ci.appveyor.com/api/projects/status/gl3qxq2o728sqbev?svg=true)](https://ci.appveyor.com/project/apla/me-apla-cordova-app-preferences), 12 | Browser: [![iOS and browser status](https://travis-ci.org/apla/me.apla.cordova.app-preferences.svg)](https://travis-ci.org/apla/me.apla.cordova.app-preferences)) 13 | * Supports simple and complex data structures 14 | * Supports removal of the keys 15 | * Have preference pane generator for application (for Android and iOS) and can show native preferences 16 | * (Untested) reference change notification [#37](apla/me.apla.cordova.app-preferences#37) 17 | * (Untested) named preferences files for android and iOS suites [#97](apla/me.apla.cordova.app-preferences#97) 18 | * (Untested) synchronized preferences via iCloud or windows roaming [#75](apla/me.apla.cordova.app-preferences#75) 19 | 20 | Installing 21 | --- 22 | 23 | From plugin registry: 24 | 25 | $ cordova plugin add cordova-plugin-app-preferences 26 | 27 | From the repo: 28 | 29 | $ cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 30 | 31 | From a local clone: 32 | 33 | $ cordova plugin add /path/to/me.apla.cordova.app-preferences/folder 34 | 35 | 36 | https://github.com/apla/me.apla.cordova.app-preferences/issues/97 37 | 38 | More information: 39 | [Command-line Interface Guide](http://cordova.apache.org/docs/en/edge/guide_cli_index.md.html#The%20Command-line%20Interface). 40 | 41 | [Using Plugman to Manage Plugins](http://cordova.apache.org/docs/en/edge/guide_plugin_ref_plugman.md.html). 42 | 43 | 44 | Synopsis 45 | --- 46 | 47 | ```javascript 48 | 49 | function ok (value) {} 50 | function fail (error) {} 51 | 52 | var prefs = plugins.appPreferences; 53 | 54 | // cordova interface 55 | 56 | // store key => value pair 57 | prefs.store (ok, fail, 'key', 'value'); 58 | 59 | // store key => value pair in dict (see notes) 60 | prefs.store (ok, fail, 'dict', 'key', 'value'); 61 | 62 | // fetch value by key (value will be delivered through "ok" callback) 63 | prefs.fetch (ok, fail, 'key'); 64 | 65 | // fetch value by key from dict (see notes) 66 | prefs.fetch (ok, fail, 'dict', 'key'); 67 | 68 | // remove value by key 69 | prefs.remove (ok, fail, 'key'); 70 | 71 | // show application preferences 72 | prefs.show (ok, fail); 73 | 74 | // instead of cordova interface you can use promise interface 75 | // you'll receive promise when you won't pass function reference 76 | // as first and second parameter 77 | 78 | // fetch the value for a key using promise 79 | prefs.fetch ('key').then (ok, fail); 80 | 81 | ``` 82 | 83 | [Suites](wiki/Suites) 84 | 85 | ```javascript 86 | 87 | // support for iOS suites or android named preference files (untested) 88 | var suitePrefs = prefs.suite ("suiteName"); 89 | suitePrefs.fetch (...); 90 | suitePrefs.store (...); 91 | 92 | // store preferences in synchronized cloud storage for iOS and windows 93 | var cloudSyncedPrefs = prefs.cloudSync (); 94 | cloudSyncedPrefs.fetch (...); 95 | cloudSyncedPrefs.store (...); 96 | 97 | ``` 98 | 99 | Platforms 100 | --- 101 | 1. Native execution on iOS/macOS using `NSUserDefaults` — docs for [iOS](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/index.html) / [macOS](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/index.html) 102 | 1. Native execution on Android using `android.content.SharedPreferences` — [docs](https://developer.android.com/reference/android/content/SharedPreferences.html) 103 | 1. Native execution on Windows Universal Platform using `Windows.Storage.ApplicationData` — [docs](https://msdn.microsoft.com/en-us/windows.storage.applicationdata) 104 | 1. Native execution on Windows Phone 7 using `IsolatedStorageSettings.ApplicationSettings` — [docs](https://msdn.microsoft.com/library/system.io.isolatedstorage.isolatedstoragesettings.applicationsettings\(vs.95\).aspx) 105 | 1. Execution on BlackBerry10 fallback using `localStorage` 106 | 107 | Notes 108 | --- 109 | 1. iOS, macOS, Android and Windows Phone basic values (`string`, `number`, `boolean`) are stored using typed fields. 110 | 1. Complex values, such as arrays and objects, are always stored using JSON notation. 111 | 1. Dictionaries are supported on iOS and Windows 8 only, so on other platforms instead of using the real dictionary a composite key will be written like `.` 112 | 1. On iOS/macOS dictionaries just a key, so appPrefs.store ('dict', 'key', value) and appPrefs.store ('dict', {'key': value}) have same meaning (but different result). 113 | 114 | Tests 115 | --- 116 | Tests are available in `src/test.js`. After installing plugin you can add test code from this file and then launch `testPlugin()` function. 117 | 118 | * iOS pass locally, Travis: ![iOS and browser status](https://travis-ci.org/apla/me.apla.cordova.app-preferences.svg) 119 | * Android pass locally, CircleCI: ![Android status](https://circleci.com/gh/apla/me.apla.cordova.app-preferences.svg?&style=shield&circle-token=f3e5e46c1a698c62f0450bf1d25a3694d4f714c6) 120 | * BlackBerry 10 pass locally 121 | * Windows Phone 8 tests pass locally, Appveyor: ![Windows status](https://ci.appveyor.com/api/projects/status/gl3qxq2o728sqbev?svg=true) 122 | * Browser pass locally, Travis: ![iOS and browser status](https://travis-ci.org/apla/me.apla.cordova.app-preferences.svg) 123 | 124 | Module update for cordova < 5.x 125 | --- 126 | 127 | Please note that plugin id is changed for npm publishing, so if you used 128 | this plugin before cordova@5.0.0, you'll have to reinstall it: 129 | 130 | $ cordova plugin rm me.apla.cordova.app-preferences 131 | $ cordova plugin add cordova-plugin-app-preferences 132 | 133 | 134 | Show Preference pane 135 | --- 136 | 137 | If you have generated preferences, you can programmatically show preference pane 138 | (Android and iOS at this time). On Android your application show native interface for preferences, 139 | on iOS you'll be switched to the Settings.app with application preferences opened for you. 140 | Either way, you must listen for Cordova resume event to perform preferences synchronization. 141 | 142 | Preferences interface generator 143 | --- 144 | 145 | Preferences generator installed along with plugin and run every time when you prepare you cordova app. 146 | 147 | After plugin installation you can find file `app-settings.json` within your app folder. 148 | 149 | If you want to disable this functionality, please remove `app-settings.json`. 150 | 151 | iOS note: if you have `Settings.bundle` from external source or previous version of generator 152 | `cordova prepare` will fail. Current version of preference generator trying not to rewrite 153 | existing `Settings.bundle` if it is not generated by plugin. It is save to remove `Settings.bundle` 154 | if it came from previous version of plugin. 155 | 156 | ### Supported controls for iOS: 157 | 158 | * group 159 | * combo 160 | * switch 161 | * textfield 162 | 163 | ### Supported controls for Android: 164 | 165 | * group 166 | * combo 167 | * switch - not tested 168 | * textfield - not tested 169 | 170 | TODO: Preferences UI for Windows Phone ([guide](http://blogs.msdn.com/b/glengordon/archive/2012/09/17/managing-settings-in-windows-phone-and-windows-8-store-apps.aspx), [docs](https://msdn.microsoft.com/en-US/library/windows/apps/ff769510\(v=vs.105\).aspx)) 171 | 172 | Credits 173 | --- 174 | 175 | Original version for iOS: 176 | https://github.com/phonegap/phonegap-plugins/tree/master/iOS/ApplicationPreferences 177 | 178 | Another android implementation for cordova 2.x: 179 | https://github.com/macdonst/AppPreferences 180 | -------------------------------------------------------------------------------- /app-settings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type":"group", 4 | "title":"Common", 5 | "items":[{ 6 | "title":"Title", 7 | "type":"textfield", 8 | "key":"title" 9 | }, { 10 | "title":"Language", 11 | "type":"radio", 12 | "key":"lang", 13 | "default": "en-us", 14 | "items":[{ 15 | "value":"en-us", 16 | "title":"English (US)" 17 | }, { 18 | "value":"en-gb", 19 | "title":"English (UK)" 20 | }] 21 | }, { 22 | "title":"Debug", 23 | "type":"toggle", 24 | "default":false, 25 | "key":"debug" 26 | }] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # appveyor file 2 | # http://www.appveyor.com/docs/appveyor-yml 3 | 4 | #environment: 5 | # NODE_PATH: %CD%\node_modules;%NODE_PATH% 6 | # my_var2: value2 7 | 8 | install: 9 | - SET PATH=%CD%\node_modules\.bin;%PATH% 10 | - SET NODE_PATH=%CD%\node_modules;%CD%\node_modules\cordova\node_modules;%NODE_PATH% 11 | - SET __COMPAT_LAYER=RunAsInvoker 12 | - npm install 13 | - cordova create app-preferences-app 14 | 15 | build: off 16 | 17 | test_script: 18 | # testing basic functionality of preference generator 19 | - echo running jasmine test 20 | - cd bin 21 | - jasmine 22 | - cd .. 23 | # let's create cordova app, add and remove plugin a few times 24 | - echo test plugin within cordova app 25 | - cd app-preferences-app 26 | - cordova platform add windows 27 | - cordova plugin add cordova-plugin-device 28 | - cordova plugin add https://github.com/apla/me.apla.cordova.app-preferences 29 | - cp plugins/cordova-plugin-app-preferences/src/test.js www/js/apppreferences-test.js 30 | - patch -p0 -i plugins/cordova-plugin-app-preferences/src/test.patch 31 | - cordova -d build --debug --emulator windows 32 | # cannot emulate on appveyor, need too much work https://github.com/appveyor/ci/issues/201 33 | # - node ../bin/test-server.js windows 34 | -------------------------------------------------------------------------------- /bin/after_plugin_install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (context) { 4 | var req = context.requireCordovaModule, 5 | Q = req('q'), 6 | path = req('path'), 7 | fs = require("./lib/filesystem")(Q, req('fs'), path), 8 | settings = require("./lib/settings")(fs, path), 9 | android = require("./lib/android")(context), 10 | ios = require("./lib/ios")(Q, fs, path, req('plist'), req('xcode')); 11 | 12 | return settings.get() 13 | .then(function (config) { 14 | return Q.all([ 15 | android.afterPluginInstall(config), 16 | // ios.afterPluginInstall(config) // not implemented for iOS 17 | ]); 18 | }) 19 | .catch(function(err) { 20 | if (err.code === 'NEXIST') { 21 | console.log("app-settings.json not found: creating a sample file"); 22 | return settings.create(); 23 | } 24 | 25 | console.log ('unhandled exception', err); 26 | 27 | throw err; 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /bin/after_prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (context) { 4 | var req = context.requireCordovaModule, 5 | Q = req('q'), 6 | path = req('path'), 7 | ET = req('elementtree'), 8 | cordova = req('cordova'), 9 | cordova_lib = cordova.cordova_lib, 10 | cordova_lib_util = req('cordova-lib/src/cordova/util'), 11 | fs = require("./lib/filesystem")(Q, req('fs'), path), 12 | settings = require("./lib/settings")(fs, path), 13 | platforms = {}; 14 | 15 | platforms.android = require("./lib/android")(context); 16 | platforms.ios = require("./lib/ios")(Q, fs, path, req('plist'), req('xcode')); 17 | // platforms.browser = require("./lib/browser")(Q, fs, path, req('plist'), req('xcode')); 18 | 19 | return settings.get() 20 | .then(function (config) { 21 | var promises = []; 22 | context.opts.platforms.forEach (function (platformName) { 23 | if (platforms[platformName] && platforms[platformName].build) { 24 | promises.push (platforms[platformName].build (config)); 25 | } 26 | }); 27 | return Q.all(promises); 28 | }) 29 | .catch(function(err) { 30 | if (err.code === 'NEXIST') { 31 | console.log("app-settings.json not found: skipping build"); 32 | return; 33 | } 34 | 35 | console.log ('unhandled exception', err); 36 | 37 | throw err; 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /bin/before_plugin_install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (context) { 4 | var req = context.requireCordovaModule, 5 | 6 | path = req ('path'), 7 | pathParse = require ('./lib/path-parse'); 8 | 9 | path.parse = path.parse || pathParse; 10 | 11 | return true; 12 | }; 13 | -------------------------------------------------------------------------------- /bin/before_plugin_uninstall.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (context) { 4 | var req = context.requireCordovaModule, 5 | 6 | Q = req('q'), 7 | path = req('path'), 8 | fs = require("./lib/filesystem")(Q, req('fs'), path), 9 | settings = require("./lib/settings")(fs, path), 10 | 11 | android = require("./lib/android")(context), 12 | ios = require("./lib/ios")(Q, fs, path, req('plist'), req('xcode')); 13 | 14 | return settings.get() 15 | .then(function (config) { 16 | return Q.all([ 17 | android.clean(config), 18 | ios.clean(config) 19 | ]); 20 | }) 21 | .then(settings.remove) 22 | .catch(function(err) { 23 | if (err.code === 'NEXIST') { 24 | console.log("app-settings.json not found: skipping clean"); 25 | return; 26 | } 27 | 28 | console.log ('unhandled exception', err); 29 | 30 | throw err; 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /bin/build-app-settings.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var cordova_util, ConfigParser; 6 | (function() { 7 | var cordovaLib = 'cordova', 8 | configParserLib = 'ConfigParser'; 9 | 10 | try { 11 | cordova_util = require (cordovaLib + '/src/util'); 12 | } catch (e) { 13 | cordovaLib = 'cordova/node_modules/cordova-lib'; 14 | configParserLib = 'configparser/ConfigParser'; 15 | } 16 | 17 | try { 18 | cordova_util = require (cordovaLib + '/src/cordova/util'); 19 | } catch (e) { 20 | console.error ('cordova error', e); 21 | } 22 | 23 | try { 24 | ConfigParser = cordova_util.config_parser || cordova_util.configparser; 25 | 26 | if (!ConfigParser) { 27 | ConfigParser = require(cordovaLib + '/src/' + configParserLib); 28 | } 29 | } catch (e) { 30 | console.error ('cordova error', e); 31 | } 32 | })(); 33 | 34 | var Q = require('q'); 35 | var path = require('path'); 36 | var fs = require('./lib/filesystem')(Q, require('fs'), path); 37 | var settings = require("./lib/settings")(fs, path); 38 | 39 | var android = require('./lib/android')(fs, path, require('elementtree'), cordova_util, ConfigParser); 40 | var ios = require('./lib/ios')(Q, fs, path, require('plist'), require('xcode')); 41 | 42 | return settings.get() 43 | .then(function (config) { 44 | return Q.all([ 45 | android.build(config), 46 | ios.build(config) 47 | ]); 48 | }) 49 | .catch(function(err) { 50 | if (err.code === 'NEXIST') { 51 | console.error('app-settings.json not found'); 52 | return; 53 | } 54 | 55 | console.error(err); 56 | console.log(err.stack); 57 | throw err; 58 | }); 59 | -------------------------------------------------------------------------------- /bin/lib/android.js: -------------------------------------------------------------------------------- 1 | var mappings = require("./mappings"), 2 | platformName = "android"; 3 | 4 | module.exports = function (context) { 5 | 6 | var 7 | req = context ? context.requireCordovaModule : require, 8 | Q = req('q'), 9 | path = req('path'), 10 | ET = req('elementtree'), 11 | cordova = req('cordova'), 12 | cordova_lib = cordova.cordova_lib, 13 | ConfigParser = cordova_lib.configparser, 14 | cordova_util = req('cordova-lib/src/cordova/util'), 15 | fs = require("./filesystem")(Q, req('fs'), path), 16 | platforms = {}; 17 | 18 | // fs, path, ET, cordova_util, ConfigParser 19 | 20 | function mapConfig(config) { 21 | var element = { 22 | attrs: {}, 23 | children: [] 24 | }; 25 | 26 | if (!config.type) { 27 | throw "no type defined for "+JSON.stringify (config, null, "\t"); 28 | } 29 | 30 | var mapping = mappings[config.type]; 31 | 32 | if (!mapping) 33 | throw "no mapping for "+ config.type; 34 | 35 | element.tagname = mapping[platformName]; 36 | 37 | if (mapping.required) { 38 | mapping.required.forEach (function (k) { 39 | if (!(k in config)) { 40 | throw ['attribute', k, 'not found for', config.title, '(' + config.type + ')'].join (" "); 41 | } 42 | }); 43 | } 44 | 45 | if (mapping.attrs) { 46 | for (var attrName in mapping.attrs) { 47 | if (!config.hasOwnProperty(attrName)) 48 | continue; 49 | var attrConfig = mapping.attrs[attrName]; 50 | var elementKey = attrConfig[platformName]; 51 | 52 | var targetCheck = elementKey.split ('@'); 53 | var targetAttr; 54 | if (targetCheck.length === 2 && targetCheck[0] === '') { 55 | targetAttr = targetCheck[1]; 56 | if (!element.attrs) 57 | element.attrs = {}; 58 | element.attrs[targetAttr] = []; 59 | } 60 | if (attrConfig.value) { 61 | if (!attrConfig.value[config[attrName]] || !attrConfig.value[config[attrName]][platformName]) 62 | throw "no mapping for type: "+ config.type + ", attr: " + attrName + ", value: " + config[attrName]; 63 | if (targetAttr) 64 | element.attrs[targetAttr].push (attrConfig.value[config[attrName]][platformName]); 65 | else 66 | element[elementKey] = attrConfig.value[config[attrName]][platformName] 67 | } else { 68 | 69 | if (targetAttr) 70 | element.attrs[targetAttr].push (config[attrName]); 71 | else 72 | element[elementKey] = config[attrName]; 73 | } 74 | } 75 | } 76 | 77 | if (mapping.fixup && mapping.fixup[platformName]) { 78 | mapping.fixup[platformName] (element, config, mapping); 79 | } 80 | 81 | return element; 82 | } 83 | 84 | function buildNode(parent, config, stringsArrays) { 85 | 86 | for (var attr in config.attrs) { 87 | if (config.attrs[attr] && config.attrs[attr].constructor === Array) 88 | config.attrs[attr] = config.attrs[attr].join ('|'); 89 | } 90 | 91 | var newNode = new ET.SubElement(parent, config.tagname); 92 | newNode.attrib = config.attrs; 93 | 94 | if (config.strings) { 95 | console.log("will push strings array "+JSON.stringify(config.strings)); 96 | stringsArrays.push(config.strings); 97 | } 98 | 99 | if (config.children) { 100 | config.children.forEach(function(child){ 101 | buildNode(newNode, child, stringsArrays); 102 | }); 103 | } 104 | } 105 | 106 | 107 | // build Android settings XML 108 | function buildSettings(configJson) { 109 | var screenNode = new ET.Element('PreferenceScreen'), 110 | resourcesNode = new ET.Element('resources'), 111 | stringsArrays = []; 112 | 113 | screenNode.set('xmlns:android', 'http://schemas.android.com/apk/res/android'); 114 | 115 | // Generate base settings file 116 | configJson.forEach(function (preference) { 117 | var node = mapConfig(preference); 118 | 119 | if (preference.type === 'group' && preference.items && preference.items.length) { 120 | preference.items.forEach(function(childNode) { 121 | node.children.push(mapConfig(childNode)); 122 | }); 123 | } 124 | 125 | buildNode(screenNode, node, stringsArrays); 126 | }); 127 | 128 | // Generate resource file 129 | stringsArrays.forEach(function (stringsArray) { 130 | var titlesXml = new ET.SubElement(resourcesNode, 'string-array'), 131 | valuesXml = new ET.SubElement(resourcesNode, 'string-array'); 132 | 133 | titlesXml.set("name", "apppreferences_" + stringsArray.name); 134 | valuesXml.set("name", "apppreferences_" + stringsArray.name + 'Values'); 135 | 136 | for (var i=0, l=stringsArray.titles.length; i indent) {delete vname[i]}} 16 | if (length($3) > 0) { 17 | vn=""; for (i=0; i=5.4.1", 27 | "elementtree": "^0.1.6", 28 | "ios-sim": ">=5.0.5", 29 | "jasmine": "^2.3.2", 30 | "plist": "1.1.0", 31 | "q": "^1.4.1", 32 | "serve-me": "^0.8.2" 33 | }, 34 | "keywords": [ 35 | "preferences", 36 | "settings", 37 | "ecosystem:cordova", 38 | "cordova-android", 39 | "cordova-ios", 40 | "cordova-osx", 41 | "cordova-wp7", 42 | "cordova-wp8", 43 | "cordova-windows", 44 | "cordova-windows8", 45 | "cordova-blackberry10" 46 | ], 47 | "author": "Ivan Baktsheev", 48 | "license": "Apache-2.0", 49 | "bugs": { 50 | "url": "https://github.com/apla/me.apla.cordova.app-preferences/issues" 51 | }, 52 | "homepage": "https://github.com/apla/me.apla.cordova.app-preferences" 53 | } 54 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | AppPreferences 9 | Application preferences plugin and preference pane generator 10 | Apache 11 | preferences, settings 12 | https://github.com/apla/me.apla.cordova.app-preferences 13 | https://github.com/apla/me.apla.cordova.app-preferences/issues 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/android/AppPreferences.java: -------------------------------------------------------------------------------- 1 | package me.apla.cordova; 2 | 3 | import org.apache.cordova.CallbackContext; 4 | import org.apache.cordova.CordovaPlugin; 5 | import org.apache.cordova.CordovaWebView; 6 | import org.apache.cordova.PluginResult; 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | import org.json.JSONStringer; 11 | import org.json.JSONTokener; 12 | 13 | import android.content.Context; 14 | import android.content.Intent; 15 | import android.content.SharedPreferences; 16 | import android.content.SharedPreferences.Editor; 17 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 18 | import android.preference.PreferenceManager; 19 | import android.util.Log; 20 | 21 | public class AppPreferences extends CordovaPlugin implements OnSharedPreferenceChangeListener { 22 | 23 | // private static final String LOG_TAG = "AppPreferences"; 24 | // private static final int NO_PROPERTY = 0; 25 | // private static final int NO_PREFERENCE_ACTIVITY = 1; 26 | private static final int COMMIT_FAILED = 2; 27 | private static final int NULL_VALUE = 3; 28 | private static CordovaWebView cdvWebView; 29 | private static boolean watchChanges = false; 30 | 31 | // useful info about default values: http://codetheory.in/saving-user-settings-with-android-preferences/ 32 | @Override 33 | protected void pluginInitialize() { 34 | cdvWebView = this.webView; 35 | 36 | Context context = cordova.getActivity().getApplicationContext(); 37 | 38 | String packageName = context.getPackageName(); 39 | 40 | int resId = context.getResources().getIdentifier("apppreferences", "xml", packageName); 41 | 42 | if (resId > 0) { 43 | PreferenceManager.setDefaultValues(context, resId, false); 44 | } 45 | } 46 | 47 | public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, final String key) { 48 | Log.d("", "PREFERENCE CHANGE DETECTED FOR " + key); 49 | //cordova.getThreadPool().execute(new Runnable() { 50 | // public void run() { 51 | // TODO: use json 52 | cdvWebView.loadUrl("javascript:cordova.fireDocumentEvent('preferencesChanged',{'key': '" + key + "'})"); 53 | // } 54 | //}); 55 | } 56 | 57 | // TODO: 58 | // cloud sync example: https://developers.google.com/games/services/android/savedgames 59 | // real project: https://github.com/takahirom/WearSharedPreferences/blob/master/wear-shared-preferences/src/main/java/com/kogitune/wearsharedpreference/PreferencesSaveService.java 60 | 61 | @Override 62 | public void onResume(boolean multitasking) { 63 | if (this.watchChanges) 64 | PreferenceManager.getDefaultSharedPreferences(cordova.getActivity()) 65 | .registerOnSharedPreferenceChangeListener(this); 66 | } 67 | 68 | @Override 69 | public void onPause(boolean multitasking) { 70 | if (this.watchChanges) 71 | PreferenceManager.getDefaultSharedPreferences(cordova.getActivity()) 72 | .unregisterOnSharedPreferenceChangeListener(this); 73 | } 74 | 75 | @Override 76 | public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { 77 | // String result = ""; 78 | 79 | JSONObject options = new JSONObject(); 80 | if (args.length() > 0) 81 | options = args.optJSONObject (0); 82 | 83 | String suiteName = options.optString("suiteName", null); 84 | 85 | SharedPreferences sharedPrefs; 86 | if (suiteName != null && suiteName != "") { 87 | sharedPrefs = cordova.getActivity().getSharedPreferences(suiteName, Context.MODE_PRIVATE); 88 | } else { 89 | sharedPrefs = PreferenceManager.getDefaultSharedPreferences(cordova.getActivity()); 90 | } 91 | 92 | if (action.equals ("show")) { 93 | return this.showPreferencesActivity(callbackContext); 94 | } else if (action.equals("clearAll")) { 95 | return this.clearAll(sharedPrefs, callbackContext); 96 | } else if (action.equals("watch")) { 97 | watchChanges = options.optBoolean("subscribe", true); 98 | 99 | if (!watchChanges) { 100 | this.onPause(false); 101 | } else { 102 | this.onResume(false); 103 | } 104 | 105 | callbackContext.success(); 106 | return true; 107 | } 108 | 109 | String key = options.getString("key"); 110 | String dict = options.optString("dict"); 111 | 112 | if (!"".equals(dict)) 113 | key = dict + '.' + key; 114 | // Log.d ("", "key is " + key); 115 | 116 | 117 | if (action.equals("fetch")) { 118 | return this.fetchValueByKey(sharedPrefs, key, callbackContext); 119 | } else if (action.equals("store")) { 120 | String value = options.getString("value"); 121 | String type = options.optString("type"); 122 | return this.storeValueByKey(sharedPrefs, key, type, value, callbackContext); 123 | } else if (action.equals("remove")) { 124 | return this.removeValueByKey(sharedPrefs, key, callbackContext); 125 | } 126 | // callbackContext.sendPluginResult(new PluginResult (PluginResult.Status.JSON_EXCEPTION)); 127 | return false; 128 | } 129 | 130 | private boolean clearAll (final SharedPreferences sharedPrefs, final CallbackContext callbackContext) { 131 | cordova.getThreadPool().execute(new Runnable() {public void run() { 132 | 133 | Editor editor = sharedPrefs.edit(); 134 | editor.clear(); 135 | editor.commit(); 136 | if (editor.commit()) { 137 | callbackContext.success(); 138 | } else { 139 | try { 140 | callbackContext.error(createErrorObj(COMMIT_FAILED, "Cannot commit change")); 141 | } catch (JSONException e) { 142 | // TODO Auto-generated catch block 143 | e.printStackTrace(); 144 | } 145 | } 146 | }}); 147 | return true; 148 | } 149 | 150 | /* 151 | private boolean getKeys (final SharedPreferences sharedPrefs, final CallbackContext callbackContext) { 152 | cordova.getThreadPool().execute(new Runnable() {public void run() { 153 | 154 | Map keys = prefs.getAll(); 155 | 156 | String keysJSONArray = null; 157 | try { 158 | JSONStringer jsonArray = new JSONStringer ().array (); 159 | 160 | for (Map.Entry entry : keys.entrySet ()) { 161 | jsonArray.value (entry.getKey ()); 162 | } 163 | 164 | String keysJSONArray = jsonArray.endArray ().toString (); 165 | 166 | } catch (JSONException e) { 167 | 168 | e.printStackTrace (); 169 | callbackContext.error (0); 170 | return; 171 | } 172 | 173 | callbackContext.success(keysJSONArray); 174 | 175 | }}); 176 | return true; 177 | } 178 | */ 179 | 180 | private boolean showPreferencesActivity (final CallbackContext callbackContext) { 181 | cordova.getThreadPool().execute(new Runnable() {public void run() { 182 | Class preferenceActivity; 183 | try { 184 | preferenceActivity = Class.forName("me.apla.cordova.AppPreferencesActivity"); 185 | Intent i = new Intent(cordova.getActivity(), preferenceActivity); 186 | cordova.getActivity().startActivity(i); 187 | String result = null; 188 | callbackContext.success(result); 189 | } catch(ClassNotFoundException e) { 190 | callbackContext.error("Class me.apla.cordova.AppPreferencesActivity not found. Please run preference generator."); 191 | e.printStackTrace(); 192 | } catch (Exception e) { 193 | callbackContext.error("Intent launch error"); 194 | e.printStackTrace(); 195 | } 196 | }}); 197 | return true; 198 | } 199 | 200 | private boolean fetchValueByKey(final SharedPreferences sharedPrefs, final String key, final CallbackContext callbackContext) { 201 | cordova.getThreadPool().execute(new Runnable() {public void run() { 202 | 203 | String returnVal = null; 204 | if (sharedPrefs.contains(key)) { 205 | Object obj = sharedPrefs.getAll().get(key); 206 | String objClass = obj.getClass().getName(); 207 | if (objClass.equals("java.lang.Integer") || objClass.equals("java.lang.Long")) { 208 | returnVal = obj.toString(); 209 | } else if (objClass.equals("java.lang.Float") || objClass.equals("java.lang.Double")) { 210 | returnVal = obj.toString(); 211 | } else if (objClass.equals("java.lang.Boolean")) { 212 | returnVal = (Boolean)obj ? "true" : "false"; 213 | } else if (objClass.equals("java.lang.String")) { 214 | if (sharedPrefs.contains("_" + key + "_type")) { 215 | // here we have json encoded string 216 | returnVal = (String)obj; 217 | } else { 218 | String fakeArray = null; 219 | try { 220 | fakeArray = new JSONStringer().array().value((String)obj).endArray().toString(); 221 | } catch (JSONException e) { 222 | // TODO Auto-generated catch block 223 | e.printStackTrace(); 224 | callbackContext.error(0); 225 | return; 226 | } 227 | returnVal = fakeArray.substring(1, fakeArray.length()-1); 228 | // returnVal = new JSONStringer().value((String)obj).toString(); 229 | } 230 | 231 | } else { 232 | Log.d("", "unhandled type: " + objClass); 233 | } 234 | // JSONObject jsonValue = new JSONObject((Map) obj); 235 | callbackContext.success(returnVal); 236 | } else { 237 | // Log.d("", "no value"); 238 | callbackContext.success(returnVal); 239 | // callbackContext.sendPluginResult(new PluginResult ()); 240 | } 241 | 242 | }}); 243 | 244 | return true; 245 | } 246 | 247 | private boolean removeValueByKey(final SharedPreferences sharedPrefs, final String key, final CallbackContext callbackContext) { 248 | cordova.getThreadPool().execute(new Runnable() { public void run() { 249 | 250 | if (sharedPrefs.contains(key)) { 251 | Editor editor = sharedPrefs.edit(); 252 | editor.remove(key); 253 | if (sharedPrefs.contains("_" + key + "_type")) { 254 | editor.remove("_" + key + "_type"); 255 | } 256 | 257 | if (editor.commit()) { 258 | callbackContext.success(); 259 | } else { 260 | try { 261 | callbackContext.error(createErrorObj(COMMIT_FAILED, "Cannot commit change")); 262 | } catch (JSONException e) { 263 | // TODO Auto-generated catch block 264 | e.printStackTrace(); 265 | } 266 | } 267 | } else { 268 | callbackContext.sendPluginResult(new PluginResult (PluginResult.Status.NO_RESULT)); 269 | } 270 | 271 | }}); 272 | 273 | return true; 274 | } 275 | 276 | private boolean storeValueByKey(final SharedPreferences sharedPrefs, final String key, final String type, final String value, final CallbackContext callbackContext) { 277 | cordova.getThreadPool().execute(new Runnable() {public void run() { 278 | 279 | Editor editor = sharedPrefs.edit(); 280 | // editor.putString(key, value); 281 | 282 | Object nv = null; 283 | try { 284 | JSONTokener jt = new JSONTokener(value); 285 | nv = jt.nextValue(); 286 | } catch (NullPointerException e) { 287 | e.printStackTrace(); 288 | } catch (JSONException e) { 289 | // TODO Auto-generated catch block 290 | e.printStackTrace(); 291 | } 292 | 293 | if(nv == null){ 294 | try { 295 | callbackContext.error(createErrorObj(NULL_VALUE, "Error creating/getting json token")); 296 | return; 297 | } catch (JSONException e) { 298 | // TODO Auto-generated catch block 299 | e.printStackTrace(); 300 | } 301 | } 302 | 303 | String className = nv.getClass().getName(); 304 | 305 | // Log.d("", "value is: " + nv.toString() + " js type is: " + type + " " + args.toString()); 306 | if (type != null) { 307 | if (sharedPrefs.contains("_" + key + "_type")) { 308 | editor.remove("_" + key + "_type"); 309 | } 310 | if (type.equals("string") ) { 311 | editor.putString (key, (String)nv); 312 | } else if (type.equals("number")) { 313 | if (className.equals("java.lang.Double")) { 314 | editor.putFloat(key, ((Double) nv).floatValue()); 315 | } else if (className.equals("java.lang.Integer")) { 316 | editor.putInt(key, (Integer) nv); 317 | } else if (className.equals("java.lang.Long")) { 318 | editor.putLong(key, (Long) nv); 319 | } 320 | } else if (type.equals("boolean")) { 321 | editor.putBoolean (key, (Boolean)nv); 322 | } else { 323 | editor.putString(key, value); 324 | editor.putString ("_" + key + "_type", "json"); 325 | // Log.d("", "complex thing stored"); 326 | } 327 | 328 | } 329 | 330 | if (editor.commit()) { 331 | callbackContext.success(); 332 | } else { 333 | try { 334 | callbackContext.error(createErrorObj(COMMIT_FAILED, "Cannot commit change")); 335 | } catch (JSONException e) { 336 | // TODO Auto-generated catch block 337 | e.printStackTrace(); 338 | } 339 | } 340 | 341 | }}); 342 | 343 | return true; 344 | } 345 | 346 | private JSONObject createErrorObj(int code, String message) throws JSONException { 347 | JSONObject errorObj = new JSONObject(); 348 | errorObj.put("code", code); 349 | errorObj.put("message", message); 350 | return errorObj; 351 | } 352 | 353 | } 354 | -------------------------------------------------------------------------------- /src/android/AppPreferencesActivity.template: -------------------------------------------------------------------------------- 1 | package me.apla.cordova; 2 | 3 | // this is not needed anymore 4 | // import ANDROID_PACKAGE_NAME.R; 5 | 6 | import android.app.Activity; 7 | import android.content.Context; 8 | import android.os.Bundle; 9 | import android.preference.PreferenceActivity; 10 | import android.preference.PreferenceFragment; 11 | 12 | public class AppPreferencesActivity extends PreferenceActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | getFragmentManager().beginTransaction().replace( 18 | android.R.id.content, new AppPFragment() 19 | ).commit(); 20 | } 21 | 22 | public static class AppPFragment extends PreferenceFragment 23 | { 24 | Activity app_activity; 25 | Context app_context; 26 | 27 | @Override 28 | public void onCreate(final Bundle savedInstanceState) 29 | { 30 | super.onCreate(savedInstanceState); 31 | 32 | if (app_context == null) { 33 | app_context = app_activity.getApplicationContext(); 34 | } 35 | 36 | String packageName = app_context.getPackageName(); 37 | 38 | int resId = app_context.getResources().getIdentifier("apppreferences", "xml", packageName); 39 | 40 | if (resId > 0) { 41 | addPreferencesFromResource(resId); 42 | } 43 | 44 | } 45 | 46 | @Override 47 | public void onAttach(Activity act) { 48 | super.onAttach(act); 49 | app_activity = act; 50 | } 51 | 52 | @Override 53 | public void onAttach(Context ctx) { 54 | super.onAttach(ctx); 55 | app_context = ctx; 56 | } 57 | 58 | @Override 59 | public void onDetach() { 60 | super.onDetach(); 61 | app_context = null; 62 | app_activity = null; 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/android/PreferencesActivity.java: -------------------------------------------------------------------------------- 1 | package me.apla.cordova.app-preferences; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.media.Ringtone; 7 | import android.media.RingtoneManager; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.preference.ListPreference; 12 | import android.preference.Preference; 13 | import android.preference.PreferenceActivity; 14 | import android.preference.PreferenceCategory; 15 | import android.preference.PreferenceFragment; 16 | import android.preference.PreferenceManager; 17 | import android.preference.RingtonePreference; 18 | import android.text.TextUtils; 19 | 20 | import {PACKAGE_ID}.R; 21 | 22 | import java.util.List; 23 | 24 | /** 25 | * A {@link PreferenceActivity} that presents a set of application settings. On 26 | * handset devices, settings are presented as a single list. On tablets, 27 | * settings are split by category, with category headers shown to the left of 28 | * the list of settings. 29 | *

30 | * See 31 | * Android Design: Settings for design guidelines and the Settings 33 | * API Guide for more information on developing a Settings UI. 34 | */ 35 | public class PreferencesActivity extends PreferenceActivity { 36 | /** 37 | * Determines whether to always show the simplified settings UI, where 38 | * settings are presented in a single list. When false, settings are shown 39 | * as a master/detail two-pane view on tablets. When true, a single pane is 40 | * shown on tablets. 41 | */ 42 | private static final boolean ALWAYS_SIMPLE_PREFS = false; 43 | 44 | @Override 45 | protected void onPostCreate(Bundle savedInstanceState) { 46 | super.onPostCreate(savedInstanceState); 47 | 48 | setupSimplePreferencesScreen(); 49 | } 50 | 51 | /** 52 | * Shows the simplified settings UI if the device configuration if the 53 | * device configuration dictates that a simplified, single-pane UI should be 54 | * shown. 55 | */ 56 | private void setupSimplePreferencesScreen() { 57 | if (!isSimplePreferences(this)) { 58 | return; 59 | } 60 | 61 | // In the simplified UI, fragments are not used at all and we instead 62 | // use the older PreferenceActivity APIs. 63 | 64 | // Add 'general' preferences. 65 | addPreferencesFromResource(R.xml.pref_general); 66 | 67 | // Add 'notifications' preferences, and a corresponding header. 68 | PreferenceCategory fakeHeader = new PreferenceCategory(this); 69 | fakeHeader.setTitle(R.string.pref_header_notifications); 70 | getPreferenceScreen().addPreference(fakeHeader); 71 | addPreferencesFromResource(R.xml.pref_notification); 72 | 73 | // Add 'data and sync' preferences, and a corresponding header. 74 | fakeHeader = new PreferenceCategory(this); 75 | fakeHeader.setTitle(R.string.pref_header_data_sync); 76 | getPreferenceScreen().addPreference(fakeHeader); 77 | addPreferencesFromResource(R.xml.pref_data_sync); 78 | 79 | // Bind the summaries of EditText/List/Dialog/Ringtone preferences to 80 | // their values. When their values change, their summaries are updated 81 | // to reflect the new value, per the Android Design guidelines. 82 | bindPreferenceSummaryToValue(findPreference("example_text")); 83 | bindPreferenceSummaryToValue(findPreference("example_list")); 84 | bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone")); 85 | bindPreferenceSummaryToValue(findPreference("sync_frequency")); 86 | } 87 | 88 | /** {@inheritDoc} */ 89 | @Override 90 | public boolean onIsMultiPane() { 91 | return isXLargeTablet(this) && !isSimplePreferences(this); 92 | } 93 | 94 | /** 95 | * Helper method to determine if the device has an extra-large screen. For 96 | * example, 10" tablets are extra-large. 97 | */ 98 | private static boolean isXLargeTablet(Context context) { 99 | return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; 100 | } 101 | 102 | /** 103 | * Determines whether the simplified settings UI should be shown. This is 104 | * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device 105 | * doesn't have newer APIs like {@link PreferenceFragment}, or the device 106 | * doesn't have an extra-large screen. In these cases, a single-pane 107 | * "simplified" settings UI should be shown. 108 | */ 109 | private static boolean isSimplePreferences(Context context) { 110 | return ALWAYS_SIMPLE_PREFS 111 | || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB 112 | || !isXLargeTablet(context); 113 | } 114 | 115 | /** {@inheritDoc} */ 116 | @Override 117 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 118 | public void onBuildHeaders(List

target) { 119 | if (!isSimplePreferences(this)) { 120 | loadHeadersFromResource(R.xml.pref_headers, target); 121 | } 122 | } 123 | 124 | /** 125 | * A preference value change listener that updates the preference's summary 126 | * to reflect its new value. 127 | */ 128 | private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { 129 | @Override 130 | public boolean onPreferenceChange(Preference preference, Object value) { 131 | String stringValue = value.toString(); 132 | 133 | if (preference instanceof ListPreference) { 134 | // For list preferences, look up the correct display value in 135 | // the preference's 'entries' list. 136 | ListPreference listPreference = (ListPreference) preference; 137 | int index = listPreference.findIndexOfValue(stringValue); 138 | 139 | // Set the summary to reflect the new value. 140 | preference 141 | .setSummary(index >= 0 ? listPreference.getEntries()[index] 142 | : null); 143 | 144 | } else if (preference instanceof RingtonePreference) { 145 | // For ringtone preferences, look up the correct display value 146 | // using RingtoneManager. 147 | if (TextUtils.isEmpty(stringValue)) { 148 | // Empty values correspond to 'silent' (no ringtone). 149 | preference.setSummary(R.string.pref_ringtone_silent); 150 | 151 | } else { 152 | Ringtone ringtone = RingtoneManager.getRingtone( 153 | preference.getContext(), Uri.parse(stringValue)); 154 | 155 | if (ringtone == null) { 156 | // Clear the summary if there was a lookup error. 157 | preference.setSummary(null); 158 | } else { 159 | // Set the summary to reflect the new ringtone display 160 | // name. 161 | String name = ringtone 162 | .getTitle(preference.getContext()); 163 | preference.setSummary(name); 164 | } 165 | } 166 | 167 | } else { 168 | // For all other preferences, set the summary to the value's 169 | // simple string representation. 170 | preference.setSummary(stringValue); 171 | } 172 | return true; 173 | } 174 | }; 175 | 176 | /** 177 | * Binds a preference's summary to its value. More specifically, when the 178 | * preference's value is changed, its summary (line of text below the 179 | * preference title) is updated to reflect the value. The summary is also 180 | * immediately updated upon calling this method. The exact display format is 181 | * dependent on the type of preference. 182 | * 183 | * @see #sBindPreferenceSummaryToValueListener 184 | */ 185 | private static void bindPreferenceSummaryToValue(Preference preference) { 186 | // Set the listener to watch for value changes. 187 | preference 188 | .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); 189 | 190 | // Trigger the listener immediately with the preference's 191 | // current value. 192 | sBindPreferenceSummaryToValueListener.onPreferenceChange( 193 | preference, 194 | PreferenceManager.getDefaultSharedPreferences( 195 | preference.getContext()).getString(preference.getKey(), 196 | "")); 197 | } 198 | 199 | /** 200 | * This fragment shows general preferences only. It is used when the 201 | * activity is showing a two-pane settings UI. 202 | */ 203 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 204 | public static class GeneralPreferenceFragment extends PreferenceFragment { 205 | @Override 206 | public void onCreate(Bundle savedInstanceState) { 207 | super.onCreate(savedInstanceState); 208 | addPreferencesFromResource(R.xml.pref_general); 209 | 210 | // Bind the summaries of EditText/List/Dialog/Ringtone preferences 211 | // to their values. When their values change, their summaries are 212 | // updated to reflect the new value, per the Android Design 213 | // guidelines. 214 | bindPreferenceSummaryToValue(findPreference("example_text")); 215 | bindPreferenceSummaryToValue(findPreference("example_list")); 216 | } 217 | } 218 | 219 | /** 220 | * This fragment shows notification preferences only. It is used when the 221 | * activity is showing a two-pane settings UI. 222 | */ 223 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 224 | public static class NotificationPreferenceFragment extends 225 | PreferenceFragment { 226 | @Override 227 | public void onCreate(Bundle savedInstanceState) { 228 | super.onCreate(savedInstanceState); 229 | addPreferencesFromResource(R.xml.pref_notification); 230 | 231 | // Bind the summaries of EditText/List/Dialog/Ringtone preferences 232 | // to their values. When their values change, their summaries are 233 | // updated to reflect the new value, per the Android Design 234 | // guidelines. 235 | bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone")); 236 | } 237 | } 238 | 239 | /** 240 | * This fragment shows data and sync preferences only. It is used when the 241 | * activity is showing a two-pane settings UI. 242 | */ 243 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 244 | public static class DataSyncPreferenceFragment extends PreferenceFragment { 245 | @Override 246 | public void onCreate(Bundle savedInstanceState) { 247 | super.onCreate(savedInstanceState); 248 | addPreferencesFromResource(R.xml.pref_data_sync); 249 | 250 | // Bind the summaries of EditText/List/Dialog/Ringtone preferences 251 | // to their values. When their values change, their summaries are 252 | // updated to reflect the new value, per the Android Design 253 | // guidelines. 254 | bindPreferenceSummaryToValue(findPreference("sync_frequency")); 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/android/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apla/me.apla.cordova.app-preferences/2f4924e16df3f3689b7f10750d0847094db563d5/src/android/libs/android-support-v4.jar -------------------------------------------------------------------------------- /src/android/xml/pref_data_sync.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/android/xml/pref_general.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 20 | 21 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/android/xml/pref_headers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
8 |
11 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/android/xml/pref_notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/blackberry10/platform.js: -------------------------------------------------------------------------------- 1 | function AppPreferencesLocalStorage() { 2 | 3 | } 4 | 5 | AppPreferencesLocalStorage.prototype.fetch = function(successCallback, errorCallback, dict, key) { 6 | 7 | var self = this; 8 | 9 | var args = this.prepareKey ('get', dict, key); 10 | 11 | if (!args.key) { 12 | errorCallback (); 13 | return; 14 | } 15 | 16 | var key = args.key; 17 | 18 | if (args.dict) 19 | key = args.dict + '.' + args.key; 20 | 21 | var result = window.localStorage.getItem (key); 22 | 23 | var value = result; 24 | if (result) { 25 | try { 26 | value = JSON.parse (result); 27 | } catch (e) { 28 | } 29 | successCallback (value); 30 | } else { 31 | errorCallback(); 32 | } 33 | }; 34 | 35 | AppPreferencesLocalStorage.prototype.store = function(successCallback, errorCallback, dict, key, value) { 36 | 37 | var self = this; 38 | 39 | var args = this.prepareKey ('set', dict, key, value); 40 | 41 | if (!args.key || args.value === null || args.value === undefined) { 42 | errorCallback (); 43 | return; 44 | } 45 | 46 | var key = args.key; 47 | 48 | if (args.dict) 49 | key = args.dict + '.' + args.key; 50 | 51 | var value = JSON.stringify (args.value); 52 | 53 | window.localStorage.setItem (key, value); 54 | 55 | successCallback (); 56 | }; 57 | 58 | module.exports = new AppPreferencesLocalStorage(); 59 | -------------------------------------------------------------------------------- /src/browser/platform.js: -------------------------------------------------------------------------------- 1 | function AppPreferencesLocalStorage() { 2 | this.watchChanges = false; 3 | } 4 | 5 | AppPreferencesLocalStorage.prototype.nativeFetch = function(successCallback, errorCallback, args) { 6 | 7 | var self = this; 8 | 9 | var key = args.key; 10 | 11 | if (args.dict) 12 | key = args.dict + '.' + args.key; 13 | 14 | var result = window.localStorage.getItem (key); 15 | 16 | var value = result; 17 | try { 18 | value = JSON.parse (result); 19 | } catch (e) { 20 | } 21 | successCallback (value); 22 | }; 23 | 24 | AppPreferencesLocalStorage.prototype.nativeRemove = function(successCallback, errorCallback, args) { 25 | 26 | var self = this; 27 | 28 | var key = args.key; 29 | 30 | if (args.dict) 31 | key = args.dict + '.' + args.key; 32 | 33 | var result = window.localStorage.removeItem (key); 34 | 35 | if (typeof cordova !== "undefined" && this.watchChanges) { 36 | // https://w3c.github.io/webstorage/#the-storage-event 37 | 38 | // If the event is being fired due to an invocation of the setItem() or removeItem() methods, 39 | // the event must have its key attribute initialised to the name of the key in question, 40 | // its oldValue attribute initialised to the old value of the key in question, 41 | // or null if the key is newly added, and its newValue attribute initialised 42 | // to the new value of the key in question, or null if the key was removed. 43 | 44 | // Otherwise, if the event is being fired due to an invocation of the clear() method, 45 | // the event must have its key, oldValue, and newValue attributes initialised to null. 46 | 47 | // In addition, the event must have its url attribute initialised 48 | // to the address of the document whose Storage object was affected; 49 | // and its storageArea attribute initialised to the Storage object from the Window object 50 | // of the target Document that represents the same kind of Storage area as was affected 51 | // (i.e. session or local). 52 | 53 | cordova.fireDocumentEvent('preferencesChanged', {'key': args.key, 'dict': args.dict}); 54 | } 55 | 56 | successCallback (true); 57 | }; 58 | 59 | AppPreferencesLocalStorage.prototype.nativeStore = function(successCallback, errorCallback, args) { 60 | 61 | var self = this; 62 | 63 | var key = args.key; 64 | 65 | if (args.dict) 66 | key = args.dict + '.' + args.key; 67 | 68 | var value = JSON.stringify (args.value); 69 | 70 | window.localStorage.setItem (key, value); 71 | 72 | if (typeof cordova !== "undefined" && this.watchChanges) { 73 | cordova.fireDocumentEvent('preferencesChanged', {'key': args.key, 'dict': args.dict}); 74 | } 75 | 76 | successCallback (); 77 | }; 78 | 79 | AppPreferencesLocalStorage.prototype.clearAll = function (successCallback, errorCallback) { 80 | 81 | var self = this; 82 | 83 | window.localStorage.clear (); 84 | 85 | if (typeof cordova !== "undefined" && this.watchChanges) { 86 | cordova.fireDocumentEvent('preferencesChanged', {'key': null, 'dict': null, all: true}); 87 | } 88 | 89 | successCallback (); 90 | }; 91 | 92 | AppPreferencesLocalStorage.prototype.show = function (successCallback, errorCallback) { 93 | 94 | var self = this; 95 | 96 | errorCallback ('not implemented'); 97 | }; 98 | 99 | AppPreferencesLocalStorage.prototype.watch = function (successCallback, errorCallback, watchChanges) { 100 | // http://dev.w3.org/html5/webstorage/#localStorageEvent 101 | // http://stackoverflow.com/questions/4671852/how-to-bind-to-localstorage-change-event-using-jquery-for-all-browsers 102 | // When the setItem(), removeItem(), and clear() methods are called on a Storage object x 103 | // that is associated with a local storage area, if the methods did something, 104 | // then in every Document object whose Window object's localStorage attribute's 105 | // Storage object is associated with the same storage area, other than x, 106 | // a storage event must be fired 107 | 108 | // In other words, a storage event is fired on every window/tab except for the one 109 | // that updated the localStorage object and caused the event. 110 | 111 | // Firefox seems to have corrected the problem, however IE9 and IE10 still do not follow the spec. 112 | // They fire the event in ALL windows/tabs, including the one that originated the change. 113 | // I've raised this bug with Microsoft here (requires login unfortunately): 114 | // connect.microsoft.com/IE/feedback/details/774798/… – Dave Lockhart Dec 19 '12 at 22:53 115 | 116 | // Doing as a workaround seems to work. – estecb Dec 31 '13 at 8:38 117 | 118 | this.watchChanges = watchChanges === undefined ? true : watchChanges; 119 | } 120 | 121 | if (typeof module !== "undefined") { 122 | module.exports = new AppPreferencesLocalStorage(); 123 | } 124 | -------------------------------------------------------------------------------- /src/ios/AppPreferences.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppPreferences.h 3 | // 4 | // 5 | // Created by Tue Topholm on 31/01/11. 6 | // Copyright 2011 Sugee. All rights reserved. 7 | // 8 | // Modified by Ivan Baktsheev, 2012-2016 9 | // 10 | 11 | #import 12 | 13 | #import 14 | 15 | @interface AppPreferences : CDVPlugin 16 | 17 | - (void)defaultsChanged:(NSNotification *)notification; 18 | - (void)watch:(CDVInvokedUrlCommand*)command; 19 | - (void)fetch:(CDVInvokedUrlCommand*)command; 20 | - (void)remove:(CDVInvokedUrlCommand*)command; 21 | - (void)clearAll:(CDVInvokedUrlCommand*)command; 22 | - (void)show:(CDVInvokedUrlCommand*)command; 23 | - (void)store:(CDVInvokedUrlCommand*)command; 24 | - (NSString*)getSettingFromBundle:(NSString*)settingsName; 25 | 26 | - (NSDictionary*)validateOptions:(CDVInvokedUrlCommand*)command; 27 | - (id)getStoreForOptions:(NSDictionary*)options; 28 | 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /src/ios/AppPreferences.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppPreferences.m 3 | // 4 | // 5 | // Created by Tue Topholm on 31/01/11. 6 | // Copyright 2011 Sugee. All rights reserved. 7 | // 8 | // Modified by Ivan Baktsheev, 2012-2016 9 | // 10 | // THIS HAVEN'T BEEN TESTED WITH CHILD PANELS YET. 11 | 12 | #import "AppPreferences.h" 13 | 14 | @implementation AppPreferences 15 | 16 | - (void)pluginInitialize 17 | { 18 | 19 | } 20 | 21 | // http://useyourloaf.com/blog/sync-preference-data-with-icloud/ 22 | - (void)defaultsChanged:(NSNotification *)notification { 23 | 24 | NSString * jsCallBack = [NSString stringWithFormat:@"cordova.fireDocumentEvent('preferencesChanged');"]; 25 | 26 | // if ([notification.name isEqualToString:NSUserDefaultsDidChangeNotification]) 27 | // else 28 | if ([notification.name isEqualToString:NSUbiquitousKeyValueStoreDidChangeExternallyNotification]) { 29 | 30 | NSNumber *changeReasonNumber = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey]; 31 | if (changeReasonNumber) { 32 | NSInteger changeReason = [changeReasonNumber intValue]; 33 | 34 | // preference store can be synchronized with cloud 35 | // Good sync example: https://github.com/Relfos/TERRA-Engine/blob/7ef17e6b67968a40212fbb678135af0000246097/Engine/OS/iOS/ObjectiveC/TERRA_iCloudSync.m 36 | // Another one: http://useyourloaf.com/blog/sync-preference-data-with-icloud/ 37 | /* 38 | 39 | if (changeReason == NSUbiquitousKeyValueStoreServerChange || changeReason == NSUbiquitousKeyValueStoreInitialSyncChange || changeReason == NSUbiquitousKeyValueStoreAccountChange) { 40 | //id localStore = [self _storeForLocation:CQSettingsLocationDevice]; 41 | //id cloudStore = [self _storeForLocation:CQSettingsLocationCloud]; 42 | 43 | for (NSString *key in notification.userInfo[NSUbiquitousKeyValueStoreChangedKeysKey]) 44 | localStore[key] = cloudStore[key]; 45 | } 46 | 47 | */ 48 | } 49 | } 50 | 51 | // https://github.com/EddyVerbruggen/cordova-plugin-3dtouch/blob/master/src/ios/app/AppDelegate+threedeetouch.m 52 | if ([self.webView respondsToSelector:@selector(stringByEvaluatingJavaScriptFromString:)]) { 53 | // UIWebView 54 | [self.webView performSelectorOnMainThread:@selector(stringByEvaluatingJavaScriptFromString:) withObject:jsCallBack waitUntilDone:NO]; 55 | } else if ([self.webView respondsToSelector:@selector(evaluateJavaScript:completionHandler:)]) { 56 | // WKWebView 57 | [self.webView performSelector:@selector(evaluateJavaScript:completionHandler:) withObject:jsCallBack withObject:nil]; 58 | } else { 59 | NSLog(@"No compatible method found to send notification to the webview. Please notify the plugin author."); 60 | } 61 | } 62 | 63 | 64 | 65 | - (void)watch:(CDVInvokedUrlCommand*)command 66 | { 67 | 68 | __block CDVPluginResult* result = nil; 69 | 70 | NSDictionary* options = [self validateOptions:command]; 71 | 72 | if (!options) 73 | return; 74 | 75 | bool watchChanges = true; 76 | NSNumber *subscribe = [options objectForKey:@"subscribe"]; 77 | if (subscribe != nil) { 78 | watchChanges = [subscribe boolValue]; 79 | } 80 | 81 | if (watchChanges) { 82 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(defaultsChanged:) name:NSUserDefaultsDidChangeNotification object:nil]; 83 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(defaultsChanged:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil]; 84 | } else { 85 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:nil]; 86 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil]; 87 | } 88 | 89 | [self.commandDelegate runInBackground:^{ 90 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 91 | }]; 92 | } 93 | 94 | - (NSDictionary*)validateOptions:(CDVInvokedUrlCommand*)command 95 | { 96 | NSDictionary* options = [[command arguments] objectAtIndex:0]; 97 | 98 | if (!options) { 99 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no options given"]; 100 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 101 | return nil; 102 | } 103 | 104 | return options; 105 | } 106 | 107 | - (id)getStoreForOptions:(NSDictionary*)options 108 | { 109 | NSString *suiteName = [options objectForKey:@"suiteName"]; 110 | NSString *cloudSync = [options objectForKey:@"cloudSync"]; 111 | 112 | id dataStore = nil; 113 | 114 | if (suiteName != nil && ![@"" isEqualToString:suiteName]) { 115 | dataStore = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; 116 | } else if (cloudSync != nil) { 117 | dataStore = [NSUbiquitousKeyValueStore defaultStore]; 118 | } else { 119 | dataStore = [NSUserDefaults standardUserDefaults]; 120 | } 121 | 122 | return dataStore; 123 | } 124 | 125 | - (void)fetch:(CDVInvokedUrlCommand*)command 126 | { 127 | 128 | __block CDVPluginResult* result = nil; 129 | 130 | NSDictionary* options = [self validateOptions:command]; 131 | 132 | if (!options) 133 | return; 134 | 135 | NSString *settingsDict = [options objectForKey:@"dict"]; 136 | NSString *settingsName = [options objectForKey:@"key"]; 137 | 138 | id dataStore = [self getStoreForOptions:options]; 139 | 140 | __block id target = dataStore; 141 | 142 | [self.commandDelegate runInBackground:^{ 143 | 144 | // NSMutableDictionary *mutable = [[dict mutableCopy] autorelease]; 145 | // NSDictionary *dict = [[mutable copy] autorelease]; 146 | 147 | @try { 148 | 149 | NSString *returnVar; 150 | id settingsValue = nil; 151 | 152 | if (settingsDict) { 153 | target = [dataStore dictionaryForKey:settingsDict]; 154 | if (target == nil) { 155 | returnVar = nil; 156 | } 157 | } 158 | 159 | if (target != nil) { 160 | settingsValue = [target objectForKey:settingsName]; 161 | } 162 | 163 | if (settingsValue != nil) { 164 | if ([settingsValue isKindOfClass:[NSString class]]) { 165 | NSString *escaped = [(NSString*)settingsValue stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; 166 | escaped = [escaped stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; 167 | returnVar = [NSString stringWithFormat:@"\"%@\"", escaped]; 168 | } else if ([settingsValue isKindOfClass:[NSNumber class]]) { 169 | if ((NSNumber*)settingsValue == (void*)kCFBooleanFalse || (NSNumber*)settingsValue == (void*)kCFBooleanTrue) { 170 | // const char * x = [(NSNumber*)settingsValue objCType]; 171 | // NSLog(@"boolean %@", [(NSNumber*)settingsValue boolValue] == NO ? @"false" : @"true"); 172 | returnVar = [NSString stringWithFormat:@"%@", [(NSNumber*)settingsValue boolValue] == YES ? @"true": @"false"]; 173 | } else { 174 | // TODO: int, float 175 | // NSLog(@"number"); 176 | returnVar = [NSString stringWithFormat:@"%@", (NSNumber*)settingsValue]; 177 | } 178 | 179 | } else if ([settingsValue isKindOfClass:[NSData class]]) { // NSData 180 | returnVar = [[NSString alloc] initWithData:(NSData*)settingsValue encoding:NSUTF8StringEncoding]; 181 | } 182 | } else { 183 | // TODO: also submit dict 184 | returnVar = [self getSettingFromBundle:settingsName]; //Parsing Root.plist 185 | 186 | // if (returnVar == nil) 187 | // @throw [NSException exceptionWithName:nil reason:@"Key not found" userInfo:nil];; 188 | } 189 | 190 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:returnVar]; 191 | 192 | } @catch (NSException * e) { 193 | 194 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 195 | 196 | } @finally { 197 | 198 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 199 | } 200 | }]; 201 | } 202 | 203 | - (void)remove:(CDVInvokedUrlCommand*)command 204 | { 205 | 206 | __block CDVPluginResult* result = nil; 207 | 208 | NSDictionary* options = [self validateOptions:command]; 209 | 210 | if (!options) 211 | return; 212 | 213 | NSString *settingsDict = [options objectForKey:@"dict"]; 214 | NSString *settingsName = [options objectForKey:@"key"]; 215 | 216 | id dataStore = [self getStoreForOptions:options]; 217 | 218 | __block id target = dataStore; 219 | 220 | //[self.commandDelegate runInBackground:^{ 221 | 222 | @try { 223 | 224 | NSString *returnVar; 225 | 226 | if (settingsDict) { 227 | target = [dataStore dictionaryForKey:settingsDict]; 228 | if (target) 229 | target = [target mutableCopy]; 230 | } 231 | 232 | if (target != nil) { 233 | [target removeObjectForKey:settingsName]; 234 | if (target != dataStore) 235 | [dataStore setObject:(NSMutableDictionary*)target forKey:settingsDict]; 236 | [dataStore synchronize]; 237 | } 238 | 239 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:returnVar]; 240 | 241 | } @catch (NSException * e) { 242 | 243 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 244 | 245 | } @finally { 246 | 247 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 248 | } 249 | //}]; 250 | } 251 | 252 | - (void)clearAll:(CDVInvokedUrlCommand*)command 253 | { 254 | __block CDVPluginResult* result = nil; 255 | 256 | NSDictionary* options = [self validateOptions:command]; 257 | 258 | if (!options) 259 | return; 260 | 261 | NSString *settingsDict = [options objectForKey:@"dict"]; 262 | NSString *suiteName = [options objectForKey:@"suiteName"]; 263 | NSString *cloudSync = [options objectForKey:@"cloudSync"]; 264 | 265 | id dataStore = [self getStoreForOptions:options]; 266 | 267 | __block id target = dataStore; 268 | 269 | //[self.commandDelegate runInBackground:^{ 270 | 271 | @try { 272 | 273 | NSString *appDomain; 274 | 275 | if (suiteName != nil) { 276 | appDomain = suiteName; 277 | [dataStore removePersistentDomainForName:appDomain]; 278 | } else if (cloudSync) { 279 | for (NSString *key in [dataStore allKeys]) { 280 | [dataStore removeObjectForKey:key]; 281 | } 282 | } else { 283 | appDomain = [[NSBundle mainBundle] bundleIdentifier]; 284 | [dataStore removePersistentDomainForName:appDomain]; 285 | } 286 | 287 | [dataStore synchronize]; 288 | 289 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 290 | 291 | } @catch (NSException * e) { 292 | 293 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 294 | 295 | } @finally { 296 | 297 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 298 | } 299 | 300 | //}]; 301 | } 302 | 303 | 304 | - (void)show:(CDVInvokedUrlCommand*)command 305 | { 306 | __block CDVPluginResult* result; 307 | 308 | if(&UIApplicationOpenSettingsURLString != nil) { 309 | 310 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; 311 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 312 | 313 | } else { 314 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"switching to preferences not supported"]; 315 | } 316 | 317 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 318 | 319 | } 320 | 321 | - (void)store:(CDVInvokedUrlCommand*)command 322 | { 323 | __block CDVPluginResult* result; 324 | 325 | NSDictionary* options = [self validateOptions:command]; 326 | 327 | if (!options) 328 | return; 329 | 330 | NSString *settingsDict = [options objectForKey:@"dict"]; 331 | NSString *settingsName = [options objectForKey:@"key"]; 332 | NSString *settingsValue = [options objectForKey:@"value"]; 333 | NSString *settingsType = [options objectForKey:@"type"]; 334 | 335 | // NSLog(@"%@ = %@ (%@)", settingsName, settingsValue, settingsType); 336 | 337 | //[self.commandDelegate runInBackground:^{ 338 | id dataStore = [self getStoreForOptions:options]; 339 | 340 | id target = dataStore; 341 | 342 | // NSMutableDictionary *mutable = [[dict mutableCopy] autorelease]; 343 | // NSDictionary *dict = [[mutable copy] autorelease]; 344 | 345 | if (settingsDict) { 346 | target = [[dataStore dictionaryForKey:settingsDict] mutableCopy]; 347 | if (!target) { 348 | target = [[NSMutableDictionary alloc] init]; 349 | #if !__has_feature(objc_arc) 350 | [target autorelease]; 351 | #endif 352 | } 353 | } 354 | 355 | NSError* error = nil; 356 | id JSONObj = [NSJSONSerialization 357 | JSONObjectWithData:[settingsValue dataUsingEncoding:NSUTF8StringEncoding] 358 | options:NSJSONReadingAllowFragments 359 | error:&error 360 | ]; 361 | 362 | if (error != nil) { 363 | NSLog(@"NSString JSONObject error: %@", [error localizedDescription]); 364 | } 365 | 366 | @try { 367 | 368 | if ([settingsType isEqual: @"string"] && [JSONObj isKindOfClass:[NSString class]]) { 369 | [target setObject:(NSString*)JSONObj forKey:settingsName]; 370 | } else if ([settingsType isEqual: @"number"] && [JSONObj isKindOfClass:[NSNumber class]]) { 371 | [target setObject:(NSNumber*)JSONObj forKey:settingsName]; 372 | // setInteger: forKey, setFloat: forKey: 373 | } else if ([settingsType isEqual: @"boolean"]) { 374 | [target setObject:JSONObj forKey:settingsName]; 375 | } else { 376 | // data 377 | [target setObject:[settingsValue dataUsingEncoding:NSUTF8StringEncoding] forKey:settingsName]; 378 | } 379 | 380 | if (target != dataStore) 381 | [dataStore setObject:(NSMutableDictionary*)target forKey:settingsDict]; 382 | [dataStore synchronize]; 383 | 384 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 385 | 386 | } @catch (NSException * e) { 387 | 388 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 389 | 390 | } @finally { 391 | 392 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 393 | } 394 | //}]; 395 | } 396 | 397 | /* 398 | Parsing the Root.plist for the key, because there is a bug/feature in Settings.bundle 399 | So if the user haven't entered the Settings for the app, the default values aren't accessible through NSUserDefaults. 400 | */ 401 | 402 | - (NSString*)getSettingFromBundle:(NSString*)settingsName 403 | { 404 | NSString *pathStr = [[NSBundle mainBundle] bundlePath]; 405 | NSString *settingsBundlePath = [pathStr stringByAppendingPathComponent:@"Settings.bundle"]; 406 | NSString *finalPath = [settingsBundlePath stringByAppendingPathComponent:@"Root.plist"]; 407 | 408 | NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:finalPath]; 409 | NSArray *prefSpecifierArray = [settingsDict objectForKey:@"PreferenceSpecifiers"]; 410 | NSDictionary *prefItem; 411 | for (prefItem in prefSpecifierArray) 412 | { 413 | if ([[prefItem objectForKey:@"Key"] isEqualToString:settingsName]) 414 | return [prefItem objectForKey:@"DefaultValue"]; 415 | } 416 | return nil; 417 | 418 | } 419 | @end 420 | -------------------------------------------------------------------------------- /src/osx/AppPreferences.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppPreferences.h 3 | // 4 | // 5 | // Created by Tue Topholm on 31/01/11. 6 | // Copyright 2011 Sugee. All rights reserved. 7 | // 8 | // Modified by Ivan Baktsheev, 2012-2015 9 | // Modified by Tobias Bocanegra, 2015 10 | // 11 | 12 | #import 13 | 14 | #import 15 | #import 16 | 17 | @interface AppPreferences : CDVPlugin 18 | 19 | - (void)defaultsChanged:(NSNotification *)notification; 20 | - (void)watch:(CDVInvokedUrlCommand*)command; 21 | - (void)fetch:(CDVInvokedUrlCommand*)command; 22 | - (void)remove:(CDVInvokedUrlCommand*)command; 23 | - (void)clearAll:(CDVInvokedUrlCommand*)command; 24 | - (void)show:(CDVInvokedUrlCommand*)command; 25 | - (void)store:(CDVInvokedUrlCommand*)command; 26 | - (NSString*)getSettingFromBundle:(NSString*)settingsName; 27 | 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /src/osx/AppPreferences.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppPreferences.m 3 | // 4 | // 5 | // Created by Tue Topholm on 31/01/11. 6 | // Copyright 2011 Sugee. All rights reserved. 7 | // 8 | // Modified by Ivan Baktsheev, 2012-2015 9 | // Modified by Tobias Bocanegra, 2015 10 | // 11 | // THIS HAVEN'T BEEN TESTED WITH CHILD PANELS YET. 12 | 13 | #import "AppPreferences.h" 14 | 15 | @implementation AppPreferences 16 | 17 | - (void)pluginInitialize 18 | { 19 | 20 | } 21 | 22 | - (void)defaultsChanged:(NSNotification *)notification { 23 | 24 | NSString * jsCallBack = [NSString stringWithFormat:@"cordova.fireDocumentEvent('preferencesChanged');"]; 25 | 26 | // https://github.com/EddyVerbruggen/cordova-plugin-3dtouch/blob/master/src/ios/app/AppDelegate+threedeetouch.m 27 | if ([self.webView respondsToSelector:@selector(stringByEvaluatingJavaScriptFromString:)]) { 28 | // UIWebView 29 | [self.webView performSelectorOnMainThread:@selector(stringByEvaluatingJavaScriptFromString:) withObject:jsCallBack waitUntilDone:NO]; 30 | } else if ([self.webView respondsToSelector:@selector(evaluateJavaScript:completionHandler:)]) { 31 | // WKWebView 32 | [self.webView performSelector:@selector(evaluateJavaScript:completionHandler:) withObject:jsCallBack withObject:nil]; 33 | } else { 34 | NSLog(@"No compatible method found to send notification to the webview. Please notify the plugin author."); 35 | } 36 | } 37 | 38 | 39 | 40 | - (void)watch:(CDVInvokedUrlCommand*)command 41 | { 42 | 43 | __block CDVPluginResult* result = nil; 44 | 45 | NSNumber *option = command.arguments[0]; 46 | bool watchChanges = true; 47 | if (option) { 48 | watchChanges = [option boolValue]; 49 | } 50 | 51 | if (watchChanges) { 52 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(defaultsChanged) name:NSUserDefaultsDidChangeNotification object:nil]; 53 | } else { 54 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 55 | } 56 | 57 | [self.commandDelegate runInBackground:^{ 58 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 59 | }]; 60 | } 61 | 62 | 63 | 64 | - (void)fetch:(CDVInvokedUrlCommand*)command 65 | { 66 | 67 | __block CDVPluginResult* result = nil; 68 | 69 | NSDictionary* options = command.arguments[0]; 70 | 71 | if (!options) { 72 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no options given"]; 73 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 74 | return; 75 | } 76 | 77 | NSString *settingsDict = options[@"dict"]; 78 | NSString *settingsName = options[@"key"]; 79 | NSString *suiteName = options[@"iosSuiteName"]; 80 | 81 | [self.commandDelegate runInBackground:^{ 82 | 83 | NSUserDefaults *defaults; 84 | 85 | if (suiteName != nil) { 86 | defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; 87 | } else { 88 | defaults = [NSUserDefaults standardUserDefaults]; 89 | } 90 | 91 | 92 | id target = defaults; 93 | 94 | // NSMutableDictionary *mutable = [[dict mutableCopy] autorelease]; 95 | // NSDictionary *dict = [[mutable copy] autorelease]; 96 | 97 | @try { 98 | 99 | NSString *returnVar; 100 | id settingsValue = nil; 101 | 102 | if (settingsDict) { 103 | target = [defaults dictionaryForKey:settingsDict]; 104 | if (target == nil) { 105 | returnVar = nil; 106 | } 107 | } 108 | 109 | if (target != nil) { 110 | settingsValue = [target objectForKey:settingsName]; 111 | } 112 | 113 | if (settingsValue != nil) { 114 | if ([settingsValue isKindOfClass:[NSString class]]) { 115 | NSString *escaped = [(NSString*)settingsValue stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; 116 | escaped = [escaped stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; 117 | returnVar = [NSString stringWithFormat:@"\"%@\"", escaped]; 118 | } else if ([settingsValue isKindOfClass:[NSNumber class]]) { 119 | if ([@YES isEqual:settingsValue]) { 120 | returnVar = @"true"; 121 | } else if ([@NO isEqual:settingsValue]) { 122 | returnVar = @"false"; 123 | } else { 124 | // TODO: int, float 125 | returnVar = [NSString stringWithFormat:@"%@", (NSNumber*)settingsValue]; 126 | } 127 | } else if ([settingsValue isKindOfClass:[NSData class]]) { // NSData 128 | returnVar = [[NSString alloc] initWithData:(NSData*)settingsValue encoding:NSUTF8StringEncoding]; 129 | } 130 | } else { 131 | // TODO: also submit dict 132 | returnVar = [self getSettingFromBundle:settingsName]; //Parsing Root.plist 133 | } 134 | 135 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:returnVar]; 136 | 137 | } @catch (NSException * e) { 138 | 139 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 140 | 141 | } @finally { 142 | 143 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 144 | } 145 | }]; 146 | } 147 | 148 | - (void)remove:(CDVInvokedUrlCommand*)command 149 | { 150 | 151 | __block CDVPluginResult* result = nil; 152 | 153 | NSDictionary* options = command.arguments[0]; 154 | 155 | if (!options) { 156 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no options given"]; 157 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 158 | return; 159 | } 160 | 161 | NSString *settingsDict = options[@"dict"]; 162 | NSString *settingsName = options[@"key"]; 163 | NSString *suiteName = options[@"iosSuiteName"]; 164 | 165 | //[self.commandDelegate runInBackground:^{ 166 | 167 | NSUserDefaults *defaults; 168 | 169 | if (suiteName != nil) { 170 | defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; 171 | } else { 172 | defaults = [NSUserDefaults standardUserDefaults]; 173 | } 174 | 175 | id target = defaults; 176 | 177 | // NSMutableDictionary *mutable = [[dict mutableCopy] autorelease]; 178 | // NSDictionary *dict = [[mutable copy] autorelease]; 179 | 180 | @try { 181 | 182 | NSString *returnVar; 183 | 184 | if (settingsDict) { 185 | target = [defaults dictionaryForKey:settingsDict]; 186 | if (target) 187 | target = [target mutableCopy]; 188 | } 189 | 190 | if (target != nil) { 191 | [target removeObjectForKey:settingsName]; 192 | if (target != defaults) 193 | [defaults setObject:(NSMutableDictionary*)target forKey:settingsDict]; 194 | [defaults synchronize]; 195 | } 196 | 197 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:returnVar]; 198 | 199 | } @catch (NSException * e) { 200 | 201 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 202 | 203 | } @finally { 204 | 205 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 206 | } 207 | //}]; 208 | } 209 | 210 | - (void)clearAll:(CDVInvokedUrlCommand*)command 211 | { 212 | __block CDVPluginResult* result = nil; 213 | 214 | NSDictionary* options = [[command arguments] objectAtIndex:0]; 215 | 216 | if (!options) { 217 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no options given"]; 218 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 219 | return; 220 | } 221 | 222 | NSString *settingsDict = [options objectForKey:@"dict"]; 223 | NSString *suiteName = [options objectForKey:@"iosSuiteName"]; 224 | 225 | //[self.commandDelegate runInBackground:^{ 226 | 227 | @try { 228 | 229 | NSString *returnVar; 230 | 231 | NSUserDefaults *defaults; 232 | NSString *appDomain; 233 | 234 | if (suiteName != nil) { 235 | appDomain = suiteName; 236 | defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; 237 | } else { 238 | appDomain = [[NSBundle mainBundle] bundleIdentifier]; 239 | defaults = [NSUserDefaults standardUserDefaults]; 240 | } 241 | 242 | [defaults removePersistentDomainForName:appDomain]; 243 | 244 | [defaults synchronize]; 245 | 246 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:returnVar]; 247 | 248 | } @catch (NSException * e) { 249 | 250 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 251 | 252 | } @finally { 253 | 254 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 255 | } 256 | 257 | //}]; 258 | } 259 | 260 | - (void)show:(CDVInvokedUrlCommand*)command 261 | { 262 | __block CDVPluginResult* result; 263 | NSLog(@"OSX version of this plugin does not support show() yet."); 264 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"switching to preferences not supported"]; 265 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 266 | 267 | } 268 | 269 | - (void)store:(CDVInvokedUrlCommand*)command 270 | { 271 | __block CDVPluginResult* result; 272 | 273 | NSDictionary* options = command.arguments[0]; 274 | 275 | if (!options) { 276 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no options given"]; 277 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 278 | return; 279 | } 280 | 281 | NSString *settingsDict = options[@"dict"]; 282 | NSString *settingsName = options[@"key"]; 283 | NSString *settingsValue = options[@"value"]; 284 | NSString *settingsType = options[@"type"]; 285 | NSString *suiteName = options[@"iosSuiteName"]; 286 | 287 | // NSLog(@"%@ = %@ (%@)", settingsName, settingsValue, settingsType); 288 | 289 | //[self.commandDelegate runInBackground:^{ 290 | NSUserDefaults *defaults; 291 | 292 | if (suiteName != nil) { 293 | defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; 294 | } else { 295 | defaults = [NSUserDefaults standardUserDefaults]; 296 | } 297 | 298 | id target = defaults; 299 | 300 | // NSMutableDictionary *mutable = [[dict mutableCopy] autorelease]; 301 | // NSDictionary *dict = [[mutable copy] autorelease]; 302 | 303 | if (settingsDict) { 304 | target = [[defaults dictionaryForKey:settingsDict] mutableCopy]; 305 | if (!target) { 306 | target = [[NSMutableDictionary alloc] init]; 307 | #if !__has_feature(objc_arc) 308 | [target autorelease]; 309 | #endif 310 | } 311 | } 312 | 313 | NSError* error = nil; 314 | id JSONObj = [NSJSONSerialization 315 | JSONObjectWithData:[settingsValue dataUsingEncoding:NSUTF8StringEncoding] 316 | options:NSJSONReadingAllowFragments 317 | error:&error 318 | ]; 319 | 320 | if (error != nil) { 321 | NSLog(@"NSString JSONObject error: %@", [error localizedDescription]); 322 | } 323 | 324 | @try { 325 | 326 | if ([settingsType isEqual: @"string"] && [JSONObj isKindOfClass:[NSString class]]) { 327 | [target setObject:(NSString*)JSONObj forKey:settingsName]; 328 | } else if ([settingsType isEqual: @"number"] && [JSONObj isKindOfClass:[NSNumber class]]) { 329 | [target setObject:(NSNumber*)JSONObj forKey:settingsName]; 330 | // setInteger: forKey, setFloat: forKey: 331 | } else if ([settingsType isEqual: @"boolean"]) { 332 | [target setObject:JSONObj forKey:settingsName]; 333 | } else { 334 | // data 335 | [target setObject:[settingsValue dataUsingEncoding:NSUTF8StringEncoding] forKey:settingsName]; 336 | } 337 | 338 | if (target != defaults) 339 | [defaults setObject:(NSMutableDictionary*)target forKey:settingsDict]; 340 | [defaults synchronize]; 341 | 342 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 343 | 344 | } @catch (NSException * e) { 345 | 346 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT messageAsString:[e reason]]; 347 | 348 | } @finally { 349 | 350 | [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; 351 | } 352 | //}]; 353 | } 354 | 355 | /* 356 | Parsing the Root.plist for the key, because there is a bug/feature in Settings.bundle 357 | So if the user haven't entered the Settings for the app, the default values aren't accessible through NSUserDefaults. 358 | */ 359 | 360 | - (NSString*)getSettingFromBundle:(NSString*)settingsName 361 | { 362 | NSString *pathStr = [[NSBundle mainBundle] bundlePath]; 363 | NSString *settingsBundlePath = [pathStr stringByAppendingPathComponent:@"Settings.bundle"]; 364 | NSString *finalPath = [settingsBundlePath stringByAppendingPathComponent:@"Root.plist"]; 365 | 366 | NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:finalPath]; 367 | NSArray *prefSpecifierArray = settingsDict[@"PreferenceSpecifiers"]; 368 | NSDictionary *prefItem; 369 | for (prefItem in prefSpecifierArray) 370 | { 371 | if ([prefItem[@"Key"] isEqualToString:settingsName]) 372 | return prefItem[@"DefaultValue"]; 373 | } 374 | return nil; 375 | 376 | } 377 | @end 378 | -------------------------------------------------------------------------------- /src/platform-android-emulator.patch: -------------------------------------------------------------------------------- 1 | --- platforms/android/cordova/lib/emulator.js.orig 2017-04-26 17:27:16.000000000 +0300 2 | +++ platforms/android/cordova/lib/emulator.js 2017-04-26 17:28:53.000000000 +0300 3 | @@ -53,7 +53,7 @@ 4 | } 5 | */ 6 | module.exports.list_images = function() { 7 | - return spawn('android', ['list', 'avds']) 8 | + return spawn('android', ['list', 'avd']) 9 | .then(function(output) { 10 | var response = output.split('\n'); 11 | var emulator_list = []; 12 | @@ -383,7 +383,7 @@ 13 | function adbInstallWithOptions(target, apk, opts) { 14 | events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...'); 15 | 16 | - var command = 'adb -s ' + target + ' install -r "' + apk + '"'; 17 | + var command = 'adb uninstall "' + pkgName + '"; adb -s ' + target + ' install -r "' + apk + '"'; 18 | return Q.promise(function (resolve, reject) { 19 | child_process.exec(command, opts, function(err, stdout, stderr) { 20 | if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr)); 21 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | function testPlugin (cb, logger) { 2 | var tests = { 3 | "bool-test": true, 4 | "false-test": false, 5 | "float-test": 123.456, 6 | "int-test": 1, 7 | "zero-test": 0, 8 | "string-test": "xxx", 9 | "empty-string-test": "", 10 | "string-with-quotes-test": "xx\"xx", 11 | "obj-test": {a: "b"}, 12 | "arr-test": ["a", "b"], 13 | "empty-arr-test": [] 14 | }; 15 | 16 | var fail = []; 17 | var pass = 0; 18 | 19 | 20 | function addFailure (msg) { 21 | var err = new Error (msg); 22 | var errObject = {}; 23 | for (var k in err) { 24 | errObject[k] = err[k] 25 | } 26 | errObject.message = err.message; 27 | errObject.location = err.stack.split (/\n/)[1].match (/(\:\d+\:\d+)$/)[1]; 28 | 29 | fail.push (msg + ';' + errObject.location); 30 | } 31 | 32 | var nonExistingKeyName = 'test-key-must-not-exists'; 33 | 34 | var appp = (typeof AppPreferences !== "undefined") ? new AppPreferences () : plugins.appPreferences; 35 | 36 | var nativePlatforms = { 37 | iOS: true, 38 | Android: true, 39 | windows: true 40 | }; 41 | 42 | function fetchIncrementStore (keyName) { 43 | var testRunCount; 44 | appp.fetch (keyName).then (function (value) { 45 | testRunCount = value || 0; 46 | testRunCount++; 47 | pass++; 48 | }, function (err) { 49 | console.error (err); 50 | addFailure ('promise '+keyName+' failed'); 51 | }).then (function () { 52 | appp.store (keyName, testRunCount) 53 | }).then (function () { 54 | console.info ("test run #"+testRunCount); 55 | }, function (err) { 56 | console.error (err); 57 | }); 58 | } 59 | 60 | fetchIncrementStore ("test-run-count"); 61 | 62 | appp.fetch ("test-promise").then (function () { 63 | pass++; 64 | }, function (err) { 65 | addFailure ('promise fetch failed'); 66 | }); 67 | 68 | appp.fetch (function (ok) { 69 | if (ok === null) { 70 | console.log ("non existing key fetch result is success with null"); 71 | pass++; 72 | appp.store (function (ok) { 73 | pass++; 74 | appp.fetch (function (ok) { 75 | if (ok !== null && ok) { 76 | pass++; 77 | } else { 78 | addFailure ('fetch>store>fetch '+nonExistingKeyName); 79 | } 80 | appp.remove (function (ok) { 81 | pass++; 82 | }, function (err) { 83 | addFailure ('fetch>store>fetch>remove '+nonExistingKeyName + ', error: '+err); 84 | }, nonExistingKeyName); 85 | }, function (err) { 86 | addFailure ('fetch>store>fetch null '+nonExistingKeyName); 87 | }, nonExistingKeyName); 88 | }, function (err) { 89 | addFailure ('fetch>store '+nonExistingKeyName); 90 | }, nonExistingKeyName, true); 91 | } else { 92 | appp.remove (function (ok) { 93 | pass++; 94 | }, function (err) { 95 | addFailure ('fetch>remove '+nonExistingKeyName + '="'+err+'"'); 96 | }, nonExistingKeyName); 97 | addFailure ('fetch exists '+nonExistingKeyName + '="'+ok+'"'); 98 | } 99 | }, function (err) { 100 | addFailure ('fetch '+nonExistingKeyName); 101 | }, nonExistingKeyName); 102 | 103 | appp.fetch (function (ok) { 104 | if (ok === null) { 105 | pass++; 106 | } else { 107 | addFailure ('fetch not null '+'dict2.'+nonExistingKeyName + '="'+ok+'"'); 108 | } 109 | }, function (err) { 110 | addFailure ('fetch '+'dict2.'+nonExistingKeyName); 111 | }, "dict2", nonExistingKeyName); 112 | 113 | for (var testK in tests) { 114 | (function (testName, testValue) { 115 | console.log ('trying to store', testName); 116 | appp.store (function (ok) { 117 | console.log ('stored', testName); 118 | pass ++; 119 | appp.fetch (function (ok) { 120 | if (ok == testValue || (typeof testValue == "object" && JSON.stringify (ok) == JSON.stringify (testValue))) 121 | pass ++; 122 | else { 123 | console.error ('fetched incorrect value for ' + testName + ': expected ' + JSON.stringify (testValue) + ' got ' + JSON.stringify (ok)); 124 | addFailure ('store>fetch not equal '+testName); 125 | } 126 | }, function (err) { 127 | console.error ('fetch value failed for ' + testName + ' and value ' + testValue); 128 | addFailure ('store>fetch '+testName); 129 | }, testName); 130 | if ('device' in window && device.platform && nativePlatforms[device.platform]) { 131 | // TODO: replace by localStorage fallback module 132 | var lsValue = localStorage.getItem (testName); 133 | if (lsValue === null) { 134 | pass ++; 135 | } else if (lsValue === testValue) { 136 | addFailure ('store>fetch (localStorage) '+testName); 137 | } else { 138 | console.error ('localStorage contains unexpected value: "' + lsValue + '" / "' + testValue + '"'); 139 | pass ++; 140 | } 141 | } 142 | 143 | }, function (err) { 144 | console.error ('store value failed for ' + testName + ' and value ' + testValue); 145 | addFailure ('store '+testName); 146 | }, testName, testValue); 147 | console.log ('trying to store', "dict.x" + testName); 148 | appp.store (function (ok) { 149 | console.log ('stored', "dict.x" + testName); 150 | pass ++; 151 | appp.fetch (function (ok) { 152 | if (ok == testValue || (typeof testValue == "object" && JSON.stringify (ok) == JSON.stringify (testValue))) 153 | pass ++; 154 | else { 155 | console.error ('fetched incorrect value for dict.x' + testName + ': expected ' + JSON.stringify (testValue) + ' got ' + JSON.stringify (ok)); 156 | addFailure ('store>fetch not equal '+'dict.x'+testName); 157 | } 158 | }, function (err) { 159 | console.error ('fetch value failed for ' + "dict.x" + testName + ' and value ' + testValue); 160 | addFailure ('store>fetch '+'dict.x'+testName); 161 | }, "dict", "x" + testName); 162 | }, function (err) { 163 | console.error ('store value failed for ' + "dictx" + testName + ' and value ' + testValue); 164 | addFailure ('store '+'dict.x'+testName); 165 | }, "dict", "x" + testName, testValue); 166 | 167 | }) (testK, tests[testK]); 168 | } 169 | 170 | setTimeout (function () { 171 | var prompt = 'AppPreferences plugin tests'; 172 | 173 | if (fail && fail.length) { 174 | console.error ('%s passed: %d, failed: %d', prompt, pass, fail); 175 | } else { 176 | console.log ('%s — all %d passed', prompt, pass); 177 | } 178 | 179 | cb && cb (pass, fail); 180 | }, 1000); 181 | } 182 | 183 | function testPluginAndCallback () { 184 | var contentTag = ''; 185 | var url = contentTag.split ('"')[1]; 186 | // location.href = url; 187 | var oReq = new XMLHttpRequest(); 188 | oReq.addEventListener("load", function () {}); 189 | 190 | var appp = (typeof AppPreferences !== "undefined") ? new AppPreferences () : plugins.appPreferences; 191 | 192 | // some css fixes 193 | var appNode = document.querySelector ('div.app'); 194 | if (appNode) appNode.style.cssText = "top: 150px;"; 195 | 196 | var deviceReadyNode = document.querySelector ('div#deviceready'); 197 | if (deviceReadyNode) deviceReadyNode.classList.remove ('blink'); 198 | 199 | if (deviceReadyNode) { 200 | var statusNode = document.createElement ('p'); 201 | statusNode.className = 'event test'; 202 | statusNode.style.cssText = 'display: none'; 203 | deviceReadyNode.parentNode.appendChild (statusNode); 204 | 205 | deviceReadyNode.parentNode.appendChild (document.createElement ('p')); 206 | 207 | var showPrefsNode = document.createElement ('p'); 208 | showPrefsNode.className = 'event prefs'; 209 | showPrefsNode.style.cssText = 'display: block'; 210 | deviceReadyNode.parentNode.appendChild (showPrefsNode); 211 | 212 | showPrefsNode.addEventListener ('click', function () {appp.show()}, false); 213 | 214 | } 215 | 216 | // end css fixes 217 | 218 | testPlugin (function (pass, fail) { 219 | 220 | var statusColor; 221 | var statusMessage; 222 | 223 | if (fail.length) { 224 | url += "/test/fail?" + fail.join (';'); 225 | statusColor = '#ba4848'; 226 | statusMessage = 'Tests failed: ' + fail + '/' + (fail + pass); 227 | } else { 228 | url += "/test/success"; 229 | statusColor = '#48bab5'; 230 | statusMessage = 'All tests passed'; 231 | } 232 | 233 | if (deviceReadyNode) { 234 | statusNode.textContent = statusMessage; 235 | statusNode.style.cssText = 'display: block; background-color: '+statusColor+';'; 236 | deviceReadyNode.querySelector ('.received').style.cssText = 'display: none;'; 237 | 238 | showPrefsNode.textContent = 'Show preference pane'; 239 | showPrefsNode.style.cssText = 'display: block; background-color: #2d2d90'; 240 | } 241 | 242 | oReq.open("GET", url); 243 | oReq.send(); 244 | }); 245 | } 246 | -------------------------------------------------------------------------------- /src/test.patch: -------------------------------------------------------------------------------- 1 | --- www/index.html 2016-01-10 02:46:31.000000000 +0300 2 | +++ www/index.html 2016-01-10 02:46:05.000000000 +0300 3 | @@ -44,6 +44,7 @@ 4 | 5 | 6 | 7 | 8 | + 9 | 10 | 11 | --- www/js/apppreferences-test.js 2016-01-11 01:09:18.000000000 +0300 12 | +++ www/js/apppreferences-test.js 2016-01-11 01:36:51.000000000 +0300 13 | @@ -157,3 +157,4 @@ 14 | oReq.send(); 15 | }); 16 | } 17 | +document.addEventListener("deviceready", testPluginAndCallback, false); 18 | --- www/index.html 2016-01-13 18:04:36.000000000 +0300 19 | +++ www/index.html 2016-01-13 18:12:50.000000000 +0300 20 | @@ -28,7 +28,7 @@ 21 | * Disables use of inline scripts in order to mitigate risk of XSS vulnerabilities. To change this: 22 | * Enable inline JS: add 'unsafe-inline' to default-src 23 | --> 24 | - 25 | + 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/windows8/apppreferences.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | App settings flyout 5 | 7 | 10 | 11 | 12 | 13 |
15 | 16 |
17 | 18 |
Defaults
19 | 20 |
21 |
22 |
23 |

Toggle switch

24 |

Use toggle switches to let users set Boolean values.

25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |

Push button

36 |

With a push button, users initiate an immediate action.

37 | 38 | 39 |
40 |
41 |

Select control

42 |

Use the select control to allow users to select one item from a set of text-only items.

43 | 44 | 63 |
64 |
65 |

Hyperlink

66 |

Use a hyperlink when the associated action will take the user out of this flyout.

67 | View privacy statement 68 |
69 |
70 |

Text input box

71 |

Use a text input box to allow users to enter text. Set the type of the text input box according to the type of text you’re capturing from the user (e.g. email or password).

72 | 73 | 74 | 75 |
76 |
77 |

Radio button group

78 |

Lets users choose one item from a small set of mutually exclusive, related options.

79 | 80 | 81 | 82 | 83 |
84 |
85 |
86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/windows8/platform.js: -------------------------------------------------------------------------------- 1 | function AppPreferencesW8() { 2 | 3 | } 4 | 5 | // http://blogs.msdn.com/b/going_metro/archive/2012/04/22/integrating-with-windows-8-settings-charm.aspx 6 | // http://msdn.microsoft.com/en-us/library/windows/apps/hh770544.aspx 7 | // http://www.silverlightshow.net/items/Windows-8-Metro-Add-settings-to-your-application.aspx 8 | // http://blogs.msdn.com/b/glengordon/archive/2012/09/17/managing-settings-in-windows-phone-and-windows-8-store-apps.aspx 9 | 10 | function getContainer (settings, dict, create) { 11 | var hasContainer; 12 | 13 | if (!dict) { 14 | return settings; 15 | } 16 | 17 | hasContainer = settings.containers.hasKey (dict); 18 | 19 | if (!hasContainer) { 20 | 21 | if (create) { 22 | return settings.createContainer(dict, Windows.Storage.ApplicationDataCreateDisposition.Always); 23 | } 24 | 25 | return null; 26 | } 27 | 28 | return settings.containers.lookup(dict) 29 | 30 | } 31 | 32 | function watchEventHandler (event) { 33 | if (typeof cordova !== "undefined" && this.watchChanges) { 34 | cordova.fireDocumentEvent('preferencesChanged', {}); 35 | } 36 | } 37 | 38 | AppPreferencesW8.prototype.nativeWatch = function (args) { 39 | // Occurs when roaming application data is synchronized. 40 | // https://msdn.microsoft.com/en-us/magazine/dn857358.aspx 41 | var applicationData = Windows.Storage.ApplicationData.current; 42 | var eventHandler = watchEventHandler.bind (this); 43 | 44 | 45 | if (args.subscribe) { 46 | applicationData.addEventListener ("datachanged", eventHandler); 47 | } else { 48 | applicationData.removeEventListener ("datachanged", eventHandler); 49 | } 50 | } 51 | 52 | AppPreferencesW8.prototype.nativeFetch = function(successCallback, errorCallback, args) { 53 | 54 | var self = this; 55 | 56 | var settings = Windows.Storage.ApplicationData.current[args.cloudSync ? 'roamingSettings' : 'localSettings']; 57 | 58 | var container = getContainer (settings, args.dict); 59 | 60 | if (container === null) { 61 | return successCallback(null); 62 | } 63 | 64 | var result = null; 65 | 66 | if (container.values.hasKey(args.key)) { 67 | result = container.values[args.key]; 68 | } 69 | 70 | var value = null; 71 | if (result) { 72 | try { 73 | value = JSON.parse (result); 74 | } catch (e) { 75 | value = result; 76 | } 77 | } 78 | successCallback(value); 79 | 80 | // argscheck.checkArgs('fF', 'Device.getInfo', arguments); 81 | // exec(successCallback, errorCallback, "Device", "getDeviceInfo", []); 82 | }; 83 | 84 | AppPreferencesW8.prototype.nativeStore = function(successCallback, errorCallback, args) { 85 | 86 | var self = this; 87 | 88 | args.value = JSON.stringify(args.value); 89 | 90 | var settings = Windows.Storage.ApplicationData.current[args.cloudSync ? 'roamingSettings' : 'localSettings']; 91 | 92 | var container = getContainer (settings, args.dict, true); 93 | 94 | if (container === null) { 95 | return successCallback(null); 96 | } 97 | 98 | var result = null; 99 | 100 | container.values[args.key] = args.value; 101 | 102 | successCallback (); 103 | }; 104 | 105 | AppPreferencesW8.prototype.nativeRemove = function (successCallback, errorCallback, args) { 106 | 107 | var self = this; 108 | 109 | var settings = Windows.Storage.ApplicationData.current[args.cloudSync ? 'roamingSettings' : 'localSettings']; 110 | 111 | var container = getContainer (settings, args.dict); 112 | 113 | if (container === null) { 114 | return successCallback(null); 115 | } 116 | 117 | var result = null; 118 | 119 | if (container.values.hasKey(args.key)) { 120 | result = container.values.remove (args.key); 121 | } 122 | 123 | successCallback(); 124 | }; 125 | 126 | AppPreferencesW8.prototype.nativeClearAll = function (successCallback, errorCallback, args) { 127 | 128 | var self = this; 129 | 130 | var settings = Windows.Storage.ApplicationData.current[args.cloudSync ? 'roamingSettings' : 'localSettings']; 131 | 132 | var container = getContainer (settings, args.dict); 133 | 134 | if (container === null) { 135 | return successCallback(null); 136 | } 137 | 138 | var result = null; 139 | 140 | container.clear (); 141 | 142 | successCallback(); 143 | }; 144 | 145 | AppPreferencesW8.prototype.show = function (successCallback, errorCallback, args) { 146 | 147 | var self = this; 148 | 149 | // The Show method raises an exception if one of the following is true: 150 | // •It is called from a snapped app. 151 | // •It is called when the current app does not have focus. 152 | // •It is called when the pane is already displayed. 153 | 154 | try { 155 | var settingsPane = Windows.UI.ApplicationSettings.SettingsPane.show(); 156 | } catch (e) { 157 | 158 | } 159 | 160 | successCallback(); 161 | 162 | return; 163 | 164 | // adding command to settings charm example 165 | 166 | var settingsPane = Windows.UI.ApplicationSettings.SettingsPane.getForCurrentView(); 167 | 168 | function commandsRequested(eventArgs) { 169 | 170 | var applicationCommands = eventArgs.request.applicationCommands; 171 | 172 | var privacyCommand = new Windows.UI.ApplicationSettings.SettingsCommand('privacy', 'Privacy Policy', function () { 173 | 174 | window.open('index.html'); 175 | 176 | }); 177 | 178 | applicationCommands.append(privacyCommand); 179 | 180 | } 181 | 182 | settingsPane.addEventListener("commandsrequested", commandsRequested); 183 | 184 | return; 185 | 186 | // another way (not works) 187 | "use strict"; 188 | var page = WinJS.UI.Pages.define("/html/4-ProgrammaticInvocation.html", { 189 | ready: function (element, options) { 190 | document.getElementById("scenario4Add").addEventListener("click", scenario4AddSettingsFlyout, false); 191 | document.getElementById("scenario4Show").addEventListener("click", scenario4ShowSettingsFlyout, false); 192 | 193 | // clear out the current on settings handler to ensure scenarios are atomic 194 | WinJS.Application.onsettings = null; 195 | 196 | // Display invocation instructions in the SDK sample output region 197 | WinJS.log && WinJS.log("To show the settings charm, invoke the charm bar by swiping your finger on the right edge of the screen or bringing your mouse to the lower-right corner of the screen, then select Settings. Or you can just press Windows logo + i. To dismiss the settings charm, tap in the application, swipe a screen edge, right click, invoke another charm or application.", "sample", "status"); 198 | } 199 | }); 200 | 201 | function scenario4AddSettingsFlyout() { 202 | WinJS.Application.onsettings = function (e) { 203 | e.detail.applicationcommands = { 204 | "defaults": { 205 | title: "Defaults", 206 | href: "/html/4-SettingsFlyout-Settings.html" 207 | } 208 | }; 209 | WinJS.UI.SettingsFlyout.populateSettings(e); 210 | }; 211 | // Make sure the following is called after the DOM has initialized. Typically this would be part of app initialization 212 | WinJS.Application.start(); 213 | 214 | // Display a status message in the SDK sample output region 215 | WinJS.log && WinJS.log("Defaults settings flyout added from 4-SettingsFlyout-Settings.html", "samples", "status"); 216 | } 217 | 218 | function scenario4ShowSettingsFlyout() { 219 | WinJS.UI.SettingsFlyout.showSettings("defaults", "/html/4-SettingsFlyout-Settings.html"); 220 | 221 | // Display a status message in the SDK sample output region 222 | WinJS.log && WinJS.log("Defaults settings flyout showing", "samples", "status"); 223 | } 224 | }; 225 | 226 | 227 | if (!window.WinJS.UI.SettingsFlyout) { 228 | var scriptElem = document.createElement("script"); 229 | 230 | if (navigator.appVersion.indexOf('MSAppHost/3.0') !== -1) { 231 | // Windows 10 UWP 232 | scriptElem.src = '/WinJS/js/ui.js'; 233 | } else if (navigator.appVersion.indexOf("Windows Phone 8.1;") !== -1) { 234 | // not supported: https://msdn.microsoft.com/en-us/library/windows/apps/hh701253.aspx 235 | // windows phone 8.1 + Mobile IE 11 236 | // scriptElem.src = "//Microsoft.Phone.WinJS.2.1/js/ui.js"; 237 | } else if (navigator.appVersion.indexOf("MSAppHost/2.0;") !== -1) { 238 | // windows 8.1 + IE 11 239 | scriptElem.src = "//Microsoft.WinJS.2.0/js/ui.js"; 240 | } else { 241 | // windows 8.0 + IE 10 242 | scriptElem.src = "//Microsoft.WinJS.1.0/js/ui.js"; 243 | } 244 | scriptElem.addEventListener("load", onWinFlyoutReady); 245 | document.head.appendChild(scriptElem); 246 | } 247 | else { 248 | onWinFlyoutReady(); 249 | } 250 | 251 | function onWinFlyoutReady() { 252 | AddSettingsFlyout(); 253 | } 254 | 255 | function AddSettingsFlyout() { 256 | WinJS.Application.onsettings = function (e) { 257 | e.detail.applicationcommands = { 258 | "Preferences": { 259 | title: "Preferences", 260 | href: "/www/apppreferences.html" 261 | } 262 | }; 263 | WinJS.UI.SettingsFlyout.populateSettings(e); 264 | }; 265 | // Make sure the following is called after the DOM has initialized. Typically this would be part of app initialization 266 | WinJS.Application.start(); 267 | 268 | // Display a status message in the SDK sample output region 269 | WinJS.log && WinJS.log("Defaults settings flyout added from 4-SettingsFlyout-Settings.html", "samples", "status"); 270 | } 271 | 272 | module.exports = new AppPreferencesW8(); 273 | -------------------------------------------------------------------------------- /src/wp/AppPreferences.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO.IsolatedStorage; 4 | using System.Runtime.Serialization; 5 | using System.Runtime.Serialization.Json; 6 | using System.Collections.Generic; 7 | using System.Globalization; 8 | 9 | namespace WPCordovaClassLib.Cordova.Commands 10 | { 11 | 12 | // settings gui, the first one 13 | // http://msdn.microsoft.com/en-us/library/windowsphone/develop/ff769510(v=vs.105).aspx 14 | // http://windowsphone.interoperabilitybridges.com/articles/chapter-7-iphone-to-wp7-application-preference-migration#h2Section3 15 | 16 | // http://msdn.microsoft.com/en-us/library/microsoft.phone.net.networkinformation(v=VS.92).aspx 17 | // http://msdn.microsoft.com/en-us/library/microsoft.phone.net.networkinformation.devicenetworkinformation(v=VS.92).aspx 18 | 19 | public class AppPreferences : BaseCommand 20 | { 21 | public AppPreferences() 22 | { 23 | } 24 | 25 | public class JSONString 26 | { 27 | public string contents; 28 | } 29 | 30 | [DataContract] 31 | public class AppPreferenceArgs 32 | { 33 | [DataMember(Name = "key", IsRequired = true)] 34 | public string key; 35 | [DataMember(Name = "dict", IsRequired = false)] 36 | public string dict; 37 | [DataMember(Name = "value", IsRequired = false)] 38 | public string value; 39 | [DataMember(Name = "type", IsRequired = false)] 40 | public string type; 41 | 42 | public object parsedValue() 43 | { 44 | if (type == "boolean") 45 | { 46 | return value == "true" ? true : false; 47 | } 48 | else if (type == "number") 49 | { 50 | return float.Parse(value, CultureInfo.InvariantCulture); 51 | } 52 | else if (type == "string") 53 | { 54 | return JSON.JsonHelper.Deserialize(value); 55 | } 56 | else 57 | { 58 | var jsonString = new JSONString(); 59 | jsonString.contents = value; 60 | // System.Diagnostics.Debug.WriteLine("\njsonString type for " + (string)value + " is: " + jsonString.GetType() + "\n"); 61 | return jsonString; 62 | } 63 | } 64 | 65 | public string fullKey() 66 | { 67 | if (this.dict != null) 68 | { 69 | return this.dict + '.' + this.key; 70 | } 71 | else 72 | { 73 | return this.key; 74 | } 75 | } 76 | } 77 | 78 | public void fetch(string argsString) 79 | { 80 | AppPreferenceArgs preference; 81 | object value; 82 | string returnVal; 83 | string[] args = JSON.JsonHelper.Deserialize(argsString); 84 | string optionsString = args[0]; 85 | string callbackId = args[1]; 86 | // System.Diagnostics.Debug.WriteLine("\nfetch args: " + argsString + "\n"); 87 | //BrowserOptions opts = JSON.JsonHelper.Deserialize(options); 88 | 89 | try 90 | { 91 | preference = JSON.JsonHelper.Deserialize(optionsString); 92 | IsolatedStorageSettings userSettings = IsolatedStorageSettings.ApplicationSettings; 93 | 94 | userSettings.TryGetValue(preference.fullKey(), out value); 95 | // System.Diagnostics.Debug.WriteLine("\ntype is: " + value.GetType() + "\n"); 96 | if (value is JSONString) 97 | { 98 | returnVal = ((JSONString)value).contents; 99 | } 100 | else 101 | { 102 | returnVal = JSON.JsonHelper.Serialize(value); 103 | 104 | } 105 | // System.Diagnostics.Debug.WriteLine("\nserialized value for " + value.GetType() + " is: " + returnVal + "\n"); 106 | 107 | } 108 | catch (NullReferenceException) 109 | { 110 | DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); 111 | return; 112 | } 113 | catch (Exception) 114 | { 115 | DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); 116 | return; 117 | } 118 | DispatchCommandResult(new PluginResult(PluginResult.Status.OK, returnVal), callbackId); 119 | } 120 | 121 | public void store(string argsString) 122 | { 123 | AppPreferenceArgs preference; 124 | //BrowserOptions opts = JSON.JsonHelper.Deserialize(options); 125 | string[] args = JSON.JsonHelper.Deserialize(argsString); 126 | string optionsString = args[0]; 127 | string callbackId = args[1]; 128 | 129 | try 130 | { 131 | preference = JSON.JsonHelper.Deserialize(optionsString); 132 | IsolatedStorageSettings userSettings = IsolatedStorageSettings.ApplicationSettings; 133 | if (userSettings.Contains(preference.fullKey())) 134 | { 135 | userSettings[preference.fullKey()] = preference.parsedValue(); 136 | } 137 | else 138 | { 139 | userSettings.Add(preference.fullKey(), preference.parsedValue()); 140 | } 141 | userSettings.Save(); 142 | } 143 | catch (Exception) 144 | { 145 | DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); 146 | // System.Diagnostics.Debug.WriteLine("\nJSON Exception was thrown\n"); 147 | return; 148 | } 149 | DispatchCommandResult(new PluginResult(PluginResult.Status.OK, ""), callbackId); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /www/apppreferences.js: -------------------------------------------------------------------------------- 1 | var platform = {}; 2 | 3 | if (typeof AppPreferencesLocalStorage === "undefined") { 4 | try { 5 | platform = require ('./platform'); 6 | } catch (e) { 7 | } 8 | } else { 9 | platform = new AppPreferencesLocalStorage (); 10 | } 11 | 12 | /** 13 | * @constructor 14 | */ 15 | function AppPreferences (defaultArgs) { 16 | this.defaultArgs = defaultArgs || {}; 17 | } 18 | 19 | var promiseLib; 20 | if (typeof Promise !== "undefined") { 21 | promiseLib = Promise; 22 | } else if (typeof WinJS !== "undefined" && WinJS.Promise) { 23 | promiseLib = WinJS.Promise; 24 | } else if (typeof $ !== "undefined" && $.Deferred) { 25 | promiseLib = function (init) { 26 | var d = $.Deferred (); 27 | init (d.resolve.bind (d), d.reject.bind (d)); 28 | return d.promise (); 29 | } 30 | } 31 | 32 | function promiseCheck (maxArgs, successCallback, errorCallback) { 33 | if ( 34 | typeof successCallback !== 'function' && typeof errorCallback !== 'function' 35 | && arguments.length <= maxArgs + 1 // argCount 36 | && promiseLib 37 | ) { 38 | return true; 39 | } else { 40 | return false; 41 | } 42 | } 43 | 44 | if (!platform.nativeExec && typeof cordova !== "undefined") 45 | platform.nativeExec = cordova.exec.bind (cordova); 46 | 47 | AppPreferences.prototype.prepareKey = platform.prepareKey || function (mode, dict, key, value) { 48 | 49 | var args = {}; 50 | 51 | for (var k in this.defaultArgs) { 52 | args[k] = this.defaultArgs[k]; 53 | } 54 | 55 | var argList = [].slice.apply(arguments); 56 | argList.shift(); 57 | if ( 58 | (mode == 'get' && argList.length == 1) || 59 | (mode == 'get' && argList.length == 2 && argList[1] == null) || 60 | (mode == 'set' && argList.length == 2) || 61 | (mode == 'set' && argList.length == 3 && argList[2] == null) 62 | ) { 63 | argList.unshift (undefined); 64 | } 65 | 66 | args.key = argList[1]; 67 | 68 | if (argList[0] !== undefined) 69 | args.dict = argList[0] 70 | 71 | if (mode == 'set') 72 | args.value = argList[2]; 73 | 74 | // console.log (JSON.stringify (argList), JSON.stringify (args)); 75 | return args; 76 | } 77 | 78 | /** 79 | * Get a preference value 80 | * 81 | * @param {Function} successCallback The function to call when the value is available 82 | * @param {Function} errorCallback The function to call when value is unavailable 83 | * @param {String} dict Dictionary for key (OPTIONAL) 84 | * @param {String} key Key 85 | */ 86 | AppPreferences.prototype.fetch = platform.fetch || function ( 87 | successCallback, errorCallback, dict, key 88 | ) { 89 | 90 | var argCount = 2; // dict, key 91 | var promise = promiseCheck.apply (this, [argCount].concat ([].slice.call(arguments))); 92 | // for promises 93 | if (promise) { 94 | dict = successCallback; 95 | key = errorCallback; 96 | } 97 | 98 | var args = this.prepareKey ('get', dict, key); 99 | 100 | var _successCallback = function (_value) { 101 | var value = _value; 102 | try { 103 | value = JSON.parse (_value); 104 | } catch (e) { 105 | } 106 | successCallback (value); 107 | } 108 | 109 | var nativeExec = function (resolve, reject) { 110 | if (!args.key) { 111 | return reject (); 112 | } 113 | 114 | if (resolve !== successCallback) { 115 | successCallback = resolve; 116 | } 117 | 118 | if (platform.nativeFetch) { 119 | return platform.nativeFetch(_successCallback, reject, args); 120 | } 121 | return platform.nativeExec(_successCallback, reject, "AppPreferences", "fetch", [args]); 122 | } 123 | 124 | if (promise) { 125 | return new promiseLib (nativeExec); 126 | } else { 127 | nativeExec (successCallback, errorCallback); 128 | } 129 | 130 | }; 131 | 132 | /** 133 | * Set a preference value 134 | * 135 | * @param {Function} successCallback The function to call when the value is set successfully 136 | * @param {Function} errorCallback The function to call when value is not set 137 | * @param {String} dict Dictionary for key (OPTIONAL) 138 | * @param {String} key Key 139 | * @param {String} value Value 140 | */ 141 | AppPreferences.prototype.store = platform.store || function ( 142 | successCallback, errorCallback, dict, key, value 143 | ) { 144 | 145 | var argCount = 3; // dict, key, value 146 | var promise = promiseCheck.apply (this, [argCount].concat ([].slice.call(arguments))); 147 | // for promises 148 | if (promise) { 149 | value = dict; 150 | key = errorCallback; 151 | dict = successCallback; 152 | } 153 | 154 | var args = this.prepareKey ('set', dict, key, value); 155 | 156 | args.type = typeof args.value; 157 | 158 | // VERY IMPORTANT THING 159 | // WP platform has some limitations, so we need to encode all values to JSON. 160 | // On plugin side we store value according to it's type. 161 | // So, every platform plugin must check for type, decode JSON and store 162 | // value decoded for basic types. 163 | // TODO: don't think about array of strings, it's android only. 164 | // Complex structures must be stored as JSON string. 165 | // On iOS strings stored as strings and JSON stored as NSData 166 | // Android: 167 | // Now, interesting thing: how to differentiate between string value 168 | // and complex value, encoded as json and stored as string? 169 | // I'm introduce setting named __type with value "JSON" 170 | // Windows Phone ? 171 | args.value = JSON.stringify (args.value); 172 | 173 | var nativeExec = function (resolve, reject) { 174 | if (!args.key || args.value === null || args.value === undefined) { 175 | return reject (); 176 | } 177 | 178 | if (platform.nativeStore) { 179 | return platform.nativeStore (resolve, reject, args); 180 | } 181 | return platform.nativeExec (resolve, reject, "AppPreferences", "store", [args]); 182 | } 183 | 184 | if (promise) { 185 | return new promiseLib (nativeExec); 186 | } else { 187 | nativeExec (successCallback, errorCallback); 188 | } 189 | 190 | }; 191 | 192 | /** 193 | * Remove value from preferences 194 | * 195 | * @param {Function} successCallback The function to call when the value is available 196 | * @param {Function} errorCallback The function to call when value is unavailable 197 | * @param {String} dict Dictionary for key (OPTIONAL) 198 | * @param {String} key Key 199 | */ 200 | AppPreferences.prototype.remove = platform.remove || function ( 201 | successCallback, errorCallback, dict, key 202 | ) { 203 | 204 | var argCount = 2; // dict, key 205 | var promise = promiseCheck.apply (this, [argCount].concat ([].slice.call(arguments))); 206 | // for promises 207 | if (promise) { 208 | key = errorCallback; 209 | dict = successCallback; 210 | } 211 | 212 | var args = this.prepareKey ('get', dict, key); 213 | 214 | var nativeExec = function (resolve, reject) { 215 | if (!args.key) { 216 | return reject (); 217 | } 218 | 219 | if (platform.nativeRemove) { 220 | return platform.nativeRemove (resolve, reject, args); 221 | } 222 | return platform.nativeExec (resolve, reject, "AppPreferences", "remove", [args]); 223 | } 224 | 225 | if (promise) { 226 | return new promiseLib (nativeExec); 227 | } else { 228 | nativeExec (successCallback, errorCallback); 229 | } 230 | 231 | }; 232 | 233 | /** 234 | * Clear preferences 235 | * 236 | * @param {Function} successCallback The function to call when the value is available 237 | * @param {Function} errorCallback The function to call when value is unavailable 238 | * @param {String} dict Dictionary for key (OPTIONAL) 239 | * @param {String} key Key 240 | */ 241 | AppPreferences.prototype.clearAll = platform.clearAll || function ( 242 | successCallback, errorCallback 243 | ) { 244 | 245 | var argCount = 0; 246 | var promise = promiseCheck.apply (this, [argCount].concat ([].slice.call(arguments))); 247 | 248 | var args = {}; 249 | 250 | for (var k in this.defaultArgs) { 251 | args[k] = this.defaultArgs[k]; 252 | } 253 | 254 | var nativeExec = function (resolve, reject) { 255 | 256 | if (platform.nativeClearAll) { 257 | return platform.nativeClearAll (resolve, reject, args); 258 | } 259 | 260 | return platform.nativeExec (resolve, reject, "AppPreferences", "clearAll", [args]); 261 | } 262 | 263 | if (promise) { 264 | return new promiseLib (nativeExec); 265 | } else { 266 | nativeExec (successCallback, errorCallback); 267 | } 268 | 269 | }; 270 | 271 | /** 272 | * Show native preferences interface 273 | * 274 | * @param {Function} successCallback The function to call when the value is available 275 | * @param {Function} errorCallback The function to call when value is unavailable 276 | * @param {String} dict Dictionary for key (OPTIONAL) 277 | * @param {String} key Key 278 | */ 279 | AppPreferences.prototype.show = platform.show || function ( 280 | successCallback, errorCallback 281 | ) { 282 | 283 | var argCount = 0; 284 | var promise = promiseCheck.apply (this, [argCount].concat ([].slice.call(arguments))); 285 | 286 | var nativeExec = function (resolve, reject) { 287 | return platform.nativeExec (resolve, reject, "AppPreferences", "show", []); 288 | } 289 | 290 | if (promise) { 291 | return new promiseLib (nativeExec); 292 | } else { 293 | nativeExec (successCallback, errorCallback); 294 | } 295 | }; 296 | 297 | /** 298 | * Watch for preferences change 299 | * 300 | * @param {Function} successCallback The function to call when the value is available 301 | * @param {Function} errorCallback The function to call when value is unavailable 302 | * @param {Boolean} subscribe true value to subscribe, false - unsubscribe 303 | * @example How to get notified: 304 | * ```javascript 305 | * plugins.appPreferences.watch(); 306 | * document.addEventListener ('preferencesChanged', function (evt) { 307 | * // with some platforms can give you details what is changed 308 | * if (evt.key) { 309 | * // handle key change 310 | * } else if (evt.all) { 311 | * // after clearAll 312 | * } 313 | * }); 314 | * ``` 315 | */ 316 | AppPreferences.prototype.watch = platform.watch || function ( 317 | successCallback, errorCallback, subscribe 318 | ) { 319 | if (typeof subscribe === "undefined") { 320 | subscribe = true; 321 | } 322 | 323 | var args = {}; 324 | 325 | for (var k in this.defaultArgs) { 326 | args[k] = this.defaultArgs[k]; 327 | } 328 | 329 | args.subscribe = subscribe; 330 | 331 | var nativeExec = function (resolve, reject) { 332 | 333 | if (platform.nativeWatch) { 334 | return platform.nativeWatch (resolve, reject, args); 335 | } 336 | 337 | return platform.nativeExec (resolve, reject, "AppPreferences", "watch", [args]); 338 | } 339 | 340 | nativeExec (successCallback, errorCallback); 341 | }; 342 | 343 | /** 344 | * Return named configuration context 345 | * In iOS you'll get a suite configuration, on Android — named file 346 | * Supports: Android, iOS 347 | * @param {String} suiteName suite name 348 | * @returns {AppPreferences} AppPreferences object, bound to that suite 349 | */ 350 | 351 | AppPreferences.prototype.iosSuite = 352 | AppPreferences.prototype.suite = 353 | function (suiteName) { 354 | 355 | var appPrefs = new AppPreferences ({ 356 | iosSuiteName: suiteName, // deprecated, remove when ios code is ready 357 | suiteName: suiteName, 358 | }); 359 | 360 | return appPrefs; 361 | } 362 | 363 | /** 364 | * Return cloud synchronized configuration context 365 | * Currently supports Windows and iOS/macOS 366 | * @returns {AppPreferences} AppPreferences object, bound to that suite 367 | */ 368 | 369 | AppPreferences.prototype.cloudSync = function () { 370 | var appPrefs = new AppPreferences ({cloudSync: true}); 371 | 372 | return appPrefs; 373 | } 374 | 375 | /** 376 | * Return default configuration context 377 | * Currently supports Windows and iOS/macOS 378 | * @returns {AppPreferences} AppPreferences object, bound to that suite 379 | */ 380 | 381 | AppPreferences.prototype.defaults = function () { 382 | var appPrefs = new AppPreferences (); 383 | 384 | return appPrefs; 385 | } 386 | 387 | 388 | // WIP: functions to bind selected preferences to the form 389 | 390 | function setFormFields (formEl, fieldsData) { 391 | for (var i = 0; i < formEl.elements.length; i ++) { 392 | var formField = formEl.elements[i]; 393 | if (!(formField.name in fieldsData)) { 394 | continue; 395 | } 396 | 397 | // TODO: multiple checkboxes value for one form field 398 | if (formField.type === 'radio' || formField.type === 'checkbox') { 399 | if ( 400 | formField.value === fieldsData[formField.name] 401 | || formField.value === fieldsData[formField.name].toString() 402 | ) { 403 | formField.checked = true; 404 | } 405 | } else { 406 | formField.value = fieldsData[formField.name]; 407 | } 408 | } 409 | } 410 | 411 | function bindFormToData (formEl, formData) { 412 | [].slice.apply (formEl.elements).forEach (function (el) { 413 | if (el.type.match (/^(?:radio|checkbox)$/)) { 414 | el.addEventListener ('change', getFormFields.bind (window, formEl, formData), false); 415 | } else { 416 | el.addEventListener ('input', getFormFields.bind (window, formEl, formData), false); 417 | } 418 | }); 419 | } 420 | 421 | function getFormFields (formEl, formData) { 422 | formData = formData || {}; 423 | for (var k in formData) { 424 | delete formData[k]; 425 | } 426 | for (var i = 0; i < formEl.elements.length; i ++) { 427 | var formField = formEl.elements[i]; 428 | var checkedType = formField.type.match (/^(?:radio|checkbox)$/); 429 | if ((checkedType && formField.checked) || !checkedType) { 430 | formData[formField.name] = formField.value; 431 | } 432 | } 433 | // console.log (formData); 434 | return formData; 435 | } 436 | 437 | if (typeof module !== "undefined") { 438 | module.exports = new AppPreferences(); 439 | } 440 | 441 | -------------------------------------------------------------------------------- /www/task/AppPreferences.js: -------------------------------------------------------------------------------- 1 | var define; 2 | if (typeof define === "undefined") 3 | define = function (classInstance) { 4 | classInstance (require, exports, module); 5 | } 6 | 7 | define (function (require, exports, module) { 8 | 9 | var dataflows = require ('dataflo.ws'); 10 | 11 | var taskBase = dataflows.task ('base'); 12 | 13 | var AppPreferenceTask = module.exports = function (config) { 14 | // there is no options to netinfo class 15 | this.init (config); 16 | }; 17 | 18 | util.inherits (AppPreferenceTask, taskBase); 19 | 20 | util.extend (AppPreferenceTask.prototype, { 21 | 22 | fetch: function () { 23 | var self = this; 24 | 25 | console.log('MOBRO PREFERENCE GET PREPARE'); 26 | 27 | var successCallback = function (response) { 28 | var result; 29 | try { 30 | result = JSON.parse (response); 31 | } catch (e) { 32 | result = response; 33 | } 34 | var returnValue = {forKey: self.forKey}; 35 | if (result) { 36 | returnValue.value = result; 37 | } else { 38 | returnValue.noValue = true; 39 | } 40 | 41 | console.log ('MOBRO PREFERENCE GET DONE'); 42 | console.log (returnValue); 43 | 44 | self.completed (returnValue); 45 | }; 46 | 47 | var errorCallback = function (error) { 48 | console.log (error); 49 | self.completed ({ 50 | forKey: self.forKey, 51 | noValue: true 52 | }); 53 | }; 54 | 55 | // if (device.platform == "BlackBerry" && parseInt(device.version) == 10) { 56 | // self.completed ({ 57 | // forKey: self.forKey, 58 | // noValue: true 59 | // }); 60 | // return; 61 | // } 62 | 63 | var cordovaModule = cordova.require ('me.apla.cordova.app-preferences.apppreferences'); 64 | cordovaModule.fetch (successCallback, errorCallback, this.forKey, this.inDict); 65 | }, 66 | store: function () { 67 | var self = this; 68 | 69 | var args = {}; 70 | args.key = this.forKey; 71 | args.dict = this.inDict; 72 | args.value = this.value; 73 | 74 | if (!this.forKey || !this.value) { 75 | self.completed (); 76 | return; 77 | } 78 | 79 | console.log ('MOBRO PREFERENCE SET PREPARE'); 80 | console.log (this.value); 81 | 82 | var successCallback = function (response) { 83 | self.completed (); 84 | }; 85 | 86 | var errorCallback = function (error) { 87 | self.failed ({'undefined': true}); 88 | console.log (error); 89 | }; 90 | 91 | if (device.platform == "BlackBerry" && parseInt(device.version) == 10) { 92 | self.completed (); 93 | return; 94 | } 95 | 96 | var cordovaModule = cordova.require ('me.apla.cordova.app-preferences.apppreferences'); 97 | cordovaModule.store (successCallback, errorCallback, this.forKey, this.inDict, this.value); 98 | } 99 | 100 | }); 101 | 102 | dataflows.register ('task', 'AppPreferences', AppPreferenceTask); 103 | 104 | return AppPreferenceTask; 105 | 106 | }); --------------------------------------------------------------------------------