diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a57bdb44d6d089ca6f6b403e4b1bc1000cf9cfa9..aed22eba9fe6f20a56c9654f3abcfda2b0518260 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version='1.0' encoding='utf-8'?> -<manifest android:hardwareAccelerated="true" android:versionCode="106012" android:versionName="1.6.2-alpha" package="fr.duniter.cesium" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> +<manifest android:hardwareAccelerated="true" android:versionCode="106030" android:versionName="1.6.3" package="fr.duniter.cesium" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" /> <uses-permission android:name="android.permission.INTERNET" /> - <application android:hardwareAccelerated="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:supportsRtl="true"> + <application tools:replace="android:appComponentFactory" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:hardwareAccelerated="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:supportsRtl="true"> <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" android:label="@string/activity_name" android:launchMode="singleTop" android:name="MainActivity" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:windowSoftInputMode="adjustResize"> <intent-filter android:label="@string/launcher_name"> <action android:name="android.intent.action.MAIN" /> @@ -15,12 +15,12 @@ <activity android:clearTaskOnLaunch="true" android:configChanges="orientation|keyboardHidden|screenSize" android:exported="false" android:name="com.google.zxing.client.android.CaptureActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:windowSoftInputMode="stateAlwaysHidden" /> <activity android:label="Share" android:name="com.google.zxing.client.android.encode.EncodeActivity" /> </application> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.FLASHLIGHT" /> <uses-feature android:name="android.hardware.camera" android:required="true" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="28" tools:overrideLibrary="org.kaliumjni.lib" /> + <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="29" tools:overrideLibrary="org.kaliumjni.lib" /> </manifest> diff --git a/CordovaLib/build.gradle b/CordovaLib/build.gradle index ff6c034051b5025b82f4a011ca4dc3bfb6390d46..512d20910d034b2d63a6355f3cc4a18e03c70d56 100644 --- a/CordovaLib/build.gradle +++ b/CordovaLib/build.gradle @@ -25,15 +25,12 @@ ext { buildscript { repositories { jcenter() - maven { - url "https://maven.google.com" - } google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' + classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' } } diff --git a/android.json b/android.json index 69eac73fc48c2c238c6c42a43105e6d6ed9339e8..a5dd9f710a3b8c87e8726f68e39aeba27887f878 100644 --- a/android.json +++ b/android.json @@ -67,6 +67,14 @@ { "xml": "<feature name=\"BarcodeScanner\"><param name=\"android-package\" value=\"com.phonegap.plugins.barcodescanner.BarcodeScanner\" /></feature>", "count": 1 + }, + { + "xml": "<feature name=\"File\"><param name=\"android-package\" value=\"org.apache.cordova.file.FileUtils\" /><param name=\"onload\" value=\"true\" /></feature>", + "count": 1 + }, + { + "xml": "<allow-navigation href=\"cdvfile:*\" />", + "count": 1 } ] } @@ -75,11 +83,11 @@ "parents": { "/*": [ { - "xml": "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />", + "xml": "<uses-permission android:name=\"android.permission.INTERNET\" />", "count": 1 }, { - "xml": "<uses-permission android:name=\"android.permission.INTERNET\" />", + "xml": "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />", "count": 1 } ], @@ -210,6 +218,9 @@ "cordova-plugin-ionic-webview": { "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+", "PACKAGE_NAME": "fr.duniter.cesium" + }, + "cordova-plugin-file": { + "PACKAGE_NAME": "fr.duniter.cesium" } }, "dependent_plugins": {}, @@ -387,6 +398,179 @@ "clobbers": [ "Ionic.WebView" ] + }, + { + "id": "cordova-plugin-file.DirectoryEntry", + "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.DirectoryEntry" + ] + }, + { + "id": "cordova-plugin-file.DirectoryReader", + "file": "plugins/cordova-plugin-file/www/DirectoryReader.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.DirectoryReader" + ] + }, + { + "id": "cordova-plugin-file.Entry", + "file": "plugins/cordova-plugin-file/www/Entry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Entry" + ] + }, + { + "id": "cordova-plugin-file.File", + "file": "plugins/cordova-plugin-file/www/File.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.File" + ] + }, + { + "id": "cordova-plugin-file.FileEntry", + "file": "plugins/cordova-plugin-file/www/FileEntry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileEntry" + ] + }, + { + "id": "cordova-plugin-file.FileError", + "file": "plugins/cordova-plugin-file/www/FileError.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileError" + ] + }, + { + "id": "cordova-plugin-file.FileReader", + "file": "plugins/cordova-plugin-file/www/FileReader.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileReader" + ] + }, + { + "id": "cordova-plugin-file.FileSystem", + "file": "plugins/cordova-plugin-file/www/FileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileSystem" + ] + }, + { + "id": "cordova-plugin-file.FileUploadOptions", + "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileUploadOptions" + ] + }, + { + "id": "cordova-plugin-file.FileUploadResult", + "file": "plugins/cordova-plugin-file/www/FileUploadResult.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileUploadResult" + ] + }, + { + "id": "cordova-plugin-file.FileWriter", + "file": "plugins/cordova-plugin-file/www/FileWriter.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileWriter" + ] + }, + { + "id": "cordova-plugin-file.Flags", + "file": "plugins/cordova-plugin-file/www/Flags.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Flags" + ] + }, + { + "id": "cordova-plugin-file.LocalFileSystem", + "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.LocalFileSystem" + ], + "merges": [ + "window" + ] + }, + { + "id": "cordova-plugin-file.Metadata", + "file": "plugins/cordova-plugin-file/www/Metadata.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Metadata" + ] + }, + { + "id": "cordova-plugin-file.ProgressEvent", + "file": "plugins/cordova-plugin-file/www/ProgressEvent.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.ProgressEvent" + ] + }, + { + "id": "cordova-plugin-file.fileSystems", + "file": "plugins/cordova-plugin-file/www/fileSystems.js", + "pluginId": "cordova-plugin-file" + }, + { + "id": "cordova-plugin-file.requestFileSystem", + "file": "plugins/cordova-plugin-file/www/requestFileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.requestFileSystem" + ] + }, + { + "id": "cordova-plugin-file.resolveLocalFileSystemURI", + "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "window" + ] + }, + { + "id": "cordova-plugin-file.isChrome", + "file": "plugins/cordova-plugin-file/www/browser/isChrome.js", + "pluginId": "cordova-plugin-file", + "runs": true + }, + { + "id": "cordova-plugin-file.androidFileSystem", + "file": "plugins/cordova-plugin-file/www/android/FileSystem.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "FileSystem" + ] + }, + { + "id": "cordova-plugin-file.fileSystems-roots", + "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js", + "pluginId": "cordova-plugin-file", + "runs": true + }, + { + "id": "cordova-plugin-file.fileSystemPaths", + "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "cordova" + ], + "runs": true } ], "plugin_metadata": { @@ -407,6 +591,7 @@ "ionic-plugin-keyboard": "2.2.1", "phonegap-plugin-barcodescanner": "7.0.0", "cordova-plugin-ionic-keyboard": "2.2.0", - "cordova-plugin-ionic-webview": "4.1.3" + "cordova-plugin-ionic-webview": "4.1.3", + "cordova-plugin-file": "6.0.2" } -} +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..f1f2de17f8d82f97c056ff3735a5a2de52668df7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,70 @@ +/* Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +// GENERATED FILE! DO NOT EDIT! + +buildscript { + repositories { + jcenter() + google() + } + + // Switch the Android Gradle plugin version requirement depending on the + // installed version of Gradle. This dependency is documented at + // http://tools.android.com/tech-docs/new-build-system/version-compatibility + // and https://issues.apache.org/jira/browse/CB-8143 + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0+' + } +} + +apply plugin: 'com.android.library' + +dependencies { + compile fileTree(dir: 'libs', include: '*.jar') + debugCompile project(path: ":CordovaLib", configuration: "debug") + releaseCompile project(path: ":CordovaLib", configuration: "release") +} + +android { + compileSdkVersion 29 + buildToolsVersion '30.0.0 rc2' + publishNonDefault true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_6 + targetCompatibility JavaVersion.VERSION_1_6 + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } +} + +if (file('build-extras.gradle').exists()) { + apply from: 'build-extras.gradle' +} diff --git a/assets/www/config.js b/assets/www/config.js index 7fc038ed900be23c8443114c5bac47227d1092af..7b3bb56f0d871fd7e2282a2f67540ee2d4ae87b4 100644 --- a/assets/www/config.js +++ b/assets/www/config.js @@ -95,8 +95,8 @@ angular.module("cesium.config", []) "defaultCountry": "France" } }, - "version": "1.6.2", - "build": "2020-04-13T21:20:46.576Z", + "version": "1.6.3", + "build": "2020-04-14T15:27:14.343Z", "newIssueUrl": "https://git.duniter.org/clients/cesium-grp/cesium/issues/new" }) diff --git a/assets/www/cordova_plugins.js b/assets/www/cordova_plugins.js index ba983110e27562fc0ba420708c60bc70bb2d28b9..b649a5ce2cff6f26e98624791764b0b116c16cd1 100644 --- a/assets/www/cordova_plugins.js +++ b/assets/www/cordova_plugins.js @@ -173,6 +173,179 @@ module.exports = [ "clobbers": [ "Ionic.WebView" ] + }, + { + "id": "cordova-plugin-file.DirectoryEntry", + "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.DirectoryEntry" + ] + }, + { + "id": "cordova-plugin-file.DirectoryReader", + "file": "plugins/cordova-plugin-file/www/DirectoryReader.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.DirectoryReader" + ] + }, + { + "id": "cordova-plugin-file.Entry", + "file": "plugins/cordova-plugin-file/www/Entry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Entry" + ] + }, + { + "id": "cordova-plugin-file.File", + "file": "plugins/cordova-plugin-file/www/File.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.File" + ] + }, + { + "id": "cordova-plugin-file.FileEntry", + "file": "plugins/cordova-plugin-file/www/FileEntry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileEntry" + ] + }, + { + "id": "cordova-plugin-file.FileError", + "file": "plugins/cordova-plugin-file/www/FileError.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileError" + ] + }, + { + "id": "cordova-plugin-file.FileReader", + "file": "plugins/cordova-plugin-file/www/FileReader.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileReader" + ] + }, + { + "id": "cordova-plugin-file.FileSystem", + "file": "plugins/cordova-plugin-file/www/FileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileSystem" + ] + }, + { + "id": "cordova-plugin-file.FileUploadOptions", + "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileUploadOptions" + ] + }, + { + "id": "cordova-plugin-file.FileUploadResult", + "file": "plugins/cordova-plugin-file/www/FileUploadResult.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileUploadResult" + ] + }, + { + "id": "cordova-plugin-file.FileWriter", + "file": "plugins/cordova-plugin-file/www/FileWriter.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileWriter" + ] + }, + { + "id": "cordova-plugin-file.Flags", + "file": "plugins/cordova-plugin-file/www/Flags.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Flags" + ] + }, + { + "id": "cordova-plugin-file.LocalFileSystem", + "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.LocalFileSystem" + ], + "merges": [ + "window" + ] + }, + { + "id": "cordova-plugin-file.Metadata", + "file": "plugins/cordova-plugin-file/www/Metadata.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Metadata" + ] + }, + { + "id": "cordova-plugin-file.ProgressEvent", + "file": "plugins/cordova-plugin-file/www/ProgressEvent.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.ProgressEvent" + ] + }, + { + "id": "cordova-plugin-file.fileSystems", + "file": "plugins/cordova-plugin-file/www/fileSystems.js", + "pluginId": "cordova-plugin-file" + }, + { + "id": "cordova-plugin-file.requestFileSystem", + "file": "plugins/cordova-plugin-file/www/requestFileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.requestFileSystem" + ] + }, + { + "id": "cordova-plugin-file.resolveLocalFileSystemURI", + "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "window" + ] + }, + { + "id": "cordova-plugin-file.isChrome", + "file": "plugins/cordova-plugin-file/www/browser/isChrome.js", + "pluginId": "cordova-plugin-file", + "runs": true + }, + { + "id": "cordova-plugin-file.androidFileSystem", + "file": "plugins/cordova-plugin-file/www/android/FileSystem.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "FileSystem" + ] + }, + { + "id": "cordova-plugin-file.fileSystems-roots", + "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js", + "pluginId": "cordova-plugin-file", + "runs": true + }, + { + "id": "cordova-plugin-file.fileSystemPaths", + "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "cordova" + ], + "runs": true } ]; module.exports.metadata = @@ -195,7 +368,8 @@ module.exports.metadata = "ionic-plugin-keyboard": "2.2.1", "phonegap-plugin-barcodescanner": "7.0.0", "cordova-plugin-ionic-keyboard": "2.2.0", - "cordova-plugin-ionic-webview": "4.1.3" + "cordova-plugin-ionic-webview": "4.1.3", + "cordova-plugin-file": "6.0.2" }; // BOTTOM OF METADATA }); \ No newline at end of file diff --git a/assets/www/dist_js/cesium.js b/assets/www/dist_js/cesium.js index 66a4da938ffc711b6c4fa517dc29244b6f8da2be..b462bdc2992aedc8a86a90fa3ff27512e6660563 100644 --- a/assets/www/dist_js/cesium.js +++ b/assets/www/dist_js/cesium.js @@ -21039,17 +21039,6 @@ $templateCache.put('templates/blockchain/lookup.html','<ion-view><ion-nav-title> $templateCache.put('templates/blockchain/lookup_lg.html','<ion-view><ion-nav-title><span translate>BLOCKCHAIN.LOOKUP.TITLE</span></ion-nav-title><ion-content class="padding no-padding-xs no-padding-sm" scroll="true"><ng-include src="::\'templates/blockchain/list_blocks_lg.html\'"></ng-include></ion-content></ion-view>'); $templateCache.put('templates/blockchain/unlock_condition_popover.html','<ion-popover-view class="fit"><ion-header-bar><h1 class="title" translate>BLOCKCHAIN.VIEW.TX_OUTPUT_UNLOCK_CONDITIONS</h1></ion-header-bar><ion-content scroll="true"><div class="row" ng-repeat="condition in popoverData.unlockConditions track by $index" ng-style="::condition.style"><span class="gray" ng-if="::condition.operator">{{::\'BLOCKCHAIN.VIEW.TX_OUTPUT_OPERATOR.\'+condition.operator|translate}} </span><div ng-if="::condition.type==\'SIG\'"><i class="icon ion-key dark"></i> <span class="dark" ng-bind-html="::\'BLOCKCHAIN.VIEW.TX_OUTPUT_FUNCTION.SIG\' | translate"></span> <a ng-click="goState(\'app.wot_identity\', {pubkey:condition.value})" style="text-decoration: none" class="positive">{{condition.value|formatPubkey}}</a></div><div ng-if="::condition.type==\'XHX\'"><i class="icon ion-lock-combination dark"></i> <span class="dark" ng-bind-html="::\'BLOCKCHAIN.VIEW.TX_OUTPUT_FUNCTION.XHX\' | translate"></span> <a copy-on-click="{{::condition.value}}" class="positive">{{::condition.value|formatPubkey}}...</a></div><div ng-if="condition.type==\'CSV\'"><i class="icon ion-clock dark"></i> <span class="dark" ng-bind-html="::\'BLOCKCHAIN.VIEW.TX_OUTPUT_FUNCTION.CSV\' | translate"></span> {{::condition.value|formatDuration}}</div><div ng-if="condition.type==\'CLTV\'"><i class="icon ion-clock dark"></i> <span class="dark" ng-bind-html="::\'BLOCKCHAIN.VIEW.TX_OUTPUT_FUNCTION.CLTV\' | translate"></span> {{::condition.value|medianDate}}</div></div></ion-content></ion-popover-view>'); $templateCache.put('templates/blockchain/view_block.html','<ion-view><ion-nav-title><span class="title visible-xs visible-sm" ng-if="number==\'current\'">{{\'BLOCKCHAIN.VIEW.TITLE_CURRENT\'|translate}}</span> <span class="title visible-xs visible-sm" ng-if="number!=\'current\'">{{\'BLOCKCHAIN.VIEW.TITLE\'|translate:formData}}</span></ion-nav-title><ion-content class="no-padding-xs no-padding-sm" scroll="true"><div class="row no-padding"><div class="col no-padding"><div class="center padding" ng-if="loading"><ion-spinner icon="android"></ion-spinner></div><div class="list item-text-wrap no-padding-xs" ng-if="!loading"><div class="item item-text-wrap"><h3><span class="dark"><i class="icon ion-clock"></i> {{formData.medianTime | medianFromNowAndDate}}</span></h3><h3><span class="dark"><i class="icon ion-lock-combination"></i> {{\'BLOCKCHAIN.VIEW.COMPUTED_BY\'|translate}} </span><a class="positive" ui-sref="app.wot_identity({pubkey:issuer.pubkey, uid: issuer.uid})"><i class="icon ion-person positive"></i> {{issuer.name||issuer.uid}} <span class="gray" ng-if="issuer.name">({{issuer.uid}})</span></a></h3><h3><a ng-click="openRawBlock($event)"><i class="icon ion-share"></i> {{\'BLOCKCHAIN.VIEW.SHOW_RAW\'|translate}}</a></h3></div><span class="item item-divider">{{\'BLOCKCHAIN.VIEW.TECHNICAL_DIVIDER\' | translate}}</span><ion-item class="item-icon-left item-text-wrap" ng-if="!compactMode || $root.settings.expertMode"><i class="icon ion-gear-b"></i> {{\'BLOCKCHAIN.VIEW.VERSION\'|translate}} <span class="badge badge-stable">{{::formData.version}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="!compactMode || $root.settings.expertMode" copy-on-click="{{::formData.powMin}}"><i class="icon ion-lock-combination"></i> {{\'BLOCKCHAIN.VIEW.POW_MIN\'|translate}}<h4 class="gray">{{\'BLOCKCHAIN.VIEW.POW_MIN_HELP\'|translate}}</h4><span class="badge badge-stable">{{::formData.powMin}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" copy-on-click="{{::formData.hash}}"><i class="icon ion-pound"></i> {{\'BLOCKCHAIN.VIEW.HASH\'|translate}}<h5 class="visible-xs visible-sm dark">{{::formData.hash}}</h5></ion-item><span class="item item-divider">{{\'BLOCKCHAIN.VIEW.DATA_DIVIDER\' | translate}}</span><ion-item ng-if="compactMode && formData.empty" class="item-icon-left item-text-wrap">{{\'BLOCKCHAIN.VIEW.EMPTY\'|translate}}</ion-item><ion-item ng-if="!compactMode || formData.dividend" class="item-icon-left item-text-wrap" copy-on-click="{{::formData.dividend/100}}"><i class="icon ion-arrow-up-c"></i><div class="col col-60">{{\'COMMON.UNIVERSAL_DIVIDEND\'|translate}}<h4 class="gray">{{\'BLOCKCHAIN.VIEW.UNIVERSAL_DIVIDEND_HELP\'|translate: {membersCount: formData.membersCount} }}</h4></div><span class="badge badge-balanced" ng-if="formData.dividend">+1 <span ng-bind-html="formData.currency|currencySymbol: {useRelative: true} "></span> / {{\'COMMON.MEMBER\'|translate|lowercase}} </span><span class="badge badge-stable" ng-if="!formData.dividend">0</span> <span class="badge badge-secondary" ng-if="formData.dividend">+ {{formData.dividend| formatAmount: {currency: formData.currency, useRelative: false} }} / {{\'COMMON.MEMBER\'|translate|lowercase}}</span></ion-item><ng-if ng-if="!compactMode || formData.identitiesCount"><ion-item class="item-icon-left"><i class="icon ion-person"></i> <b class="ion-clock" style="position: absolute; top: 16px; left: 39px; font-size: 12px"></b> {{\'BLOCKCHAIN.VIEW.IDENTITIES_COUNT\'|translate}} <span class="badge badge-balanced" ng-if="formData.identitiesCount">+{{::formData.identitiesCount}}</span> <span class="badge badge-stable" ng-if="!formData.identitiesCount">0</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding" ng-if="formData.identitiesCount"><ion-item ng-repeat="identity in ::formData.identities" class="item-border-large item-small-height" ng-include="::\'templates/blockchain/link_identity.html\'"></ion-item></div></ng-if><ng-if ng-if="!compactMode || formData.joinersCount"><ion-item class="item-icon-left"><i class="icon ion-person-add"></i> {{\'BLOCKCHAIN.VIEW.JOINERS_COUNT\'|translate}} <span class="badge badge-balanced" ng-if="formData.joinersCount">+{{::formData.joinersCount}}</span> <span class="badge badge-stable" ng-if="!formData.joinersCount">0</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding" ng-if="formData.joinersCount"><ion-item ng-repeat="identity in ::formData.joiners" class="item-border-large item-small-height" ng-include="::\'templates/blockchain/link_identity.html\'"></ion-item></div></ng-if><ng-if ng-if="!compactMode || formData.activesCount"><ion-item class="item-icon-left"><i class="icon ion-person"></i> <b class="ion-refresh" style="position: absolute; top: 25px; left: 39px; font-size: 12px"></b> {{\'BLOCKCHAIN.VIEW.ACTIVES_COUNT\'|translate}}<h4 class="gray">{{\'BLOCKCHAIN.VIEW.ACTIVES_COUNT_HELP\'|translate}}</h4><span class="badge badge-balanced" ng-if="formData.activesCount">{{::formData.activesCount}}</span> <span class="badge badge-stable" ng-if="!formData.activesCount">0</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding" ng-if="formData.activesCount"><ion-item ng-repeat="identity in ::formData.actives" class="item-border-large item-small-height" ng-include="::\'templates/blockchain/link_identity.html\'"></ion-item></div></ng-if><ng-if ng-if="!compactMode || (formData.excludedCount-formData.revokedCount)"><ion-item class="item-icon-left"><i class="icon ion-person"></i> <b class="ion-close dark" style="position: absolute; top: 25px; left: 39px; font-size: 12px"></b> {{\'BLOCKCHAIN.VIEW.EXCLUDED_COUNT\'|translate}}<h4 class="gray">{{\'BLOCKCHAIN.VIEW.EXCLUDED_COUNT_HELP\'|translate}}</h4><span class="badge badge-assertive" ng-if="formData.excludedCount-formData.revokedCount">-{{::formData.excludedCount-formData.revokedCount}}</span> <span class="badge badge-stable" ng-if="!(formData.excludedCount-formData.revokedCount)">0</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding" ng-if="formData.excludedCount"><ion-item ng-repeat="identity in ::formData.excluded" class="item-border-large item-small-height" ng-include="::\'templates/blockchain/link_identity.html\'"></ion-item></div></ng-if><ng-if ng-if="!compactMode || formData.leaversCount"><ion-item class="item-icon-left" ng-if="!compactMode || formData.leaversCount"><i class="icon ion-person"></i> <b class="ion-minus" style="position: absolute; top: 25px; left: 39px; font-size: 12px"></b> {{\'BLOCKCHAIN.VIEW.LEAVERS_COUNT\'|translate}}<h4 class="gray">{{\'BLOCKCHAIN.VIEW.LEAVERS_COUNT_HELP\'|translate}}</h4><span class="badge badge-assertive" ng-if="formData.leaversCount">-{{::formData.leaversCount}}</span> <span class="badge badge-stable" ng-if="!formData.leaversCount">0</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding" ng-if="formData.leaversCount"><ion-item ng-repeat="identity in ::formData.leavers" class="item-border-large item-small-height" ng-include="::\'templates/blockchain/link_identity.html\'"></ion-item></div></ng-if><ng-if ng-if="!compactMode || formData.revokedCount"><ion-item class="item-icon-left"><i class="icon ion-person"></i> <b class="ion-minus-circled assertive" style="position: absolute; top: 25px; left: 39px; font-size: 12px"></b> {{\'BLOCKCHAIN.VIEW.REVOKED_COUNT\'|translate}}<h4 class="gray">{{\'BLOCKCHAIN.VIEW.REVOKED_COUNT_HELP\'|translate}}</h4><span class="badge badge-balanced" ng-if="formData.revokedCount">-{{::formData.revokedCount}}</span> <span class="badge badge-stable" ng-if="!formData.revokedCount">0</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding" ng-if="formData.revokedCount"><ion-item ng-repeat="identity in ::formData.revoked" class="item-border-large item-small-height" ng-include="::\'templates/blockchain/link_identity.html\'"></ion-item></div></ng-if><ng-if ng-if="!compactMode || formData.certificationsCount"><ion-item class="item-icon-left"><i class="icon ion-ribbon-a"></i> {{\'BLOCKCHAIN.VIEW.CERT_COUNT\'|translate}} <span class="badge badge-stable" ng-class="{\'badge-positive\':formData.certificationsCount}">{{::formData.certificationsCount}}</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding no-padding-xs" ng-if="formData.certificationsCount"><div ng-repeat="(key, certs) in formData.certifications" class="item item-border-large item-small-height"><div class="row no-padding"><div class="col col-center no-padding"><ng-repeat ng-repeat="cert in certs"><ng-include src="::\'templates/blockchain/link_identity.html\'" onload="identity=cert.from"></ng-include><br></ng-repeat></div><div class="col col-10 col-center gray text-center no-padding"><h2><i class="icon ion-arrow-right-a"></i></h2></div><div class="col col-40 col-center no-padding" ng-include="::\'templates/blockchain/link_identity.html\'" onload="identity=certs[0].to"></div></div></div></div></ng-if><ng-if ng-if="!compactMode || formData.transactionsCount"><ion-item class="item-icon-left"><i class="icon ion-card"></i> {{\'BLOCKCHAIN.VIEW.TX_COUNT\'|translate}} <span class="badge badge-stable" ng-class="{\'badge-positive\':formData.transactionsCount}">{{::formData.transactionsCount}}</span></ion-item><div class="padding-bottom item-icon-left-padding item-icon-right-padding no-padding-xs" ng-if="formData.transactionsCount"><div ng-repeat="tx in ::formData.transactions" class="item item-small-height item-border-large"><div class="row no-padding" style="padding-top: 3px"><div class="col col-40 col-center no-padding list no-margin"><div ng-repeat="identity in ::tx.issuers" class="item no-padding item-small-height"><ng-include src="\'templates/blockchain/link_identity.html\'"></ng-include></div></div><div class="col col-10 col-center gray text-center no-padding"><h2><i class="icon ion-arrow-right-a"></i></h2></div><div class="col no-padding padding-right no-padding-xs col-text-wrap list no-margin"><span class="gray" ng-if="tx.toHimself" translate="">BLOCKCHAIN.VIEW.TX_TO_HIMSELF</span><div ng-repeat="output in ::tx.outputs" class="item no-padding item-small-height"><ng-include ng-if="::output.pubkey" src="\'templates/blockchain/link_identity.html\'" onload="identity=output"></ng-include><span ng-if="::!output.pubkey && output.unlockFunctions"><i class="icon ion-locked"></i> (<a ng-click="showUnlockConditionPopover(output, $event)"> <i ng-repeat="unlockFunction in ::output.unlockFunctions" ng-class="::{\'ion-key\': (unlockFunction==\'SIG\'), \'ion-clock\': (unlockFunction==\'CSV\' || unlockFunction==\'CLTV\'), \'ion-lock-combination\': (unlockFunction==\'XHX\') }" class="icon"></i> </a>) </span><span class="badge badge-balanced" ng-bind-html="::output.amount | formatAmount:{currency: formData.currency, useRelative: false} "></span></div></div></div></div></div></ng-if></div></div></div></ion-content></ion-view>'); -$templateCache.put('templates/common/badge_certification_count.html','<span ng-attr-id="{{$ctrl.csId}}" class="badge badge-balanced" ng-class="{\'badge-energized\': $ctrl.requirements.willNeedCertificationCount || ($ctrl.requirements.needCertificationCount + $ctrl.requirements.pendingCertificationCount >= $ctrl.parameters.sigQty),\n \'badge-assertive\': ($ctrl.requirements.needCertificationCount + $ctrl.requirements.pendingCertificationCount < $ctrl.parameters.sigQty)}"><span ng-if="$ctrl.requirements.certificationCount || !$ctrl.requirements.pendingCertificationCount"><i ng-if="!$ctrl.requirements.needCertificationCount" class="ion-android-done"></i> {{$ctrl.requirements.certificationCount}} <i ng-if="$ctrl.requirements.willNeedCertificationCount" class="ion-android-warning"></i> </span><span ng-if="$ctrl.requirements.pendingCertificationCount"><ng-if ng-if="$ctrl.requirements.certificationCount">+</ng-if><i class="ion-clock"></i> {{$ctrl.requirements.pendingCertificationCount}}</span></span>'); -$templateCache.put('templates/common/badge_given_certification_count.html','<div ng-attr-id="{{$ctrl.csId}}" class="badge badge-calm" ng-class="{\'badge-assertive\': $ctrl.identity.given_cert.length >= $ctrl.parameters.sigStock}"><span><i ng-if="$ctrl.identity.given_cert.length" class="ion-android-done"></i> {{$ctrl.identity.given_cert.length}} </span><span ng-if="$ctrl.identity.given_cert_pending.length">(<ng-if ng-if="$ctrl.identity.given_cert.length">+</ng-if><i class="ion-clock"></i> {{$ctrl.identity.given_cert_pending.length}}) </span><small>/ {{$ctrl.parameters.sigStock}}</small></div>'); -$templateCache.put('templates/common/form_error_messages.html','<div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT"></span></div><div class="form-error" ng-message="maxlength"><span translate="ERROR.FIELD_TOO_LONG"></span></div><div class="form-error" ng-message="pattern"><span translate="ERROR.FIELD_ACCENT"></span></div><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div>'); -$templateCache.put('templates/common/popover_copy.html','<ion-popover-view class="popover-copy" style="height: {{(!rows || rows <= 1) ? 50 : rows*22}}px"><ion-content scroll="false"><div class="list"><div class="item item-input"><input type="text" autocomplete="off" ng-if="!rows || rows <= 1" ng-model="value"><textarea ng-if="rows && rows > 1" ng-model="value" rows="{{rows}}" cols="10">\n </textarea></div></div></ion-content></ion-popover-view>'); -$templateCache.put('templates/common/popover_helptip.html','<ion-popover-view class="popover-helptip"><ion-content scroll="false" class="list"><p><i ng-if="icon.position && !icon.position.startsWith(\'bottom-\')" class="{{icon.class}} icon-{{icon.position}} hidden-xs" style="{{icon.style}}"></i><a ng-click="closePopover()" class="pull-right button-close" ng-class="{\'pull-left\': icon.position === \'right\', \'pull-right\': icon.position !== \'right\'}"><i class="ion-close"></i> </a><span> </span></p><p class="padding light"><ng-bind-html ng-bind-html="content | translate:contentParams"></ng-bind-html><ng-bind-html ng-bind-html="trustContent"></ng-bind-html></p><div class="text-center" ng-if="!tour"><button class="button button-small button-stable" ng-if="!hasNext" ng-click="closePopover(true)" translate>COMMON.BTN_UNDERSTOOD</button> <button class="button button-small button-stable" id="helptip-btn-ok" ng-if="hasNext" ng-click="closePopover(false)" translate>COMMON.BTN_UNDERSTOOD</button> <button id="helptip-btn-ok" class="button button-small button-positive icon-right ink" ng-if="hasNext" ng-click="closePopover(true)"><i class="icon ion-chevron-right"></i></button></div><div class="text-center" ng-if="tour"><button class="button button-small button-positive" id="helptip-btn-ok" ng-if="!hasNext" ng-click="closePopover(false)" translate>COMMON.BTN_CLOSE</button> <button id="helptip-btn-ok" class="button button-small button-positive icon-right ink" ng-if="hasNext" ng-click="closePopover(true)">{{\'COMMON.BTN_CONTINUE\'|translate}} <i class="icon ion-chevron-right"></i></button></div><p><i ng-if="icon.position && icon.position.startsWith(\'bottom-\')" class="{{icon.class}} icon-{{icon.position}} hidden-xs"></i></p></ion-content></ion-popover-view>'); -$templateCache.put('templates/common/popover_locales.html','<ion-popover-view class="fit popover-locales" style="height: {{locales.length*48}}px"><ion-content scroll="false"><div class="list item-text-wrap block"><a ng-repeat="l in locales track by l.id" class="item item-icon-left ink" ng-click="changeLanguage(l.id)"><i class="item-image avatar" style="background-image: url(./img/flag-{{l.flag}}.png)"></i> {{l.label | translate}}</a></div></ion-content></ion-popover-view>'); -$templateCache.put('templates/common/popover_profile.html',''); -$templateCache.put('templates/common/popover_share.html','<ion-popover-view class="popover-share"><ion-content scroll="false"><div class="bar bar-header"><h1 class="title">{{titleKey|translate:titleValues}}</h1><span class="gray pull-right">{{time|formatDate}}</span></div><div class="list no-margin no-padding has-header has-footer block"><div class="item item-input"><input type="text" autocomplete="off" ng-model="value"></div></div><div class="bar bar-footer"><div class="button-bar"><a class="button button-icon positive icon ion-social-facebook" href="https://www.facebook.com/sharer/sharer.php?u={{postUrl|formatEncodeURI}}&title={{postMessage|formatEncodeURI}}" onclick="window.open(this.href, \'facebook-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_FACEBOOK\'|translate}}"></a> <a class="button button-icon positive icon ion-social-twitter" href="https://twitter.com/intent/tweet?url={{postUrl|formatEncodeURI}}&text={{postMessage|formatEncodeURI}}" onclick="window.open(this.href, \'twitter-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_TWITTER\'|translate}}"></a> <a class="button button-icon positive icon ion-social-googleplus" href="https://plus.google.com/share?url={{postUrl|formatEncodeURI}}" onclick="window.open(this.href, \'google-plus-share\', \'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=296,width=580\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_GOOGLEPLUS\'|translate}}"></a> <a class="button button-icon positive icon ion-social-diaspora" href="https://sharetodiaspora.github.io/?title={{postMessage|formatEncodeURI}}&url={{postUrl|formatEncodeURI}}" onclick="window.open(this.href, \'diaspora-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_DIASPORA\'|translate}}"></a> <a class="button-close" title="{{\'COMMON.BTN_CLOSE\'|translate}}" ng-click="closePopover()"><i class="icon ion-close"></i></a></div></div></ion-content></ion-popover-view>'); -$templateCache.put('templates/common/popup_password.html','<form name="pwdForm" ng-submit="submit($event)"><div class="list" ng-init="setForm(pwdForm)"><label class="item item-input" ng-class="{\'item-input-error\': pwdForm.$submitted && pwdForm.password.$invalid}"><input name="password" type="password" placeholder="{{\'ACCOUNT.SECURITY.KEYFILE.PASSWORD_POPUP.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-minlength="1" required></label><div class="form-errors" ng-if="pwdForm.$submitted && pwdForm.pseudo.$error" ng-messages="pwdForm.password.$error"><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div><div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT"></span></div></div><div class="form-errors" ng-if="error"><div class="form-error">{{error|translate}}</div></div></div></form>'); -$templateCache.put('templates/common/qrcode.html','<a ng-attr-id="{{ qrcodeId }}" ng-show="!loading" class="qrcode fade-in pull-right" ng-class="{\'active\': toggleQRCode}" ng-click="toggleQRCode = !toggleQRCode"><div class="content"></div><div class="footer item item-icon-left item-text-wrap ink" on-hold="copy(formData.pubkey)" copy-on-click="{{:rebind:formData.pubkey}}" ng-click="$event.stopPropagation()"><i class="icon ion-key"></i> <span>{{:locale:\'COMMON.PUBKEY\'|translate}}</span><h4 id="pubkey" class="dark">{{:rebind:formData.pubkey}}</h4></div></a>'); -$templateCache.put('templates/common/view_passcode.html','<ion-view left-buttons="leftButtons"><ion-nav-title><span class="visible-xs visible-sm" translate>COMMON.PASSCODE.TITLE</span></ion-nav-title><ion-content scroll="false"></ion-content></ion-view>'); $templateCache.put('templates/currency/items_network.html','<ion-item id="helptip-network-blockchain" class="item-icon-left item-text-wrap"><i class="icon ion-clock"></i> <span class="col col-60" translate="">CURRENCY.VIEW.MEDIAN_TIME</span> <span class="badge badge-stable">{{formData.medianTime | medianDate}}</span></ion-item><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-lock-combination"></i> <span class="col col-75" translate="">CURRENCY.VIEW.POW_MIN</span> <span class="badge badge-stable">{{formData.difficulty | formatInteger}}</span></ion-item><cs-extension-point name="network-actual"></cs-extension-point><div class="item item-divider"><span translate="">CURRENCY.VIEW.NETWORK_RULES_DIVIDER</span></div><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-clock" style="position: absolute; font-size: 20px; left: 16px; margin-top: 11px"></i> <b class="icon-secondary ion-lock-combination" style="left: 14px; margin-top: -4px"></b> <b class="icon-secondary ion-arrow-right-c" style="font-size: 12px; left: 28px; margin-top: -4px"></b> <b class="icon-secondary ion-lock-combination" style="left: 38px; margin-top: -4px"></b> <span class="col col-75" translate="">CURRENCY.VIEW.AVG_GEN_TIME</span> <span class="badge badge-stable">{{formData.avgGenTime | formatDuration}}</span></ion-item><div id="helptip-network-peers" class="item item-divider"><div class="pull-left"><span ng-if="search.type==\'member\'" translate="">PEER.MEMBERS</span> <span ng-if="search.type==\'mirror\'" translate="">PEER.MIRRORS</span> <span ng-if="search.type==\'offline\'" translate="">PEER.OFFLINE</span> <span ng-if="!search.type" translate="">PEER.PEERS</span> <span ng-if="!search.loading">({{search.results.length}})</span></div><div class="buttons pull-right"><ion-spinner class="icon" icon="android" ng-if="search.loading"></ion-spinner></div></div><ng-include src="::\'templates/network/items_peers.html\'"></ng-include>'); $templateCache.put('templates/currency/items_parameters.html','<div bind-notifier="{ rebind:formData.useRelative }"><ion-item class="item-icon-left item-text-wrap visible-xs visible-sm"><i class="icon ion-android-bookmark"></i> <span translate>CURRENCY.VIEW.CURRENCY_NAME</span><div class="item-note dark" ng-if="!loading">{{formData.currency}} (<span ng-bind-html=":rebind:formData.currency | currencySymbol:formData.useRelative"></span>)</div></ion-item><ion-item id="helptip-currency-mass-member" class="item-icon-left item-text-wrap"><i class="icon ion-pie-graph"></i><div class="col col-60"><span translate>CURRENCY.VIEW.SHARE</span> <span class="gray">(M<sub>t</sub>/N<sub>t</sub>)</span></div><span id="helptip-currency-mass-member-unit" ng-if="!loading" class="badge badge-calm" ng-bind-html=":rebind:formData.MoverN | formatAmount:{currency: formData.currency, useRelative: formData.useRelative, currentUD: formData.currentUD}"></span></ion-item><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-record"></i><div class="col col-60"><span translate>CURRENCY.VIEW.MASS</span> <span class="gray">(M<sub>t</sub>)</span></div><span class="badge badge-energized" ng-if="!loading" ng-bind-html=":rebind:formData.M | formatAmount:{currency: formData.currency, useRelative: formData.useRelative, currentUD: formData.currentUD}"></span></ion-item><cs-extension-point name="parameters-actual"></cs-extension-point><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-arrow-graph-up-right"></i><div class="col col-60"><span translate>CURRENCY.VIEW.C_ACTUAL</span> <span class="gray">(c<sub>{{\'CURRENCY.VIEW.CURRENT\'|translate}}</sub>)</span></div><span class="badge badge-stable">{{formData.cactual | formatNumeral: \'0,0.00\'}} % / {{formData.dt | formatPeriod}}</span></ion-item><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-load-c"></i><div class="col col-60"><span translate>CURRENCY.VIEW.UD</span> <span class="gray">({{\'COMMON.UD\'|translate}}<sub>t</sub>)</span></div><div class="badge badge-royal" ng-if="!loading"><span ng-if="formData.useRelative">1<ng-bind-html ng-bind-html=":rebind:formData.currency| currencySymbol:true"></ng-bind-html></span><span ng-if="!formData.useRelative" ng-bind-html=":rebind:formData.currentUD | formatAmount:{currency: formData.currency, useRelative: formData.useRelative, currentUD: formData.currentUD}"></span> / {{formData.dt | formatPeriod}}</div></ion-item><div class="item item-toggle dark"><div class="item-label text-right gray" translate>COMMON.BTN_RELATIVE_UNIT</div><label class="toggle toggle-royal" id="helptip-currency-change-unit"><input type="checkbox" ng-model="formData.useRelative"><div class="track"><div class="handle"></div></div></label></div><a name="helptip-currency-rules-anchor"></a><div class="item item-divider" id="helptip-currency-rules"><span translate>CURRENCY.VIEW.MONEY_RULES_DIVIDER</span></div><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-arrow-graph-up-right"></i><div class="col col-60"><span translate>CURRENCY.VIEW.C_RULE</span> <span class="gray">(c)</span></div><span class="item-note dark" ng-if="!loading && !formData.udReevalTime0">{{formData.c*100 | formatNumeral: \'0,0.00\'}} % / {{formData.dt | formatPeriod}}</span><span class="badge badge-stable" ng-if="!loading && formData.udReevalTime0">{{formData.c*100 | formatNumeral: \'0,0.00\'}} % / {{formData.dtReeval | formatDuration}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.udReevalTime0 && formData.allRules"><i class="icon ion-load-c"></i> <b class="ion-clock icon-secondary" style="font-size: 18px; left: 36px; top: -12px"></b><div class="col col-60"><span translate>CURRENCY.VIEW.DT_REEVAL</span> <span class="gray">(dt<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub>)</span></div><span class="item-note dark" ng-if="!loading" translate="CURRENCY.VIEW.DT_REEVAL_VALUE" translate-values="formData"></span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.udReevalTime0 && formData.allRules"><i class="icon ion-load-c"></i> <b class="ion-calendar icon-secondary" style="font-size: 18px; left: 36px; top: -12px"></b><div class="col col-60"><span translate>CURRENCY.VIEW.UD_REEVAL_TIME0</span> <span class="gray">(t0<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub>)</span></div><span class="item-note dark" ng-if="!loading">{{formData.udReevalTime0|medianDate}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allRules"><i class="icon ion-load-c"></i> <b class="ion-calculator icon-secondary" style="font-size: 18px; left: 36px; top: -12px"></b><div class="col col-60"><span translate>CURRENCY.VIEW.UD_RULE</span> <span class="gray" ng-if="formData.udReevalTime0">- {{\'COMMON.UD\'|translate}}<sub>{{formData.dt|formatPeriod}}</sub>(t<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub>)</span></div><span class="item-note dark" ng-if="!loading && !formData.udReevalTime0">{{\'COMMON.UD\'|translate}}<sub>t-1</sub> + c<sup>2</sup> * M<sub>t-1</sub>/N<sub>t-1</sub></span><span class="item-note dark" ng-if="!loading && formData.udReevalTime0">{{\'COMMON.UD\'|translate}}<sub>{{formData.dt|formatPeriod}}</sub>(t<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub> - dt<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub>)+ c<sup>2</sup> * (M/N)(t<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub> - dt<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub>) / dt<sub>{{\'CURRENCY.VIEW.REEVAL_SYMBOL\'|translate}}</sub></span></ion-item><div class="item item-toggle dark"><div class="item-label text-right gray" translate>CURRENCY.VIEW.DISPLAY_ALL_RULES</div><label class="toggle toggle-royal"><input type="checkbox" ng-model="formData.allRules"><div class="track"><div class="handle"></div></div></label></div></div>'); $templateCache.put('templates/currency/items_wot.html','<div bind-notifier="{ rebind:formData.useRelative }"><a name="helptip-currency-newcomers-anchor"></a><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-person-stalker"></i><div class="col col-60"><span translate>CURRENCY.VIEW.MEMBERS</span> <span class="gray">(N<sub>{{\'CURRENCY.VIEW.CURRENT\'|translate}}</sub>)</span></div><span class="badge badge-calm" ng-if="!loading">{{formData.N | formatInteger}}</span></ion-item><ion-item id="helptip-currency-newcomers" class="item-icon-left item-text-wrap"><i class="icon ion-arrow-graph-up-right"></i><div class="col col-75"><span translate="CURRENCY.VIEW.MEMBERS_VARIATION" translate-values="{duration: formData.durationFromLastUD}"></span> <span class="gray">(ΔN)</span></div><div class="badge" ng-if="!loading" ng-class="{\'badge-balanced\': (formData.N>formData.Nprev), \'badge-stable\': (formData.N==formData.Nprev) ,\'badge-assertive\': (formData.Nprev>formData.N)}">{{formData.N > formData.Nprev ? \'+\' : \'\'}}{{formData.N - formData.Nprev}}</div></ion-item><cs-extension-point name="wot-actual"></cs-extension-point><div class="item item-divider"><span translate>CURRENCY.VIEW.WOT_RULES_DIVIDER</span></div><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-ribbon-b"></i> <span class="col col-75" translate>CURRENCY.VIEW.SIG_QTY_RULE</span> <span class="badge badge-balanced" ng-if="!loading">{{formData.sigQty}}</span></ion-item><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-person"></i> <b class="ion-clock icon-secondary" style="font-size: 18px; left: 33px; top: -12px"></b> <span class="col col-60" translate>CURRENCY.VIEW.MS_WINDOW</span> <span class="badge badge-assertive" ng-if="!loading">{{formData.msWindow | formatDuration}}</span></ion-item><ion-item class="item-icon-left item-text-wrap"><i class="icon ion-person"></i> <b class="ion-calendar icon-secondary" style="font-size: 18px; left: 33px; top: -12px"></b> <span class="col col-60" translate>CURRENCY.VIEW.MS_VALIDITY</span> <span class="badge badge-balanced" ng-if="!loading">{{formData.msValidity | formatDuration}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-ribbon-b"></i> <b class="ion-clock icon-secondary" style="font-size: 18px; left: 33px; top: -12px"></b> <span class="col col-60" translate>CURRENCY.VIEW.SIG_WINDOW</span> <span class="badge badge-stable" ng-if="!loading">{{formData.sigWindow | formatDuration}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-ribbon-b"></i> <b class="ion-calendar icon-secondary" style="font-size: 18px; left: 33px; top: -12px"></b> <span class="col col-60" translate>CURRENCY.VIEW.SIG_VALIDITY</span> <span class="badge badge-balanced" ng-if="!loading">{{formData.sigValidity | formatDuration}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-ribbon-a"></i> <span class="col col-75" translate>CURRENCY.VIEW.SIG_STOCK</span> <span class="badge badge-stable" ng-if="!loading">{{formData.sigStock}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-clock" style="position: absolute; font-size: 20px; left: 16px"></i> <b class="ion-ribbon-a icon-secondary" style="left: 16px; top: -15px"></b> <b class="ion-arrow-right-c icon-secondary" style="left: 28px; top: -15px"></b> <b class="ion-ribbon-a icon-secondary" style="left: 40px; top: -15px"></b> <span class="col col-75" translate>CURRENCY.VIEW.SIG_PERIOD</span> <span class="badge badge-stable" ng-if="!loading">{{formData.sigPeriod | formatDuration}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-steam"></i> <b class="ion-person icon-secondary" style="left: 38px; top: -17px"></b><div class="col col-75"><span ng-bind-html="\'CURRENCY.VIEW.STEP_MAX\'|translate"></span> <span class="gray">(stepMax)</span></div><span class="badge badge-assertive" ng-if="!loading">{{formData.stepMax}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-ribbon-b"></i> <b class="ion-star icon-secondary" style="color: yellow; font-size: 16px; left: 25px; top: -7px"></b> <span class="col col-75" translate>CURRENCY.VIEW.SENTRIES</span> <span class="badge badge-stable" ng-if="!loading">{{formData.sentries}}</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-ribbon-b"></i> <b class="ion-star icon-secondary" style="color: yellow; font-size: 16px; left: 25px; top: -7px"></b> <span class="col col-75" translate>CURRENCY.VIEW.SENTRIES_FORMULA</span> <span class="item-note dark" ng-if="!loading">{{\'CURRENCY.VIEW.MATH_CEILING\'| translate}}( N<sub>t</sub><sup>^ (1 / stepMax)</sup>)</span></ion-item><ion-item class="item-icon-left item-text-wrap" ng-if="formData.allWotRules"><i class="icon ion-pull-request"></i> <span class="col col-75" translate>CURRENCY.VIEW.XPERCENT</span> <span class="badge badge-stable" ng-if="!loading">{{formData.xpercent*100| formatNumeral: \'0,0\'}} %</span></ion-item><div class="item item-toggle dark"><div class="item-label text-right gray" translate>CURRENCY.VIEW.DISPLAY_ALL_RULES</div><label class="toggle toggle-royal"><input type="checkbox" ng-model="formData.allWotRules"><div class="track"><div class="handle"></div></div></label></div></div>'); @@ -21062,6 +21051,17 @@ $templateCache.put('templates/currency/view_currency_lg.html','<ion-view left-bu $templateCache.put('templates/help/help.html','<a name="join"></a><h2 translate>HELP.JOIN.SECTION</h2><a name="join-salt"></a><div class="row responsive-sm" ng-class="itemsClass[\'join-salt\']"><div class="col col-20" translate>LOGIN.SALT</div><div class="col" translate>HELP.JOIN.SALT</div></div><a name="join-password"></a><div class="row responsive-sm" ng-class="itemsClass[\'join-password\']"><div class="col col-20" translate>LOGIN.PASSWORD</div><div class="col" translate>HELP.JOIN.PASSWORD</div></div><a name="join-pseudo"></a><div class="row responsive-sm" ng-class="itemsClass[\'join-pseudo\']"><div class="col col-20" translate>ACCOUNT.NEW.PSEUDO</div><div class="col" translate>HELP.JOIN.PSEUDO</div></div><a name="login"></a><h2 translate>HELP.LOGIN.SECTION</h2><a name="login-pubkey"></a><div class="row responsive-sm" ng-class="itemsClass[\'login-pubkey\']"><div class="col col-20" translate>HELP.LOGIN.PUBKEY</div><div class="col" translate>HELP.LOGIN.PUBKEY_DEF</div></div><a name="login-method"></a><div class="row responsive-sm" ng-class="itemsClass[\'login-method\']"><div class="col col-20" translate>HELP.LOGIN.METHOD</div><div class="col" translate>HELP.LOGIN.METHOD_DEF</div></div><a name="glossary"></a><h2 translate>HELP.GLOSSARY.SECTION</h2><a name="pubkey"></a><div class="row responsive-sm" ng-class="itemsClass.pubkey"><div class="col col-20" translate>COMMON.PUBKEY</div><div class="col" translate>HELP.GLOSSARY.PUBKEY_DEF</div></div><a name="blockchain"></a><div class="row responsive-sm" ng-class="itemsClass.blockchain"><div class="col col-20" translate>HELP.GLOSSARY.BLOCKCHAIN</div><div class="col" translate>HELP.GLOSSARY.BLOCKCHAIN_DEF</div></div><a name="universal_dividend"></a> <a name="ud"></a><div class="row responsive-sm" ng-class="itemsClass.ud"><div class="col col-20" translate>COMMON.UNIVERSAL_DIVIDEND</div><div class="col" translate>HELP.GLOSSARY.UNIVERSAL_DIVIDEND_DEF</div></div><a name="member"></a><div class="row responsive-sm" ng-class="itemsClass.member"><div class="col col-20" translate>HELP.GLOSSARY.MEMBER</div><div class="col" translate>HELP.GLOSSARY.MEMBER_DEF</div></div><a name="wot"></a><div class="row responsive-sm" ng-class="itemsClass.wot"><div class="col col-20" translate>HELP.GLOSSARY.WOT</div><div class="col" translate>HELP.GLOSSARY.WOT_DEF</div></div><a name="currency_rules"></a><div class="row responsive-sm" ng-class="itemsClass.currency_rules"><div class="col col-20" translate>HELP.GLOSSARY.CURRENCY_RULES</div><div class="col" translate>HELP.GLOSSARY.CURRENCY_RULES_DEF</div></div><a name="distance_rule"></a><div class="row responsive-sm" ng-class="itemsClass.distance_rule"><div class="col col-20" translate>HELP.GLOSSARY.DISTANCE_RULE</div><div class="col" translate>HELP.GLOSSARY.DISTANCE_RULE_DEF</div></div>'); $templateCache.put('templates/help/modal_help.html','<ion-modal-view class="modal-full-height modal-help"><ion-header-bar class="bar-positive"><button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CLOSE</button><h1 class="title" translate>HELP.TITLE</h1></ion-header-bar><ion-content scroll="true" class="padding no-padding-xs"><div ng-class="listClass"><ng-include src="::\'templates/help/help.html\'"></ng-include></div><div class="padding hidden-xs text-center"><button class="button button-positive ink" type="submit" ng-click="closeModal()">{{\'COMMON.BTN_CLOSE\' | translate}}</button></div></ion-content></ion-modal-view>'); $templateCache.put('templates/help/view_help.html','<ion-view left-buttons="leftButtons"><ion-nav-title><span class="visible-xs visible-sm" translate="">HELP.TITLE</span></ion-nav-title><ion-content scroll="true" class="padding"><ng-include src="::\'templates/help/help.html\'"></ng-include></ion-content></ion-view>'); +$templateCache.put('templates/common/badge_certification_count.html','<span ng-attr-id="{{$ctrl.csId}}" class="badge badge-balanced" ng-class="{\'badge-energized\': $ctrl.requirements.willNeedCertificationCount || ($ctrl.requirements.needCertificationCount + $ctrl.requirements.pendingCertificationCount >= $ctrl.parameters.sigQty),\n \'badge-assertive\': ($ctrl.requirements.needCertificationCount + $ctrl.requirements.pendingCertificationCount < $ctrl.parameters.sigQty)}"><span ng-if="$ctrl.requirements.certificationCount || !$ctrl.requirements.pendingCertificationCount"><i ng-if="!$ctrl.requirements.needCertificationCount" class="ion-android-done"></i> {{$ctrl.requirements.certificationCount}} <i ng-if="$ctrl.requirements.willNeedCertificationCount" class="ion-android-warning"></i> </span><span ng-if="$ctrl.requirements.pendingCertificationCount"><ng-if ng-if="$ctrl.requirements.certificationCount">+</ng-if><i class="ion-clock"></i> {{$ctrl.requirements.pendingCertificationCount}}</span></span>'); +$templateCache.put('templates/common/badge_given_certification_count.html','<div ng-attr-id="{{$ctrl.csId}}" class="badge badge-calm" ng-class="{\'badge-assertive\': $ctrl.identity.given_cert.length >= $ctrl.parameters.sigStock}"><span><i ng-if="$ctrl.identity.given_cert.length" class="ion-android-done"></i> {{$ctrl.identity.given_cert.length}} </span><span ng-if="$ctrl.identity.given_cert_pending.length">(<ng-if ng-if="$ctrl.identity.given_cert.length">+</ng-if><i class="ion-clock"></i> {{$ctrl.identity.given_cert_pending.length}}) </span><small>/ {{$ctrl.parameters.sigStock}}</small></div>'); +$templateCache.put('templates/common/form_error_messages.html','<div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT"></span></div><div class="form-error" ng-message="maxlength"><span translate="ERROR.FIELD_TOO_LONG"></span></div><div class="form-error" ng-message="pattern"><span translate="ERROR.FIELD_ACCENT"></span></div><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div>'); +$templateCache.put('templates/common/popover_copy.html','<ion-popover-view class="popover-copy" style="height: {{(!rows || rows <= 1) ? 50 : rows*22}}px"><ion-content scroll="false"><div class="list"><div class="item item-input"><input type="text" autocomplete="off" ng-if="!rows || rows <= 1" ng-model="value"><textarea ng-if="rows && rows > 1" ng-model="value" rows="{{rows}}" cols="10">\n </textarea></div></div></ion-content></ion-popover-view>'); +$templateCache.put('templates/common/popover_helptip.html','<ion-popover-view class="popover-helptip"><ion-content scroll="false" class="list"><p><i ng-if="icon.position && !icon.position.startsWith(\'bottom-\')" class="{{icon.class}} icon-{{icon.position}} hidden-xs" style="{{icon.style}}"></i><a ng-click="closePopover()" class="pull-right button-close" ng-class="{\'pull-left\': icon.position === \'right\', \'pull-right\': icon.position !== \'right\'}"><i class="ion-close"></i> </a><span> </span></p><p class="padding light"><ng-bind-html ng-bind-html="content | translate:contentParams"></ng-bind-html><ng-bind-html ng-bind-html="trustContent"></ng-bind-html></p><div class="text-center" ng-if="!tour"><button class="button button-small button-stable" ng-if="!hasNext" ng-click="closePopover(true)" translate>COMMON.BTN_UNDERSTOOD</button> <button class="button button-small button-stable" id="helptip-btn-ok" ng-if="hasNext" ng-click="closePopover(false)" translate>COMMON.BTN_UNDERSTOOD</button> <button id="helptip-btn-ok" class="button button-small button-positive icon-right ink" ng-if="hasNext" ng-click="closePopover(true)"><i class="icon ion-chevron-right"></i></button></div><div class="text-center" ng-if="tour"><button class="button button-small button-positive" id="helptip-btn-ok" ng-if="!hasNext" ng-click="closePopover(false)" translate>COMMON.BTN_CLOSE</button> <button id="helptip-btn-ok" class="button button-small button-positive icon-right ink" ng-if="hasNext" ng-click="closePopover(true)">{{\'COMMON.BTN_CONTINUE\'|translate}} <i class="icon ion-chevron-right"></i></button></div><p><i ng-if="icon.position && icon.position.startsWith(\'bottom-\')" class="{{icon.class}} icon-{{icon.position}} hidden-xs"></i></p></ion-content></ion-popover-view>'); +$templateCache.put('templates/common/popover_locales.html','<ion-popover-view class="fit popover-locales" style="height: {{locales.length*48}}px"><ion-content scroll="false"><div class="list item-text-wrap block"><a ng-repeat="l in locales track by l.id" class="item item-icon-left ink" ng-click="changeLanguage(l.id)"><i class="item-image avatar" style="background-image: url(./img/flag-{{l.flag}}.png)"></i> {{l.label | translate}}</a></div></ion-content></ion-popover-view>'); +$templateCache.put('templates/common/popover_profile.html',''); +$templateCache.put('templates/common/popover_share.html','<ion-popover-view class="popover-share"><ion-content scroll="false"><div class="bar bar-header"><h1 class="title">{{titleKey|translate:titleValues}}</h1><span class="gray pull-right">{{time|formatDate}}</span></div><div class="list no-margin no-padding has-header has-footer block"><div class="item item-input"><input type="text" autocomplete="off" ng-model="value"></div></div><div class="bar bar-footer"><div class="button-bar"><a class="button button-icon positive icon ion-social-facebook" href="https://www.facebook.com/sharer/sharer.php?u={{postUrl|formatEncodeURI}}&title={{postMessage|formatEncodeURI}}" onclick="window.open(this.href, \'facebook-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_FACEBOOK\'|translate}}"></a> <a class="button button-icon positive icon ion-social-twitter" href="https://twitter.com/intent/tweet?url={{postUrl|formatEncodeURI}}&text={{postMessage|formatEncodeURI}}" onclick="window.open(this.href, \'twitter-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_TWITTER\'|translate}}"></a> <a class="button button-icon positive icon ion-social-googleplus" href="https://plus.google.com/share?url={{postUrl|formatEncodeURI}}" onclick="window.open(this.href, \'google-plus-share\', \'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=296,width=580\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_GOOGLEPLUS\'|translate}}"></a> <a class="button button-icon positive icon ion-social-diaspora" href="https://sharetodiaspora.github.io/?title={{postMessage|formatEncodeURI}}&url={{postUrl|formatEncodeURI}}" onclick="window.open(this.href, \'diaspora-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_DIASPORA\'|translate}}"></a> <a class="button-close" title="{{\'COMMON.BTN_CLOSE\'|translate}}" ng-click="closePopover()"><i class="icon ion-close"></i></a></div></div></ion-content></ion-popover-view>'); +$templateCache.put('templates/common/popup_password.html','<form name="pwdForm" ng-submit="submit($event)"><div class="list" ng-init="setForm(pwdForm)"><label class="item item-input" ng-class="{\'item-input-error\': pwdForm.$submitted && pwdForm.password.$invalid}"><input name="password" type="password" placeholder="{{\'ACCOUNT.SECURITY.KEYFILE.PASSWORD_POPUP.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-minlength="1" required></label><div class="form-errors" ng-if="pwdForm.$submitted && pwdForm.pseudo.$error" ng-messages="pwdForm.password.$error"><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div><div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT"></span></div></div><div class="form-errors" ng-if="error"><div class="form-error">{{error|translate}}</div></div></div></form>'); +$templateCache.put('templates/common/qrcode.html','<a ng-attr-id="{{ qrcodeId }}" ng-show="!loading" class="qrcode fade-in pull-right" ng-class="{\'active\': toggleQRCode}" ng-click="toggleQRCode = !toggleQRCode"><div class="content"></div><div class="footer item item-icon-left item-text-wrap ink" on-hold="copy(formData.pubkey)" copy-on-click="{{:rebind:formData.pubkey}}" ng-click="$event.stopPropagation()"><i class="icon ion-key"></i> <span>{{:locale:\'COMMON.PUBKEY\'|translate}}</span><h4 id="pubkey" class="dark">{{:rebind:formData.pubkey}}</h4></div></a>'); +$templateCache.put('templates/common/view_passcode.html','<ion-view left-buttons="leftButtons"><ion-nav-title><span class="visible-xs visible-sm" translate>COMMON.PASSCODE.TITLE</span></ion-nav-title><ion-content scroll="false"></ion-content></ion-view>'); $templateCache.put('templates/home/home.html','<ion-view id="home"><ion-nav-title></ion-nav-title><ion-nav-buttons side="secondary"></ion-nav-buttons><ion-content class="positive-900-bg circle-bg-dark"><div class="row padding-horizontal no-padding-xxs responsive-lg"><div class="col text-center no-padding-xs main-container"><div id="helptip-home-logo" class="logo"></div><h4><span class="hidden-xs" translate="">HOME.WELCOME</span> <b ng-show="!loading" translate-values=":currency:{currency: $root.currency.name}" translate="">HOME.MESSAGE</b></h4><div class="center padding" ng-if="loading"><ion-spinner icon="android"></ion-spinner></div><div class="center padding animate-fade-in animate-show-hide ng-hide" ng-show="!loading && error"><div class="card card-item padding"><p class="item-content item-text-wrap"><span class="dark" trust-as-html="\'HOME.CONNECTION_ERROR\'|translate:node"></span></p><button type="button" class="button button-positive icon icon-left ion-refresh ink" ng-click="reload()">{{\'COMMON.BTN_REFRESH\'|translate}}</button></div></div><div class="center animate-show-hide ng-hide" ng-show="!loading && !error"><button type="button" class="button button-block button-stable button-raised icon-left icon ion-easel ink-dark hidden-xs" ng-show="login" ng-click="startHelpTour($event)">{{\'COMMON.BTN_HELP_TOUR\'|translate}}</button> <button type="button" class="button button-block button-positive button-raised ink-dark" ng-click="showJoinModal()" ng-if="!login" translate="">LOGIN.CREATE_FREE_ACCOUNT</button> <button type="button" class="item button button-block button-raised icon icon-left ion-person ink-dark" ng-class="{\'button-stable\': smallscreen, \'button-positive\': !smallscreen}" ui-sref="app.view_wallet" ng-show="login" translate="">MENU.ACCOUNT</button> <button type="button" class="item button button-block button-stable button-raised icon icon-left ion-card ink-dark visible-xs" ui-sref="app.view_wallet_tx" ng-if="login"><b class="icon-secondary ion-clock" style="left: 52px; top: -2px; font-size: 10pt; display: block"></b> {{\'MENU.TRANSACTIONS\'|translate}}</button> <button type="button" class="item button button-block button-positive button-raised icon icon-left ion-paper-airplane ink-dark visible-xs" ng-click="showTransferModal()" ng-if="login" translate="">COMMON.BTN_SEND_MONEY</button><br class="visible-xs visible-sm"><div class="text-center no-padding" ng-show="!login"><br class="visible-xs visible-sm">{{\'LOGIN.HAVE_ACCOUNT_QUESTION\'|translate}} <b></b></div><div class="text-center no-padding" ng-show="login"><br class="visible-xs visible-sm"><span ng-bind-html="\'HOME.NOT_YOUR_ACCOUNT_QUESTION\'|translate:{pubkey: walletData.pubkey}"></span><br><b><a class="assertive" ng-click="logout({askConfirm: true})" translate="">HOME.BTN_CHANGE_ACCOUNT</a></b></div><button type="button" class="button button-block button-stable button-raised ink visible-xs visible-sm" ui-sref="app.view_wallet" ng-if="!login" translate="">COMMON.BTN_LOGIN</button><div class="text-center no-padding visible-xs stable"><br>{{\'COMMON.APP_VERSION\'|translate:{version: config.version} }} | <a href="#" ng-click="showAboutModal()" translate="">HOME.BTN_ABOUT</a></div></div></div><div class="col no-padding" ng-class="{\'col-30\': !feed, \'col-10\': feed}"> </div><div class="col col-30 no-padding" ng-if="feed"><div class="feed padding-horizontal no-padding-xs padding-top"><h3 class="padding-left"><i class="icon ion-speakerphone"></i> {{feed.title}} <small><a ng-click="openLink($event, feed.home_page_url)" class="gray"><span translate="">HOME.SHOW_ALL_FEED</span> <i class="icon ion-chevron-right"></i></a></small></h3><div class="animate-show-hide ng-hide" ng-show="feed"><div ng-repeat="item in feed.items" class="card padding no-margin-xs"><div class="header"><i ng-if="item.author.avatar" class="avatar" style="background-image: url({{item.author.avatar}})"></i> <a ng-class="{\'avatar-left-padding\': item.author.avatar}" class="author" ng-click="item.author.url && openLink($event, item.author.url)">{{item.author.name}} </a><a ng-if="item.time" title="{{item.time|formatDate}}" ng-click="openLink($event, item.url)" class="item-note"><small><i class="icon ion-clock"></i> {{item.time|formatFromNow}}</small></a></div><h2 class="title feed-title"><a ng-click="openLink($event, item.url)">{{item.title}}</a></h2><div ng-if="item.content" class="content feed-content" trust-as-html="item.content"></div><h4 class="card-footer feed-footer text-right positive-100"><a ng-click="openLink($event, item.url)"><span ng-if="item.truncated" translate="">HOME.READ_MORE</span> <span ng-if="!item.truncated" translate="">COMMON.BTN_SHOW</span> <i class="icon ion-chevron-right"></i></a></h4></div></div></div></div></div></ion-content></ion-view>'); $templateCache.put('templates/join/modal_choose_account_type.html','<ion-modal-view class="modal-full-height"><ion-header-bar class="bar-positive"><button class="button button-clear visible-xs" ng-if="!slides.slider.activeIndex" ng-click="closeModal()" translate="">COMMON.BTN_CANCEL</button> <button class="button button-icon button-clear icon ion-ios-arrow-back buttons header-item" ng-click="slidePrev()" ng-if="slides.slider.activeIndex"></button><h1 class="title" translate="">ACCOUNT.NEW.TITLE</h1><button class="button button-clear icon-right visible-xs" ng-if="slides.slider.activeIndex === 0" ng-click="slideNext()"><span translate="">COMMON.BTN_NEXT</span> <i class="icon ion-ios-arrow-right"></i></button></ion-header-bar><ion-slides options="slides.options" slider="slides.slider"><ion-slide-page><ion-content class="has-header padding"><div class="center padding" ng-if="loading"><ion-spinner class="icon" icon="android"></ion-spinner></div><div ng-if="!loading"><p ng-bind-html="\'ACCOUNT.NEW.INTRO_WARNING_TIME\'|translate:currency"></p><div class="row responsive-sm"><div class="col"><div class="item card item-icon-left padding item-text-wrap stable-bg"><i class="icon ion-android-warning assertive"></i><p class="item-content item-icon-left-padding"><span class="dark" translate="">ACCOUNT.NEW.INTRO_WARNING_SECURITY</span><br><small translate="">ACCOUNT.NEW.INTRO_WARNING_SECURITY_HELP</small></p></div></div><div class="col"><div class="item card item-icon-left padding item-text-wrap stable-bg"><i class="icon ion-information-circled positive"></i><p class="item-content item-icon-left-padding"><span class="dark" trust-as-html="\'ACCOUNT.NEW.REGISTRATION_NODE\'|translate:currency.node"></span><br><small trust-as-html="\'ACCOUNT.NEW.REGISTRATION_NODE_HELP\'|translate:currency.node"></small></p></div></div></div></div><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate="">COMMON.BTN_CANCEL</button> <button class="button button-positive icon-right ion-chevron-right ink" ng-click="slideNext()" ng-disabled="loading" type="button" translate="">COMMON.BTN_START</button></div></ion-content></ion-slide-page><ion-slide-page><ion-content class="has-header padding"><p translate="">ACCOUNT.NEW.SELECT_ACCOUNT_TYPE</p><div class="list"><div class="item item-complex card stable-bg item-icon-left item-icon-right ink" ng-click="selectAccountTypeAndClose(\'member\')"><div class="item-content item-text-wrap"><i class="item-image icon dark ion-person"></i><h2 translate="">ACCOUNT.NEW.MEMBER_ACCOUNT</h2><h4 class="gray" ng-bind-html="\'ACCOUNT.NEW.MEMBER_ACCOUNT_HELP\'|translate:currency"></h4><i class="icon dark ion-ios-arrow-right"></i></div></div><cs-extension-point name="select-account-type"></cs-extension-point><div class="item item-complex card stable-bg item-icon-left item-icon-right ink" ng-click="selectAccountTypeAndClose(\'wallet\')"><div class="item-content item-text-wrap"><i class="item-image icon dark ion-card"></i><h2 translate="">ACCOUNT.NEW.WALLET_ACCOUNT</h2><h4 class="gray" translate="">ACCOUNT.NEW.WALLET_ACCOUNT_HELP</h4><i class="icon dark ion-ios-arrow-right"></i></div></div></div><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate="">COMMON.BTN_CANCEL</button></div></ion-content></ion-slide-page></ion-slides></ion-modal-view>'); $templateCache.put('templates/join/modal_join_member.html','<ion-modal-view class="modal-full-height"><ion-header-bar class="bar-positive"><button class="button button-clear visible-xs" ng-if="!slides.slider.activeIndex" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button> <button class="button button-icon button-clear icon ion-ios-arrow-back buttons header-item" ng-click="doPrev()" ng-if="slides.slider.activeIndex && slideBehavior.hasPreviousButton"></button> <button class="button button-icon button-clear icon ion-ios-help-outline visible-xs" ng-if="slideBehavior.helpAnchor" ng-click="showHelpModal(slideBehavior.helpAnchor)"></button><h1 class="title" translate>ACCOUNT.NEW.MEMBER_ACCOUNT_TITLE</h1><button class="button button-clear icon-right visible-xs" ng-if="slideBehavior.hasNextButton" ng-click="doNext()"><span translate>COMMON.BTN_NEXT</span> <i class="icon ion-ios-arrow-right"></i></button> <button class="button button-clear icon-right visible-xs" ng-class="{\'button-text-stable\': !isLicenseRead}" ng-if="slideBehavior.hasAcceptButton" ng-click="isLicenseRead ? doNext() : undefined"><span translate>ACCOUNT.NEW.BTN_ACCEPT</span> <i class="icon ion-ios-arrow-right"></i></button> <button class="button button-clear icon-right visible-xs" ng-if="slideBehavior.hasSendButton" ng-click="doNewAccount()"><i class="icon ion-android-send"></i></button></ion-header-bar><ion-slides options="slides.options" slider="slides.slider"><ion-slide-page ng-if="licenseFileUrl"><ion-content class="has-header" scroll="false"><div class="padding" translate>ACCOUNT.NEW.INFO_LICENSE</div><div class="center padding" ng-if="loading"><ion-spinner class="icon" icon="android"></ion-spinner></div><iframe ng-if="!loading" class="padding-left padding-right no-padding-xs iframe-license" id="iframe-license" ng-src="{{licenseFileUrl}}"></iframe><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL</button> <button class="button button-calm icon-right ion-chevron-right ink" ng-click="doNext(\'licenceForm\')" ng-disabled="!isLicenseRead" type="button" translate>ACCOUNT.NEW.BTN_ACCEPT_LICENSE</button></div></ion-content></ion-slide-page><ion-slide-page><ion-content class="has-header" scroll="true"><form name="pseudoForm" novalidate="" ng-submit="doNext(\'pseudoForm\')"><div class="item item-text-wrap text-center padding"><a class="pull-right icon-help hidden-xs" ng-click="showHelpModal(\'join-pseudo\')"></a> <span translate>ACCOUNT.NEW.PSEUDO_WARNING</span></div><div class="list" ng-init="setForm(pseudoForm, \'pseudoForm\')"><div class="item item-input" ng-class="{\'item-input-error\': (pseudoForm.$submitted && pseudoForm.pseudo.$invalid) || (uiAlreadyUsed && formData.pseudo)}"><span class="input-label" translate>ACCOUNT.NEW.PSEUDO</span> <input id="pseudo" name="pseudo" type="text" placeholder="{{\'ACCOUNT.NEW.PSEUDO_HELP\' | translate}}" ng-model="formData.pseudo" autocomplete="off" ng-minlength="3" ng-maxlength="100" ng-pattern="userIdPattern" ng-model-options="{ debounce: 250 }" required></div><div class="form-errors" ng-show="pseudoForm.$submitted && pseudoForm.pseudo.$error" ng-messages="pseudoForm.pseudo.$error"><div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 3}"></span></div><div class="form-error" ng-message="maxlength"><span translate="ERROR.FIELD_TOO_LONG_WITH_LENGTH" translate-values="{maxLength: 100}"></span></div><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div><div class="form-error" ng-message="pattern"><span translate="ERROR.INVALID_USER_ID"></span></div></div><div class="text-right" style="min-height: 18px"><div class="form-error gray" ng-if="formData.computing && formData.pseudo"><ion-spinner class="icon ion-spinner-small" icon="android" ng-if="formData.computing && formData.pseudo"></ion-spinner><span translate>ACCOUNT.NEW.CHECKING_PSEUDO</span></div><ng-if ng-if="!formData.computing && formData.pseudo"><div class="form-error balanced" ng-if="!uiAlreadyUsed "><i class="icon ion-checkmark balanced"></i> <span translate>ACCOUNT.NEW.PSEUDO_AVAILABLE</span></div><div class="form-error" ng-if="uiAlreadyUsed"><i class="icon ion-close-circled assertive"></i> <span translate>ACCOUNT.NEW.PSEUDO_NOT_AVAILABLE</span></div></ng-if></div><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL</button> <button class="button button-calm icon-right ion-chevron-right ink" type="submit" ng-disabled="uiAlreadyUsed" translate>COMMON.BTN_NEXT</button></div></div></form></ion-content></ion-slide-page><ion-slide-page ng-if="!formData.pubkey"><ion-content class="has-header" scroll="true"><form name="saltForm" novalidate="" ng-submit="doNext(\'saltForm\')"><div class="list" ng-init="setForm(saltForm, \'saltForm\')"><div class="item item-text-wrap text-center padding hidden-xs"><a class="pull-right icon-help" ng-click="showHelpModal(\'join-salt\')"></a> <span translate>ACCOUNT.NEW.SALT_WARNING</span></div><div class="item item-input" ng-class="{ \'item-input-error\': saltForm.$submitted && saltForm.username.$invalid}"><span class="input-label" translate>LOGIN.SALT</span> <input ng-if="!showUsername" name="username" type="password" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" ng-change="formDataChanged()" ng-model="formData.username" autocomplete="off" ng-minlength="8" different-to="formData.pseudo" required> <input ng-if="showUsername" name="username" type="text" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" ng-change="formDataChanged()" ng-model="formData.username" autocomplete="off" ng-minlength="8" different-to="formData.pseudo" required></div><div class="form-errors" ng-show="saltForm.$submitted && saltForm.username.$error" ng-messages="saltForm.username.$error"><div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 8}"></span></div><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div><div class="form-error" ng-message="differentTo"><span translate="ERROR.EQUALS_TO_PSEUDO"></span></div></div><div class="item item-input" ng-class="{ \'item-input-error\': saltForm.$submitted && saltForm.confirmSalt.$invalid}"><span class="input-label pull-right" translate>ACCOUNT.NEW.SALT_CONFIRM</span> <input ng-if="!showUsername" name="confirmUsername" type="password" placeholder="{{\'ACCOUNT.NEW.SALT_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmUsername" autocomplete="off" compare-to="formData.username"> <input ng-if="showUsername" name="confirmUsername" type="text" placeholder="{{\'ACCOUNT.NEW.SALT_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmUsername" autocomplete="off" compare-to="formData.username"></div><div class="form-errors" ng-show="saltForm.$submitted && saltForm.confirmUsername.$error" ng-messages="saltForm.confirmUsername.$error"><div class="form-error" ng-message="compareTo"><span translate="ERROR.SALT_NOT_CONFIRMED"></span></div></div><div class="item item-toggle dark"><span translate>COMMON.SHOW_VALUES</span><label class="toggle toggle-royal"><input type="checkbox" ng-model="showUsername"><div class="track"><div class="handle"></div></div></label></div><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL</button> <button class="button button-calm icon-right ion-chevron-right ink" type="submit" translate>COMMON.BTN_NEXT <i class="icon ion-arrow-right-a"></i></button></div></div></form></ion-content></ion-slide-page><ion-slide-page ng-if="!formData.pubkey"><ion-content class="has-header" scroll="true"><form name="passwordForm" novalidate="" ng-submit="doNext(\'passwordForm\')"><div class="item item-text-wrap text-center padding hidden-xs"><a class="pull-right icon-help" ng-click="showHelpModal(\'join-password\')"></a> <span translate>ACCOUNT.NEW.PASSWORD_WARNING</span></div><div class="list" ng-init="setForm(passwordForm, \'passwordForm\')"><div class="item item-input" ng-class="{ \'item-input-error\': passwordForm.$submitted && passwordForm.password.$invalid}"><span class="input-label" translate>LOGIN.PASSWORD</span> <input ng-if="!showPassword" name="password" type="password" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" autocomplete="off" ng-change="formDataChanged()" ng-minlength="8" different-to="formData.username" required> <input ng-if="showPassword" name="text" type="text" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" autocomplete="off" ng-change="formDataChanged()" ng-minlength="8" different-to="formData.username" required></div><div class="form-errors" ng-show="passwordForm.$submitted && passwordForm.password.$error" ng-messages="passwordForm.password.$error"><div class="form-error" ng-message="minlength"><span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 8}"></span></div><div class="form-error" ng-message="required"><span translate="ERROR.FIELD_REQUIRED"></span></div><div class="form-error" ng-message="differentTo"><span translate="ERROR.EQUALS_TO_SALT"></span></div></div><div class="item item-input" ng-class="{ \'item-input-error\': passwordForm.$submitted && passwordForm.confirmPassword.$invalid}"><span class="input-label" translate>ACCOUNT.NEW.PASSWORD_CONFIRM</span> <input ng-if="!showPassword" name="confirmPassword" type="password" placeholder="{{\'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmPassword" autocomplete="off" compare-to="formData.password"> <input ng-if="showPassword" name="confirmPassword" type="text" placeholder="{{\'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmPassword" autocomplete="off" compare-to="formData.password"></div><div class="form-errors" ng-show="passwordForm.$submitted && passwordForm.confirmPassword.$error" ng-messages="passwordForm.confirmPassword.$error"><div class="form-error" ng-message="compareTo"><span translate="ERROR.PASSWORD_NOT_CONFIRMED"></span></div></div><div class="item item-toggle dark"><span translate>COMMON.SHOW_VALUES</span><label class="toggle toggle-royal"><input type="checkbox" ng-model="showPassword"><div class="track"><div class="handle"></div></div></label></div></div><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL</button> <button class="button button-calm icon-right ion-chevron-right ink" type="submit" ng-click="getRevocationDocument()" translate>COMMON.BTN_NEXT</button></div><div class="padding hidden-xs"></div></form></ion-content></ion-slide-page><ion-slide-page><ion-content class="has-header" scroll="true"><div class="center padding" ng-if="formData.computing"><ion-spinner icon="android"></ion-spinner></div><ng-if ng-if="!formData.computing"><div class="animate-fade-in animate-show-hide ng-hide" ng-show="accountAvailable"><div class="padding text-center" translate>ACCOUNT.NEW.LAST_SLIDE_CONGRATULATION</div><div class="list"><ion-item class="item text-center item-text-wrap"><h3 class="gray" translate>LOGIN.ASSOCIATED_PUBKEY</h3><h3 class="dark bold">{{formData.pubkey}}</h3></ion-item></div><div class="padding hidden-xs text-right"><button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL</button> <button class="button button-positive ink" ng-click="doNewAccount()" translate>COMMON.BTN_SEND <i class="icon ion-android-send"></i></button></div></div><div class="animate-fade-in animate-show-hide ng-hide" ng-show="!accountAvailable"><ion-item class="item-icon-left item-text-wrap text-center"><i class="icon ion-minus-circled assertive"></i> <span id="modal-license" trust-as-html="\'ERROR.EXISTING_ACCOUNT\'|translate"></span></ion-item><div class="list"><ion-item class="item item-text-wrap item-border"><div class="padding text-center"><span class="gray text-no-wrap">{{formData.pubkey}}</span></div></ion-item><div class="padding text-center"><span translate>ERROR.EXISTING_ACCOUNT_REQUEST</span></div></div><div class="padding hidden-xs text-left"><button class="button button-assertive icon-left ion-chevron-left ink" ng-click="identifierRecovery()" translate>COMMON.BTN_MODIFY</button></div></div></ng-if></ion-content></ion-slide-page></ion-slides></ion-modal-view>'); @@ -32171,6 +32171,34 @@ $templateCache.put('plugins/rml9/templates/07-button.html','<!-- Button: Open a $templateCache.put('plugins/rml9/templates/07-view.html','<leaflet id="map-geojson" center="map.center" geojson="map.geojson"></leaflet>\n'); $templateCache.put('plugins/rml9/templates/final-button.html','<!-- Button: Open a view, using `ui-sref` attribute -->\n<button ng-if class="button button-balanced button-small-padding icon ion-android-archive"\n ui-sref="app.rml9({pubkey: formData.pubkey})"\n title="{{\'RML9.BTN_SWOW_TX\' | translate}}">\n</button>\n\n'); $templateCache.put('plugins/rml9/templates/final-view.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n {{\'RML9.VIEW.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content>\n <div class="list">\n\n <!-- buttons bar -->\n <div class="center padding">\n <div class="buttons">\n <button class="button button-balanced icon-left icon ion-archive"\n ng-click="onExportButtonClick()">{{\'RML9.BTN_EXPORT\' | translate}}\n </button>\n </div>\n </div>\n\n <!-- the pubkey -->\n <div class="item">\n {{\'COMMON.PUBKEY\'|translate}}\n <div class="badge">{{pubkey|formatPubkey}}</div>\n </div>\n\n <!-- the balance -->\n <div class="item">\n {{\'RML9.VIEW.BALANCE\'|translate}}\n <div class="badge badge-calm">\n {{balance|formatAmount}} <span ng-bind-html="$root.currency.name|currencySymbol"></span>\n </div>\n </div>\n\n <!-- a text divider-->\n <div class="item item-divider">{{\'RML9.VIEW.DIVIDER\'|translate:{pubkey: pubkey} }}</div>\n\n <!-- iterate on each TX -->\n <div class="row">\n <div class="col col-75">\n <div class="item item-text-wrap" ng-repeat="item in items">\n\n <h3>\n {{item.time|formatDate}}\n <span ng-if="item.comment" class="gray"> | {{item.comment}}</span>\n </h3>\n\n <h4 ng-if="item.uid" class="positive"><i class="icon ion-person"></i> {{item.name||item.uid}}</h4>\n <h4 ng-if="!item.uid" class="gray"><i class="icon ion-key"></i> {{item.pubkey|formatPubkey}}</h4>\n\n <div class="badge"\n ng-class="{\'badge-balanced\': item.amount > 0}">\n {{item.amount|formatAmount}} <span ng-bind-html="$root.currency.name|currencySymbol"></span>\n </div>\n </div>\n\n </div>\n\n <div class="col col-25">\n\n <!-- [NEW] TX input chart -->\n <p class="gray" translate>RML9.CHART.INPUT_CHART_TITLE</p>\n <canvas id="chart-received-pie" class="chart-pie"\n chart-data="inputChart.data"\n chart-labels="inputChart.labels">\n </canvas>\n\n <!-- [NEW] TX input chart -->\n <p class="gray" translate>RML9.CHART.OUTPUT_CHART_TITLE</p>\n <canvas id="chart-sent-pie" class="chart-pie"\n chart-data="outputChart.data"\n chart-labels="outputChart.labels">\n </canvas>\n </div>\n </div>\n </div>\n </ion-content>\n</ion-view>\n'); +$templateCache.put('plugins/graph/templates/account/graph_balance.html','\n <!-- button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <div class="padding-left padding-right">\n <canvas id="account-balance" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-dataset-override="datasetOverride"\n chart-colors="colors"\n chart-options="options"\n chart-labels="labels"\n chart-click="onChartClick">\n </canvas>\n </div>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); +$templateCache.put('plugins/graph/templates/account/graph_certifications.html','\n <div class="padding-left padding-right">\n <canvas id="account-certifications" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-dataset-override="datasetOverride"\n chart-colors="colors"\n chart-options="options"\n chart-labels="labels"\n chart-click="onChartClick">\n </canvas>\n </div>\n'); +$templateCache.put('plugins/graph/templates/account/graph_sum_tx.html','<div class="row responsive-sm" ng-if="!loading">\n\n <div class="col col-10 hidden-xs hidden-sm"> </div>\n\n <div class="col text-center">\n\n <!-- TX input chart -->\n <p class="gray padding text-wrap"\n ng-if="inputChart.data.length"\n translate>GRAPH.ACCOUNT.INPUT_CHART_TITLE</p>\n <canvas id="chart-received-pie" class="chart-pie"\n chart-data="inputChart.data"\n chart-labels="inputChart.labels"\n chart-colors="inputChart.colors"\n chart-click="onInputChartClick">\n </canvas>\n\n </div>\n\n <div class="col col-10 hidden-xs hidden-sm"> </div>\n\n <div class="col text-center">\n\n <!-- TX output chart -->\n <p class="gray padding text-wrap"\n ng-if="outputChart.data.length"\n translate>GRAPH.ACCOUNT.OUTPUT_CHART_TITLE</p>\n <canvas id="chart-sent-pie" class="chart-pie"\n chart-data="outputChart.data"\n chart-labels="outputChart.labels"\n chart-colors="outputChart.colors"\n chart-click="onOutputChartClick">\n </canvas>\n\n </div>\n\n <div class="col col-10 hidden-xs hidden-sm"> </div>\n\n</div>\n'); +$templateCache.put('plugins/graph/templates/account/view_identity_tx_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n\n <button class="button button-stable button-small-padding icon ion-stats-bars"\n ui-sref="app.wot_identity_stats({pubkey: formData.pubkey})"\n title="{{\'GRAPH.ACCOUNT.BTN_SHOW_STATS\' | translate}}">\n </button>\n\n</ng-if>\n'); +$templateCache.put('plugins/graph/templates/account/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.ACCOUNT.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n\n\n <div class="list" >\n\n <!-- - - - - Balance - - - - -->\n <ng-controller ng-controller="GpAccountBalanceCtrl">\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item item-divider" ng-if="!loading" >\n {{\'GRAPH.ACCOUNT.BALANCE_DIVIDER\'|translate}}\n <ion-spinner ng-if="loadingRange" class="ion-spinner-small" icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/account/graph_balance.html\'"\n ng-init="setSize(350, 1000)">\n </div>\n </ng-controller>\n\n </div>\n\n <div class="item no-padding-xs"\n ng-include="::\'plugins/graph/templates/account/graph_sum_tx.html\'"\n ng-controller="GpAccountSumTxCtrl">\n </div>\n\n <!-- - - - - WOT - - - -\n <div class="item item-divider" translate>\n GRAPH.ACCOUNT.WOT_DIVIDER\n </div>\n\n <div class="item no-padding-xs"\n ng-include="::\'plugins/graph/templates/account/graph_certifications.html\'"\n ng-controller="GpAccountCertificationCtrl"\n ng-init="setSize(350, 1000)">\n </div>-->\n\n </ion-content>\n\n</ion-view>\n'); +$templateCache.put('plugins/graph/templates/account/view_wallet_tx_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n\n <button class="button button-stable button-small-padding icon ion-stats-bars"\n ui-sref="app.wot_identity_stats({pubkey: formData.pubkey})"\n title="{{\'GRAPH.ACCOUNT.BTN_SHOW_STATS\' | translate}}">\n </button>\n\n</ng-if>\n'); +$templateCache.put('plugins/graph/templates/blockchain/graph_block_issuers.html','\n <div class="row responsive-lg">\n\n <!-- bar -->\n <div class="col col-75">\n <canvas id="bar" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-options="barOptions"\n chart-click="onChartClick">\n </canvas>\n </div>\n\n <!-- pie -->\n <div class="col col-25 padding-top">\n <canvas id="blocksByIssuer-pie" class="chart-pie"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-click="onChartClick">\n </canvas>\n\n <div class="gray padding-top text-center">\n <small ng-bind-html="\'GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_HELP\'| translate:{issuerCount: data.length, blockCount: blockCount }"></small>\n </div>\n </div>\n </div>\n'); +$templateCache.put('plugins/graph/templates/blockchain/graph_tx_count.html','\n <!-- button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <div class="padding-left padding-right">\n <canvas id="tx-line" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-dataset-override="datasetOverride"\n chart-colors="colors"\n chart-options="options"\n chart-labels="labels"\n chart-click="onChartClick">\n </canvas>\n </div>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); +$templateCache.put('plugins/graph/templates/blockchain/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.BLOCKCHAIN.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list" ng-if="!loading">\n\n\n <!-- TX count -->\n <ng-controller ng-controller="GpBlockchainTxCountCtrl">\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item item-divider" ng-if="!loading" >\n {{\'GRAPH.BLOCKCHAIN.TX_DIVIDER\'|translate}}\n <ion-spinner ng-if="loadingRange" class="ion-spinner-small" icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs no-padding-sm"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/blockchain/graph_tx_count.html\'"\n ng-init="setSize(350, 1000)">\n </div>\n </ng-controller>\n\n\n <!-- Blocks issuer -->\n <ng-controller ng-controller="GpBlockchainIssuersCtrl">\n\n <div class="item item-divider" ng-if="!loading" translate>GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_DIVIDER</div>\n\n <div class="item no-padding-xs no-padding-sm"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/blockchain/graph_block_issuers.html\'"\n ng-init="setSize(300, 750)">\n </div>\n\n </ng-controller>\n\n </ion-content>\n\n</ion-view>\n'); +$templateCache.put('plugins/graph/templates/common/graph_range_bar.html','\n <div class="range range-positive no-padding-left no-padding-right">\n <a\n class="button button-stable button-clear no-padding pull-left"\n ng-click="goPreviousRange($event)">\n <i class="icon ion-chevron-left"></i>\n </a>\n <input type="range"\n ng-model="formData.timePct"\n name="timePct"\n min="0" max="100"\n value="{{formData.timePct}}"\n ng-change="onRangeChanged();"\n ng-model-options="{ debounce: 250 }">\n <a\n class="button button-stable button-clear no-padding pull-right"\n ng-click="goNextRange($event)">\n <i class="icon ion-chevron-right"></i>\n </a>\n </div>\n'); +$templateCache.put('plugins/graph/templates/common/popover_range_actions.html','<ion-popover-view class="has-header popover-graph-currency">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <!-- scale -->\n <a class="item item-icon-left ink"\n ng-click="toggleScale()">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.scale==\'logarithmic\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.LOGARITHMIC_SCALE\' | translate"></span>\n </a>\n\n <!-- duration divider -->\n <div class="item item-divider">\n {{\'GRAPH.COMMON.RANGE_DURATION_DIVIDER\'|translate}}\n </div>\n\n <!-- duration: hour -->\n <a class="item item-icon-left ink"\n ng-click="setRangeDuration(\'hour\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'hour\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.HOUR\' | translate"></span>\n </a>\n\n <!-- duration: day -->\n <a class="item item-icon-left ink"\n ng-click="setRangeDuration(\'day\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'day\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.DAY\' | translate"></span>\n </a>\n\n <!-- duration: month -->\n <a class="item item-icon-left ink"\n ng-click="setRangeDuration(\'month\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'month\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.MONTH\' | translate"></span>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n'); +$templateCache.put('plugins/graph/templates/currency/graph_du.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs no-padding-sm pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="monetaryMass-bar" class="chart-bar"\n height="{{height}}"\n width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-dataset-override="datasetOverride"\n chart-options="options"\n chart-click="showBlock">\n </canvas>\n'); +$templateCache.put('plugins/graph/templates/currency/graph_members_count.html',' <canvas id="membersCount-bar" class="chart-line"\n height="{{height}}"\n width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-options="options"\n chart-dataset-override="datasetOverride"\n chart-click="onChartClick">\n </canvas>\n'); +$templateCache.put('plugins/graph/templates/currency/graph_monetary_mass.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="monetaryMass-bar"\n class="chart-bar"\n height="{{height}}"\n width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-dataset-override="datasetOverride"\n chart-options="options"\n chart-click="onChartClick">\n </canvas>\n'); +$templateCache.put('plugins/graph/templates/currency/popover_monetary_mass_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink"\n ng-click="toggleScale()">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.scale==\'logarithmic\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.LOGARITHMIC_SCALE\' | translate"></span>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n'); +$templateCache.put('plugins/graph/templates/currency/tab_blocks_extend.html','<!-- buttons -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n <div class="item item-divider">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_blocks_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n'); +$templateCache.put('plugins/graph/templates/currency/view_currency_extend.html','\n<!-- section actual parameters -->\n<ng-if ng-if=":state:enable && extensionPoint === \'parameters-actual\'" >\n\n <ng-if ng-if="!smallscreen">\n <div class="item padding-left padding-right no-padding-xs no-padding-sm"\n ng-include="::\'plugins/graph/templates/currency/graph_monetary_mass.html\'"\n ng-controller="GpCurrencyMonetaryMassCtrl"\n ng-init="displayShareAxis=false;">\n </div>\n <div class="item buttons no-padding-top ">\n <a class="pull-right button button-text button-small button-small-padding ink" ui-sref="app.currency_stats_lg">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_DETAILED_STATS</span>\n </a>\n </div>\n </ng-if>\n\n <div class="item item-divider"\n ng-if="smallscreen">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_parameters_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n\n<!-- section Wot -->\n<ng-if ng-if=":state:enable && extensionPoint === \'wot-actual\'" >\n\n <ng-if ng-if="!smallscreen">\n <div class="item padding-left padding-right no-padding-xs no-padding-sm"\n ng-include="::\'plugins/graph/templates/currency/graph_members_count.html\'"\n ng-controller="GpCurrencyMembersCountCtrl">\n </div>\n <div class="item buttons no-padding-top ">\n <a class="pull-right button button-text button-small button-small-padding ink" ui-sref="app.currency_stats_lg">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_DETAILED_STATS</span>\n </a>\n </div>\n </ng-if>\n\n <div class="item item-divider"\n ng-if="smallscreen">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_wot_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n\n<!-- section Wot -->\n<ng-if ng-if=":state:enable && extensionPoint === \'network-actual\'" >\n\n <div class="item padding-left padding-right no-padding-xs no-padding-sm"\n ng-if="!smallscreen"\n ng-include="::\'plugins/graph/templates/blockchain/graph_block_issuers.html\'"\n ng-controller="GpBlockchainIssuersCtrl">\n </div>\n\n <div class="item item-divider"\n ng-if="smallscreen">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_network_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n\n'); +$templateCache.put('plugins/graph/templates/currency/view_stats_lg.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.BLOCKCHAIN.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding" >\n\n\n\n <div class="list" >\n\n <!-- Monetary mass -->\n <ng-controller ng-controller="GpCurrencyMonetaryMassCtrl" >\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading"\n ng-include="::\'plugins/graph/templates/currency/graph_monetary_mass.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n <div ng-if="!loading"\n class="item item-toggle dark no-border text-right">\n <span class="" translate>COMMON.BTN_RELATIVE_UNIT</span>\n <label class="toggle toggle-royal" id="helptip-currency-change-unit">\n <input type="checkbox" ng-model="formData.useRelative">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n </ng-controller>\n\n <!-- DU -->\n <ng-controller ng-controller="GpCurrencyDUCtrl" >\n <div class="item no-padding-xs"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/currency/graph_du.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n </ng-controller>\n\n <!-- Member count -->\n <ng-controller ng-controller="GpCurrencyMembersCountCtrl" >\n <div class="item no-padding-xs"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/currency/graph_members_count.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n\n </div>\n\n </ion-content>\n\n</ion-view>\n'); +$templateCache.put('plugins/graph/templates/currency/view_wot_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n\n <ion-content scroll="true" >\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <ng-include\n ng-if="!loading"\n src="\'plugins/graph/templates/currency/graph_members_count.html\'" ></ng-include>\n </ion-content>\n </ion-view>\n'); +$templateCache.put('plugins/graph/templates/docstats/graph.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs no-padding-sm pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="{{::chartIdPrefix}}{{chart.id}}"\n class="chart-line"\n height="{{height}}"\n width="{{width}}"\n chart-data="chart.data"\n chart-labels="labels"\n chart-dataset-override="chart.datasetOverride"\n chart-options="chart.options"\n chart-click="onChartClick">\n </canvas>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); +$templateCache.put('plugins/graph/templates/docstats/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.DOC_STATS.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding" >\n\n <div class="list" >\n\n <!-- Doc stat -->\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs no-padding-sm" ng-if="!loading"\n ng-repeat="chart in charts"\n ng-include="::\'plugins/graph/templates/docstats/graph.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n'); +$templateCache.put('plugins/graph/templates/network/view_es_network_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'documents-buttons\'">\n <a class="button button-text button-small ink"\n ui-sref="app.doc_stats_lg" >\n <i class="icon ion-stats-bars"></i>\n <span>{{\'NETWORK.VIEW.BTN_GRAPH\'|translate}}</span>\n </a>\n</ng-if>\n'); +$templateCache.put('plugins/graph/templates/network/view_es_peer_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink"\n ng-if="isReachable"\n ui-sref="app.doc_stats_lg(node)">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.DOC_STATS.TITLE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink"\n ng-if="isReachable"\n ui-sref="app.doc_synchro_lg(node)">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.SYNCHRO.TITLE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n</ng-if>\n\n'); +$templateCache.put('plugins/graph/templates/network/view_network_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'blockchain-buttons\'">\n <a class="button button-text button-small ink"\n ui-sref="app.blockchain_stats" >\n <i class="icon ion-stats-bars"></i>\n <span>{{\'NETWORK.VIEW.BTN_GRAPH\'|translate}}</span>\n </a>\n</ng-if>\n'); +$templateCache.put('plugins/graph/templates/network/view_peer_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink"\n ui-sref="app.view_peer_stats({pubkey: node.pubkey})">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.PEER.VIEW.BLOCK_COUNT_LABEL</span>\n <span class="badge"\n ng-if="!loading"\n ng-class="{\'badge-stable\': blockCount > 0, \'badge-assertive\': !blockCount}">\n {{!blockCount ? \'GRAPH.PEER.VIEW.NO_BLOCK\' : \'GRAPH.PEER.VIEW.BLOCK_COUNT\' | translate:{count: blockCount} }}\n </span>\n <ion-spinner class="badge" icon="android" ng-if="loading"></ion-spinner>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n</ng-if>\n\n'); +$templateCache.put('plugins/graph/templates/network/view_peer_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.BLOCKCHAIN.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list" ng-if="!loading">\n\n <!-- - - - - TX divider - - - - -->\n <div class="item item-divider hidden-xs hidden-sm" translate>\n GRAPH.BLOCKCHAIN.TX_DIVIDER\n </div>\n\n <div class="item no-padding-xs"\n ng-include="::\'plugins/graph/templates/blockchain/graph_tx_count.html\'"\n ng-init="setSize(350, 1000)">\n </div>\n\n </ion-content>\n\n</ion-view>\n'); +$templateCache.put('plugins/graph/templates/synchro/graph.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="synchro-chart-{{chart.id}}"\n class="chart-bar"\n height="{{height}}"\n width="{{width}}"\n chart-data="chart.data"\n chart-labels="labels"\n chart-dataset-override="chart.datasetOverride"\n chart-options="chart.options">\n </canvas>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); +$templateCache.put('plugins/graph/templates/synchro/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.SYNCHRO.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding" >\n\n <div class="list" >\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading"\n ng-repeat="chart in charts"\n ng-include="::\'plugins/graph/templates/synchro/graph.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n'); $templateCache.put('plugins/es/templates/blockchain/items_blocks.html','<div class="padding gray" ng-if=":rebind:!search.loading && !search.results.length" translate="">COMMON.SEARCH_NO_RESULT</div><ng-if ng-if=":rebind:!smallscreen"><ng-repeat ng-repeat="block in :rebind:search.results track by block.number" ng-include="!block.empty ? \'templates/blockchain/item_block_lg.html\' : \'templates/blockchain/item_block_empty_lg.html\'"></ng-repeat></ng-if><ng-if ng-if=":rebind:smallscreen"><ng-repeat ng-repeat="block in :rebind:search.results track by block.number" ng-include="::\'templates/blockchain/item_block.html\'"></ng-repeat></ng-if>'); $templateCache.put('plugins/es/templates/blockchain/lookup.html','<ion-view><ion-nav-title><span translate>BLOCKCHAIN.LOOKUP.TITLE</span></ion-nav-title><ion-nav-buttons side="secondary"><button class="button button-icon button-clear icon ion-navicon visible-xs visible-sm" ng-click="toggleCompactMode()"><b class="icon-secondary" ng-class="{\'ion-arrow-down-b\': !compactMode, \'ion-arrow-up-b\': compactMode}" style="top: -12px; left: 11px; font-size: 10px"></b> <b class="icon-secondary" ng-class="{\'ion-arrow-up-b\': !compactMode,\'ion-arrow-down-b\': compactMode}" style="top: 12px; left: 11px; font-size: 10px"></b></button> <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)"></button></ion-nav-buttons><ion-content class="padding no-padding-xs no-padding-sm" scroll="true"><ng-include src="::\'plugins/es/templates/blockchain/lookup_form.html\'"></ng-include></ion-content></ion-view>'); $templateCache.put('plugins/es/templates/blockchain/lookup_form.html','<div class="lookupForm"><div class="item no-padding"><div class="button button-small button-text button-stable button-icon-event padding no-padding-right ink" ng-repeat="filter in search.filters" ng-if="filter"><span ng-bind-html="\'BLOCKCHAIN.LOOKUP.TX_SEARCH_FILTER.\'+filter.type|translate:filter"></span> <i class="icon ion-close" ng-click="itemRemove($index)"></i></div><label class="item-input"><i class="icon ion-search placeholder-icon"></i> <input type="text" class="visible-xs visible-sm" placeholder="{{\'BLOCKCHAIN.LOOKUP.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearchText()"><div class="helptip-anchor-center"><a id="helptip-blockchain-search-text"></a></div></label></div><div class="padding-top padding-xs" style="display: block; height: 60px"><div class="pull-left"><h4 ng-if="search.type==\'last\'" translate="">BLOCKCHAIN.LOOKUP.LAST_BLOCKS</h4><h4 ng-if="search.type==\'text\'">{{\'COMMON.RESULTS_LIST\'|translate}}</h4><h5 class="dark" ng-if="!search.loading && search.total"><span translate="COMMON.RESULTS_COUNT" translate-values="{count: search.total}"></span> <small class="gray" ng-if=":rebind:search.took && expertMode">- {{:rebind:\'COMMON.EXECUTION_TIME\'|translate: {duration: search.took} }} </small><small class="gray" ng-if=":rebind:expertMode && search.filters && search.filters.length">- <a ng-click="toggleShowQuery()" ng-if="!showQuery">{{\'DOCUMENT.LOOKUP.SHOW_QUERY\'|translate }} <i class="icon ion-arrow-down-b gray"></i> </a><a ng-click="toggleShowQuery()" ng-if="showQuery">{{\'DOCUMENT.LOOKUP.HIDE_QUERY\'|translate }} <i class="icon ion-arrow-up-b gray"></i></a></small></h5><h5 class="gray" ng-if="search.loading"><ion-spinner class="icon ion-spinner-small" icon="android"></ion-spinner><span translate="">COMMON.SEARCHING</span><br></h5></div></div><div class="item no-border no-padding" ng-if=":rebind:search.filters && search.filters.length && expertMode"><small class="no-padding no-margin" ng-if="showQuery"><span class="gray text-wrap dark">{{:rebind:search.query}}</span></small></div><ion-list class="list list-blocks" ng-class="::motion.ionListClass"><ng-include src="::\'plugins/es/templates/blockchain/items_blocks.html\'"></ng-include></ion-list><ion-infinite-scroll ng-if="search.hasMore" spinner="android" on-infinite="showMore()" distance="1%"></ion-infinite-scroll></div>'); @@ -32271,34 +32299,6 @@ $templateCache.put('plugins/es/templates/wot/popover_certification_actions.html' $templateCache.put('plugins/es/templates/wot/view_certifications_extend.html','<ng-if ng-if=":state:enable && extensionPoint === \'nav-buttons\'"><button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showCertificationActionsPopover($event)"></button></ng-if><ng-if ng-if=":state:enable && extensionPoint === \'buttons\'"><button class="button button-stable button-small-padding icon ion-android-more-vertical" ng-click="showCertificationActionsPopover($event)" title="{{\'COMMON.POPOVER_ACTIONS_TITLE\' | translate}}"></button></ng-if>'); $templateCache.put('plugins/es/templates/wot/view_identity_extend.html','<ng-if ng-if=":state:enable && extensionPoint === \'hero\'"><small class="light" style="display: inline-block" ng-include="::\'plugins/es/templates/common/view_likes.html\'"></small></ng-if><ng-if ng-if=":state:enable && extensionPoint === \'buttons-top-fab\'"><button id="fab-compose-{{:rebind:formData.pubkey}}" class="button button-fab button-fab-top-left button-fab-hero mini button-stable spin" style="left: 88px" ng-click="showNewMessageModal()"><i class="icon ion-email"></i></button></ng-if><ng-if ng-if=":state:enable && extensionPoint === \'buttons\'"><button class="button button-stable button-small-padding icon ion-email" ng-disabled="loading" ng-click="showNewMessageModal()" title="{{\'MESSAGE.BTN_WRITE\' | translate}}"></button></ng-if><ng-if ng-if=":state:enable && extensionPoint === \'after-buttons\'"><button class="button button-stable button-small-padding icon ion-android-more-vertical" ng-click="showActionsPopover($event)"></button></ng-if><ng-if ng-if=":state:enable && extensionPoint === \'after-general\'"><span class="item item-divider item-divider-top-border"><span>{{\'PROFILE.PROFILE_DIVIDER\' | translate}} <a style="font-size: 12pt; cursor: pointer" ng-click="showProfileHelp=!showProfileHelp" class="icon positive ion-ios-help-outline ink" title="{{\'PROFILE.PROFILE_DIVIDER_HELP\' | translate}}"></a></span></span><div class="item item-text-wrap positive item-small-height" ng-show="showProfileHelp"><small translate>PROFILE.PROFILE_DIVIDER_HELP</small></div><ng-include src="::\'plugins/es/templates/user/items_profile.html\'" ng-init="showName=false;"></ng-include></ng-if>'); $templateCache.put('plugins/es/templates/wot/view_popover_actions.html','<ion-popover-view class="fit has-header"><ion-header-bar><h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1></ion-header-bar><ion-content scroll="false"><div class="list item-text-wrap"><a class="item item-icon-left ink visible-xs visible-sm" ng-click="showSharePopover($event)"><i class="icon ion-android-share-alt"></i> {{\'COMMON.BTN_SHARE\' | translate}} </a><a class="item item-icon-left assertive ink" ng-if="canDelete" ng-click="delete()"><i class="icon ion-trash-a"></i> {{\'COMMON.BTN_DELETE\' | translate}} </a><a class="item item-icon-left ink" ng-if="!canEdit && likeData.likes" ng-click="hideActionsPopover() && toggleLike($event)"><i class="icon" ng-class="{\'ion-heart-broken\': likeData.likes.wasHit, \'ion-heart\': !likeData.likes.wasHit}"></i> {{(likeData.likes.wasHit ? \'COMMON.BTN_LIKE_REMOVE\' : \'COMMON.BTN_LIKE\' )| translate}} </a><a class="item item-icon-left ink" ng-if="!canEdit && likeData.abuses" ng-disabled="!!likeData.abuses.wasHitCount" ng-class="{\'gray\': !!likeData.abuses.wasHitCount}" ng-click="hideActionsPopover() && reportAbuse($event)"><i class="icon ion-android-warning"></i> {{\'COMMON.BTN_REPORT_ABUSE_DOTS\' | translate}}</a></div></ion-content></ion-popover-view>'); -$templateCache.put('plugins/graph/templates/account/graph_balance.html','\n <!-- button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <div class="padding-left padding-right">\n <canvas id="account-balance" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-dataset-override="datasetOverride"\n chart-colors="colors"\n chart-options="options"\n chart-labels="labels"\n chart-click="onChartClick">\n </canvas>\n </div>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); -$templateCache.put('plugins/graph/templates/account/graph_certifications.html','\n <div class="padding-left padding-right">\n <canvas id="account-certifications" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-dataset-override="datasetOverride"\n chart-colors="colors"\n chart-options="options"\n chart-labels="labels"\n chart-click="onChartClick">\n </canvas>\n </div>\n'); -$templateCache.put('plugins/graph/templates/account/graph_sum_tx.html','<div class="row responsive-sm" ng-if="!loading">\n\n <div class="col col-10 hidden-xs hidden-sm"> </div>\n\n <div class="col text-center">\n\n <!-- TX input chart -->\n <p class="gray padding text-wrap"\n ng-if="inputChart.data.length"\n translate>GRAPH.ACCOUNT.INPUT_CHART_TITLE</p>\n <canvas id="chart-received-pie" class="chart-pie"\n chart-data="inputChart.data"\n chart-labels="inputChart.labels"\n chart-colors="inputChart.colors"\n chart-click="onInputChartClick">\n </canvas>\n\n </div>\n\n <div class="col col-10 hidden-xs hidden-sm"> </div>\n\n <div class="col text-center">\n\n <!-- TX output chart -->\n <p class="gray padding text-wrap"\n ng-if="outputChart.data.length"\n translate>GRAPH.ACCOUNT.OUTPUT_CHART_TITLE</p>\n <canvas id="chart-sent-pie" class="chart-pie"\n chart-data="outputChart.data"\n chart-labels="outputChart.labels"\n chart-colors="outputChart.colors"\n chart-click="onOutputChartClick">\n </canvas>\n\n </div>\n\n <div class="col col-10 hidden-xs hidden-sm"> </div>\n\n</div>\n'); -$templateCache.put('plugins/graph/templates/account/view_identity_tx_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n\n <button class="button button-stable button-small-padding icon ion-stats-bars"\n ui-sref="app.wot_identity_stats({pubkey: formData.pubkey})"\n title="{{\'GRAPH.ACCOUNT.BTN_SHOW_STATS\' | translate}}">\n </button>\n\n</ng-if>\n'); -$templateCache.put('plugins/graph/templates/account/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.ACCOUNT.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n\n\n <div class="list" >\n\n <!-- - - - - Balance - - - - -->\n <ng-controller ng-controller="GpAccountBalanceCtrl">\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item item-divider" ng-if="!loading" >\n {{\'GRAPH.ACCOUNT.BALANCE_DIVIDER\'|translate}}\n <ion-spinner ng-if="loadingRange" class="ion-spinner-small" icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/account/graph_balance.html\'"\n ng-init="setSize(350, 1000)">\n </div>\n </ng-controller>\n\n </div>\n\n <div class="item no-padding-xs"\n ng-include="::\'plugins/graph/templates/account/graph_sum_tx.html\'"\n ng-controller="GpAccountSumTxCtrl">\n </div>\n\n <!-- - - - - WOT - - - -\n <div class="item item-divider" translate>\n GRAPH.ACCOUNT.WOT_DIVIDER\n </div>\n\n <div class="item no-padding-xs"\n ng-include="::\'plugins/graph/templates/account/graph_certifications.html\'"\n ng-controller="GpAccountCertificationCtrl"\n ng-init="setSize(350, 1000)">\n </div>-->\n\n </ion-content>\n\n</ion-view>\n'); -$templateCache.put('plugins/graph/templates/account/view_wallet_tx_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n\n <button class="button button-stable button-small-padding icon ion-stats-bars"\n ui-sref="app.wot_identity_stats({pubkey: formData.pubkey})"\n title="{{\'GRAPH.ACCOUNT.BTN_SHOW_STATS\' | translate}}">\n </button>\n\n</ng-if>\n'); -$templateCache.put('plugins/graph/templates/blockchain/graph_block_issuers.html','\n <div class="row responsive-lg">\n\n <!-- bar -->\n <div class="col col-75">\n <canvas id="bar" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-options="barOptions"\n chart-click="onChartClick">\n </canvas>\n </div>\n\n <!-- pie -->\n <div class="col col-25 padding-top">\n <canvas id="blocksByIssuer-pie" class="chart-pie"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-click="onChartClick">\n </canvas>\n\n <div class="gray padding-top text-center">\n <small ng-bind-html="\'GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_HELP\'| translate:{issuerCount: data.length, blockCount: blockCount }"></small>\n </div>\n </div>\n </div>\n'); -$templateCache.put('plugins/graph/templates/blockchain/graph_tx_count.html','\n <!-- button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <div class="padding-left padding-right">\n <canvas id="tx-line" class="chart-bar"\n height="{{height}}" width="{{width}}"\n chart-data="data"\n chart-dataset-override="datasetOverride"\n chart-colors="colors"\n chart-options="options"\n chart-labels="labels"\n chart-click="onChartClick">\n </canvas>\n </div>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); -$templateCache.put('plugins/graph/templates/blockchain/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.BLOCKCHAIN.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list" ng-if="!loading">\n\n\n <!-- TX count -->\n <ng-controller ng-controller="GpBlockchainTxCountCtrl">\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item item-divider" ng-if="!loading" >\n {{\'GRAPH.BLOCKCHAIN.TX_DIVIDER\'|translate}}\n <ion-spinner ng-if="loadingRange" class="ion-spinner-small" icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs no-padding-sm"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/blockchain/graph_tx_count.html\'"\n ng-init="setSize(350, 1000)">\n </div>\n </ng-controller>\n\n\n <!-- Blocks issuer -->\n <ng-controller ng-controller="GpBlockchainIssuersCtrl">\n\n <div class="item item-divider" ng-if="!loading" translate>GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_DIVIDER</div>\n\n <div class="item no-padding-xs no-padding-sm"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/blockchain/graph_block_issuers.html\'"\n ng-init="setSize(300, 750)">\n </div>\n\n </ng-controller>\n\n </ion-content>\n\n</ion-view>\n'); -$templateCache.put('plugins/graph/templates/common/graph_range_bar.html','\n <div class="range range-positive no-padding-left no-padding-right">\n <a\n class="button button-stable button-clear no-padding pull-left"\n ng-click="goPreviousRange($event)">\n <i class="icon ion-chevron-left"></i>\n </a>\n <input type="range"\n ng-model="formData.timePct"\n name="timePct"\n min="0" max="100"\n value="{{formData.timePct}}"\n ng-change="onRangeChanged();"\n ng-model-options="{ debounce: 250 }">\n <a\n class="button button-stable button-clear no-padding pull-right"\n ng-click="goNextRange($event)">\n <i class="icon ion-chevron-right"></i>\n </a>\n </div>\n'); -$templateCache.put('plugins/graph/templates/common/popover_range_actions.html','<ion-popover-view class="has-header popover-graph-currency">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <!-- scale -->\n <a class="item item-icon-left ink"\n ng-click="toggleScale()">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.scale==\'logarithmic\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.LOGARITHMIC_SCALE\' | translate"></span>\n </a>\n\n <!-- duration divider -->\n <div class="item item-divider">\n {{\'GRAPH.COMMON.RANGE_DURATION_DIVIDER\'|translate}}\n </div>\n\n <!-- duration: hour -->\n <a class="item item-icon-left ink"\n ng-click="setRangeDuration(\'hour\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'hour\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.HOUR\' | translate"></span>\n </a>\n\n <!-- duration: day -->\n <a class="item item-icon-left ink"\n ng-click="setRangeDuration(\'day\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'day\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.DAY\' | translate"></span>\n </a>\n\n <!-- duration: month -->\n <a class="item item-icon-left ink"\n ng-click="setRangeDuration(\'month\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'month\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.MONTH\' | translate"></span>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n'); -$templateCache.put('plugins/graph/templates/currency/graph_du.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs no-padding-sm pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="monetaryMass-bar" class="chart-bar"\n height="{{height}}"\n width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-dataset-override="datasetOverride"\n chart-options="options"\n chart-click="showBlock">\n </canvas>\n'); -$templateCache.put('plugins/graph/templates/currency/graph_members_count.html',' <canvas id="membersCount-bar" class="chart-line"\n height="{{height}}"\n width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-options="options"\n chart-dataset-override="datasetOverride"\n chart-click="onChartClick">\n </canvas>\n'); -$templateCache.put('plugins/graph/templates/currency/graph_monetary_mass.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="monetaryMass-bar"\n class="chart-bar"\n height="{{height}}"\n width="{{width}}"\n chart-data="data"\n chart-labels="labels"\n chart-colors="colors"\n chart-dataset-override="datasetOverride"\n chart-options="options"\n chart-click="onChartClick">\n </canvas>\n'); -$templateCache.put('plugins/graph/templates/currency/popover_monetary_mass_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink"\n ng-click="toggleScale()">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.scale==\'logarithmic\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.LOGARITHMIC_SCALE\' | translate"></span>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n'); -$templateCache.put('plugins/graph/templates/currency/tab_blocks_extend.html','<!-- buttons -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n <div class="item item-divider">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_blocks_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n'); -$templateCache.put('plugins/graph/templates/currency/view_currency_extend.html','\n<!-- section actual parameters -->\n<ng-if ng-if=":state:enable && extensionPoint === \'parameters-actual\'" >\n\n <ng-if ng-if="!smallscreen">\n <div class="item padding-left padding-right no-padding-xs no-padding-sm"\n ng-include="::\'plugins/graph/templates/currency/graph_monetary_mass.html\'"\n ng-controller="GpCurrencyMonetaryMassCtrl"\n ng-init="displayShareAxis=false;">\n </div>\n <div class="item buttons no-padding-top ">\n <a class="pull-right button button-text button-small button-small-padding ink" ui-sref="app.currency_stats_lg">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_DETAILED_STATS</span>\n </a>\n </div>\n </ng-if>\n\n <div class="item item-divider"\n ng-if="smallscreen">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_parameters_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n\n<!-- section Wot -->\n<ng-if ng-if=":state:enable && extensionPoint === \'wot-actual\'" >\n\n <ng-if ng-if="!smallscreen">\n <div class="item padding-left padding-right no-padding-xs no-padding-sm"\n ng-include="::\'plugins/graph/templates/currency/graph_members_count.html\'"\n ng-controller="GpCurrencyMembersCountCtrl">\n </div>\n <div class="item buttons no-padding-top ">\n <a class="pull-right button button-text button-small button-small-padding ink" ui-sref="app.currency_stats_lg">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_DETAILED_STATS</span>\n </a>\n </div>\n </ng-if>\n\n <div class="item item-divider"\n ng-if="smallscreen">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_wot_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n\n<!-- section Wot -->\n<ng-if ng-if=":state:enable && extensionPoint === \'network-actual\'" >\n\n <div class="item padding-left padding-right no-padding-xs no-padding-sm"\n ng-if="!smallscreen"\n ng-include="::\'plugins/graph/templates/blockchain/graph_block_issuers.html\'"\n ng-controller="GpBlockchainIssuersCtrl">\n </div>\n\n <div class="item item-divider"\n ng-if="smallscreen">\n <a class="badge button button-text button-small button-small-padding ink" ui-sref="app.currency.tab_network_stats">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.COMMON.BTN_SHOW_STATS</span>\n </a>\n </div>\n</ng-if>\n\n'); -$templateCache.put('plugins/graph/templates/currency/view_stats_lg.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.BLOCKCHAIN.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding" >\n\n\n\n <div class="list" >\n\n <!-- Monetary mass -->\n <ng-controller ng-controller="GpCurrencyMonetaryMassCtrl" >\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading"\n ng-include="::\'plugins/graph/templates/currency/graph_monetary_mass.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n <div ng-if="!loading"\n class="item item-toggle dark no-border text-right">\n <span class="" translate>COMMON.BTN_RELATIVE_UNIT</span>\n <label class="toggle toggle-royal" id="helptip-currency-change-unit">\n <input type="checkbox" ng-model="formData.useRelative">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n </ng-controller>\n\n <!-- DU -->\n <ng-controller ng-controller="GpCurrencyDUCtrl" >\n <div class="item no-padding-xs"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/currency/graph_du.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n </ng-controller>\n\n <!-- Member count -->\n <ng-controller ng-controller="GpCurrencyMembersCountCtrl" >\n <div class="item no-padding-xs"\n ng-if="!loading"\n ng-include="::\'plugins/graph/templates/currency/graph_members_count.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n\n </div>\n\n </ion-content>\n\n</ion-view>\n'); -$templateCache.put('plugins/graph/templates/currency/view_wot_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n\n <ion-content scroll="true" >\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <ng-include\n ng-if="!loading"\n src="\'plugins/graph/templates/currency/graph_members_count.html\'" ></ng-include>\n </ion-content>\n </ion-view>\n'); -$templateCache.put('plugins/graph/templates/docstats/graph.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs no-padding-sm pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="{{::chartIdPrefix}}{{chart.id}}"\n class="chart-line"\n height="{{height}}"\n width="{{width}}"\n chart-data="chart.data"\n chart-labels="labels"\n chart-dataset-override="chart.datasetOverride"\n chart-options="chart.options"\n chart-click="onChartClick">\n </canvas>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); -$templateCache.put('plugins/graph/templates/docstats/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.DOC_STATS.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding" >\n\n <div class="list" >\n\n <!-- Doc stat -->\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs no-padding-sm" ng-if="!loading"\n ng-repeat="chart in charts"\n ng-include="::\'plugins/graph/templates/docstats/graph.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n'); -$templateCache.put('plugins/graph/templates/network/view_es_network_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'documents-buttons\'">\n <a class="button button-text button-small ink"\n ui-sref="app.doc_stats_lg" >\n <i class="icon ion-stats-bars"></i>\n <span>{{\'NETWORK.VIEW.BTN_GRAPH\'|translate}}</span>\n </a>\n</ng-if>\n'); -$templateCache.put('plugins/graph/templates/network/view_es_peer_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink"\n ng-if="isReachable"\n ui-sref="app.doc_stats_lg(node)">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.DOC_STATS.TITLE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink"\n ng-if="isReachable"\n ui-sref="app.doc_synchro_lg(node)">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.SYNCHRO.TITLE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n</ng-if>\n\n'); -$templateCache.put('plugins/graph/templates/network/view_network_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'blockchain-buttons\'">\n <a class="button button-text button-small ink"\n ui-sref="app.blockchain_stats" >\n <i class="icon ion-stats-bars"></i>\n <span>{{\'NETWORK.VIEW.BTN_GRAPH\'|translate}}</span>\n </a>\n</ng-if>\n'); -$templateCache.put('plugins/graph/templates/network/view_peer_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink"\n ui-sref="app.view_peer_stats({pubkey: node.pubkey})">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.PEER.VIEW.BLOCK_COUNT_LABEL</span>\n <span class="badge"\n ng-if="!loading"\n ng-class="{\'badge-stable\': blockCount > 0, \'badge-assertive\': !blockCount}">\n {{!blockCount ? \'GRAPH.PEER.VIEW.NO_BLOCK\' : \'GRAPH.PEER.VIEW.BLOCK_COUNT\' | translate:{count: blockCount} }}\n </span>\n <ion-spinner class="badge" icon="android" ng-if="loading"></ion-spinner>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n</ng-if>\n\n'); -$templateCache.put('plugins/graph/templates/network/view_peer_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.BLOCKCHAIN.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list" ng-if="!loading">\n\n <!-- - - - - TX divider - - - - -->\n <div class="item item-divider hidden-xs hidden-sm" translate>\n GRAPH.BLOCKCHAIN.TX_DIVIDER\n </div>\n\n <div class="item no-padding-xs"\n ng-include="::\'plugins/graph/templates/blockchain/graph_tx_count.html\'"\n ng-init="setSize(350, 1000)">\n </div>\n\n </ion-content>\n\n</ion-view>\n'); -$templateCache.put('plugins/graph/templates/synchro/graph.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline "\n style="top: 33px; margin-top:-33px; position: relative;">\n <button\n class="button button-stable button-clear no-padding-xs pull-right"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="synchro-chart-{{chart.id}}"\n class="chart-bar"\n height="{{height}}"\n width="{{width}}"\n chart-data="chart.data"\n chart-labels="labels"\n chart-dataset-override="chart.datasetOverride"\n chart-options="chart.options">\n </canvas>\n\n <ng-include src="::\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n'); -$templateCache.put('plugins/graph/templates/synchro/view_stats.html','<ion-view left-buttons="leftButtons"\n cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.SYNCHRO.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding" >\n\n <div class="list" >\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading"\n ng-repeat="chart in charts"\n ng-include="::\'plugins/graph/templates/synchro/graph.html\'"\n ng-init="setSize(250, 1000)">\n </div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n'); $templateCache.put('plugins/map/templates/common/edit_position_extend.html','<div class="item no-padding hidden-xs hidden-sm {{ionItemClass}}" ng-if="formData.geoPoint && formData.geoPoint.lat && formData.geoPoint.lon">\n <leaflet id="{{::mapId}}"\n height="250px"\n center="map.center"\n markers="map.markers"\n defaults="map.defaults">\n </leaflet>\n</div>\n'); $templateCache.put('plugins/map/templates/network/item_search_tooltip.html','<a href="#">\n {{peer.dns || peer.server}}\n <span class="{{peer.uid ? \'positive\' : \'gray\'}}">\n <i class="icon {{peer.uid ? \'ion-person\' : \'ion-key\'}}"></i>\n {{peer.uid ? (peer.name||peer.uid) : (peer.pubkey|formatPubkey) }}\n </span>\n <span class="gray">{{peer.ipv4 ? (peer.ipv4 + \':\' + peer.port) : \'\'}}</span>\n <span class="{{peer.bma.useSsl ? \'\' : \'ng-hide\'}}"><i class="ion-locked"></i> <small>SSL</small></span>\n</a>\n'); $templateCache.put('plugins/map/templates/network/lookup_extend.html','<!-- FIXME issue #755 - https://git.duniter.org/clients/cesium-grp/cesium/issues/755\n<a ng-if="enable"\n class="button button-text button-small ink hidden-sm hidden-xs"\n title="{{\'MAP.NETWORK.LOOKUP.BTN_MAP_HELP\' | translate}}"\n ui-sref="app.view_network_map">\n <i class="icon ion-ios-location"></i>\n {{\'MAP.NETWORK.LOOKUP.BTN_MAP\' | translate}}\n</a>\n -->\n'); @@ -32312,12 +32312,12 @@ $templateCache.put('plugins/map/templates/wot/item_search_tooltip.html','<a href $templateCache.put('plugins/map/templates/wot/lookup_lg_extend.html','<a ng-if="enable"\n class="button button-text button-small ink hidden-sm hidden-xs"\n title="{{\'MAP.WOT.LOOKUP.BTN_MAP_HELP\' | translate}}"\n ui-sref="app.view_wot_map">\n <i class="icon ion-ios-location"></i>\n {{\'MAP.WOT.LOOKUP.BTN_MAP\' | translate}}\n</a>\n'); $templateCache.put('plugins/map/templates/wot/popup_marker.html','\n<div class="item no-border no-padding item-avatar "\n ng-if="loadingMarker">\n\n <i class="item-image icon ion-person"></i>\n\n <div class="item-content item-avatar-left-padding padding-top" >\n <h2 class="stable-bg">\n \n </h2>\n <h4 class="stable-bg col-75">\n \n </h4>\n <h4 class="stable-bg col-50">\n \n </h4>\n </div>\n</div>\n\n<a class="item no-border no-padding item-avatar ink animate-fade-in animate-show-hide ng-hide"\n ng-show="!loadingMarker"\n ui-sref="app.wot_identity({pubkey: formData.pubkey, uid: formData.uid})">\n\n <i ng-if="formData.avatar" class="item-image avatar" style="background-image: url({{::formData.avatar.src}})"></i>\n <i ng-if="!formData.avatar && formData.uid" class="item-image icon ion-person"></i>\n <i ng-if="!formData.avatar && !formData.uid" class="item-image icon ion-card"></i>\n\n <div class="item-content item-avatar-left-padding padding-top">\n <h2 class="dark">\n {{formData.name}}\n </h2>\n <h4>\n <span ng-if="formData.uid" class="positive">\n <b class="ion-person"></b>\n {{formData.uid}}\n </span>\n <span class="gray" title="{{formData.pubkey}}"><b class="ion-key"></b> {{formData.pubkey|formatPubkey}}</span>\n <span class="assertive" ng-if="!formData.isMember">\n {{::\'WOT.NOT_MEMBER_PARENTHESIS\'|translate}}\n </span>\n </h4>\n <h4 ng-if="formData.profile.city" class="gray" title="{{formData.profile.city}}">\n <b class="ion-location"></b> {{formData.profile.city}}\n </h4>\n </div>\n</a>\n<!-- buttons -->\n<div class="item no-border no-padding">\n <div class="pull-left gray">\n <!-- show description -->\n <a class="animate-fade-in animate-show-hide gray ng-hide"\n ng-class="{\'ion-arrow-down-b\': !showDescription, \'ion-arrow-up-b\': showDescription}"\n ng-click="showDescription=!showDescription;"\n title="{{\'PROFILE.DESCRIPTION\'|translate}}"\n ng-show="!loadingMarker && formData.profile.description">\n \n </a>\n </div>\n <div style="font-size: 18px;" class="pull-right gray">\n <!-- share -->\n <a class="icon ion-android-share-alt "\n ng-click="showSharePopover($event)"\n title="{{\'COMMON.BTN_SHARE\' | translate}}"> </a>\n <!-- certify -->\n <a class="icon ion-ribbon-b"\n ng-click="certify()"\n title="{{\'WOT.BTN_CERTIFY\' | translate}}"\n ng-hide="!canCertify"> </a>\n <!-- compose message -->\n <a class="icon ion-compose"\n ng-click="showNewMessageModal()"\n title="{{\'MESSAGE.BTN_WRITE\' | translate}}"> </a>\n <!-- transfer -->\n <a class="icon ion-card"\n ng-click="showTransferModal({pubkey:formData.pubkey, uid: formData.name||formData.uid})"\n title="{{\'COMMON.BTN_SEND_MONEY\' | translate}}"> </a>\n\n </div>\n</div>\n<div class="item no-border no-padding item-text-wrap hidden-xs ng-hide" ng-show="showDescription">\n <small trust-as-html="formData.profile.description|truncText:500"></small>\n</div>\n'); $templateCache.put('plugins/map/templates/wot/view_map.html','<ion-view left-buttons="leftButtons" class="view-map-wot">\n <ion-nav-title>\n <span class="hidden-xs" translate>MAP.WOT.VIEW.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="load()">\n </button>\n </ion-nav-buttons>\n\n <ion-content data-tap-disabled="true">\n <a id="helptip-map-wot" style="left: 150px; top: 50px; position: relative;"></a>\n <leaflet id="{{::mapId}}"\n height="100%"\n layers="map.layers"\n markers="map.markers"\n lf-center="map.center"\n bounds="map.bounds">\n </leaflet>\n </ion-content>\n</ion-view>\n'); -$templateCache.put('plugins/es/templates/message/tabs/tab_list.html','<ion-view><ion-nav-buttons side="secondary"><cs-extension-point name="nav-buttons"></cs-extension-point><button class="button button-icon button-clear icon ion-android-more-vertical" ng-click="showActionsPopover($event)"></button></ion-nav-buttons><ion-content><ion-refresher pulling-text="{{\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="refresh()"></ion-refresher><cs-extension-point name="buttons"></cs-extension-point><ng-include src="::\'plugins/es/templates/message/list.html\'"></ng-include></ion-content><div class="visible-xs visible-sm"><button ng-if="fabButtonNewMessageId" id="{{::fabButtonNewMessageId}}" class="button button-fab button-fab-bottom-right button-assertive spin has-footer" ng-click="showNewMessageModal()"><i class="icon ion-compose"></i></button></div></ion-view>'); -$templateCache.put('plugins/es/templates/registry/tabs/tab_registry.html','<ion-view><ion-nav-buttons side="secondary"><cs-extension-point name="nav-buttons"></cs-extension-point><button class="button button-icon button-clear" ng-click="showFiltersPopover($event)"><i class="icon ion-android-funnel"></i></button> <button class="button button-icon button-clear icon ion-android-more-vertical" ng-click="showActionsPopover($event)"></button></ion-nav-buttons><ion-content><ion-refresher pulling-text="{{\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="doSearch()"></ion-refresher><cs-extension-point name="buttons"></cs-extension-point><ng-include src="::\'plugins/es/templates/registry/lookup_form.html\'"></ng-include><ng-include src="::\'plugins/es/templates/registry/lookup_list.html\'"></ng-include></ion-content></ion-view>'); $templateCache.put('plugins/graph/templates/currency/tabs/tab_blocks_stats.html','<ion-view>\n <ion-content>\n <div\n ng-include="::\'plugins/graph/templates/blockchain/graph_tx_count.html\'"\n ng-controller="GpBlockchainTxCountCtrl"\n ng-init="setSize(500,700,false)">\n </div>\n </ion-content>\n</ion-view>\n'); $templateCache.put('plugins/graph/templates/currency/tabs/tab_network_stats.html','<ion-view>\n <ion-content>\n\n <div class="list">\n <div class="item"\n ng-include="::\'plugins/graph/templates/blockchain/graph_block_issuers.html\'"\n ng-controller="GpBlockchainIssuersCtrl"\n ng-init="setSize(500,700,true)">\n </div>\n </div>\n </ion-content>\n</ion-view>\n'); $templateCache.put('plugins/graph/templates/currency/tabs/tab_parameters_stats.html','<ion-view>\n <ion-content>\n <div class="list no-padding-xs no-padding-sm">\n\n <ng-container ng-controller="GpCurrencyMonetaryMassCtrl">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <!-- Monetary mass -->\n <div class="item"\n ng-include="::\'plugins/graph/templates/currency/graph_monetary_mass.html\'"\n ng-init="setSize(500,700,true)">\n </div>\n </ng-container>\n\n <!-- DU -->\n <ng-container ng-controller="GpCurrencyDUCtrl">\n <div class="item"\n ng-include="::\'plugins/graph/templates/currency/graph_du.html\'"\n ng-init="setSize(500,700,true)">\n </div>\n </ng-container>\n </div>\n </ion-content>\n</ion-view>\n'); -$templateCache.put('plugins/graph/templates/currency/tabs/tab_wot_stats.html','<ion-view>\n <ion-content>\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list no-padding">\n <div class="item no-padding-top"\n ng-include="::\'plugins/graph/templates/currency/graph_members_count.html\'"\n ng-init="setSize(600,700,false)">\n </div>\n </div>\n </ion-content>\n</ion-view>\n');}]); +$templateCache.put('plugins/graph/templates/currency/tabs/tab_wot_stats.html','<ion-view>\n <ion-content>\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list no-padding">\n <div class="item no-padding-top"\n ng-include="::\'plugins/graph/templates/currency/graph_members_count.html\'"\n ng-init="setSize(600,700,false)">\n </div>\n </div>\n </ion-content>\n</ion-view>\n'); +$templateCache.put('plugins/es/templates/message/tabs/tab_list.html','<ion-view><ion-nav-buttons side="secondary"><cs-extension-point name="nav-buttons"></cs-extension-point><button class="button button-icon button-clear icon ion-android-more-vertical" ng-click="showActionsPopover($event)"></button></ion-nav-buttons><ion-content><ion-refresher pulling-text="{{\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="refresh()"></ion-refresher><cs-extension-point name="buttons"></cs-extension-point><ng-include src="::\'plugins/es/templates/message/list.html\'"></ng-include></ion-content><div class="visible-xs visible-sm"><button ng-if="fabButtonNewMessageId" id="{{::fabButtonNewMessageId}}" class="button button-fab button-fab-bottom-right button-assertive spin has-footer" ng-click="showNewMessageModal()"><i class="icon ion-compose"></i></button></div></ion-view>'); +$templateCache.put('plugins/es/templates/registry/tabs/tab_registry.html','<ion-view><ion-nav-buttons side="secondary"><cs-extension-point name="nav-buttons"></cs-extension-point><button class="button button-icon button-clear" ng-click="showFiltersPopover($event)"><i class="icon ion-android-funnel"></i></button> <button class="button button-icon button-clear icon ion-android-more-vertical" ng-click="showActionsPopover($event)"></button></ion-nav-buttons><ion-content><ion-refresher pulling-text="{{\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="doSearch()"></ion-refresher><cs-extension-point name="buttons"></cs-extension-point><ng-include src="::\'plugins/es/templates/registry/lookup_form.html\'"></ng-include><ng-include src="::\'plugins/es/templates/registry/lookup_list.html\'"></ng-include></ion-content></ion-view>');}]); angular.module('cesium.es.plugin', [ // Services diff --git a/assets/www/plugins/cordova-plugin-file/www/DirectoryEntry.js b/assets/www/plugins/cordova-plugin-file/www/DirectoryEntry.js new file mode 100644 index 0000000000000000000000000000000000000000..bb676eb6c995b051b51fd9bbb6b65508b79b6aeb --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/DirectoryEntry.js @@ -0,0 +1,120 @@ +cordova.define("cordova-plugin-file.DirectoryEntry", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var argscheck = require('cordova/argscheck'); +var utils = require('cordova/utils'); +var exec = require('cordova/exec'); +var Entry = require('./Entry'); +var FileError = require('./FileError'); +var DirectoryReader = require('./DirectoryReader'); + +/** + * An interface representing a directory on the file system. + * + * {boolean} isFile always false (readonly) + * {boolean} isDirectory always true (readonly) + * {DOMString} name of the directory, excluding the path leading to it (readonly) + * {DOMString} fullPath the absolute full path to the directory (readonly) + * {FileSystem} filesystem on which the directory resides (readonly) + */ +var DirectoryEntry = function (name, fullPath, fileSystem, nativeURL) { + + // add trailing slash if it is missing + if ((fullPath) && !/\/$/.test(fullPath)) { + fullPath += '/'; + } + // add trailing slash if it is missing + if (nativeURL && !/\/$/.test(nativeURL)) { + nativeURL += '/'; + } + DirectoryEntry.__super__.constructor.call(this, false, true, name, fullPath, fileSystem, nativeURL); +}; + +utils.extend(DirectoryEntry, Entry); + +/** + * Creates a new DirectoryReader to read entries from this directory + */ +DirectoryEntry.prototype.createReader = function () { + return new DirectoryReader(this.toInternalURL()); +}; + +/** + * Creates or looks up a directory + * + * @param {DOMString} path either a relative or absolute path from this directory in which to look up or create a directory + * @param {Flags} options to create or exclusively create the directory + * @param {Function} successCallback is called with the new entry + * @param {Function} errorCallback is called with a FileError + */ +DirectoryEntry.prototype.getDirectory = function (path, options, successCallback, errorCallback) { + argscheck.checkArgs('sOFF', 'DirectoryEntry.getDirectory', arguments); + var fs = this.filesystem; + var win = successCallback && function (result) { + var entry = new DirectoryEntry(result.name, result.fullPath, fs, result.nativeURL); + successCallback(entry); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getDirectory', [this.toInternalURL(), path, options]); +}; + +/** + * Deletes a directory and all of it's contents + * + * @param {Function} successCallback is called with no parameters + * @param {Function} errorCallback is called with a FileError + */ +DirectoryEntry.prototype.removeRecursively = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'DirectoryEntry.removeRecursively', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(successCallback, fail, 'File', 'removeRecursively', [this.toInternalURL()]); +}; + +/** + * Creates or looks up a file + * + * @param {DOMString} path either a relative or absolute path from this directory in which to look up or create a file + * @param {Flags} options to create or exclusively create the file + * @param {Function} successCallback is called with the new entry + * @param {Function} errorCallback is called with a FileError + */ +DirectoryEntry.prototype.getFile = function (path, options, successCallback, errorCallback) { + argscheck.checkArgs('sOFF', 'DirectoryEntry.getFile', arguments); + var fs = this.filesystem; + var win = successCallback && function (result) { + var FileEntry = require('./FileEntry'); + var entry = new FileEntry(result.name, result.fullPath, fs, result.nativeURL); + successCallback(entry); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getFile', [this.toInternalURL(), path, options]); +}; + +module.exports = DirectoryEntry; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/DirectoryReader.js b/assets/www/plugins/cordova-plugin-file/www/DirectoryReader.js new file mode 100644 index 0000000000000000000000000000000000000000..417c85f10769b3ad49cf1caa26a0812357f5e7cb --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/DirectoryReader.js @@ -0,0 +1,75 @@ +cordova.define("cordova-plugin-file.DirectoryReader", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var FileError = require('./FileError'); + +/** + * An interface that lists the files and directories in a directory. + */ +function DirectoryReader (localURL) { + this.localURL = localURL || null; + this.hasReadEntries = false; +} + +/** + * Returns a list of entries from a directory. + * + * @param {Function} successCallback is called with a list of entries + * @param {Function} errorCallback is called with a FileError + */ +DirectoryReader.prototype.readEntries = function (successCallback, errorCallback) { + // If we've already read and passed on this directory's entries, return an empty list. + if (this.hasReadEntries) { + successCallback([]); + return; + } + var reader = this; + var win = typeof successCallback !== 'function' ? null : function (result) { + var retVal = []; + for (var i = 0; i < result.length; i++) { + var entry = null; + if (result[i].isDirectory) { + entry = new (require('./DirectoryEntry'))(); + } else if (result[i].isFile) { + entry = new (require('./FileEntry'))(); + } + entry.isDirectory = result[i].isDirectory; + entry.isFile = result[i].isFile; + entry.name = result[i].name; + entry.fullPath = result[i].fullPath; + entry.filesystem = new (require('./FileSystem'))(result[i].filesystemName); + entry.nativeURL = result[i].nativeURL; + retVal.push(entry); + } + reader.hasReadEntries = true; + successCallback(retVal); + }; + var fail = typeof errorCallback !== 'function' ? null : function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'readEntries', [this.localURL]); +}; + +module.exports = DirectoryReader; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/Entry.js b/assets/www/plugins/cordova-plugin-file/www/Entry.js new file mode 100644 index 0000000000000000000000000000000000000000..b296d999e9284fe07b1419adc4f0fd2b64ec282f --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/Entry.js @@ -0,0 +1,263 @@ +cordova.define("cordova-plugin-file.Entry", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var argscheck = require('cordova/argscheck'); +var exec = require('cordova/exec'); +var FileError = require('./FileError'); +var Metadata = require('./Metadata'); + +/** + * Represents a file or directory on the local file system. + * + * @param isFile + * {boolean} true if Entry is a file (readonly) + * @param isDirectory + * {boolean} true if Entry is a directory (readonly) + * @param name + * {DOMString} name of the file or directory, excluding the path + * leading to it (readonly) + * @param fullPath + * {DOMString} the absolute full path to the file or directory + * (readonly) + * @param fileSystem + * {FileSystem} the filesystem on which this entry resides + * (readonly) + * @param nativeURL + * {DOMString} an alternate URL which can be used by native + * webview controls, for example media players. + * (optional, readonly) + */ +function Entry (isFile, isDirectory, name, fullPath, fileSystem, nativeURL) { + this.isFile = !!isFile; + this.isDirectory = !!isDirectory; + this.name = name || ''; + this.fullPath = fullPath || ''; + this.filesystem = fileSystem || null; + this.nativeURL = nativeURL || null; +} + +/** + * Look up the metadata of the entry. + * + * @param successCallback + * {Function} is called with a Metadata object + * @param errorCallback + * {Function} is called with a FileError + */ +Entry.prototype.getMetadata = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'Entry.getMetadata', arguments); + var success = successCallback && function (entryMetadata) { + var metadata = new Metadata({ + size: entryMetadata.size, + modificationTime: entryMetadata.lastModifiedDate + }); + successCallback(metadata); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(success, fail, 'File', 'getFileMetadata', [this.toInternalURL()]); +}; + +/** + * Set the metadata of the entry. + * + * @param successCallback + * {Function} is called with a Metadata object + * @param errorCallback + * {Function} is called with a FileError + * @param metadataObject + * {Object} keys and values to set + */ +Entry.prototype.setMetadata = function (successCallback, errorCallback, metadataObject) { + argscheck.checkArgs('FFO', 'Entry.setMetadata', arguments); + exec(successCallback, errorCallback, 'File', 'setMetadata', [this.toInternalURL(), metadataObject]); +}; + +/** + * Move a file or directory to a new location. + * + * @param parent + * {DirectoryEntry} the directory to which to move this entry + * @param newName + * {DOMString} new name of the entry, defaults to the current name + * @param successCallback + * {Function} called with the new DirectoryEntry object + * @param errorCallback + * {Function} called with a FileError + */ +Entry.prototype.moveTo = function (parent, newName, successCallback, errorCallback) { + argscheck.checkArgs('oSFF', 'Entry.moveTo', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + var srcURL = this.toInternalURL(); + // entry name + var name = newName || this.name; + var success = function (entry) { + if (entry) { + if (successCallback) { + // create appropriate Entry object + var newFSName = entry.filesystemName || (entry.filesystem && entry.filesystem.name); + var fs = newFSName ? new FileSystem(newFSName, { name: '', fullPath: '/' }) : new FileSystem(parent.filesystem.name, { name: '', fullPath: '/' }); // eslint-disable-line no-undef + var result = (entry.isDirectory) ? new (require('./DirectoryEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL) : new (require('cordova-plugin-file.FileEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL); + successCallback(result); + } + } else { + // no Entry object returned + if (fail) { + fail(FileError.NOT_FOUND_ERR); + } + } + }; + + // copy + exec(success, fail, 'File', 'moveTo', [srcURL, parent.toInternalURL(), name]); +}; + +/** + * Copy a directory to a different location. + * + * @param parent + * {DirectoryEntry} the directory to which to copy the entry + * @param newName + * {DOMString} new name of the entry, defaults to the current name + * @param successCallback + * {Function} called with the new Entry object + * @param errorCallback + * {Function} called with a FileError + */ +Entry.prototype.copyTo = function (parent, newName, successCallback, errorCallback) { + argscheck.checkArgs('oSFF', 'Entry.copyTo', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + var srcURL = this.toInternalURL(); + // entry name + var name = newName || this.name; + // success callback + var success = function (entry) { + if (entry) { + if (successCallback) { + // create appropriate Entry object + var newFSName = entry.filesystemName || (entry.filesystem && entry.filesystem.name); + var fs = newFSName ? new FileSystem(newFSName, { name: '', fullPath: '/' }) : new FileSystem(parent.filesystem.name, { name: '', fullPath: '/' }); // eslint-disable-line no-undef + var result = (entry.isDirectory) ? new (require('./DirectoryEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL) : new (require('cordova-plugin-file.FileEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL); + successCallback(result); + } + } else { + // no Entry object returned + if (fail) { + fail(FileError.NOT_FOUND_ERR); + } + } + }; + + // copy + exec(success, fail, 'File', 'copyTo', [srcURL, parent.toInternalURL(), name]); +}; + +/** + * Return a URL that can be passed across the bridge to identify this entry. + */ +Entry.prototype.toInternalURL = function () { + if (this.filesystem && this.filesystem.__format__) { + return this.filesystem.__format__(this.fullPath, this.nativeURL); + } +}; + +/** + * Return a URL that can be used to identify this entry. + * Use a URL that can be used to as the src attribute of a <video> or + * <audio> tag. If that is not possible, construct a cdvfile:// URL. + */ +Entry.prototype.toURL = function () { + if (this.nativeURL) { + return this.nativeURL; + } + // fullPath attribute may contain the full URL in the case that + // toInternalURL fails. + return this.toInternalURL() || 'file://localhost' + this.fullPath; +}; + +/** + * Backwards-compatibility: In v1.0.0 - 1.0.2, .toURL would only return a + * cdvfile:// URL, and this method was necessary to obtain URLs usable by the + * webview. + * See CB-6051, CB-6106, CB-6117, CB-6152, CB-6199, CB-6201, CB-6243, CB-6249, + * and CB-6300. + */ +Entry.prototype.toNativeURL = function () { + console.log("DEPRECATED: Update your code to use 'toURL'"); + return this.toURL(); +}; + +/** + * Returns a URI that can be used to identify this entry. + * + * @param {DOMString} mimeType for a FileEntry, the mime type to be used to interpret the file, when loaded through this URI. + * @return uri + */ +Entry.prototype.toURI = function (mimeType) { + console.log("DEPRECATED: Update your code to use 'toURL'"); + return this.toURL(); +}; + +/** + * Remove a file or directory. It is an error to attempt to delete a + * directory that is not empty. It is an error to attempt to delete a + * root directory of a file system. + * + * @param successCallback {Function} called with no parameters + * @param errorCallback {Function} called with a FileError + */ +Entry.prototype.remove = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'Entry.remove', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(successCallback, fail, 'File', 'remove', [this.toInternalURL()]); +}; + +/** + * Look up the parent DirectoryEntry of this entry. + * + * @param successCallback {Function} called with the parent DirectoryEntry object + * @param errorCallback {Function} called with a FileError + */ +Entry.prototype.getParent = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'Entry.getParent', arguments); + var fs = this.filesystem; + var win = successCallback && function (result) { + var DirectoryEntry = require('./DirectoryEntry'); + var entry = new DirectoryEntry(result.name, result.fullPath, fs, result.nativeURL); + successCallback(entry); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getParent', [this.toInternalURL()]); +}; + +module.exports = Entry; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/File.js b/assets/www/plugins/cordova-plugin-file/www/File.js new file mode 100644 index 0000000000000000000000000000000000000000..c7717865329ab6d9170601ffe9395f6292cb3ac3 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/File.js @@ -0,0 +1,81 @@ +cordova.define("cordova-plugin-file.File", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Constructor. + * name {DOMString} name of the file, without path information + * fullPath {DOMString} the full path of the file, including the name + * type {DOMString} mime type + * lastModifiedDate {Date} last modified date + * size {Number} size of the file in bytes + */ + +var File = function (name, localURL, type, lastModifiedDate, size) { + this.name = name || ''; + this.localURL = localURL || null; + this.type = type || null; + this.lastModified = lastModifiedDate || null; + // For backwards compatibility, store the timestamp in lastModifiedDate as well + this.lastModifiedDate = lastModifiedDate || null; + this.size = size || 0; + + // These store the absolute start and end for slicing the file. + this.start = 0; + this.end = this.size; +}; + +/** + * Returns a "slice" of the file. Since Cordova Files don't contain the actual + * content, this really returns a File with adjusted start and end. + * Slices of slices are supported. + * start {Number} The index at which to start the slice (inclusive). + * end {Number} The index at which to end the slice (exclusive). + */ +File.prototype.slice = function (start, end) { + var size = this.end - this.start; + var newStart = 0; + var newEnd = size; + if (arguments.length) { + if (start < 0) { + newStart = Math.max(size + start, 0); + } else { + newStart = Math.min(size, start); + } + } + + if (arguments.length >= 2) { + if (end < 0) { + newEnd = Math.max(size + end, 0); + } else { + newEnd = Math.min(end, size); + } + } + + var newFile = new File(this.name, this.localURL, this.type, this.lastModified, this.size); + newFile.start = this.start + newStart; + newFile.end = this.start + newEnd; + return newFile; +}; + +module.exports = File; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileEntry.js b/assets/www/plugins/cordova-plugin-file/www/FileEntry.js new file mode 100644 index 0000000000000000000000000000000000000000..6651b5542e23e9be8bcbbacaf7a00ce64230936d --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileEntry.js @@ -0,0 +1,95 @@ +cordova.define("cordova-plugin-file.FileEntry", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var utils = require('cordova/utils'); +var exec = require('cordova/exec'); +var Entry = require('./Entry'); +var FileWriter = require('./FileWriter'); +var File = require('./File'); +var FileError = require('./FileError'); + +/** + * An interface representing a file on the file system. + * + * {boolean} isFile always true (readonly) + * {boolean} isDirectory always false (readonly) + * {DOMString} name of the file, excluding the path leading to it (readonly) + * {DOMString} fullPath the absolute full path to the file (readonly) + * {FileSystem} filesystem on which the file resides (readonly) + */ +var FileEntry = function (name, fullPath, fileSystem, nativeURL) { + // remove trailing slash if it is present + if (fullPath && /\/$/.test(fullPath)) { + fullPath = fullPath.substring(0, fullPath.length - 1); + } + if (nativeURL && /\/$/.test(nativeURL)) { + nativeURL = nativeURL.substring(0, nativeURL.length - 1); + } + + FileEntry.__super__.constructor.apply(this, [true, false, name, fullPath, fileSystem, nativeURL]); +}; + +utils.extend(FileEntry, Entry); + +/** + * Creates a new FileWriter associated with the file that this FileEntry represents. + * + * @param {Function} successCallback is called with the new FileWriter + * @param {Function} errorCallback is called with a FileError + */ +FileEntry.prototype.createWriter = function (successCallback, errorCallback) { + this.file(function (filePointer) { + var writer = new FileWriter(filePointer); + + if (writer.localURL === null || writer.localURL === '') { + if (errorCallback) { + errorCallback(new FileError(FileError.INVALID_STATE_ERR)); + } + } else { + if (successCallback) { + successCallback(writer); + } + } + }, errorCallback); +}; + +/** + * Returns a File that represents the current state of the file that this FileEntry represents. + * + * @param {Function} successCallback is called with the new File object + * @param {Function} errorCallback is called with a FileError + */ +FileEntry.prototype.file = function (successCallback, errorCallback) { + var localURL = this.toInternalURL(); + var win = successCallback && function (f) { + var file = new File(f.name, localURL, f.type, f.lastModifiedDate, f.size); + successCallback(file); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getFileMetadata', [localURL]); +}; + +module.exports = FileEntry; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileError.js b/assets/www/plugins/cordova-plugin-file/www/FileError.js new file mode 100644 index 0000000000000000000000000000000000000000..f378c38746f37e505b25c091b78f691526440788 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileError.js @@ -0,0 +1,49 @@ +cordova.define("cordova-plugin-file.FileError", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * FileError + */ +function FileError (error) { + this.code = error || null; +} + +// File error codes +// Found in DOMException +FileError.NOT_FOUND_ERR = 1; +FileError.SECURITY_ERR = 2; +FileError.ABORT_ERR = 3; + +// Added by File API specification +FileError.NOT_READABLE_ERR = 4; +FileError.ENCODING_ERR = 5; +FileError.NO_MODIFICATION_ALLOWED_ERR = 6; +FileError.INVALID_STATE_ERR = 7; +FileError.SYNTAX_ERR = 8; +FileError.INVALID_MODIFICATION_ERR = 9; +FileError.QUOTA_EXCEEDED_ERR = 10; +FileError.TYPE_MISMATCH_ERR = 11; +FileError.PATH_EXISTS_ERR = 12; + +module.exports = FileError; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileReader.js b/assets/www/plugins/cordova-plugin-file/www/FileReader.js new file mode 100644 index 0000000000000000000000000000000000000000..5c030913b9eb7caa32e7f137760562fe578525e1 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileReader.js @@ -0,0 +1,301 @@ +cordova.define("cordova-plugin-file.FileReader", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var modulemapper = require('cordova/modulemapper'); +var utils = require('cordova/utils'); +var FileError = require('./FileError'); +var ProgressEvent = require('./ProgressEvent'); +var origFileReader = modulemapper.getOriginalSymbol(window, 'FileReader'); + +/** + * This class reads the mobile device file system. + * + * For Android: + * The root directory is the root of the file system. + * To read from the SD card, the file name is "sdcard/my_file.txt" + * @constructor + */ +var FileReader = function () { + this._readyState = 0; + this._error = null; + this._result = null; + this._progress = null; + this._localURL = ''; + this._realReader = origFileReader ? new origFileReader() : {}; // eslint-disable-line new-cap +}; + +/** + * Defines the maximum size to read at a time via the native API. The default value is a compromise between + * minimizing the overhead of many exec() calls while still reporting progress frequently enough for large files. + * (Note attempts to allocate more than a few MB of contiguous memory on the native side are likely to cause + * OOM exceptions, while the JS engine seems to have fewer problems managing large strings or ArrayBuffers.) + */ +FileReader.READ_CHUNK_SIZE = 256 * 1024; + +// States +FileReader.EMPTY = 0; +FileReader.LOADING = 1; +FileReader.DONE = 2; + +utils.defineGetter(FileReader.prototype, 'readyState', function () { + return this._localURL ? this._readyState : this._realReader.readyState; +}); + +utils.defineGetter(FileReader.prototype, 'error', function () { + return this._localURL ? this._error : this._realReader.error; +}); + +utils.defineGetter(FileReader.prototype, 'result', function () { + return this._localURL ? this._result : this._realReader.result; +}); + +function defineEvent (eventName) { + utils.defineGetterSetter(FileReader.prototype, eventName, function () { + return this._realReader[eventName] || null; + }, function (value) { + this._realReader[eventName] = value; + }); +} +defineEvent('onloadstart'); // When the read starts. +defineEvent('onprogress'); // While reading (and decoding) file or fileBlob data, and reporting partial file data (progress.loaded/progress.total) +defineEvent('onload'); // When the read has successfully completed. +defineEvent('onerror'); // When the read has failed (see errors). +defineEvent('onloadend'); // When the request has completed (either in success or failure). +defineEvent('onabort'); // When the read has been aborted. For instance, by invoking the abort() method. + +function initRead (reader, file) { + // Already loading something + if (reader.readyState === FileReader.LOADING) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + reader._result = null; + reader._error = null; + reader._progress = 0; + reader._readyState = FileReader.LOADING; + + if (typeof file.localURL === 'string') { + reader._localURL = file.localURL; + } else { + reader._localURL = ''; + return true; + } + + if (reader.onloadstart) { + reader.onloadstart(new ProgressEvent('loadstart', {target: reader})); + } +} + +/** + * Callback used by the following read* functions to handle incremental or final success. + * Must be bound to the FileReader's this along with all but the last parameter, + * e.g. readSuccessCallback.bind(this, "readAsText", "UTF-8", offset, totalSize, accumulate) + * @param readType The name of the read function to call. + * @param encoding Text encoding, or null if this is not a text type read. + * @param offset Starting offset of the read. + * @param totalSize Total number of bytes or chars to read. + * @param accumulate A function that takes the callback result and accumulates it in this._result. + * @param r Callback result returned by the last read exec() call, or null to begin reading. + */ +function readSuccessCallback (readType, encoding, offset, totalSize, accumulate, r) { + if (this._readyState === FileReader.DONE) { + return; + } + + var CHUNK_SIZE = FileReader.READ_CHUNK_SIZE; + if (readType === 'readAsDataURL') { + // Windows proxy does not support reading file slices as Data URLs + // so read the whole file at once. + CHUNK_SIZE = cordova.platformId === 'windows' ? totalSize : // eslint-disable-line no-undef + // Calculate new chunk size for data URLs to be multiply of 3 + // Otherwise concatenated base64 chunks won't be valid base64 data + FileReader.READ_CHUNK_SIZE - (FileReader.READ_CHUNK_SIZE % 3) + 3; + } + + if (typeof r !== 'undefined') { + accumulate(r); + this._progress = Math.min(this._progress + CHUNK_SIZE, totalSize); + + if (typeof this.onprogress === 'function') { + this.onprogress(new ProgressEvent('progress', {loaded: this._progress, total: totalSize})); + } + } + + if (typeof r === 'undefined' || this._progress < totalSize) { + var execArgs = [ + this._localURL, + offset + this._progress, + offset + this._progress + Math.min(totalSize - this._progress, CHUNK_SIZE)]; + if (encoding) { + execArgs.splice(1, 0, encoding); + } + exec( + readSuccessCallback.bind(this, readType, encoding, offset, totalSize, accumulate), + readFailureCallback.bind(this), + 'File', readType, execArgs); + } else { + this._readyState = FileReader.DONE; + + if (typeof this.onload === 'function') { + this.onload(new ProgressEvent('load', {target: this})); + } + + if (typeof this.onloadend === 'function') { + this.onloadend(new ProgressEvent('loadend', {target: this})); + } + } +} + +/** + * Callback used by the following read* functions to handle errors. + * Must be bound to the FileReader's this, e.g. readFailureCallback.bind(this) + */ +function readFailureCallback (e) { + if (this._readyState === FileReader.DONE) { + return; + } + + this._readyState = FileReader.DONE; + this._result = null; + this._error = new FileError(e); + + if (typeof this.onerror === 'function') { + this.onerror(new ProgressEvent('error', {target: this})); + } + + if (typeof this.onloadend === 'function') { + this.onloadend(new ProgressEvent('loadend', {target: this})); + } +} + +/** + * Abort reading file. + */ +FileReader.prototype.abort = function () { + if (origFileReader && !this._localURL) { + return this._realReader.abort(); + } + this._result = null; + + if (this._readyState === FileReader.DONE || this._readyState === FileReader.EMPTY) { + return; + } + + this._readyState = FileReader.DONE; + + // If abort callback + if (typeof this.onabort === 'function') { + this.onabort(new ProgressEvent('abort', {target: this})); + } + // If load end callback + if (typeof this.onloadend === 'function') { + this.onloadend(new ProgressEvent('loadend', {target: this})); + } +}; + +/** + * Read text file. + * + * @param file {File} File object containing file properties + * @param encoding [Optional] (see http://www.iana.org/assignments/character-sets) + */ +FileReader.prototype.readAsText = function (file, encoding) { + if (initRead(this, file)) { + return this._realReader.readAsText(file, encoding); + } + + // Default encoding is UTF-8 + var enc = encoding || 'UTF-8'; + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsText', enc, file.start, totalSize, function (r) { + if (this._progress === 0) { + this._result = ''; + } + this._result += r; + }.bind(this)); +}; + +/** + * Read file and return data as a base64 encoded data url. + * A data url is of the form: + * data:[<mediatype>][;base64],<data> + * + * @param file {File} File object containing file properties + */ +FileReader.prototype.readAsDataURL = function (file) { + if (initRead(this, file)) { + return this._realReader.readAsDataURL(file); + } + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsDataURL', null, file.start, totalSize, function (r) { + var commaIndex = r.indexOf(','); + if (this._progress === 0) { + this._result = r; + } else { + this._result += r.substring(commaIndex + 1); + } + }.bind(this)); +}; + +/** + * Read file and return data as a binary data. + * + * @param file {File} File object containing file properties + */ +FileReader.prototype.readAsBinaryString = function (file) { + if (initRead(this, file)) { + return this._realReader.readAsBinaryString(file); + } + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsBinaryString', null, file.start, totalSize, function (r) { + if (this._progress === 0) { + this._result = ''; + } + this._result += r; + }.bind(this)); +}; + +/** + * Read file and return data as a binary data. + * + * @param file {File} File object containing file properties + */ +FileReader.prototype.readAsArrayBuffer = function (file) { + if (initRead(this, file)) { + return this._realReader.readAsArrayBuffer(file); + } + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsArrayBuffer', null, file.start, totalSize, function (r) { + var resultArray = (this._progress === 0 ? new Uint8Array(totalSize) : new Uint8Array(this._result)); + resultArray.set(new Uint8Array(r), this._progress); + this._result = resultArray.buffer; + }.bind(this)); +}; + +module.exports = FileReader; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileSystem.js b/assets/www/plugins/cordova-plugin-file/www/FileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..ad9ea785932d46328fd0cbbc654c04e1b8eab7f4 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileSystem.js @@ -0,0 +1,58 @@ +cordova.define("cordova-plugin-file.FileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var DirectoryEntry = require('./DirectoryEntry'); + +/** + * An interface representing a file system + * + * @constructor + * {DOMString} name the unique name of the file system (readonly) + * {DirectoryEntry} root directory of the file system (readonly) + */ +var FileSystem = function (name, root) { + this.name = name; + if (root) { + this.root = new DirectoryEntry(root.name, root.fullPath, this, root.nativeURL); + } else { + this.root = new DirectoryEntry(this.name, '/', this); + } +}; + +FileSystem.prototype.__format__ = function (fullPath, nativeUrl) { + return fullPath; +}; + +FileSystem.prototype.toJSON = function () { + return '<FileSystem: ' + this.name + '>'; +}; + +// Use instead of encodeURI() when encoding just the path part of a URI rather than an entire URI. +FileSystem.encodeURIPath = function (path) { + // Because # is a valid filename character, it must be encoded to prevent part of the + // path from being parsed as a URI fragment. + return encodeURI(path).replace(/#/g, '%23'); +}; + +module.exports = FileSystem; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileUploadOptions.js b/assets/www/plugins/cordova-plugin-file/www/FileUploadOptions.js new file mode 100644 index 0000000000000000000000000000000000000000..6acd09334b02f780ccaa42712b837cb41bf415bc --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileUploadOptions.js @@ -0,0 +1,44 @@ +cordova.define("cordova-plugin-file.FileUploadOptions", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Options to customize the HTTP request used to upload files. + * @constructor + * @param fileKey {String} Name of file request parameter. + * @param fileName {String} Filename to be used by the server. Defaults to image.jpg. + * @param mimeType {String} Mimetype of the uploaded file. Defaults to image/jpeg. + * @param params {Object} Object with key: value params to send to the server. + * @param headers {Object} Keys are header names, values are header values. Multiple + * headers of the same name are not supported. + */ +var FileUploadOptions = function (fileKey, fileName, mimeType, params, headers, httpMethod) { + this.fileKey = fileKey || null; + this.fileName = fileName || null; + this.mimeType = mimeType || null; + this.params = params || null; + this.headers = headers || null; + this.httpMethod = httpMethod || null; +}; + +module.exports = FileUploadOptions; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileUploadResult.js b/assets/www/plugins/cordova-plugin-file/www/FileUploadResult.js new file mode 100644 index 0000000000000000000000000000000000000000..e3c3a743144a6795c8f5f2eb50919539dc68bc41 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileUploadResult.js @@ -0,0 +1,33 @@ +cordova.define("cordova-plugin-file.FileUploadResult", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * FileUploadResult + * @constructor + */ +module.exports = function FileUploadResult (size, code, content) { + this.bytesSent = size; + this.responseCode = code; + this.response = content; +}; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/FileWriter.js b/assets/www/plugins/cordova-plugin-file/www/FileWriter.js new file mode 100644 index 0000000000000000000000000000000000000000..c3e85621992e2201982bef3aebc7e04d0e7d2887 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/FileWriter.js @@ -0,0 +1,328 @@ +cordova.define("cordova-plugin-file.FileWriter", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var FileError = require('./FileError'); +var FileReader = require('./FileReader'); +var ProgressEvent = require('./ProgressEvent'); + +/** + * This class writes to the mobile device file system. + * + * For Android: + * The root directory is the root of the file system. + * To write to the SD card, the file name is "sdcard/my_file.txt" + * + * @constructor + * @param file {File} File object containing file properties + * @param append if true write to the end of the file, otherwise overwrite the file + */ +var FileWriter = function (file) { + this.fileName = ''; + this.length = 0; + if (file) { + this.localURL = file.localURL || file; + this.length = file.size || 0; + } + // default is to write at the beginning of the file + this.position = 0; + + this.readyState = 0; // EMPTY + + this.result = null; + + // Error + this.error = null; + + // Event handlers + this.onwritestart = null; // When writing starts + this.onprogress = null; // While writing the file, and reporting partial file data + this.onwrite = null; // When the write has successfully completed. + this.onwriteend = null; // When the request has completed (either in success or failure). + this.onabort = null; // When the write has been aborted. For instance, by invoking the abort() method. + this.onerror = null; // When the write has failed (see errors). +}; + +// States +FileWriter.INIT = 0; +FileWriter.WRITING = 1; +FileWriter.DONE = 2; + +/** + * Abort writing file. + */ +FileWriter.prototype.abort = function () { + // check for invalid state + if (this.readyState === FileWriter.DONE || this.readyState === FileWriter.INIT) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + // set error + this.error = new FileError(FileError.ABORT_ERR); + + this.readyState = FileWriter.DONE; + + // If abort callback + if (typeof this.onabort === 'function') { + this.onabort(new ProgressEvent('abort', {'target': this})); + } + + // If write end callback + if (typeof this.onwriteend === 'function') { + this.onwriteend(new ProgressEvent('writeend', {'target': this})); + } +}; + +/** + * Writes data to the file + * + * @param data text or blob to be written + * @param isPendingBlobReadResult {Boolean} true if the data is the pending blob read operation result + */ +FileWriter.prototype.write = function (data, isPendingBlobReadResult) { + + var that = this; + var supportsBinary = (typeof window.Blob !== 'undefined' && typeof window.ArrayBuffer !== 'undefined'); + /* eslint-disable no-undef */ + var isProxySupportBlobNatively = (cordova.platformId === 'windows8' || cordova.platformId === 'windows'); + var isBinary; + + // Check to see if the incoming data is a blob + if (data instanceof File || (!isProxySupportBlobNatively && supportsBinary && data instanceof Blob)) { + var fileReader = new FileReader(); + /* eslint-enable no-undef */ + fileReader.onload = function () { + // Call this method again, with the arraybuffer as argument + FileWriter.prototype.write.call(that, this.result, true /* isPendingBlobReadResult */); + }; + fileReader.onerror = function () { + // DONE state + that.readyState = FileWriter.DONE; + + // Save error + that.error = this.error; + + // If onerror callback + if (typeof that.onerror === 'function') { + that.onerror(new ProgressEvent('error', {'target': that})); + } + + // If onwriteend callback + if (typeof that.onwriteend === 'function') { + that.onwriteend(new ProgressEvent('writeend', {'target': that})); + } + }; + + // WRITING state + this.readyState = FileWriter.WRITING; + + if (supportsBinary) { + fileReader.readAsArrayBuffer(data); + } else { + fileReader.readAsText(data); + } + return; + } + + // Mark data type for safer transport over the binary bridge + isBinary = supportsBinary && (data instanceof ArrayBuffer); + if (isBinary && cordova.platformId === 'windowsphone') { // eslint-disable-line no-undef + // create a plain array, using the keys from the Uint8Array view so that we can serialize it + data = Array.apply(null, new Uint8Array(data)); + } + + // Throw an exception if we are already writing a file + if (this.readyState === FileWriter.WRITING && !isPendingBlobReadResult) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + // WRITING state + this.readyState = FileWriter.WRITING; + + var me = this; + + // If onwritestart callback + if (typeof me.onwritestart === 'function') { + me.onwritestart(new ProgressEvent('writestart', {'target': me})); + } + + // Write file + exec( + // Success callback + function (r) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // position always increases by bytes written because file would be extended + me.position += r; + // The length of the file is now where we are done writing. + + me.length = me.position; + + // DONE state + me.readyState = FileWriter.DONE; + + // If onwrite callback + if (typeof me.onwrite === 'function') { + me.onwrite(new ProgressEvent('write', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, + // Error callback + function (e) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // DONE state + me.readyState = FileWriter.DONE; + + // Save error + me.error = new FileError(e); + + // If onerror callback + if (typeof me.onerror === 'function') { + me.onerror(new ProgressEvent('error', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, 'File', 'write', [this.localURL, data, this.position, isBinary]); +}; + +/** + * Moves the file pointer to the location specified. + * + * If the offset is a negative number the position of the file + * pointer is rewound. If the offset is greater than the file + * size the position is set to the end of the file. + * + * @param offset is the location to move the file pointer to. + */ +FileWriter.prototype.seek = function (offset) { + // Throw an exception if we are already writing a file + if (this.readyState === FileWriter.WRITING) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + if (!offset && offset !== 0) { + return; + } + + // See back from end of file. + if (offset < 0) { + this.position = Math.max(offset + this.length, 0); + // Offset is bigger than file size so set position + // to the end of the file. + } else if (offset > this.length) { + this.position = this.length; + // Offset is between 0 and file size so set the position + // to start writing. + } else { + this.position = offset; + } +}; + +/** + * Truncates the file to the size specified. + * + * @param size to chop the file at. + */ +FileWriter.prototype.truncate = function (size) { + // Throw an exception if we are already writing a file + if (this.readyState === FileWriter.WRITING) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + // WRITING state + this.readyState = FileWriter.WRITING; + + var me = this; + + // If onwritestart callback + if (typeof me.onwritestart === 'function') { + me.onwritestart(new ProgressEvent('writestart', {'target': this})); + } + + // Write file + exec( + // Success callback + function (r) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // DONE state + me.readyState = FileWriter.DONE; + + // Update the length of the file + me.length = r; + me.position = Math.min(me.position, r); + + // If onwrite callback + if (typeof me.onwrite === 'function') { + me.onwrite(new ProgressEvent('write', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, + // Error callback + function (e) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // DONE state + me.readyState = FileWriter.DONE; + + // Save error + me.error = new FileError(e); + + // If onerror callback + if (typeof me.onerror === 'function') { + me.onerror(new ProgressEvent('error', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, 'File', 'truncate', [this.localURL, size]); +}; + +module.exports = FileWriter; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/Flags.js b/assets/www/plugins/cordova-plugin-file/www/Flags.js new file mode 100644 index 0000000000000000000000000000000000000000..cdb12bb7310cf9aabe37248fb7c72cb2247f6ea7 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/Flags.js @@ -0,0 +1,39 @@ +cordova.define("cordova-plugin-file.Flags", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Supplies arguments to methods that lookup or create files and directories. + * + * @param create + * {boolean} file or directory if it doesn't exist + * @param exclusive + * {boolean} used with create; if true the command will fail if + * target path exists + */ +function Flags (create, exclusive) { + this.create = create || false; + this.exclusive = exclusive || false; +} + +module.exports = Flags; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/LocalFileSystem.js b/assets/www/plugins/cordova-plugin-file/www/LocalFileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..4ce6848120f0cb9825f3884511db9e77f4f522c8 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/LocalFileSystem.js @@ -0,0 +1,26 @@ +cordova.define("cordova-plugin-file.LocalFileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +exports.TEMPORARY = 0; +exports.PERSISTENT = 1; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/Metadata.js b/assets/www/plugins/cordova-plugin-file/www/Metadata.js new file mode 100644 index 0000000000000000000000000000000000000000..22366e167dec22362f49f857a38c68e4451424e6 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/Metadata.js @@ -0,0 +1,43 @@ +cordova.define("cordova-plugin-file.Metadata", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Information about the state of the file or directory + * + * {Date} modificationTime (readonly) + */ +var Metadata = function (metadata) { + if (typeof metadata === 'object') { + this.modificationTime = new Date(metadata.modificationTime); + this.size = metadata.size || 0; + } else if (typeof metadata === 'undefined') { + this.modificationTime = null; + this.size = 0; + } else { + /* Backwards compatiblity with platforms that only return a timestamp */ + this.modificationTime = new Date(metadata); + } +}; + +module.exports = Metadata; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/ProgressEvent.js b/assets/www/plugins/cordova-plugin-file/www/ProgressEvent.js new file mode 100644 index 0000000000000000000000000000000000000000..cbecdb12bc11507cbc99495932af20b2dea57c93 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/ProgressEvent.js @@ -0,0 +1,70 @@ +cordova.define("cordova-plugin-file.ProgressEvent", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// If ProgressEvent exists in global context, use it already, otherwise use our own polyfill +// Feature test: See if we can instantiate a native ProgressEvent; +// if so, use that approach, +// otherwise fill-in with our own implementation. +// +// NOTE: right now we always fill in with our own. Down the road would be nice if we can use whatever is native in the webview. +var ProgressEvent = (function () { + /* + var createEvent = function(data) { + var event = document.createEvent('Events'); + event.initEvent('ProgressEvent', false, false); + if (data) { + for (var i in data) { + if (data.hasOwnProperty(i)) { + event[i] = data[i]; + } + } + if (data.target) { + // TODO: cannot call <some_custom_object>.dispatchEvent + // need to first figure out how to implement EventTarget + } + } + return event; + }; + try { + var ev = createEvent({type:"abort",target:document}); + return function ProgressEvent(type, data) { + data.type = type; + return createEvent(data); + }; + } catch(e){ + */ + return function ProgressEvent (type, dict) { + this.type = type; + this.bubbles = false; + this.cancelBubble = false; + this.cancelable = false; + this.lengthComputable = false; + this.loaded = dict && dict.loaded ? dict.loaded : 0; + this.total = dict && dict.total ? dict.total : 0; + this.target = dict && dict.target ? dict.target : null; + }; + // } +})(); + +module.exports = ProgressEvent; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/android/FileSystem.js b/assets/www/plugins/cordova-plugin-file/www/android/FileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..0124fa673d5e6cc17a04ea30592f0bc7637584c2 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/android/FileSystem.js @@ -0,0 +1,51 @@ +cordova.define("cordova-plugin-file.androidFileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +FILESYSTEM_PROTOCOL = 'cdvfile'; // eslint-disable-line no-undef + +module.exports = { + __format__: function (fullPath, nativeUrl) { + var path; + var contentUrlMatch = /^content:\/\//.exec(nativeUrl); + if (contentUrlMatch) { + // When available, use the path from a native content URL, which was already encoded by Android. + // This is necessary because JavaScript's encodeURI() does not encode as many characters as + // Android, which can result in permission exceptions when the encoding of a content URI + // doesn't match the string for which permission was originally granted. + path = nativeUrl.substring(contentUrlMatch[0].length - 1); + } else { + path = FileSystem.encodeURIPath(fullPath); // eslint-disable-line no-undef + if (!/^\//.test(path)) { + path = '/' + path; + } + + var m = /\?.*/.exec(nativeUrl); + if (m) { + path += m[0]; + } + } + + return FILESYSTEM_PROTOCOL + '://localhost/' + this.name + path; // eslint-disable-line no-undef + } +}; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/browser/isChrome.js b/assets/www/plugins/cordova-plugin-file/www/browser/isChrome.js new file mode 100644 index 0000000000000000000000000000000000000000..90450d8624dfc81805031f9359abfb3893b4416b --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/browser/isChrome.js @@ -0,0 +1,29 @@ +cordova.define("cordova-plugin-file.isChrome", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +module.exports = function () { + // window.webkitRequestFileSystem and window.webkitResolveLocalFileSystemURL are available only in Chrome and + // possibly a good flag to indicate that we're running in Chrome + return window.webkitRequestFileSystem && window.webkitResolveLocalFileSystemURL; +}; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/fileSystemPaths.js b/assets/www/plugins/cordova-plugin-file/www/fileSystemPaths.js new file mode 100644 index 0000000000000000000000000000000000000000..4bfc0f62946403acfb17abf980e4acda4c9bc4db --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/fileSystemPaths.js @@ -0,0 +1,65 @@ +cordova.define("cordova-plugin-file.fileSystemPaths", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var channel = require('cordova/channel'); + +exports.file = { + // Read-only directory where the application is installed. + applicationDirectory: null, + // Root of app's private writable storage + applicationStorageDirectory: null, + // Where to put app-specific data files. + dataDirectory: null, + // Cached files that should survive app restarts. + // Apps should not rely on the OS to delete files in here. + cacheDirectory: null, + // Android: the application space on external storage. + externalApplicationStorageDirectory: null, + // Android: Where to put app-specific data files on external storage. + externalDataDirectory: null, + // Android: the application cache on external storage. + externalCacheDirectory: null, + // Android: the external storage (SD card) root. + externalRootDirectory: null, + // iOS: Temp directory that the OS can clear at will. + tempDirectory: null, + // iOS: Holds app-specific files that should be synced (e.g. to iCloud). + syncedDataDirectory: null, + // iOS: Files private to the app, but that are meaningful to other applications (e.g. Office files) + documentsDirectory: null, + // BlackBerry10: Files globally available to all apps + sharedDirectory: null +}; + +channel.waitForInitialization('onFileSystemPathsReady'); +channel.onCordovaReady.subscribe(function () { + function after (paths) { + for (var k in paths) { + exports.file[k] = paths[k]; + } + channel.initializationComplete('onFileSystemPathsReady'); + } + exec(after, null, 'File', 'requestAllPaths', []); +}); + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/fileSystems-roots.js b/assets/www/plugins/cordova-plugin-file/www/fileSystems-roots.js new file mode 100644 index 0000000000000000000000000000000000000000..6e02953e8391d7cc5ca207ee36cc55294f59aa53 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/fileSystems-roots.js @@ -0,0 +1,49 @@ +cordova.define("cordova-plugin-file.fileSystems-roots", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// Map of fsName -> FileSystem. +var fsMap = null; +var FileSystem = require('./FileSystem'); +var exec = require('cordova/exec'); + +// Overridden by Android, BlackBerry 10 and iOS to populate fsMap. +require('./fileSystems').getFs = function (name, callback) { + function success (response) { + fsMap = {}; + for (var i = 0; i < response.length; ++i) { + var fsRoot = response[i]; + if (fsRoot) { + var fs = new FileSystem(fsRoot.filesystemName, fsRoot); + fsMap[fs.name] = fs; + } + } + callback(fsMap[name]); + } + + if (fsMap) { + callback(fsMap[name]); + } else { + exec(success, null, 'File', 'requestAllFileSystems', []); + } +}; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/fileSystems.js b/assets/www/plugins/cordova-plugin-file/www/fileSystems.js new file mode 100644 index 0000000000000000000000000000000000000000..e61ceafc8167ecc8349102a10d676651289ccaca --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/fileSystems.js @@ -0,0 +1,28 @@ +cordova.define("cordova-plugin-file.fileSystems", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// Overridden by Android, BlackBerry 10 and iOS to populate fsMap. +module.exports.getFs = function (name, callback) { + callback(null); +}; + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/requestFileSystem.js b/assets/www/plugins/cordova-plugin-file/www/requestFileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..7f652193cd85135fc5cac39f1870f828ebd1933c --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/requestFileSystem.js @@ -0,0 +1,84 @@ +cordova.define("cordova-plugin-file.requestFileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +(function () { + // For browser platform: not all browsers use this file. + function checkBrowser () { + if (cordova.platformId === 'browser' && require('./isChrome')()) { // eslint-disable-line no-undef + module.exports = window.requestFileSystem || window.webkitRequestFileSystem; + return true; + } + return false; + } + if (checkBrowser()) { + return; + } + + var argscheck = require('cordova/argscheck'); + var FileError = require('./FileError'); + var FileSystem = require('./FileSystem'); + var exec = require('cordova/exec'); + var fileSystems = require('./fileSystems'); + + /** + * Request a file system in which to store application data. + * @param type local file system type + * @param size indicates how much storage space, in bytes, the application expects to need + * @param successCallback invoked with a FileSystem object + * @param errorCallback invoked if error occurs retrieving file system + */ + var requestFileSystem = function (type, size, successCallback, errorCallback) { + argscheck.checkArgs('nnFF', 'requestFileSystem', arguments); + var fail = function (code) { + if (errorCallback) { + errorCallback(new FileError(code)); + } + }; + + if (type < 0) { + fail(FileError.SYNTAX_ERR); + } else { + // if successful, return a FileSystem object + var success = function (file_system) { + if (file_system) { + if (successCallback) { + fileSystems.getFs(file_system.name, function (fs) { + // This should happen only on platforms that haven't implemented requestAllFileSystems (windows) + if (!fs) { + fs = new FileSystem(file_system.name, file_system.root); + } + successCallback(fs); + }); + } + } else { + // no FileSystem object returned + fail(FileError.NOT_FOUND_ERR); + } + }; + exec(success, fail, 'File', 'requestFileSystem', [type, size]); + } + }; + + module.exports = requestFileSystem; +})(); + +}); diff --git a/assets/www/plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js b/assets/www/plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js new file mode 100644 index 0000000000000000000000000000000000000000..73715bc0e30458b6ed530057cff9c28cb1a5a371 --- /dev/null +++ b/assets/www/plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js @@ -0,0 +1,94 @@ +cordova.define("cordova-plugin-file.resolveLocalFileSystemURI", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ +(function () { + // For browser platform: not all browsers use overrided `resolveLocalFileSystemURL`. + function checkBrowser () { + if (cordova.platformId === 'browser' && require('./isChrome')()) { // eslint-disable-line no-undef + module.exports.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL || window.webkitResolveLocalFileSystemURL; + return true; + } + return false; + } + if (checkBrowser()) { + return; + } + + var argscheck = require('cordova/argscheck'); + var DirectoryEntry = require('./DirectoryEntry'); + var FileEntry = require('./FileEntry'); + var FileError = require('./FileError'); + var exec = require('cordova/exec'); + var fileSystems = require('./fileSystems'); + + /** + * Look up file system Entry referred to by local URI. + * @param {DOMString} uri URI referring to a local file or directory + * @param successCallback invoked with Entry object corresponding to URI + * @param errorCallback invoked if error occurs retrieving file system entry + */ + module.exports.resolveLocalFileSystemURL = module.exports.resolveLocalFileSystemURL || function (uri, successCallback, errorCallback) { + argscheck.checkArgs('sFF', 'resolveLocalFileSystemURI', arguments); + // error callback + var fail = function (error) { + if (errorCallback) { + errorCallback(new FileError(error)); + } + }; + // sanity check for 'not:valid:filename' or '/not:valid:filename' + // file.spec.12 window.resolveLocalFileSystemURI should error (ENCODING_ERR) when resolving invalid URI with leading /. + if (!uri || uri.split(':').length > 2) { + setTimeout(function () { + fail(FileError.ENCODING_ERR); + }, 0); + return; + } + // if successful, return either a file or directory entry + var success = function (entry) { + if (entry) { + if (successCallback) { + // create appropriate Entry object + var fsName = entry.filesystemName || (entry.filesystem && entry.filesystem.name) || (entry.filesystem === window.PERSISTENT ? 'persistent' : 'temporary'); // eslint-disable-line no-undef + fileSystems.getFs(fsName, function (fs) { + // This should happen only on platforms that haven't implemented requestAllFileSystems (windows) + if (!fs) { + fs = new FileSystem(fsName, {name: '', fullPath: '/'}); // eslint-disable-line no-undef + } + var result = (entry.isDirectory) ? new DirectoryEntry(entry.name, entry.fullPath, fs, entry.nativeURL) : new FileEntry(entry.name, entry.fullPath, fs, entry.nativeURL); + successCallback(result); + }); + } + } else { + // no Entry object returned + fail(FileError.NOT_FOUND_ERR); + } + }; + + exec(success, fail, 'File', 'resolveLocalFileSystemURI', [uri]); + }; + + module.exports.resolveLocalFileSystemURI = function () { + console.log('resolveLocalFileSystemURI is deprecated. Please call resolveLocalFileSystemURL instead.'); + module.exports.resolveLocalFileSystemURL.apply(this, arguments); + }; +})(); + +}); diff --git a/build-extras.gradle b/build-extras.gradle index d3275a4912d554bb917c8a433edde180eb32cea8..38129572df5b58ca187c9905a9ce7ea60d5ced72 100644 --- a/build-extras.gradle +++ b/build-extras.gradle @@ -1,5 +1,21 @@ configurations.all { resolutionStrategy { - force 'com.android.support:support-v4:27.1.0' + force 'androidx.legacy:legacy-support-v4:1.0.0' + force 'androidx.appcompat:appcompat:1.0.0' } } +dependencies { + implementation(project(path: "CordovaLib")) { + exclude group: 'com.android.support', module:'support-v4' + } +} + +// Overrides the value of minSdkVersion set in AndroidManifest.xml. Useful when creating multiple APKs based on SDK version +ext.cdvMinSdkVersion=16 + + // Overrides the automatically detected android.compileSdkVersion value +ext.cdvCompileSdkVersion=29 + +// Overrides the automatically detected android.buildToolsVersion value +ext.cdvBuildToolsVersion='29.0.2' +//ext.cdvBuildToolsVersion = '30.0.0-rc2' diff --git a/build.gradle b/build.gradle index f8c95a8d1af1eeba57f49f7af886693c3493400d..b425e7f025b5b4c6476aa0e62c59cc2e36f48810 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { } task wrapper(type: Wrapper) { - gradleVersion = '4.4.0' + gradleVersion = '4.10.3' } // Configuration properties. Set these via environment variables, build-extras.gradle, or gradle.properties. diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000000000000000000000000000000000..65dae2fae53a0d276f61d01d344a237d9e39a760 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ + + +android.useAndroidX=true +android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ddb3e1024ce810a0ed5e3707cbcaf77801c61f17..0a96057d40ba4df7cf2e3d305bab2990ed46ef6e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists distributionUrl=https://services.gradle.org/distributions/gradle-4.10.3-all.zip diff --git a/platform_www/cordova_plugins.js b/platform_www/cordova_plugins.js index ba983110e27562fc0ba420708c60bc70bb2d28b9..b649a5ce2cff6f26e98624791764b0b116c16cd1 100644 --- a/platform_www/cordova_plugins.js +++ b/platform_www/cordova_plugins.js @@ -173,6 +173,179 @@ module.exports = [ "clobbers": [ "Ionic.WebView" ] + }, + { + "id": "cordova-plugin-file.DirectoryEntry", + "file": "plugins/cordova-plugin-file/www/DirectoryEntry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.DirectoryEntry" + ] + }, + { + "id": "cordova-plugin-file.DirectoryReader", + "file": "plugins/cordova-plugin-file/www/DirectoryReader.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.DirectoryReader" + ] + }, + { + "id": "cordova-plugin-file.Entry", + "file": "plugins/cordova-plugin-file/www/Entry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Entry" + ] + }, + { + "id": "cordova-plugin-file.File", + "file": "plugins/cordova-plugin-file/www/File.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.File" + ] + }, + { + "id": "cordova-plugin-file.FileEntry", + "file": "plugins/cordova-plugin-file/www/FileEntry.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileEntry" + ] + }, + { + "id": "cordova-plugin-file.FileError", + "file": "plugins/cordova-plugin-file/www/FileError.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileError" + ] + }, + { + "id": "cordova-plugin-file.FileReader", + "file": "plugins/cordova-plugin-file/www/FileReader.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileReader" + ] + }, + { + "id": "cordova-plugin-file.FileSystem", + "file": "plugins/cordova-plugin-file/www/FileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileSystem" + ] + }, + { + "id": "cordova-plugin-file.FileUploadOptions", + "file": "plugins/cordova-plugin-file/www/FileUploadOptions.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileUploadOptions" + ] + }, + { + "id": "cordova-plugin-file.FileUploadResult", + "file": "plugins/cordova-plugin-file/www/FileUploadResult.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileUploadResult" + ] + }, + { + "id": "cordova-plugin-file.FileWriter", + "file": "plugins/cordova-plugin-file/www/FileWriter.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.FileWriter" + ] + }, + { + "id": "cordova-plugin-file.Flags", + "file": "plugins/cordova-plugin-file/www/Flags.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Flags" + ] + }, + { + "id": "cordova-plugin-file.LocalFileSystem", + "file": "plugins/cordova-plugin-file/www/LocalFileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.LocalFileSystem" + ], + "merges": [ + "window" + ] + }, + { + "id": "cordova-plugin-file.Metadata", + "file": "plugins/cordova-plugin-file/www/Metadata.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.Metadata" + ] + }, + { + "id": "cordova-plugin-file.ProgressEvent", + "file": "plugins/cordova-plugin-file/www/ProgressEvent.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.ProgressEvent" + ] + }, + { + "id": "cordova-plugin-file.fileSystems", + "file": "plugins/cordova-plugin-file/www/fileSystems.js", + "pluginId": "cordova-plugin-file" + }, + { + "id": "cordova-plugin-file.requestFileSystem", + "file": "plugins/cordova-plugin-file/www/requestFileSystem.js", + "pluginId": "cordova-plugin-file", + "clobbers": [ + "window.requestFileSystem" + ] + }, + { + "id": "cordova-plugin-file.resolveLocalFileSystemURI", + "file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "window" + ] + }, + { + "id": "cordova-plugin-file.isChrome", + "file": "plugins/cordova-plugin-file/www/browser/isChrome.js", + "pluginId": "cordova-plugin-file", + "runs": true + }, + { + "id": "cordova-plugin-file.androidFileSystem", + "file": "plugins/cordova-plugin-file/www/android/FileSystem.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "FileSystem" + ] + }, + { + "id": "cordova-plugin-file.fileSystems-roots", + "file": "plugins/cordova-plugin-file/www/fileSystems-roots.js", + "pluginId": "cordova-plugin-file", + "runs": true + }, + { + "id": "cordova-plugin-file.fileSystemPaths", + "file": "plugins/cordova-plugin-file/www/fileSystemPaths.js", + "pluginId": "cordova-plugin-file", + "merges": [ + "cordova" + ], + "runs": true } ]; module.exports.metadata = @@ -195,7 +368,8 @@ module.exports.metadata = "ionic-plugin-keyboard": "2.2.1", "phonegap-plugin-barcodescanner": "7.0.0", "cordova-plugin-ionic-keyboard": "2.2.0", - "cordova-plugin-ionic-webview": "4.1.3" + "cordova-plugin-ionic-webview": "4.1.3", + "cordova-plugin-file": "6.0.2" }; // BOTTOM OF METADATA }); \ No newline at end of file diff --git a/platform_www/plugins/cordova-plugin-file/www/DirectoryEntry.js b/platform_www/plugins/cordova-plugin-file/www/DirectoryEntry.js new file mode 100644 index 0000000000000000000000000000000000000000..bb676eb6c995b051b51fd9bbb6b65508b79b6aeb --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/DirectoryEntry.js @@ -0,0 +1,120 @@ +cordova.define("cordova-plugin-file.DirectoryEntry", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var argscheck = require('cordova/argscheck'); +var utils = require('cordova/utils'); +var exec = require('cordova/exec'); +var Entry = require('./Entry'); +var FileError = require('./FileError'); +var DirectoryReader = require('./DirectoryReader'); + +/** + * An interface representing a directory on the file system. + * + * {boolean} isFile always false (readonly) + * {boolean} isDirectory always true (readonly) + * {DOMString} name of the directory, excluding the path leading to it (readonly) + * {DOMString} fullPath the absolute full path to the directory (readonly) + * {FileSystem} filesystem on which the directory resides (readonly) + */ +var DirectoryEntry = function (name, fullPath, fileSystem, nativeURL) { + + // add trailing slash if it is missing + if ((fullPath) && !/\/$/.test(fullPath)) { + fullPath += '/'; + } + // add trailing slash if it is missing + if (nativeURL && !/\/$/.test(nativeURL)) { + nativeURL += '/'; + } + DirectoryEntry.__super__.constructor.call(this, false, true, name, fullPath, fileSystem, nativeURL); +}; + +utils.extend(DirectoryEntry, Entry); + +/** + * Creates a new DirectoryReader to read entries from this directory + */ +DirectoryEntry.prototype.createReader = function () { + return new DirectoryReader(this.toInternalURL()); +}; + +/** + * Creates or looks up a directory + * + * @param {DOMString} path either a relative or absolute path from this directory in which to look up or create a directory + * @param {Flags} options to create or exclusively create the directory + * @param {Function} successCallback is called with the new entry + * @param {Function} errorCallback is called with a FileError + */ +DirectoryEntry.prototype.getDirectory = function (path, options, successCallback, errorCallback) { + argscheck.checkArgs('sOFF', 'DirectoryEntry.getDirectory', arguments); + var fs = this.filesystem; + var win = successCallback && function (result) { + var entry = new DirectoryEntry(result.name, result.fullPath, fs, result.nativeURL); + successCallback(entry); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getDirectory', [this.toInternalURL(), path, options]); +}; + +/** + * Deletes a directory and all of it's contents + * + * @param {Function} successCallback is called with no parameters + * @param {Function} errorCallback is called with a FileError + */ +DirectoryEntry.prototype.removeRecursively = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'DirectoryEntry.removeRecursively', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(successCallback, fail, 'File', 'removeRecursively', [this.toInternalURL()]); +}; + +/** + * Creates or looks up a file + * + * @param {DOMString} path either a relative or absolute path from this directory in which to look up or create a file + * @param {Flags} options to create or exclusively create the file + * @param {Function} successCallback is called with the new entry + * @param {Function} errorCallback is called with a FileError + */ +DirectoryEntry.prototype.getFile = function (path, options, successCallback, errorCallback) { + argscheck.checkArgs('sOFF', 'DirectoryEntry.getFile', arguments); + var fs = this.filesystem; + var win = successCallback && function (result) { + var FileEntry = require('./FileEntry'); + var entry = new FileEntry(result.name, result.fullPath, fs, result.nativeURL); + successCallback(entry); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getFile', [this.toInternalURL(), path, options]); +}; + +module.exports = DirectoryEntry; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/DirectoryReader.js b/platform_www/plugins/cordova-plugin-file/www/DirectoryReader.js new file mode 100644 index 0000000000000000000000000000000000000000..417c85f10769b3ad49cf1caa26a0812357f5e7cb --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/DirectoryReader.js @@ -0,0 +1,75 @@ +cordova.define("cordova-plugin-file.DirectoryReader", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var FileError = require('./FileError'); + +/** + * An interface that lists the files and directories in a directory. + */ +function DirectoryReader (localURL) { + this.localURL = localURL || null; + this.hasReadEntries = false; +} + +/** + * Returns a list of entries from a directory. + * + * @param {Function} successCallback is called with a list of entries + * @param {Function} errorCallback is called with a FileError + */ +DirectoryReader.prototype.readEntries = function (successCallback, errorCallback) { + // If we've already read and passed on this directory's entries, return an empty list. + if (this.hasReadEntries) { + successCallback([]); + return; + } + var reader = this; + var win = typeof successCallback !== 'function' ? null : function (result) { + var retVal = []; + for (var i = 0; i < result.length; i++) { + var entry = null; + if (result[i].isDirectory) { + entry = new (require('./DirectoryEntry'))(); + } else if (result[i].isFile) { + entry = new (require('./FileEntry'))(); + } + entry.isDirectory = result[i].isDirectory; + entry.isFile = result[i].isFile; + entry.name = result[i].name; + entry.fullPath = result[i].fullPath; + entry.filesystem = new (require('./FileSystem'))(result[i].filesystemName); + entry.nativeURL = result[i].nativeURL; + retVal.push(entry); + } + reader.hasReadEntries = true; + successCallback(retVal); + }; + var fail = typeof errorCallback !== 'function' ? null : function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'readEntries', [this.localURL]); +}; + +module.exports = DirectoryReader; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/Entry.js b/platform_www/plugins/cordova-plugin-file/www/Entry.js new file mode 100644 index 0000000000000000000000000000000000000000..b296d999e9284fe07b1419adc4f0fd2b64ec282f --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/Entry.js @@ -0,0 +1,263 @@ +cordova.define("cordova-plugin-file.Entry", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var argscheck = require('cordova/argscheck'); +var exec = require('cordova/exec'); +var FileError = require('./FileError'); +var Metadata = require('./Metadata'); + +/** + * Represents a file or directory on the local file system. + * + * @param isFile + * {boolean} true if Entry is a file (readonly) + * @param isDirectory + * {boolean} true if Entry is a directory (readonly) + * @param name + * {DOMString} name of the file or directory, excluding the path + * leading to it (readonly) + * @param fullPath + * {DOMString} the absolute full path to the file or directory + * (readonly) + * @param fileSystem + * {FileSystem} the filesystem on which this entry resides + * (readonly) + * @param nativeURL + * {DOMString} an alternate URL which can be used by native + * webview controls, for example media players. + * (optional, readonly) + */ +function Entry (isFile, isDirectory, name, fullPath, fileSystem, nativeURL) { + this.isFile = !!isFile; + this.isDirectory = !!isDirectory; + this.name = name || ''; + this.fullPath = fullPath || ''; + this.filesystem = fileSystem || null; + this.nativeURL = nativeURL || null; +} + +/** + * Look up the metadata of the entry. + * + * @param successCallback + * {Function} is called with a Metadata object + * @param errorCallback + * {Function} is called with a FileError + */ +Entry.prototype.getMetadata = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'Entry.getMetadata', arguments); + var success = successCallback && function (entryMetadata) { + var metadata = new Metadata({ + size: entryMetadata.size, + modificationTime: entryMetadata.lastModifiedDate + }); + successCallback(metadata); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(success, fail, 'File', 'getFileMetadata', [this.toInternalURL()]); +}; + +/** + * Set the metadata of the entry. + * + * @param successCallback + * {Function} is called with a Metadata object + * @param errorCallback + * {Function} is called with a FileError + * @param metadataObject + * {Object} keys and values to set + */ +Entry.prototype.setMetadata = function (successCallback, errorCallback, metadataObject) { + argscheck.checkArgs('FFO', 'Entry.setMetadata', arguments); + exec(successCallback, errorCallback, 'File', 'setMetadata', [this.toInternalURL(), metadataObject]); +}; + +/** + * Move a file or directory to a new location. + * + * @param parent + * {DirectoryEntry} the directory to which to move this entry + * @param newName + * {DOMString} new name of the entry, defaults to the current name + * @param successCallback + * {Function} called with the new DirectoryEntry object + * @param errorCallback + * {Function} called with a FileError + */ +Entry.prototype.moveTo = function (parent, newName, successCallback, errorCallback) { + argscheck.checkArgs('oSFF', 'Entry.moveTo', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + var srcURL = this.toInternalURL(); + // entry name + var name = newName || this.name; + var success = function (entry) { + if (entry) { + if (successCallback) { + // create appropriate Entry object + var newFSName = entry.filesystemName || (entry.filesystem && entry.filesystem.name); + var fs = newFSName ? new FileSystem(newFSName, { name: '', fullPath: '/' }) : new FileSystem(parent.filesystem.name, { name: '', fullPath: '/' }); // eslint-disable-line no-undef + var result = (entry.isDirectory) ? new (require('./DirectoryEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL) : new (require('cordova-plugin-file.FileEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL); + successCallback(result); + } + } else { + // no Entry object returned + if (fail) { + fail(FileError.NOT_FOUND_ERR); + } + } + }; + + // copy + exec(success, fail, 'File', 'moveTo', [srcURL, parent.toInternalURL(), name]); +}; + +/** + * Copy a directory to a different location. + * + * @param parent + * {DirectoryEntry} the directory to which to copy the entry + * @param newName + * {DOMString} new name of the entry, defaults to the current name + * @param successCallback + * {Function} called with the new Entry object + * @param errorCallback + * {Function} called with a FileError + */ +Entry.prototype.copyTo = function (parent, newName, successCallback, errorCallback) { + argscheck.checkArgs('oSFF', 'Entry.copyTo', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + var srcURL = this.toInternalURL(); + // entry name + var name = newName || this.name; + // success callback + var success = function (entry) { + if (entry) { + if (successCallback) { + // create appropriate Entry object + var newFSName = entry.filesystemName || (entry.filesystem && entry.filesystem.name); + var fs = newFSName ? new FileSystem(newFSName, { name: '', fullPath: '/' }) : new FileSystem(parent.filesystem.name, { name: '', fullPath: '/' }); // eslint-disable-line no-undef + var result = (entry.isDirectory) ? new (require('./DirectoryEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL) : new (require('cordova-plugin-file.FileEntry'))(entry.name, entry.fullPath, fs, entry.nativeURL); + successCallback(result); + } + } else { + // no Entry object returned + if (fail) { + fail(FileError.NOT_FOUND_ERR); + } + } + }; + + // copy + exec(success, fail, 'File', 'copyTo', [srcURL, parent.toInternalURL(), name]); +}; + +/** + * Return a URL that can be passed across the bridge to identify this entry. + */ +Entry.prototype.toInternalURL = function () { + if (this.filesystem && this.filesystem.__format__) { + return this.filesystem.__format__(this.fullPath, this.nativeURL); + } +}; + +/** + * Return a URL that can be used to identify this entry. + * Use a URL that can be used to as the src attribute of a <video> or + * <audio> tag. If that is not possible, construct a cdvfile:// URL. + */ +Entry.prototype.toURL = function () { + if (this.nativeURL) { + return this.nativeURL; + } + // fullPath attribute may contain the full URL in the case that + // toInternalURL fails. + return this.toInternalURL() || 'file://localhost' + this.fullPath; +}; + +/** + * Backwards-compatibility: In v1.0.0 - 1.0.2, .toURL would only return a + * cdvfile:// URL, and this method was necessary to obtain URLs usable by the + * webview. + * See CB-6051, CB-6106, CB-6117, CB-6152, CB-6199, CB-6201, CB-6243, CB-6249, + * and CB-6300. + */ +Entry.prototype.toNativeURL = function () { + console.log("DEPRECATED: Update your code to use 'toURL'"); + return this.toURL(); +}; + +/** + * Returns a URI that can be used to identify this entry. + * + * @param {DOMString} mimeType for a FileEntry, the mime type to be used to interpret the file, when loaded through this URI. + * @return uri + */ +Entry.prototype.toURI = function (mimeType) { + console.log("DEPRECATED: Update your code to use 'toURL'"); + return this.toURL(); +}; + +/** + * Remove a file or directory. It is an error to attempt to delete a + * directory that is not empty. It is an error to attempt to delete a + * root directory of a file system. + * + * @param successCallback {Function} called with no parameters + * @param errorCallback {Function} called with a FileError + */ +Entry.prototype.remove = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'Entry.remove', arguments); + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(successCallback, fail, 'File', 'remove', [this.toInternalURL()]); +}; + +/** + * Look up the parent DirectoryEntry of this entry. + * + * @param successCallback {Function} called with the parent DirectoryEntry object + * @param errorCallback {Function} called with a FileError + */ +Entry.prototype.getParent = function (successCallback, errorCallback) { + argscheck.checkArgs('FF', 'Entry.getParent', arguments); + var fs = this.filesystem; + var win = successCallback && function (result) { + var DirectoryEntry = require('./DirectoryEntry'); + var entry = new DirectoryEntry(result.name, result.fullPath, fs, result.nativeURL); + successCallback(entry); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getParent', [this.toInternalURL()]); +}; + +module.exports = Entry; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/File.js b/platform_www/plugins/cordova-plugin-file/www/File.js new file mode 100644 index 0000000000000000000000000000000000000000..c7717865329ab6d9170601ffe9395f6292cb3ac3 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/File.js @@ -0,0 +1,81 @@ +cordova.define("cordova-plugin-file.File", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Constructor. + * name {DOMString} name of the file, without path information + * fullPath {DOMString} the full path of the file, including the name + * type {DOMString} mime type + * lastModifiedDate {Date} last modified date + * size {Number} size of the file in bytes + */ + +var File = function (name, localURL, type, lastModifiedDate, size) { + this.name = name || ''; + this.localURL = localURL || null; + this.type = type || null; + this.lastModified = lastModifiedDate || null; + // For backwards compatibility, store the timestamp in lastModifiedDate as well + this.lastModifiedDate = lastModifiedDate || null; + this.size = size || 0; + + // These store the absolute start and end for slicing the file. + this.start = 0; + this.end = this.size; +}; + +/** + * Returns a "slice" of the file. Since Cordova Files don't contain the actual + * content, this really returns a File with adjusted start and end. + * Slices of slices are supported. + * start {Number} The index at which to start the slice (inclusive). + * end {Number} The index at which to end the slice (exclusive). + */ +File.prototype.slice = function (start, end) { + var size = this.end - this.start; + var newStart = 0; + var newEnd = size; + if (arguments.length) { + if (start < 0) { + newStart = Math.max(size + start, 0); + } else { + newStart = Math.min(size, start); + } + } + + if (arguments.length >= 2) { + if (end < 0) { + newEnd = Math.max(size + end, 0); + } else { + newEnd = Math.min(end, size); + } + } + + var newFile = new File(this.name, this.localURL, this.type, this.lastModified, this.size); + newFile.start = this.start + newStart; + newFile.end = this.start + newEnd; + return newFile; +}; + +module.exports = File; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileEntry.js b/platform_www/plugins/cordova-plugin-file/www/FileEntry.js new file mode 100644 index 0000000000000000000000000000000000000000..6651b5542e23e9be8bcbbacaf7a00ce64230936d --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileEntry.js @@ -0,0 +1,95 @@ +cordova.define("cordova-plugin-file.FileEntry", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var utils = require('cordova/utils'); +var exec = require('cordova/exec'); +var Entry = require('./Entry'); +var FileWriter = require('./FileWriter'); +var File = require('./File'); +var FileError = require('./FileError'); + +/** + * An interface representing a file on the file system. + * + * {boolean} isFile always true (readonly) + * {boolean} isDirectory always false (readonly) + * {DOMString} name of the file, excluding the path leading to it (readonly) + * {DOMString} fullPath the absolute full path to the file (readonly) + * {FileSystem} filesystem on which the file resides (readonly) + */ +var FileEntry = function (name, fullPath, fileSystem, nativeURL) { + // remove trailing slash if it is present + if (fullPath && /\/$/.test(fullPath)) { + fullPath = fullPath.substring(0, fullPath.length - 1); + } + if (nativeURL && /\/$/.test(nativeURL)) { + nativeURL = nativeURL.substring(0, nativeURL.length - 1); + } + + FileEntry.__super__.constructor.apply(this, [true, false, name, fullPath, fileSystem, nativeURL]); +}; + +utils.extend(FileEntry, Entry); + +/** + * Creates a new FileWriter associated with the file that this FileEntry represents. + * + * @param {Function} successCallback is called with the new FileWriter + * @param {Function} errorCallback is called with a FileError + */ +FileEntry.prototype.createWriter = function (successCallback, errorCallback) { + this.file(function (filePointer) { + var writer = new FileWriter(filePointer); + + if (writer.localURL === null || writer.localURL === '') { + if (errorCallback) { + errorCallback(new FileError(FileError.INVALID_STATE_ERR)); + } + } else { + if (successCallback) { + successCallback(writer); + } + } + }, errorCallback); +}; + +/** + * Returns a File that represents the current state of the file that this FileEntry represents. + * + * @param {Function} successCallback is called with the new File object + * @param {Function} errorCallback is called with a FileError + */ +FileEntry.prototype.file = function (successCallback, errorCallback) { + var localURL = this.toInternalURL(); + var win = successCallback && function (f) { + var file = new File(f.name, localURL, f.type, f.lastModifiedDate, f.size); + successCallback(file); + }; + var fail = errorCallback && function (code) { + errorCallback(new FileError(code)); + }; + exec(win, fail, 'File', 'getFileMetadata', [localURL]); +}; + +module.exports = FileEntry; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileError.js b/platform_www/plugins/cordova-plugin-file/www/FileError.js new file mode 100644 index 0000000000000000000000000000000000000000..f378c38746f37e505b25c091b78f691526440788 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileError.js @@ -0,0 +1,49 @@ +cordova.define("cordova-plugin-file.FileError", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * FileError + */ +function FileError (error) { + this.code = error || null; +} + +// File error codes +// Found in DOMException +FileError.NOT_FOUND_ERR = 1; +FileError.SECURITY_ERR = 2; +FileError.ABORT_ERR = 3; + +// Added by File API specification +FileError.NOT_READABLE_ERR = 4; +FileError.ENCODING_ERR = 5; +FileError.NO_MODIFICATION_ALLOWED_ERR = 6; +FileError.INVALID_STATE_ERR = 7; +FileError.SYNTAX_ERR = 8; +FileError.INVALID_MODIFICATION_ERR = 9; +FileError.QUOTA_EXCEEDED_ERR = 10; +FileError.TYPE_MISMATCH_ERR = 11; +FileError.PATH_EXISTS_ERR = 12; + +module.exports = FileError; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileReader.js b/platform_www/plugins/cordova-plugin-file/www/FileReader.js new file mode 100644 index 0000000000000000000000000000000000000000..5c030913b9eb7caa32e7f137760562fe578525e1 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileReader.js @@ -0,0 +1,301 @@ +cordova.define("cordova-plugin-file.FileReader", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var modulemapper = require('cordova/modulemapper'); +var utils = require('cordova/utils'); +var FileError = require('./FileError'); +var ProgressEvent = require('./ProgressEvent'); +var origFileReader = modulemapper.getOriginalSymbol(window, 'FileReader'); + +/** + * This class reads the mobile device file system. + * + * For Android: + * The root directory is the root of the file system. + * To read from the SD card, the file name is "sdcard/my_file.txt" + * @constructor + */ +var FileReader = function () { + this._readyState = 0; + this._error = null; + this._result = null; + this._progress = null; + this._localURL = ''; + this._realReader = origFileReader ? new origFileReader() : {}; // eslint-disable-line new-cap +}; + +/** + * Defines the maximum size to read at a time via the native API. The default value is a compromise between + * minimizing the overhead of many exec() calls while still reporting progress frequently enough for large files. + * (Note attempts to allocate more than a few MB of contiguous memory on the native side are likely to cause + * OOM exceptions, while the JS engine seems to have fewer problems managing large strings or ArrayBuffers.) + */ +FileReader.READ_CHUNK_SIZE = 256 * 1024; + +// States +FileReader.EMPTY = 0; +FileReader.LOADING = 1; +FileReader.DONE = 2; + +utils.defineGetter(FileReader.prototype, 'readyState', function () { + return this._localURL ? this._readyState : this._realReader.readyState; +}); + +utils.defineGetter(FileReader.prototype, 'error', function () { + return this._localURL ? this._error : this._realReader.error; +}); + +utils.defineGetter(FileReader.prototype, 'result', function () { + return this._localURL ? this._result : this._realReader.result; +}); + +function defineEvent (eventName) { + utils.defineGetterSetter(FileReader.prototype, eventName, function () { + return this._realReader[eventName] || null; + }, function (value) { + this._realReader[eventName] = value; + }); +} +defineEvent('onloadstart'); // When the read starts. +defineEvent('onprogress'); // While reading (and decoding) file or fileBlob data, and reporting partial file data (progress.loaded/progress.total) +defineEvent('onload'); // When the read has successfully completed. +defineEvent('onerror'); // When the read has failed (see errors). +defineEvent('onloadend'); // When the request has completed (either in success or failure). +defineEvent('onabort'); // When the read has been aborted. For instance, by invoking the abort() method. + +function initRead (reader, file) { + // Already loading something + if (reader.readyState === FileReader.LOADING) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + reader._result = null; + reader._error = null; + reader._progress = 0; + reader._readyState = FileReader.LOADING; + + if (typeof file.localURL === 'string') { + reader._localURL = file.localURL; + } else { + reader._localURL = ''; + return true; + } + + if (reader.onloadstart) { + reader.onloadstart(new ProgressEvent('loadstart', {target: reader})); + } +} + +/** + * Callback used by the following read* functions to handle incremental or final success. + * Must be bound to the FileReader's this along with all but the last parameter, + * e.g. readSuccessCallback.bind(this, "readAsText", "UTF-8", offset, totalSize, accumulate) + * @param readType The name of the read function to call. + * @param encoding Text encoding, or null if this is not a text type read. + * @param offset Starting offset of the read. + * @param totalSize Total number of bytes or chars to read. + * @param accumulate A function that takes the callback result and accumulates it in this._result. + * @param r Callback result returned by the last read exec() call, or null to begin reading. + */ +function readSuccessCallback (readType, encoding, offset, totalSize, accumulate, r) { + if (this._readyState === FileReader.DONE) { + return; + } + + var CHUNK_SIZE = FileReader.READ_CHUNK_SIZE; + if (readType === 'readAsDataURL') { + // Windows proxy does not support reading file slices as Data URLs + // so read the whole file at once. + CHUNK_SIZE = cordova.platformId === 'windows' ? totalSize : // eslint-disable-line no-undef + // Calculate new chunk size for data URLs to be multiply of 3 + // Otherwise concatenated base64 chunks won't be valid base64 data + FileReader.READ_CHUNK_SIZE - (FileReader.READ_CHUNK_SIZE % 3) + 3; + } + + if (typeof r !== 'undefined') { + accumulate(r); + this._progress = Math.min(this._progress + CHUNK_SIZE, totalSize); + + if (typeof this.onprogress === 'function') { + this.onprogress(new ProgressEvent('progress', {loaded: this._progress, total: totalSize})); + } + } + + if (typeof r === 'undefined' || this._progress < totalSize) { + var execArgs = [ + this._localURL, + offset + this._progress, + offset + this._progress + Math.min(totalSize - this._progress, CHUNK_SIZE)]; + if (encoding) { + execArgs.splice(1, 0, encoding); + } + exec( + readSuccessCallback.bind(this, readType, encoding, offset, totalSize, accumulate), + readFailureCallback.bind(this), + 'File', readType, execArgs); + } else { + this._readyState = FileReader.DONE; + + if (typeof this.onload === 'function') { + this.onload(new ProgressEvent('load', {target: this})); + } + + if (typeof this.onloadend === 'function') { + this.onloadend(new ProgressEvent('loadend', {target: this})); + } + } +} + +/** + * Callback used by the following read* functions to handle errors. + * Must be bound to the FileReader's this, e.g. readFailureCallback.bind(this) + */ +function readFailureCallback (e) { + if (this._readyState === FileReader.DONE) { + return; + } + + this._readyState = FileReader.DONE; + this._result = null; + this._error = new FileError(e); + + if (typeof this.onerror === 'function') { + this.onerror(new ProgressEvent('error', {target: this})); + } + + if (typeof this.onloadend === 'function') { + this.onloadend(new ProgressEvent('loadend', {target: this})); + } +} + +/** + * Abort reading file. + */ +FileReader.prototype.abort = function () { + if (origFileReader && !this._localURL) { + return this._realReader.abort(); + } + this._result = null; + + if (this._readyState === FileReader.DONE || this._readyState === FileReader.EMPTY) { + return; + } + + this._readyState = FileReader.DONE; + + // If abort callback + if (typeof this.onabort === 'function') { + this.onabort(new ProgressEvent('abort', {target: this})); + } + // If load end callback + if (typeof this.onloadend === 'function') { + this.onloadend(new ProgressEvent('loadend', {target: this})); + } +}; + +/** + * Read text file. + * + * @param file {File} File object containing file properties + * @param encoding [Optional] (see http://www.iana.org/assignments/character-sets) + */ +FileReader.prototype.readAsText = function (file, encoding) { + if (initRead(this, file)) { + return this._realReader.readAsText(file, encoding); + } + + // Default encoding is UTF-8 + var enc = encoding || 'UTF-8'; + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsText', enc, file.start, totalSize, function (r) { + if (this._progress === 0) { + this._result = ''; + } + this._result += r; + }.bind(this)); +}; + +/** + * Read file and return data as a base64 encoded data url. + * A data url is of the form: + * data:[<mediatype>][;base64],<data> + * + * @param file {File} File object containing file properties + */ +FileReader.prototype.readAsDataURL = function (file) { + if (initRead(this, file)) { + return this._realReader.readAsDataURL(file); + } + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsDataURL', null, file.start, totalSize, function (r) { + var commaIndex = r.indexOf(','); + if (this._progress === 0) { + this._result = r; + } else { + this._result += r.substring(commaIndex + 1); + } + }.bind(this)); +}; + +/** + * Read file and return data as a binary data. + * + * @param file {File} File object containing file properties + */ +FileReader.prototype.readAsBinaryString = function (file) { + if (initRead(this, file)) { + return this._realReader.readAsBinaryString(file); + } + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsBinaryString', null, file.start, totalSize, function (r) { + if (this._progress === 0) { + this._result = ''; + } + this._result += r; + }.bind(this)); +}; + +/** + * Read file and return data as a binary data. + * + * @param file {File} File object containing file properties + */ +FileReader.prototype.readAsArrayBuffer = function (file) { + if (initRead(this, file)) { + return this._realReader.readAsArrayBuffer(file); + } + + var totalSize = file.end - file.start; + readSuccessCallback.bind(this)('readAsArrayBuffer', null, file.start, totalSize, function (r) { + var resultArray = (this._progress === 0 ? new Uint8Array(totalSize) : new Uint8Array(this._result)); + resultArray.set(new Uint8Array(r), this._progress); + this._result = resultArray.buffer; + }.bind(this)); +}; + +module.exports = FileReader; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileSystem.js b/platform_www/plugins/cordova-plugin-file/www/FileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..ad9ea785932d46328fd0cbbc654c04e1b8eab7f4 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileSystem.js @@ -0,0 +1,58 @@ +cordova.define("cordova-plugin-file.FileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var DirectoryEntry = require('./DirectoryEntry'); + +/** + * An interface representing a file system + * + * @constructor + * {DOMString} name the unique name of the file system (readonly) + * {DirectoryEntry} root directory of the file system (readonly) + */ +var FileSystem = function (name, root) { + this.name = name; + if (root) { + this.root = new DirectoryEntry(root.name, root.fullPath, this, root.nativeURL); + } else { + this.root = new DirectoryEntry(this.name, '/', this); + } +}; + +FileSystem.prototype.__format__ = function (fullPath, nativeUrl) { + return fullPath; +}; + +FileSystem.prototype.toJSON = function () { + return '<FileSystem: ' + this.name + '>'; +}; + +// Use instead of encodeURI() when encoding just the path part of a URI rather than an entire URI. +FileSystem.encodeURIPath = function (path) { + // Because # is a valid filename character, it must be encoded to prevent part of the + // path from being parsed as a URI fragment. + return encodeURI(path).replace(/#/g, '%23'); +}; + +module.exports = FileSystem; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileUploadOptions.js b/platform_www/plugins/cordova-plugin-file/www/FileUploadOptions.js new file mode 100644 index 0000000000000000000000000000000000000000..6acd09334b02f780ccaa42712b837cb41bf415bc --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileUploadOptions.js @@ -0,0 +1,44 @@ +cordova.define("cordova-plugin-file.FileUploadOptions", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Options to customize the HTTP request used to upload files. + * @constructor + * @param fileKey {String} Name of file request parameter. + * @param fileName {String} Filename to be used by the server. Defaults to image.jpg. + * @param mimeType {String} Mimetype of the uploaded file. Defaults to image/jpeg. + * @param params {Object} Object with key: value params to send to the server. + * @param headers {Object} Keys are header names, values are header values. Multiple + * headers of the same name are not supported. + */ +var FileUploadOptions = function (fileKey, fileName, mimeType, params, headers, httpMethod) { + this.fileKey = fileKey || null; + this.fileName = fileName || null; + this.mimeType = mimeType || null; + this.params = params || null; + this.headers = headers || null; + this.httpMethod = httpMethod || null; +}; + +module.exports = FileUploadOptions; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileUploadResult.js b/platform_www/plugins/cordova-plugin-file/www/FileUploadResult.js new file mode 100644 index 0000000000000000000000000000000000000000..e3c3a743144a6795c8f5f2eb50919539dc68bc41 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileUploadResult.js @@ -0,0 +1,33 @@ +cordova.define("cordova-plugin-file.FileUploadResult", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * FileUploadResult + * @constructor + */ +module.exports = function FileUploadResult (size, code, content) { + this.bytesSent = size; + this.responseCode = code; + this.response = content; +}; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/FileWriter.js b/platform_www/plugins/cordova-plugin-file/www/FileWriter.js new file mode 100644 index 0000000000000000000000000000000000000000..c3e85621992e2201982bef3aebc7e04d0e7d2887 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/FileWriter.js @@ -0,0 +1,328 @@ +cordova.define("cordova-plugin-file.FileWriter", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var FileError = require('./FileError'); +var FileReader = require('./FileReader'); +var ProgressEvent = require('./ProgressEvent'); + +/** + * This class writes to the mobile device file system. + * + * For Android: + * The root directory is the root of the file system. + * To write to the SD card, the file name is "sdcard/my_file.txt" + * + * @constructor + * @param file {File} File object containing file properties + * @param append if true write to the end of the file, otherwise overwrite the file + */ +var FileWriter = function (file) { + this.fileName = ''; + this.length = 0; + if (file) { + this.localURL = file.localURL || file; + this.length = file.size || 0; + } + // default is to write at the beginning of the file + this.position = 0; + + this.readyState = 0; // EMPTY + + this.result = null; + + // Error + this.error = null; + + // Event handlers + this.onwritestart = null; // When writing starts + this.onprogress = null; // While writing the file, and reporting partial file data + this.onwrite = null; // When the write has successfully completed. + this.onwriteend = null; // When the request has completed (either in success or failure). + this.onabort = null; // When the write has been aborted. For instance, by invoking the abort() method. + this.onerror = null; // When the write has failed (see errors). +}; + +// States +FileWriter.INIT = 0; +FileWriter.WRITING = 1; +FileWriter.DONE = 2; + +/** + * Abort writing file. + */ +FileWriter.prototype.abort = function () { + // check for invalid state + if (this.readyState === FileWriter.DONE || this.readyState === FileWriter.INIT) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + // set error + this.error = new FileError(FileError.ABORT_ERR); + + this.readyState = FileWriter.DONE; + + // If abort callback + if (typeof this.onabort === 'function') { + this.onabort(new ProgressEvent('abort', {'target': this})); + } + + // If write end callback + if (typeof this.onwriteend === 'function') { + this.onwriteend(new ProgressEvent('writeend', {'target': this})); + } +}; + +/** + * Writes data to the file + * + * @param data text or blob to be written + * @param isPendingBlobReadResult {Boolean} true if the data is the pending blob read operation result + */ +FileWriter.prototype.write = function (data, isPendingBlobReadResult) { + + var that = this; + var supportsBinary = (typeof window.Blob !== 'undefined' && typeof window.ArrayBuffer !== 'undefined'); + /* eslint-disable no-undef */ + var isProxySupportBlobNatively = (cordova.platformId === 'windows8' || cordova.platformId === 'windows'); + var isBinary; + + // Check to see if the incoming data is a blob + if (data instanceof File || (!isProxySupportBlobNatively && supportsBinary && data instanceof Blob)) { + var fileReader = new FileReader(); + /* eslint-enable no-undef */ + fileReader.onload = function () { + // Call this method again, with the arraybuffer as argument + FileWriter.prototype.write.call(that, this.result, true /* isPendingBlobReadResult */); + }; + fileReader.onerror = function () { + // DONE state + that.readyState = FileWriter.DONE; + + // Save error + that.error = this.error; + + // If onerror callback + if (typeof that.onerror === 'function') { + that.onerror(new ProgressEvent('error', {'target': that})); + } + + // If onwriteend callback + if (typeof that.onwriteend === 'function') { + that.onwriteend(new ProgressEvent('writeend', {'target': that})); + } + }; + + // WRITING state + this.readyState = FileWriter.WRITING; + + if (supportsBinary) { + fileReader.readAsArrayBuffer(data); + } else { + fileReader.readAsText(data); + } + return; + } + + // Mark data type for safer transport over the binary bridge + isBinary = supportsBinary && (data instanceof ArrayBuffer); + if (isBinary && cordova.platformId === 'windowsphone') { // eslint-disable-line no-undef + // create a plain array, using the keys from the Uint8Array view so that we can serialize it + data = Array.apply(null, new Uint8Array(data)); + } + + // Throw an exception if we are already writing a file + if (this.readyState === FileWriter.WRITING && !isPendingBlobReadResult) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + // WRITING state + this.readyState = FileWriter.WRITING; + + var me = this; + + // If onwritestart callback + if (typeof me.onwritestart === 'function') { + me.onwritestart(new ProgressEvent('writestart', {'target': me})); + } + + // Write file + exec( + // Success callback + function (r) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // position always increases by bytes written because file would be extended + me.position += r; + // The length of the file is now where we are done writing. + + me.length = me.position; + + // DONE state + me.readyState = FileWriter.DONE; + + // If onwrite callback + if (typeof me.onwrite === 'function') { + me.onwrite(new ProgressEvent('write', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, + // Error callback + function (e) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // DONE state + me.readyState = FileWriter.DONE; + + // Save error + me.error = new FileError(e); + + // If onerror callback + if (typeof me.onerror === 'function') { + me.onerror(new ProgressEvent('error', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, 'File', 'write', [this.localURL, data, this.position, isBinary]); +}; + +/** + * Moves the file pointer to the location specified. + * + * If the offset is a negative number the position of the file + * pointer is rewound. If the offset is greater than the file + * size the position is set to the end of the file. + * + * @param offset is the location to move the file pointer to. + */ +FileWriter.prototype.seek = function (offset) { + // Throw an exception if we are already writing a file + if (this.readyState === FileWriter.WRITING) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + if (!offset && offset !== 0) { + return; + } + + // See back from end of file. + if (offset < 0) { + this.position = Math.max(offset + this.length, 0); + // Offset is bigger than file size so set position + // to the end of the file. + } else if (offset > this.length) { + this.position = this.length; + // Offset is between 0 and file size so set the position + // to start writing. + } else { + this.position = offset; + } +}; + +/** + * Truncates the file to the size specified. + * + * @param size to chop the file at. + */ +FileWriter.prototype.truncate = function (size) { + // Throw an exception if we are already writing a file + if (this.readyState === FileWriter.WRITING) { + throw new FileError(FileError.INVALID_STATE_ERR); + } + + // WRITING state + this.readyState = FileWriter.WRITING; + + var me = this; + + // If onwritestart callback + if (typeof me.onwritestart === 'function') { + me.onwritestart(new ProgressEvent('writestart', {'target': this})); + } + + // Write file + exec( + // Success callback + function (r) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // DONE state + me.readyState = FileWriter.DONE; + + // Update the length of the file + me.length = r; + me.position = Math.min(me.position, r); + + // If onwrite callback + if (typeof me.onwrite === 'function') { + me.onwrite(new ProgressEvent('write', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, + // Error callback + function (e) { + // If DONE (cancelled), then don't do anything + if (me.readyState === FileWriter.DONE) { + return; + } + + // DONE state + me.readyState = FileWriter.DONE; + + // Save error + me.error = new FileError(e); + + // If onerror callback + if (typeof me.onerror === 'function') { + me.onerror(new ProgressEvent('error', {'target': me})); + } + + // If onwriteend callback + if (typeof me.onwriteend === 'function') { + me.onwriteend(new ProgressEvent('writeend', {'target': me})); + } + }, 'File', 'truncate', [this.localURL, size]); +}; + +module.exports = FileWriter; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/Flags.js b/platform_www/plugins/cordova-plugin-file/www/Flags.js new file mode 100644 index 0000000000000000000000000000000000000000..cdb12bb7310cf9aabe37248fb7c72cb2247f6ea7 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/Flags.js @@ -0,0 +1,39 @@ +cordova.define("cordova-plugin-file.Flags", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Supplies arguments to methods that lookup or create files and directories. + * + * @param create + * {boolean} file or directory if it doesn't exist + * @param exclusive + * {boolean} used with create; if true the command will fail if + * target path exists + */ +function Flags (create, exclusive) { + this.create = create || false; + this.exclusive = exclusive || false; +} + +module.exports = Flags; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/LocalFileSystem.js b/platform_www/plugins/cordova-plugin-file/www/LocalFileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..4ce6848120f0cb9825f3884511db9e77f4f522c8 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/LocalFileSystem.js @@ -0,0 +1,26 @@ +cordova.define("cordova-plugin-file.LocalFileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +exports.TEMPORARY = 0; +exports.PERSISTENT = 1; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/Metadata.js b/platform_www/plugins/cordova-plugin-file/www/Metadata.js new file mode 100644 index 0000000000000000000000000000000000000000..22366e167dec22362f49f857a38c68e4451424e6 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/Metadata.js @@ -0,0 +1,43 @@ +cordova.define("cordova-plugin-file.Metadata", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/** + * Information about the state of the file or directory + * + * {Date} modificationTime (readonly) + */ +var Metadata = function (metadata) { + if (typeof metadata === 'object') { + this.modificationTime = new Date(metadata.modificationTime); + this.size = metadata.size || 0; + } else if (typeof metadata === 'undefined') { + this.modificationTime = null; + this.size = 0; + } else { + /* Backwards compatiblity with platforms that only return a timestamp */ + this.modificationTime = new Date(metadata); + } +}; + +module.exports = Metadata; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/ProgressEvent.js b/platform_www/plugins/cordova-plugin-file/www/ProgressEvent.js new file mode 100644 index 0000000000000000000000000000000000000000..cbecdb12bc11507cbc99495932af20b2dea57c93 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/ProgressEvent.js @@ -0,0 +1,70 @@ +cordova.define("cordova-plugin-file.ProgressEvent", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// If ProgressEvent exists in global context, use it already, otherwise use our own polyfill +// Feature test: See if we can instantiate a native ProgressEvent; +// if so, use that approach, +// otherwise fill-in with our own implementation. +// +// NOTE: right now we always fill in with our own. Down the road would be nice if we can use whatever is native in the webview. +var ProgressEvent = (function () { + /* + var createEvent = function(data) { + var event = document.createEvent('Events'); + event.initEvent('ProgressEvent', false, false); + if (data) { + for (var i in data) { + if (data.hasOwnProperty(i)) { + event[i] = data[i]; + } + } + if (data.target) { + // TODO: cannot call <some_custom_object>.dispatchEvent + // need to first figure out how to implement EventTarget + } + } + return event; + }; + try { + var ev = createEvent({type:"abort",target:document}); + return function ProgressEvent(type, data) { + data.type = type; + return createEvent(data); + }; + } catch(e){ + */ + return function ProgressEvent (type, dict) { + this.type = type; + this.bubbles = false; + this.cancelBubble = false; + this.cancelable = false; + this.lengthComputable = false; + this.loaded = dict && dict.loaded ? dict.loaded : 0; + this.total = dict && dict.total ? dict.total : 0; + this.target = dict && dict.target ? dict.target : null; + }; + // } +})(); + +module.exports = ProgressEvent; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/android/FileSystem.js b/platform_www/plugins/cordova-plugin-file/www/android/FileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..0124fa673d5e6cc17a04ea30592f0bc7637584c2 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/android/FileSystem.js @@ -0,0 +1,51 @@ +cordova.define("cordova-plugin-file.androidFileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +FILESYSTEM_PROTOCOL = 'cdvfile'; // eslint-disable-line no-undef + +module.exports = { + __format__: function (fullPath, nativeUrl) { + var path; + var contentUrlMatch = /^content:\/\//.exec(nativeUrl); + if (contentUrlMatch) { + // When available, use the path from a native content URL, which was already encoded by Android. + // This is necessary because JavaScript's encodeURI() does not encode as many characters as + // Android, which can result in permission exceptions when the encoding of a content URI + // doesn't match the string for which permission was originally granted. + path = nativeUrl.substring(contentUrlMatch[0].length - 1); + } else { + path = FileSystem.encodeURIPath(fullPath); // eslint-disable-line no-undef + if (!/^\//.test(path)) { + path = '/' + path; + } + + var m = /\?.*/.exec(nativeUrl); + if (m) { + path += m[0]; + } + } + + return FILESYSTEM_PROTOCOL + '://localhost/' + this.name + path; // eslint-disable-line no-undef + } +}; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/browser/isChrome.js b/platform_www/plugins/cordova-plugin-file/www/browser/isChrome.js new file mode 100644 index 0000000000000000000000000000000000000000..90450d8624dfc81805031f9359abfb3893b4416b --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/browser/isChrome.js @@ -0,0 +1,29 @@ +cordova.define("cordova-plugin-file.isChrome", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +module.exports = function () { + // window.webkitRequestFileSystem and window.webkitResolveLocalFileSystemURL are available only in Chrome and + // possibly a good flag to indicate that we're running in Chrome + return window.webkitRequestFileSystem && window.webkitResolveLocalFileSystemURL; +}; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/fileSystemPaths.js b/platform_www/plugins/cordova-plugin-file/www/fileSystemPaths.js new file mode 100644 index 0000000000000000000000000000000000000000..4bfc0f62946403acfb17abf980e4acda4c9bc4db --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/fileSystemPaths.js @@ -0,0 +1,65 @@ +cordova.define("cordova-plugin-file.fileSystemPaths", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var exec = require('cordova/exec'); +var channel = require('cordova/channel'); + +exports.file = { + // Read-only directory where the application is installed. + applicationDirectory: null, + // Root of app's private writable storage + applicationStorageDirectory: null, + // Where to put app-specific data files. + dataDirectory: null, + // Cached files that should survive app restarts. + // Apps should not rely on the OS to delete files in here. + cacheDirectory: null, + // Android: the application space on external storage. + externalApplicationStorageDirectory: null, + // Android: Where to put app-specific data files on external storage. + externalDataDirectory: null, + // Android: the application cache on external storage. + externalCacheDirectory: null, + // Android: the external storage (SD card) root. + externalRootDirectory: null, + // iOS: Temp directory that the OS can clear at will. + tempDirectory: null, + // iOS: Holds app-specific files that should be synced (e.g. to iCloud). + syncedDataDirectory: null, + // iOS: Files private to the app, but that are meaningful to other applications (e.g. Office files) + documentsDirectory: null, + // BlackBerry10: Files globally available to all apps + sharedDirectory: null +}; + +channel.waitForInitialization('onFileSystemPathsReady'); +channel.onCordovaReady.subscribe(function () { + function after (paths) { + for (var k in paths) { + exports.file[k] = paths[k]; + } + channel.initializationComplete('onFileSystemPathsReady'); + } + exec(after, null, 'File', 'requestAllPaths', []); +}); + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/fileSystems-roots.js b/platform_www/plugins/cordova-plugin-file/www/fileSystems-roots.js new file mode 100644 index 0000000000000000000000000000000000000000..6e02953e8391d7cc5ca207ee36cc55294f59aa53 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/fileSystems-roots.js @@ -0,0 +1,49 @@ +cordova.define("cordova-plugin-file.fileSystems-roots", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// Map of fsName -> FileSystem. +var fsMap = null; +var FileSystem = require('./FileSystem'); +var exec = require('cordova/exec'); + +// Overridden by Android, BlackBerry 10 and iOS to populate fsMap. +require('./fileSystems').getFs = function (name, callback) { + function success (response) { + fsMap = {}; + for (var i = 0; i < response.length; ++i) { + var fsRoot = response[i]; + if (fsRoot) { + var fs = new FileSystem(fsRoot.filesystemName, fsRoot); + fsMap[fs.name] = fs; + } + } + callback(fsMap[name]); + } + + if (fsMap) { + callback(fsMap[name]); + } else { + exec(success, null, 'File', 'requestAllFileSystems', []); + } +}; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/fileSystems.js b/platform_www/plugins/cordova-plugin-file/www/fileSystems.js new file mode 100644 index 0000000000000000000000000000000000000000..e61ceafc8167ecc8349102a10d676651289ccaca --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/fileSystems.js @@ -0,0 +1,28 @@ +cordova.define("cordova-plugin-file.fileSystems", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +// Overridden by Android, BlackBerry 10 and iOS to populate fsMap. +module.exports.getFs = function (name, callback) { + callback(null); +}; + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/requestFileSystem.js b/platform_www/plugins/cordova-plugin-file/www/requestFileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..7f652193cd85135fc5cac39f1870f828ebd1933c --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/requestFileSystem.js @@ -0,0 +1,84 @@ +cordova.define("cordova-plugin-file.requestFileSystem", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +(function () { + // For browser platform: not all browsers use this file. + function checkBrowser () { + if (cordova.platformId === 'browser' && require('./isChrome')()) { // eslint-disable-line no-undef + module.exports = window.requestFileSystem || window.webkitRequestFileSystem; + return true; + } + return false; + } + if (checkBrowser()) { + return; + } + + var argscheck = require('cordova/argscheck'); + var FileError = require('./FileError'); + var FileSystem = require('./FileSystem'); + var exec = require('cordova/exec'); + var fileSystems = require('./fileSystems'); + + /** + * Request a file system in which to store application data. + * @param type local file system type + * @param size indicates how much storage space, in bytes, the application expects to need + * @param successCallback invoked with a FileSystem object + * @param errorCallback invoked if error occurs retrieving file system + */ + var requestFileSystem = function (type, size, successCallback, errorCallback) { + argscheck.checkArgs('nnFF', 'requestFileSystem', arguments); + var fail = function (code) { + if (errorCallback) { + errorCallback(new FileError(code)); + } + }; + + if (type < 0) { + fail(FileError.SYNTAX_ERR); + } else { + // if successful, return a FileSystem object + var success = function (file_system) { + if (file_system) { + if (successCallback) { + fileSystems.getFs(file_system.name, function (fs) { + // This should happen only on platforms that haven't implemented requestAllFileSystems (windows) + if (!fs) { + fs = new FileSystem(file_system.name, file_system.root); + } + successCallback(fs); + }); + } + } else { + // no FileSystem object returned + fail(FileError.NOT_FOUND_ERR); + } + }; + exec(success, fail, 'File', 'requestFileSystem', [type, size]); + } + }; + + module.exports = requestFileSystem; +})(); + +}); diff --git a/platform_www/plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js b/platform_www/plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js new file mode 100644 index 0000000000000000000000000000000000000000..73715bc0e30458b6ed530057cff9c28cb1a5a371 --- /dev/null +++ b/platform_www/plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js @@ -0,0 +1,94 @@ +cordova.define("cordova-plugin-file.resolveLocalFileSystemURI", function(require, exports, module) { +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ +(function () { + // For browser platform: not all browsers use overrided `resolveLocalFileSystemURL`. + function checkBrowser () { + if (cordova.platformId === 'browser' && require('./isChrome')()) { // eslint-disable-line no-undef + module.exports.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL || window.webkitResolveLocalFileSystemURL; + return true; + } + return false; + } + if (checkBrowser()) { + return; + } + + var argscheck = require('cordova/argscheck'); + var DirectoryEntry = require('./DirectoryEntry'); + var FileEntry = require('./FileEntry'); + var FileError = require('./FileError'); + var exec = require('cordova/exec'); + var fileSystems = require('./fileSystems'); + + /** + * Look up file system Entry referred to by local URI. + * @param {DOMString} uri URI referring to a local file or directory + * @param successCallback invoked with Entry object corresponding to URI + * @param errorCallback invoked if error occurs retrieving file system entry + */ + module.exports.resolveLocalFileSystemURL = module.exports.resolveLocalFileSystemURL || function (uri, successCallback, errorCallback) { + argscheck.checkArgs('sFF', 'resolveLocalFileSystemURI', arguments); + // error callback + var fail = function (error) { + if (errorCallback) { + errorCallback(new FileError(error)); + } + }; + // sanity check for 'not:valid:filename' or '/not:valid:filename' + // file.spec.12 window.resolveLocalFileSystemURI should error (ENCODING_ERR) when resolving invalid URI with leading /. + if (!uri || uri.split(':').length > 2) { + setTimeout(function () { + fail(FileError.ENCODING_ERR); + }, 0); + return; + } + // if successful, return either a file or directory entry + var success = function (entry) { + if (entry) { + if (successCallback) { + // create appropriate Entry object + var fsName = entry.filesystemName || (entry.filesystem && entry.filesystem.name) || (entry.filesystem === window.PERSISTENT ? 'persistent' : 'temporary'); // eslint-disable-line no-undef + fileSystems.getFs(fsName, function (fs) { + // This should happen only on platforms that haven't implemented requestAllFileSystems (windows) + if (!fs) { + fs = new FileSystem(fsName, {name: '', fullPath: '/'}); // eslint-disable-line no-undef + } + var result = (entry.isDirectory) ? new DirectoryEntry(entry.name, entry.fullPath, fs, entry.nativeURL) : new FileEntry(entry.name, entry.fullPath, fs, entry.nativeURL); + successCallback(result); + }); + } + } else { + // no Entry object returned + fail(FileError.NOT_FOUND_ERR); + } + }; + + exec(success, fail, 'File', 'resolveLocalFileSystemURI', [uri]); + }; + + module.exports.resolveLocalFileSystemURI = function () { + console.log('resolveLocalFileSystemURI is deprecated. Please call resolveLocalFileSystemURL instead.'); + module.exports.resolveLocalFileSystemURL.apply(this, arguments); + }; +})(); + +}); diff --git a/res/xml/config.xml b/res/xml/config.xml index 08786b47305f27aad1c445a45201366eb8ba879f..a359ac09c8647c4c55073472c01f606498571417 100644 --- a/res/xml/config.xml +++ b/res/xml/config.xml @@ -1,5 +1,5 @@ <?xml version='1.0' encoding='utf-8'?> -<widget android-versionCode="106012" id="fr.duniter.cesium" ios-CFBundleIdentifier="org.duniter.cesium" version="1.6.2-alpha" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> +<widget android-versionCode="106030" id="fr.duniter.cesium" ios-CFBundleIdentifier="org.duniter.cesium" version="1.6.3" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <feature name="Clipboard"> <param name="android-package" value="com.verso.cordova.clipboard.Clipboard" /> </feature> @@ -50,6 +50,11 @@ <feature name="BarcodeScanner"> <param name="android-package" value="com.phonegap.plugins.barcodescanner.BarcodeScanner" /> </feature> + <feature name="File"> + <param name="android-package" value="org.apache.cordova.file.FileUtils" /> + <param name="onload" value="true" /> + </feature> + <allow-navigation href="cdvfile:*" /> <feature name="CDVIonicKeyboard"> <param name="android-package" onload="true" value="io.ionic.keyboard.CDVIonicKeyboard" /> </feature> @@ -109,7 +114,7 @@ <preference name="xwalkVersion" value="19" /> <preference name="xwalkMultipleApk" value="false" /> <preference name="android-minSdkVersion" value="16" /> - <preference name="android-targetSdkVersion" value="28" /> + <preference name="android-targetSdkVersion" value="29" /> <preference name="StatusBarOverlaysWebView" value="false" /> <preference name="StatusBarBackgroundColor" value="#000" /> <preference name="StatusBarStyle" value="lightcontent" /> diff --git a/src/com/crypho/plugins/AES.java b/src/com/crypho/plugins/AES.java index 03c2e33264b365f658359254e1edcaf2fa59b61f..4fe04418b27e2e39a4bc3f7cb1c810ba0df5d011 100644 --- a/src/com/crypho/plugins/AES.java +++ b/src/com/crypho/plugins/AES.java @@ -1,9 +1,6 @@ package com.crypho.plugins; -import android.util.Log; import android.util.Base64; - -import org.json.JSONException; import org.json.JSONObject; import java.security.Key; @@ -16,67 +13,77 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.KeyGenerator; public class AES { - private static final String CIPHER_MODE = "CCM"; - private static final int KEY_SIZE = 256; - private static final int VERSION = 1; - private static final Cipher CIPHER = getCipher(); + private static final String CIPHER_MODE = "CCM"; + private static final int KEY_SIZE = 256; + private static final int VERSION = 1; + private static final Cipher GLOBAL_CIPHER = getCipher(CIPHER_MODE); + + public static JSONObject encrypt(byte[] msg, byte[] adata) throws Exception { + byte[] iv, ct, secretKeySpec_enc; + synchronized (GLOBAL_CIPHER) { + SecretKeySpec secretKeySpec = generateKeySpec(); + secretKeySpec_enc = secretKeySpec.getEncoded(); + initCipher(Cipher.ENCRYPT_MODE, secretKeySpec, null, adata, GLOBAL_CIPHER); + iv = GLOBAL_CIPHER.getIV(); + ct = GLOBAL_CIPHER.doFinal(msg); + } - public static JSONObject encrypt(byte[] msg, byte[] adata) throws Exception { - byte[] iv, ct, secretKeySpec_enc; - synchronized (CIPHER) { - SecretKeySpec secretKeySpec = generateKeySpec(); - secretKeySpec_enc = secretKeySpec.getEncoded(); - initCipher(Cipher.ENCRYPT_MODE, secretKeySpec, null, adata); - iv = CIPHER.getIV(); - ct = CIPHER.doFinal(msg); - } + JSONObject value = new JSONObject(); + value.put("iv", Base64.encodeToString(iv, Base64.DEFAULT)); + value.put("v", Integer.toString(VERSION)); + value.put("ks", Integer.toString(KEY_SIZE)); + value.put("cipher", "AES"); + value.put("mode", CIPHER_MODE); + value.put("adata", Base64.encodeToString(adata, Base64.DEFAULT)); + value.put("ct", Base64.encodeToString(ct, Base64.DEFAULT)); - JSONObject value = new JSONObject(); - value.put("iv", Base64.encodeToString(iv, Base64.DEFAULT)); - value.put("v", Integer.toString(VERSION)); - value.put("ks", Integer.toString(KEY_SIZE)); - value.put("cipher", "AES"); - value.put("mode", CIPHER_MODE); - value.put("adata", Base64.encodeToString(adata, Base64.DEFAULT)); - value.put("ct", Base64.encodeToString(ct, Base64.DEFAULT)); + JSONObject result = new JSONObject(); + result.put("key", Base64.encodeToString(secretKeySpec_enc, Base64.DEFAULT)); + result.put("value", value); + result.put("native", true); - JSONObject result = new JSONObject(); - result.put("key", Base64.encodeToString(secretKeySpec_enc, Base64.DEFAULT)); - result.put("value", value); - result.put("native", true); + return result; + } - return result; - } + public static String decrypt(byte[] buf, byte[] key, byte[] iv, byte[] adata, String cipherMode) throws Exception { + Cipher cipher; + if ( cipherMode == CIPHER_MODE ) { + cipher = GLOBAL_CIPHER; + } else { + cipher = getCipher(cipherMode); + } + SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + synchronized (cipher) { + initCipher(Cipher.DECRYPT_MODE, secretKeySpec, iv, adata, cipher); + return new String(cipher.doFinal(buf)); + } + } - public static String decrypt(byte[] buf, byte[] key, byte[] iv, byte[] adata) throws Exception { - SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); - synchronized (CIPHER) { - initCipher(Cipher.DECRYPT_MODE, secretKeySpec, iv, adata); - return new String(CIPHER.doFinal(buf)); - } - } + private static SecretKeySpec generateKeySpec() throws Exception { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(KEY_SIZE, new SecureRandom()); + SecretKey sc = keyGenerator.generateKey(); + return new SecretKeySpec(sc.getEncoded(), "AES"); + } - private static SecretKeySpec generateKeySpec() throws Exception { - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(KEY_SIZE, new SecureRandom()); - SecretKey sc = keyGenerator.generateKey(); - return new SecretKeySpec(sc.getEncoded(), "AES"); - } + private static void initCipher(int cipherMode, Key key, byte[] iv, byte[] adata, Cipher cipher) throws Exception { + if (iv != null) { + cipher.init(cipherMode, key, new IvParameterSpec(iv)); + } else { + cipher.init(cipherMode, key); + } + cipher.updateAAD(adata); + } - private static void initCipher(int cipherMode, Key key, byte[] iv, byte[] adata) throws Exception { - if (iv != null) { - CIPHER.init(cipherMode, key, new IvParameterSpec(iv)); - } else { - CIPHER.init(cipherMode, key); - } - CIPHER.updateAAD(adata); - } + private static Cipher getCipher(String cipherMode) { + if ( cipherMode == null ) { + cipherMode = CIPHER_MODE; + } - private static Cipher getCipher() { - try { - return Cipher.getInstance("AES/" + CIPHER_MODE + "/NoPadding"); - } catch (Exception e) { - return null; - } - } + try { + return Cipher.getInstance("AES/" + cipherMode + "/NoPadding"); + } catch (Exception e) { + return null; + } + } } diff --git a/src/com/crypho/plugins/AbstractRSA.java b/src/com/crypho/plugins/AbstractRSA.java new file mode 100644 index 0000000000000000000000000000000000000000..6efc10776ae97330e1954b8810f7321a916c088d --- /dev/null +++ b/src/com/crypho/plugins/AbstractRSA.java @@ -0,0 +1,124 @@ +package com.crypho.plugins; + +import android.content.Context; +import android.os.Build; +import android.security.keystore.KeyProperties; +import android.util.Log; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; + +public abstract class AbstractRSA { + protected static final String TAG = "SecureStorage"; + static final Integer CERT_VALID_YEARS = 100; + static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; + private final Cipher CIPHER = getCipher(); + + + abstract AlgorithmParameterSpec getInitParams(Context ctx, String alias, Integer userAuthenticationValidityDuration) throws Exception; + + boolean encryptionKeysAvailable(String alias) { + return isEntryAvailable(alias); + } + + String getRSAKey() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return KeyProperties.KEY_ALGORITHM_RSA; + } + return "RSA"; + } + + private Cipher getCipher() { + try { + return Cipher.getInstance("RSA/ECB/PKCS1Padding"); + } catch (Exception e) { + return null; + } + } + + private byte[] runCipher(int cipherMode, String alias, byte[] buf) throws Exception { + Key key = loadKey(cipherMode, alias); + assert CIPHER != null; + synchronized (CIPHER) { + CIPHER.init(cipherMode, key); + return CIPHER.doFinal(buf); + } + } + + public void createKeyPair(Context ctx, String alias, Integer userAuthenticationValidityDuration) throws Exception { + AlgorithmParameterSpec spec = getInitParams(ctx, alias, userAuthenticationValidityDuration); + KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance(getRSAKey(), KEYSTORE_PROVIDER); + kpGenerator.initialize(spec); + kpGenerator.generateKeyPair(); + } + + public byte[] encrypt(byte[] buf, String alias) throws Exception { + return runCipher(Cipher.ENCRYPT_MODE, alias, buf); + } + + + public byte[] decrypt(byte[] buf, String alias) throws Exception { + return runCipher(Cipher.DECRYPT_MODE, alias, buf); + } + + protected abstract boolean isEntryAvailable(String alias); + + Key loadKey(int cipherMode, String alias) throws Exception { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); + keyStore.load(null, null); + + if (!keyStore.containsAlias(alias)) { + throw new Exception("KeyStore doesn't contain alias: " + alias); + } + + Key key; + switch (cipherMode) { + case Cipher.ENCRYPT_MODE: + key = keyStore.getCertificate(alias).getPublicKey(); + if (key == null) { + throw new Exception("Failed to load the public key for " + alias); + } + break; + case Cipher.DECRYPT_MODE: + key = keyStore.getKey(alias, null); + if (key == null) { + throw new Exception("Failed to load the private key for " + alias); + } + break; + default: + throw new Exception("Invalid cipher mode parameter"); + } + return key; + } + + private void deleteKey(String alias) { + try { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); + keyStore.load(null, null); + keyStore.deleteEntry(alias); + } catch (Exception e) { + Log.e(TAG, "Exception deleting key", e); + } + } + + boolean userAuthenticationRequired(String alias) { + try { + // Do a quick encrypt/decrypt test + byte[] encrypted = encrypt(alias.getBytes(), alias); + decrypt(encrypted, alias); + return false; + } catch (InvalidKeyException noAuthEx) { + deleteKey(alias); + return true; + } catch (Exception e) { + // Other + return false; + } + } +} + diff --git a/src/com/crypho/plugins/RSA.java b/src/com/crypho/plugins/RSA.java index 6b8323e4d843727717bf5fd30f52c0584c8391b2..e09df27ea6c95d36ff328869d5040d35c82253e5 100644 --- a/src/com/crypho/plugins/RSA.java +++ b/src/com/crypho/plugins/RSA.java @@ -1,95 +1,56 @@ package com.crypho.plugins; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyInfo; +import android.security.keystore.KeyProperties; import android.util.Log; -import android.security.KeyPairGeneratorSpec; - -import java.math.BigInteger; import java.security.Key; -import java.security.KeyPairGenerator; -import java.security.KeyStore; +import java.security.KeyFactory; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.RSAKeyGenParameterSpec; import java.util.Calendar; import javax.crypto.Cipher; -import javax.security.auth.x500.X500Principal; - -public class RSA { - private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; - private static final Cipher CIPHER = getCipher(); - - public static byte[] encrypt(byte[] buf, String alias) throws Exception { - synchronized (CIPHER) { - initCipher(Cipher.ENCRYPT_MODE, alias); - return CIPHER.doFinal(buf); - } - } - - public static byte[] decrypt(byte[] encrypted, String alias) throws Exception { - synchronized (CIPHER) { - initCipher(Cipher.DECRYPT_MODE, alias); - return CIPHER.doFinal(encrypted); - } - } - - public static void createKeyPair(Context ctx, String alias) throws Exception { - Calendar notBefore = Calendar.getInstance(); - Calendar notAfter = Calendar.getInstance(); - notAfter.add(Calendar.YEAR, 100); - String principalString = String.format("CN=%s, OU=%s", alias, ctx.getPackageName()); - KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(ctx) - .setAlias(alias) - .setSubject(new X500Principal(principalString)) - .setSerialNumber(BigInteger.ONE) - .setStartDate(notBefore.getTime()) - .setEndDate(notAfter.getTime()) - .setEncryptionRequired() - .setKeySize(2048) - .setKeyType("RSA") - .build(); - KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance("RSA", KEYSTORE_PROVIDER); - kpGenerator.initialize(spec); - kpGenerator.generateKeyPair(); - } - - public static void initCipher(int cipherMode, String alias) throws Exception { - KeyStore.PrivateKeyEntry keyEntry = getKeyStoreEntry(alias); - if (keyEntry == null) { - throw new Exception("Failed to load key for " + alias); - } - Key key; - switch (cipherMode) { - case Cipher.ENCRYPT_MODE: - key = keyEntry.getCertificate().getPublicKey(); - break; - case Cipher.DECRYPT_MODE: - key = keyEntry.getPrivateKey(); - break; - default : throw new Exception("Invalid cipher mode parameter"); - } - CIPHER.init(cipherMode, key); - } - - - public static boolean isEntryAvailable(String alias) { - try { - return getKeyStoreEntry(alias) != null; - } catch (Exception e) { - return false; - } - } - - private static KeyStore.PrivateKeyEntry getKeyStoreEntry(String alias) throws Exception { - KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); - keyStore.load(null, null); - return (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); - } - private static Cipher getCipher() { - try { - return Cipher.getInstance("RSA/ECB/PKCS1Padding"); - } catch (Exception e) { - return null; - } - } +public class RSA extends AbstractRSA { + + @TargetApi(Build.VERSION_CODES.M) + public boolean isEntryAvailable(String alias) { + try { + Key privateKey = loadKey(Cipher.DECRYPT_MODE, alias); + if (privateKey == null) { + return false; + } + KeyFactory factory = KeyFactory.getInstance(privateKey.getAlgorithm(), KEYSTORE_PROVIDER); + KeyInfo keyInfo = factory.getKeySpec(privateKey, KeyInfo.class); + return keyInfo.isInsideSecureHardware(); + } catch (Exception e) { + Log.i(TAG, "Checking encryption keys failed.", e); + return false; + } + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public AlgorithmParameterSpec getInitParams(Context ctx, String alias, Integer userAuthenticationValidityDuration) { + Calendar notAfter = Calendar.getInstance(); + notAfter.add(Calendar.YEAR, CERT_VALID_YEARS); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) + .setCertificateNotBefore(Calendar.getInstance().getTime()) + .setCertificateNotAfter(notAfter.getTime()) + .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds(userAuthenticationValidityDuration) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + .setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .build(); + } + return null; + } } \ No newline at end of file diff --git a/src/com/crypho/plugins/RSAFactory.java b/src/com/crypho/plugins/RSAFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..08716ae96528fec30ae3d8f0bac614dc3f5d96a3 --- /dev/null +++ b/src/com/crypho/plugins/RSAFactory.java @@ -0,0 +1,12 @@ +package com.crypho.plugins; + +import android.os.Build; + +public class RSAFactory { + public static AbstractRSA getRSA() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return new RSALegacy(); + } + return new RSA(); + } +} diff --git a/src/com/crypho/plugins/RSALegacy.java b/src/com/crypho/plugins/RSALegacy.java new file mode 100644 index 0000000000000000000000000000000000000000..d6f9b5f1de405fa8a54fe9eb565fdcca5a960f93 --- /dev/null +++ b/src/com/crypho/plugins/RSALegacy.java @@ -0,0 +1,43 @@ +package com.crypho.plugins; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; + +import java.math.BigInteger; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Calendar; + +import javax.crypto.Cipher; +import javax.security.auth.x500.X500Principal; + +public class RSALegacy extends AbstractRSA { + + @Override + public boolean isEntryAvailable(String alias) { + try { + return loadKey(Cipher.ENCRYPT_MODE, alias) != null; + } catch (Exception e) { + return false; + } + } + + @Override + @TargetApi(Build.VERSION_CODES.KITKAT) + public AlgorithmParameterSpec getInitParams(Context ctx, String alias, Integer userAuthenticationValidityDuration) throws Exception { + Calendar notAfter = Calendar.getInstance(); + notAfter.add(Calendar.YEAR, CERT_VALID_YEARS); + + return new KeyPairGeneratorSpec.Builder(ctx) + .setAlias(alias) + .setSubject(new X500Principal(String.format("CN=%s, OU=%s", alias, ctx.getPackageName()))) + .setSerialNumber(BigInteger.ONE) + .setStartDate(Calendar.getInstance().getTime()) + .setEndDate(notAfter.getTime()) + .setEncryptionRequired() + .setKeySize(2048) + .setKeyType(getRSAKey()) + .build(); + } +} \ No newline at end of file diff --git a/src/com/crypho/plugins/SecureStorage.java b/src/com/crypho/plugins/SecureStorage.java index b4aef51f4500e117d367f83569c06570f5cec647..12cd433ea4afb06846e29ea259ef1167cc4c5f09 100644 --- a/src/com/crypho/plugins/SecureStorage.java +++ b/src/com/crypho/plugins/SecureStorage.java @@ -1,40 +1,52 @@ package com.crypho.plugins; -import java.lang.reflect.Method; -import java.util.Hashtable; - -import android.util.Log; -import android.util.Base64; -import android.os.Build; +import android.annotation.TargetApi; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; +import android.hardware.biometrics.BiometricPrompt; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Handler; +import android.provider.Settings; +import android.util.Base64; +import android.util.Log; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaArgs; import org.apache.cordova.CordovaPlugin; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.json.JSONArray; -import javax.crypto.Cipher; + +import java.lang.reflect.Method; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.Executor; public class SecureStorage extends CordovaPlugin { private static final String TAG = "SecureStorage"; - private static final boolean SUPPORTS_NATIVE_AES = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; private static final boolean SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + private static final Boolean IS_API_29_AVAILABLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + private static final Integer DEFAULT_AUTHENTICATION_VALIDITY_TIME = 60 * 60 * 24; // Fallback to 24h. Workaround to avoid asking for credentials too "often" private static final String MSG_NOT_SUPPORTED = "API 19 (Android 4.4 KitKat) is required. This device is running API " + Build.VERSION.SDK_INT; private static final String MSG_DEVICE_NOT_SECURE = "Device is not secure"; + private static final String MSG_KEYS_FAILED = "Generate RSA Encryption Keys failed. "; private Hashtable<String, SharedPreferencesHandler> SERVICE_STORAGE = new Hashtable<String, SharedPreferencesHandler>(); private String INIT_SERVICE; - private volatile CallbackContext initContext, secureDeviceContext; - private volatile boolean initContextRunning = false; + private String INIT_PACKAGENAME; + private volatile CallbackContext secureDeviceContext, generateKeysContext, unlockCredentialsContext; + private volatile boolean generateKeysContextRunning = false; + + private AbstractRSA rsa = RSAFactory.getRSA(); @Override public void onResume(boolean multitasking) { if (secureDeviceContext != null) { + if (isDeviceSecure()) { secureDeviceContext.success(); } else { @@ -43,65 +55,78 @@ public class SecureStorage extends CordovaPlugin { secureDeviceContext = null; } - if (initContext != null && !initContextRunning) { - cordova.getThreadPool().execute(new Runnable() { - public void run() { - initContextRunning = true; - try { - String alias = service2alias(INIT_SERVICE); - if (!RSA.isEntryAvailable(alias)) { - //Solves Issue #96. The RSA key may have been deleted by changing the lock type. - getStorage(INIT_SERVICE).clear(); - RSA.createKeyPair(getContext(), alias); + if (unlockCredentialsContext != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + if (unlockCredentialsContext != null) { + String alias = service2alias(INIT_SERVICE); + if (rsa.userAuthenticationRequired(alias)) { + unlockCredentialsContext.error("User not authenticated"); + } + unlockCredentialsContext.success(); + unlockCredentialsContext = null; } - initSuccess(initContext); - } catch (Exception e) { - Log.e(TAG, "Init failed :", e); - initContext.error(e.getMessage()); - } finally { - initContext = null; - initContextRunning = false; } - } - }); - } - } - - private boolean isDeviceSecure() { - KeyguardManager keyguardManager = (KeyguardManager)(getContext().getSystemService(Context.KEYGUARD_SERVICE)); - try { - Method isSecure = null; - isSecure = keyguardManager.getClass().getMethod("isDeviceSecure"); - return ((Boolean) isSecure.invoke(keyguardManager)).booleanValue(); - } catch (Exception e) { - return keyguardManager.isKeyguardSecure(); + }); + } } } @Override public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException { - if(!SUPPORTED){ + if (!SUPPORTED) { Log.w(TAG, MSG_NOT_SUPPORTED); callbackContext.error(MSG_NOT_SUPPORTED); return false; } if ("init".equals(action)) { String service = args.getString(0); + JSONObject options = args.getJSONObject(1); + + String packageName = options.optString("packageName", getContext().getPackageName()); + + Context ctx = null; + + // Solves #151. By default, we use our own ApplicationContext + // If packageName is provided, we try to get the Context of another Application with that packageName + try { + ctx = getPackageContext(packageName); + } catch (Exception e) { + // This will fail if the application with given packageName is not installed + // OR if we do not have required permissions and cause a security violation + Log.e(TAG, "Init failed :", e); + callbackContext.error(e.getMessage()); + } + + INIT_PACKAGENAME = ctx.getPackageName(); String alias = service2alias(service); INIT_SERVICE = service; - SharedPreferencesHandler PREFS = new SharedPreferencesHandler(alias + "_SS", getContext()); + SharedPreferencesHandler PREFS = new SharedPreferencesHandler(alias, ctx); SERVICE_STORAGE.put(service, PREFS); - if (!isDeviceSecure()) { Log.e(TAG, MSG_DEVICE_NOT_SECURE); callbackContext.error(MSG_DEVICE_NOT_SECURE); - } else if (!RSA.isEntryAvailable(alias)) { - initContext = callbackContext; - unlockCredentials(); + } else if (!rsa.encryptionKeysAvailable(alias)) { + // Encryption Keys aren't available, proceed to generate them + Integer userAuthenticationValidityDuration = options.optInt("userAuthenticationValidityDuration", DEFAULT_AUTHENTICATION_VALIDITY_TIME); + generateKeysContext = callbackContext; + generateEncryptionKeys(userAuthenticationValidityDuration); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + unlockCredentialsLegacy(); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && rsa.userAuthenticationRequired(alias)) { + // User has to confirm authentication via device credentials. + String title = options.optString("unlockCredentialsTitle", null); + String description = options.optString("unlockCredentialsDescription", null); + + unlockCredentialsContext = callbackContext; + unlockCredentials(title, description); } else { initSuccess(callbackContext); } + return true; } if ("set".equals(action)) { @@ -114,12 +139,12 @@ public class SecureStorage extends CordovaPlugin { try { JSONObject result = AES.encrypt(value.getBytes(), adata.getBytes()); byte[] aes_key = Base64.decode(result.getString("key"), Base64.DEFAULT); - byte[] aes_key_enc = RSA.encrypt(aes_key, service2alias(service)); + byte[] aes_key_enc = rsa.encrypt(aes_key, service2alias(service)); result.put("key", Base64.encodeToString(aes_key_enc, Base64.DEFAULT)); getStorage(service).store(key, result.toString()); - callbackContext.success(); + callbackContext.success(key); } catch (Exception e) { - Log.e(TAG, "Encrypt (RSA/AES) failed :", e); + Log.e(TAG, "Encrypt failed :", e); callbackContext.error(e.getMessage()); } } @@ -133,18 +158,18 @@ public class SecureStorage extends CordovaPlugin { if (value != null) { JSONObject json = new JSONObject(value); final byte[] encKey = Base64.decode(json.getString("key"), Base64.DEFAULT); - JSONObject data = json.getJSONObject("value"); + final JSONObject data = json.getJSONObject("value"); final byte[] ct = Base64.decode(data.getString("ct"), Base64.DEFAULT); final byte[] iv = Base64.decode(data.getString("iv"), Base64.DEFAULT); final byte[] adata = Base64.decode(data.getString("adata"), Base64.DEFAULT); cordova.getThreadPool().execute(new Runnable() { public void run() { try { - byte[] decryptedKey = RSA.decrypt(encKey, service2alias(service)); - String decrypted = new String(AES.decrypt(ct, decryptedKey, iv, adata)); + byte[] decryptedKey = rsa.decrypt(encKey, service2alias(service)); + String decrypted = new String(AES.decrypt(ct, decryptedKey, iv, adata, data.getString("mode"))); callbackContext.success(decrypted); } catch (Exception e) { - Log.e(TAG, "Decrypt (RSA/AES) failed :", e); + Log.e(TAG, "Decrypt failed :", e); callbackContext.error(e.getMessage()); } } @@ -154,70 +179,18 @@ public class SecureStorage extends CordovaPlugin { } return true; } - if ("decrypt_rsa".equals(action)) { - final String service = args.getString(0); - // getArrayBuffer does base64 decoding - final byte[] decryptMe = args.getArrayBuffer(1); - cordova.getThreadPool().execute(new Runnable() { - public void run() { - try { - byte[] decrypted = RSA.decrypt(decryptMe, service2alias(service)); - callbackContext.success(new String (decrypted)); - } catch (Exception e) { - Log.e(TAG, "Decrypt (RSA) failed :", e); - callbackContext.error(e.getMessage()); - } - } - }); - return true; - } - if ("encrypt_rsa".equals(action)) { - final String service = args.getString(0); - final String encryptMe = args.getString(1); - cordova.getThreadPool().execute(new Runnable() { - public void run() { - try { - byte[] encrypted = RSA.encrypt(encryptMe.getBytes(), service2alias(service)); - callbackContext.success(Base64.encodeToString(encrypted, Base64.DEFAULT)); - } catch (Exception e) { - Log.e(TAG, "Encrypt (RSA) failed :", e); - callbackContext.error(e.getMessage()); - } - } - }); - return true; - } - if ("secureDevice".equals(action)) { + // Open the Security Settings screen. The app developer should inform the user about + // the security requirements of the app and initialize again after the user has changed the screen-lock settings secureDeviceContext = callbackContext; - unlockCredentials(); + secureDevice(); return true; } - //SharedPreferences interface if ("remove".equals(action)) { String service = args.getString(0); String key = args.getString(1); getStorage(service).remove(key); - callbackContext.success(); - return true; - } - if ("store".equals(action)) { - String service = args.getString(0); - String key = args.getString(1); - String value = args.getString(2); - getStorage(service).store(key, value); - callbackContext.success(); - return true; - } - if ("fetch".equals(action)) { - String service = args.getString(0); - String key = args.getString(1); - String value = getStorage(service).fetch(key); - if (value != null) { - callbackContext.success(value); - } else { - callbackContext.error("Key [" + key + "] not found."); - } + callbackContext.success(key); return true; } if ("keys".equals(action)) { @@ -234,9 +207,20 @@ public class SecureStorage extends CordovaPlugin { return false; } + private boolean isDeviceSecure() { + KeyguardManager keyguardManager = (KeyguardManager) (getContext().getSystemService(Context.KEYGUARD_SERVICE)); + try { + Method isSecure = null; + isSecure = keyguardManager.getClass().getMethod("isDeviceSecure"); + return ((Boolean) isSecure.invoke(keyguardManager)).booleanValue(); + } catch (Exception e) { + return keyguardManager.isKeyguardSecure(); + } + } + private String service2alias(String service) { - String res = getContext().getPackageName() + "." + service; - return res; + String res = INIT_PACKAGENAME + "." + service; + return res; } private SharedPreferencesHandler getStorage(String service) { @@ -244,12 +228,70 @@ public class SecureStorage extends CordovaPlugin { } private void initSuccess(CallbackContext context) { - // 0 is falsy in js while 1 is truthy - context.success(SUPPORTS_NATIVE_AES ? 1 : 0); + context.success(); } - private void unlockCredentials() { + /** + * Create the Confirm Credentials screen. + * You can customize the title and description or Android will provide a generic one for you if you leave it null + * + * @param title + * @param description + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void unlockCredentials(final String title, final String description) { cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + if (IS_API_29_AVAILABLE && isDeviceSecure()) { + // Building a biometric prompt instance with custom title and description. + BiometricPrompt.Builder biometricPromptBuilder = new BiometricPrompt.Builder(getContext()); + biometricPromptBuilder.setTitle(title); + biometricPromptBuilder.setDescription(description); + //biometricPromptBuilder.setDeviceCredentialAllowed(true); + BiometricPrompt biometricPrompt = biometricPromptBuilder.build(); + CancellationSignal cancellationSignal = new CancellationSignal(); + final Executor executor = getExecutor(); + // Launching the credential confirmation popup to get biometric validation. + // If biometric is not available will open the other unlock methods. + biometricPrompt.authenticate(cancellationSignal, executor, new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + } + + @Override + public void onAuthenticationHelp(int helpCode, CharSequence helpString) { + super.onAuthenticationHelp(helpCode, helpString); + } + + @Override + public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + } + }); + } else { + KeyguardManager keyguardManager = (KeyguardManager) (getContext().getSystemService(Context.KEYGUARD_SERVICE)); + Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(title, description); + if (intent != null) { + startActivity(intent); + } else { + Log.e(TAG, "Error creating Confirm Credentials Intent"); + unlockCredentialsContext.error("Cant't unlock credentials, error creating Confirm Credentials Intent"); + } + } + } + }); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private void unlockCredentialsLegacy() { + cordova.getActivity().runOnUiThread(new Runnable() { + @Override public void run() { Intent intent = new Intent("com.android.credentials.UNLOCK"); startActivity(intent); @@ -257,10 +299,84 @@ public class SecureStorage extends CordovaPlugin { }); } + /** + * Generate Encryption Keys in the background. + * + * @param userAuthenticationValidityDuration User authentication validity duration in seconds + */ + private void generateEncryptionKeys(final Integer userAuthenticationValidityDuration) { + if (generateKeysContext != null && !generateKeysContextRunning) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + generateKeysContextRunning = true; + try { + String alias = service2alias(INIT_SERVICE); + SharedPreferencesHandler storage = getStorage(INIT_SERVICE); + if(storage.isEmpty()){ + //Solves Issue #96. The RSA key may have been deleted by changing the lock type. + getStorage(INIT_SERVICE).clear(); + rsa.createKeyPair(getContext(), alias, userAuthenticationValidityDuration); + } + generateKeysContext.success(); + } catch (Exception e) { + Log.e(TAG, MSG_KEYS_FAILED, e); + generateKeysContext.error(MSG_KEYS_FAILED + e.getMessage()); + } finally { + generateKeysContext = null; + generateKeysContextRunning = false; + } + } + }); + } + } + + /** + * Open Security settings screen. + */ + private void secureDevice() { + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + try { + Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS); + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "Error opening Security settings to secure device : ", e); + secureDeviceContext.error(e.getMessage()); + } + } + }); + } + private Context getContext() { return cordova.getActivity().getApplicationContext(); } + /** + * Creates a executor with handler to run runnable tasks. + */ + private Executor getExecutor() { + return new Executor() { + @Override + public void execute(Runnable command) { + Handler handler = new Handler(); + handler.post(command); + } + }; + } + + private Context getPackageContext(String packageName) throws Exception { + Context pkgContext = null; + + Context context = getContext(); + if (context.getPackageName().equals(packageName)) { + pkgContext = context; + } else { + pkgContext = context.createPackageContext(packageName, 0); + } + + return pkgContext; + } + private void startActivity(Intent intent) { cordova.getActivity().startActivity(intent); } diff --git a/src/com/crypho/plugins/SharedPreferencesHandler.java b/src/com/crypho/plugins/SharedPreferencesHandler.java index a6f2c02a576ae9572ad6ca3a693603150c518808..fba73b88921ee7b76662353ae0736d784862cbff 100644 --- a/src/com/crypho/plugins/SharedPreferencesHandler.java +++ b/src/com/crypho/plugins/SharedPreferencesHandler.java @@ -1,39 +1,49 @@ package com.crypho.plugins; import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; import android.content.SharedPreferences; import android.content.Context; public class SharedPreferencesHandler { private SharedPreferences prefs; - private static final String MIGRATED_TO_NATIVE_KEY = "_SS_MIGRATED_TO_NATIVE"; - private static final String MIGRATED_TO_NATIVE_STORAGE_KEY = "_SS_MIGRATED_TO_NATIVE_STORAGE"; public SharedPreferencesHandler (String prefsName, Context ctx){ - prefs = ctx.getSharedPreferences(prefsName, 0); + prefs = ctx.getSharedPreferences(prefsName + "_SS", 0); } - void store(String key, String value){ + public void store(String key, String value){ SharedPreferences.Editor editor = prefs.edit(); - editor.putString(key, value); + editor.putString("_SS_" + key, value); editor.commit(); } + boolean isEmpty() { + int numOfPrefs = prefs.getAll().size(); + return (numOfPrefs == 0); + } + String fetch (String key){ - return prefs.getString(key, null); + return prefs.getString("_SS_" + key, null); } void remove (String key){ SharedPreferences.Editor editor = prefs.edit(); - editor.remove(key); + editor.remove("_SS_" + key); editor.commit(); } Set keys (){ - Set res = prefs.getAll().keySet(); - res.remove(MIGRATED_TO_NATIVE_KEY); - res.remove(MIGRATED_TO_NATIVE_STORAGE_KEY); + Set res = new HashSet<String>(); + Iterator<String> iter = prefs.getAll().keySet().iterator(); + while (iter.hasNext()) { + String key = iter.next(); + if (key.startsWith("_SS_") && !key.startsWith("_SS_MIGRATED_")) { + res.add(key.replaceFirst("^_SS_", "")); + } + } return res; } @@ -42,4 +52,4 @@ public class SharedPreferencesHandler { editor.clear(); editor.commit(); } -} \ No newline at end of file +} diff --git a/src/org/apache/cordova/file/AssetFilesystem.java b/src/org/apache/cordova/file/AssetFilesystem.java new file mode 100644 index 0000000000000000000000000000000000000000..b035c40e6ec3ec0edc0203db933595e645f7ece8 --- /dev/null +++ b/src/org/apache/cordova/file/AssetFilesystem.java @@ -0,0 +1,294 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import android.content.res.AssetManager; +import android.net.Uri; + +import org.apache.cordova.CordovaResourceApi; +import org.apache.cordova.LOG; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.HashMap; +import java.util.Map; + +public class AssetFilesystem extends Filesystem { + + private final AssetManager assetManager; + + // A custom gradle hook creates the cdvasset.manifest file, which speeds up asset listing a tonne. + // See: http://stackoverflow.com/questions/16911558/android-assetmanager-list-incredibly-slow + private static Object listCacheLock = new Object(); + private static boolean listCacheFromFile; + private static Map<String, String[]> listCache; + private static Map<String, Long> lengthCache; + + private static final String LOG_TAG = "AssetFilesystem"; + + private void lazyInitCaches() { + synchronized (listCacheLock) { + if (listCache == null) { + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(assetManager.open("cdvasset.manifest")); + listCache = (Map<String, String[]>) ois.readObject(); + lengthCache = (Map<String, Long>) ois.readObject(); + listCacheFromFile = true; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + // Asset manifest won't exist if the gradle hook isn't set up correctly. + } finally { + if (ois != null) { + try { + ois.close(); + } catch (IOException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + } + if (listCache == null) { + LOG.w("AssetFilesystem", "Asset manifest not found. Recursive copies and directory listing will be slow."); + listCache = new HashMap<String, String[]>(); + } + } + } + } + + private String[] listAssets(String assetPath) throws IOException { + if (assetPath.startsWith("/")) { + assetPath = assetPath.substring(1); + } + if (assetPath.endsWith("/")) { + assetPath = assetPath.substring(0, assetPath.length() - 1); + } + lazyInitCaches(); + String[] ret = listCache.get(assetPath); + if (ret == null) { + if (listCacheFromFile) { + ret = new String[0]; + } else { + ret = assetManager.list(assetPath); + listCache.put(assetPath, ret); + } + } + return ret; + } + + private long getAssetSize(String assetPath) throws FileNotFoundException { + if (assetPath.startsWith("/")) { + assetPath = assetPath.substring(1); + } + lazyInitCaches(); + if (lengthCache != null) { + Long ret = lengthCache.get(assetPath); + if (ret == null) { + throw new FileNotFoundException("Asset not found: " + assetPath); + } + return ret; + } + CordovaResourceApi.OpenForReadResult offr = null; + try { + offr = resourceApi.openForRead(nativeUriForFullPath(assetPath)); + long length = offr.length; + if (length < 0) { + // available() doesn't always yield the file size, but for assets it does. + length = offr.inputStream.available(); + } + return length; + } catch (IOException e) { + FileNotFoundException fnfe = new FileNotFoundException("File not found: " + assetPath); + fnfe.initCause(e); + throw fnfe; + } finally { + if (offr != null) { + try { + offr.inputStream.close(); + } catch (IOException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + } + } + + public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi) { + super(Uri.parse("file:///android_asset/"), "assets", resourceApi); + this.assetManager = assetManager; + } + + @Override + public Uri toNativeUri(LocalFilesystemURL inputURL) { + return nativeUriForFullPath(inputURL.path); + } + + @Override + public LocalFilesystemURL toLocalUri(Uri inputURL) { + if (!"file".equals(inputURL.getScheme())) { + return null; + } + File f = new File(inputURL.getPath()); + // Removes and duplicate /s (e.g. file:///a//b/c) + Uri resolvedUri = Uri.fromFile(f); + String rootUriNoTrailingSlash = rootUri.getEncodedPath(); + rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1); + if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) { + return null; + } + String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length()); + // Strip leading slash + if (!subPath.isEmpty()) { + subPath = subPath.substring(1); + } + Uri.Builder b = new Uri.Builder() + .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL) + .authority("localhost") + .path(name); + if (!subPath.isEmpty()) { + b.appendEncodedPath(subPath); + } + if (isDirectory(subPath) || inputURL.getPath().endsWith("/")) { + // Add trailing / for directories. + b.appendEncodedPath(""); + } + return LocalFilesystemURL.parse(b.build()); + } + + private boolean isDirectory(String assetPath) { + try { + return listAssets(assetPath).length != 0; + } catch (IOException e) { + return false; + } + } + + @Override + public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException { + String pathNoSlashes = inputURL.path.substring(1); + if (pathNoSlashes.endsWith("/")) { + pathNoSlashes = pathNoSlashes.substring(0, pathNoSlashes.length() - 1); + } + + String[] files; + try { + files = listAssets(pathNoSlashes); + } catch (IOException e) { + FileNotFoundException fnfe = new FileNotFoundException(); + fnfe.initCause(e); + throw fnfe; + } + + LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length]; + for (int i = 0; i < files.length; ++i) { + entries[i] = localUrlforFullPath(new File(inputURL.path, files[i]).getPath()); + } + return entries; + } + + @Override + public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, + String path, JSONObject options, boolean directory) + throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + if (options != null && options.optBoolean("create")) { + throw new UnsupportedOperationException("Assets are read-only"); + } + + // Check whether the supplied path is absolute or relative + if (directory && !path.endsWith("/")) { + path += "/"; + } + + LocalFilesystemURL requestedURL; + if (path.startsWith("/")) { + requestedURL = localUrlforFullPath(normalizePath(path)); + } else { + requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path)); + } + + // Throws a FileNotFoundException if it doesn't exist. + getFileMetadataForLocalURL(requestedURL); + + boolean isDir = isDirectory(requestedURL.path); + if (directory && !isDir) { + throw new TypeMismatchException("path doesn't exist or is file"); + } else if (!directory && isDir) { + throw new TypeMismatchException("path doesn't exist or is directory"); + } + + // Return the directory + return makeEntryForURL(requestedURL); + } + + @Override + public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { + JSONObject metadata = new JSONObject(); + long size = inputURL.isDirectory ? 0 : getAssetSize(inputURL.path); + try { + metadata.put("size", size); + metadata.put("type", inputURL.isDirectory ? "text/directory" : resourceApi.getMimeType(toNativeUri(inputURL))); + metadata.put("name", new File(inputURL.path).getName()); + metadata.put("fullPath", inputURL.path); + metadata.put("lastModifiedDate", 0); + } catch (JSONException e) { + return null; + } + return metadata; + } + + @Override + public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) { + return false; + } + + @Override + long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset, boolean isBinary) throws NoModificationAllowedException, IOException { + throw new NoModificationAllowedException("Assets are read-only"); + } + + @Override + long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException, NoModificationAllowedException { + throw new NoModificationAllowedException("Assets are read-only"); + } + + @Override + String filesystemPathForURL(LocalFilesystemURL url) { + return new File(rootUri.getPath(), url.path).toString(); + } + + @Override + LocalFilesystemURL URLforFilesystemPath(String path) { + return null; + } + + @Override + boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException { + throw new NoModificationAllowedException("Assets are read-only"); + } + + @Override + boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws NoModificationAllowedException { + throw new NoModificationAllowedException("Assets are read-only"); + } + +} diff --git a/src/org/apache/cordova/file/ContentFilesystem.java b/src/org/apache/cordova/file/ContentFilesystem.java new file mode 100644 index 0000000000000000000000000000000000000000..6b983c0896174ae96fdaa307ea1c9185cc8def09 --- /dev/null +++ b/src/org/apache/cordova/file/ContentFilesystem.java @@ -0,0 +1,223 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import org.apache.cordova.CordovaResourceApi; +import org.json.JSONException; +import org.json.JSONObject; + +public class ContentFilesystem extends Filesystem { + + private final Context context; + + public ContentFilesystem(Context context, CordovaResourceApi resourceApi) { + super(Uri.parse("content://"), "content", resourceApi); + this.context = context; + } + + @Override + public Uri toNativeUri(LocalFilesystemURL inputURL) { + String authorityAndPath = inputURL.uri.getEncodedPath().substring(this.name.length() + 2); + if (authorityAndPath.length() < 2) { + return null; + } + String ret = "content://" + authorityAndPath; + String query = inputURL.uri.getEncodedQuery(); + if (query != null) { + ret += '?' + query; + } + String frag = inputURL.uri.getEncodedFragment(); + if (frag != null) { + ret += '#' + frag; + } + return Uri.parse(ret); + } + + @Override + public LocalFilesystemURL toLocalUri(Uri inputURL) { + if (!"content".equals(inputURL.getScheme())) { + return null; + } + String subPath = inputURL.getEncodedPath(); + if (subPath.length() > 0) { + subPath = subPath.substring(1); + } + Uri.Builder b = new Uri.Builder() + .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL) + .authority("localhost") + .path(name) + .appendPath(inputURL.getAuthority()); + if (subPath.length() > 0) { + b.appendEncodedPath(subPath); + } + Uri localUri = b.encodedQuery(inputURL.getEncodedQuery()) + .encodedFragment(inputURL.getEncodedFragment()) + .build(); + return LocalFilesystemURL.parse(localUri); + } + + @Override + public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, + String fileName, JSONObject options, boolean directory) throws IOException, TypeMismatchException, JSONException { + throw new UnsupportedOperationException("getFile() not supported for content:. Use resolveLocalFileSystemURL instead."); + } + + @Override + public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) + throws NoModificationAllowedException { + Uri contentUri = toNativeUri(inputURL); + try { + context.getContentResolver().delete(contentUri, null, null); + } catch (UnsupportedOperationException t) { + // Was seeing this on the File mobile-spec tests on 4.0.3 x86 emulator. + // The ContentResolver applies only when the file was registered in the + // first case, which is generally only the case with images. + NoModificationAllowedException nmae = new NoModificationAllowedException("Deleting not supported for content uri: " + contentUri); + nmae.initCause(t); + throw nmae; + } + return true; + } + + @Override + public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) + throws NoModificationAllowedException { + throw new NoModificationAllowedException("Cannot remove content url"); + } + + @Override + public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException { + throw new UnsupportedOperationException("readEntriesAtLocalURL() not supported for content:. Use resolveLocalFileSystemURL instead."); + } + + @Override + public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { + long size = -1; + long lastModified = 0; + Uri nativeUri = toNativeUri(inputURL); + String mimeType = resourceApi.getMimeType(nativeUri); + Cursor cursor = openCursorForURL(nativeUri); + try { + if (cursor != null && cursor.moveToFirst()) { + Long sizeForCursor = resourceSizeForCursor(cursor); + if (sizeForCursor != null) { + size = sizeForCursor.longValue(); + } + Long modified = lastModifiedDateForCursor(cursor); + if (modified != null) + lastModified = modified.longValue(); + } else { + // Some content providers don't support cursors at all! + CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(nativeUri); + size = offr.length; + } + } catch (IOException e) { + FileNotFoundException fnfe = new FileNotFoundException(); + fnfe.initCause(e); + throw fnfe; + } finally { + if (cursor != null) + cursor.close(); + } + + JSONObject metadata = new JSONObject(); + try { + metadata.put("size", size); + metadata.put("type", mimeType); + metadata.put("name", name); + metadata.put("fullPath", inputURL.path); + metadata.put("lastModifiedDate", lastModified); + } catch (JSONException e) { + return null; + } + return metadata; + } + + @Override + public long writeToFileAtURL(LocalFilesystemURL inputURL, String data, + int offset, boolean isBinary) throws NoModificationAllowedException { + throw new NoModificationAllowedException("Couldn't write to file given its content URI"); + } + @Override + public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) + throws NoModificationAllowedException { + throw new NoModificationAllowedException("Couldn't truncate file given its content URI"); + } + + protected Cursor openCursorForURL(Uri nativeUri) { + ContentResolver contentResolver = context.getContentResolver(); + try { + return contentResolver.query(nativeUri, null, null, null, null); + } catch (UnsupportedOperationException e) { + return null; + } + } + + private Long resourceSizeForCursor(Cursor cursor) { + int columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE); + if (columnIndex != -1) { + String sizeStr = cursor.getString(columnIndex); + if (sizeStr != null) { + return Long.parseLong(sizeStr); + } + } + return null; + } + + protected Long lastModifiedDateForCursor(Cursor cursor) { + int columnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED); + if (columnIndex == -1) { + columnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED); + } + if (columnIndex != -1) { + String dateStr = cursor.getString(columnIndex); + if (dateStr != null) { + return Long.parseLong(dateStr); + } + } + return null; + } + + @Override + public String filesystemPathForURL(LocalFilesystemURL url) { + File f = resourceApi.mapUriToFile(toNativeUri(url)); + return f == null ? null : f.getAbsolutePath(); + } + + @Override + public LocalFilesystemURL URLforFilesystemPath(String path) { + // Returns null as we don't support reverse mapping back to content:// URLs + return null; + } + + @Override + public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) { + return true; + } +} diff --git a/src/org/apache/cordova/file/DirectoryManager.java b/src/org/apache/cordova/file/DirectoryManager.java new file mode 100644 index 0000000000000000000000000000000000000000..07af5ea207407c499a45bf1171b96cc3e6db2b21 --- /dev/null +++ b/src/org/apache/cordova/file/DirectoryManager.java @@ -0,0 +1,134 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.file; + +import android.os.Environment; +import android.os.StatFs; + +import java.io.File; + +/** + * This class provides file directory utilities. + * All file operations are performed on the SD card. + * + * It is used by the FileUtils class. + */ +public class DirectoryManager { + + @SuppressWarnings("unused") + private static final String LOG_TAG = "DirectoryManager"; + + /** + * Determine if a file or directory exists. + * @param name The name of the file to check. + * @return T=exists, F=not found + */ + public static boolean testFileExists(String name) { + boolean status; + + // If SD card exists + if ((testSaveLocationExists()) && (!name.equals(""))) { + File path = Environment.getExternalStorageDirectory(); + File newPath = constructFilePaths(path.toString(), name); + status = newPath.exists(); + } + // If no SD card + else { + status = false; + } + return status; + } + + /** + * Get the free space in external storage + * + * @return Size in KB or -1 if not available + */ + public static long getFreeExternalStorageSpace() { + String status = Environment.getExternalStorageState(); + long freeSpaceInBytes = 0; + + // Check if external storage exists + if (status.equals(Environment.MEDIA_MOUNTED)) { + freeSpaceInBytes = getFreeSpaceInBytes(Environment.getExternalStorageDirectory().getPath()); + } else { + // If no external storage then return -1 + return -1; + } + + return freeSpaceInBytes / 1024; + } + + /** + * Given a path return the number of free bytes in the filesystem containing the path. + * + * @param path to the file system + * @return free space in bytes + */ + public static long getFreeSpaceInBytes(String path) { + try { + StatFs stat = new StatFs(path); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + return availableBlocks * blockSize; + } catch (IllegalArgumentException e) { + // The path was invalid. Just return 0 free bytes. + return 0; + } + } + + /** + * Determine if SD card exists. + * + * @return T=exists, F=not found + */ + public static boolean testSaveLocationExists() { + String sDCardStatus = Environment.getExternalStorageState(); + boolean status; + + // If SD card is mounted + if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) { + status = true; + } + + // If no SD card + else { + status = false; + } + return status; + } + + /** + * Create a new file object from two file paths. + * + * @param file1 Base file path + * @param file2 Remaining file path + * @return File object + */ + private static File constructFilePaths (String file1, String file2) { + File newPath; + if (file2.startsWith(file1)) { + newPath = new File(file2); + } + else { + newPath = new File(file1 + "/" + file2); + } + return newPath; + } +} diff --git a/src/org/apache/cordova/file/EncodingException.java b/src/org/apache/cordova/file/EncodingException.java new file mode 100644 index 0000000000000000000000000000000000000000..e9e1653bd1facdf88a131e9e2a26c45a483efa57 --- /dev/null +++ b/src/org/apache/cordova/file/EncodingException.java @@ -0,0 +1,29 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +package org.apache.cordova.file; + +@SuppressWarnings("serial") +public class EncodingException extends Exception { + + public EncodingException(String message) { + super(message); + } + +} diff --git a/src/org/apache/cordova/file/FileExistsException.java b/src/org/apache/cordova/file/FileExistsException.java new file mode 100644 index 0000000000000000000000000000000000000000..5c4d83dc458a02c0b78457686a58109f02601ea2 --- /dev/null +++ b/src/org/apache/cordova/file/FileExistsException.java @@ -0,0 +1,29 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +package org.apache.cordova.file; + +@SuppressWarnings("serial") +public class FileExistsException extends Exception { + + public FileExistsException(String msg) { + super(msg); + } + +} diff --git a/src/org/apache/cordova/file/FileUtils.java b/src/org/apache/cordova/file/FileUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1d6e61fa3392746b050aca7db49184c56451a086 --- /dev/null +++ b/src/org/apache/cordova/file/FileUtils.java @@ -0,0 +1,1225 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.util.Base64; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PermissionHelper; +import org.apache.cordova.PluginResult; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.security.Permission; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This class provides file and directory services to JavaScript. + */ +public class FileUtils extends CordovaPlugin { + private static final String LOG_TAG = "FileUtils"; + + public static int NOT_FOUND_ERR = 1; + public static int SECURITY_ERR = 2; + public static int ABORT_ERR = 3; + + public static int NOT_READABLE_ERR = 4; + public static int ENCODING_ERR = 5; + public static int NO_MODIFICATION_ALLOWED_ERR = 6; + public static int INVALID_STATE_ERR = 7; + public static int SYNTAX_ERR = 8; + public static int INVALID_MODIFICATION_ERR = 9; + public static int QUOTA_EXCEEDED_ERR = 10; + public static int TYPE_MISMATCH_ERR = 11; + public static int PATH_EXISTS_ERR = 12; + + /* + * Permission callback codes + */ + + public static final int ACTION_GET_FILE = 0; + public static final int ACTION_WRITE = 1; + public static final int ACTION_GET_DIRECTORY = 2; + + public static final int WRITE = 3; + public static final int READ = 4; + + public static int UNKNOWN_ERR = 1000; + + private boolean configured = false; + + private PendingRequests pendingRequests; + + + + /* + * We need both read and write when accessing the storage, I think. + */ + + private String [] permissions = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE }; + + // This field exists only to support getEntry, below, which has been deprecated + private static FileUtils filePlugin; + + private interface FileOp { + void run(JSONArray args) throws Exception; + } + + private ArrayList<Filesystem> filesystems; + + public void registerFilesystem(Filesystem fs) { + if (fs != null && filesystemForName(fs.name)== null) { + this.filesystems.add(fs); + } + } + + private Filesystem filesystemForName(String name) { + for (Filesystem fs:filesystems) { + if (fs != null && fs.name != null && fs.name.equals(name)) { + return fs; + } + } + return null; + } + + protected String[] getExtraFileSystemsPreference(Activity activity) { + String fileSystemsStr = preferences.getString("androidextrafilesystems", "files,files-external,documents,sdcard,cache,cache-external,assets,root"); + return fileSystemsStr.split(","); + } + + protected void registerExtraFileSystems(String[] filesystems, HashMap<String, String> availableFileSystems) { + HashSet<String> installedFileSystems = new HashSet<String>(); + + /* Register filesystems in order */ + for (String fsName : filesystems) { + if (!installedFileSystems.contains(fsName)) { + String fsRoot = availableFileSystems.get(fsName); + if (fsRoot != null) { + File newRoot = new File(fsRoot); + if (newRoot.mkdirs() || newRoot.isDirectory()) { + registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot)); + installedFileSystems.add(fsName); + } else { + LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping"); + } + } else { + LOG.d(LOG_TAG, "Unrecognized extra filesystem identifier: " + fsName); + } + } + } + } + + protected HashMap<String, String> getAvailableFileSystems(Activity activity) { + Context context = activity.getApplicationContext(); + HashMap<String, String> availableFileSystems = new HashMap<String,String>(); + + availableFileSystems.put("files", context.getFilesDir().getAbsolutePath()); + availableFileSystems.put("documents", new File(context.getFilesDir(), "Documents").getAbsolutePath()); + availableFileSystems.put("cache", context.getCacheDir().getAbsolutePath()); + availableFileSystems.put("root", "/"); + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + try { + availableFileSystems.put("files-external", context.getExternalFilesDir(null).getAbsolutePath()); + availableFileSystems.put("sdcard", Environment.getExternalStorageDirectory().getAbsolutePath()); + availableFileSystems.put("cache-external", context.getExternalCacheDir().getAbsolutePath()); + } + catch(NullPointerException e) { + LOG.d(LOG_TAG, "External storage unavailable, check to see if USB Mass Storage Mode is on"); + } + } + + return availableFileSystems; + } + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + this.filesystems = new ArrayList<Filesystem>(); + this.pendingRequests = new PendingRequests(); + + String tempRoot = null; + String persistentRoot = null; + + Activity activity = cordova.getActivity(); + String packageName = activity.getPackageName(); + + String location = preferences.getString("androidpersistentfilelocation", "internal"); + + tempRoot = activity.getCacheDir().getAbsolutePath(); + if ("internal".equalsIgnoreCase(location)) { + persistentRoot = activity.getFilesDir().getAbsolutePath() + "/files/"; + this.configured = true; + } else if ("compatibility".equalsIgnoreCase(location)) { + /* + * Fall-back to compatibility mode -- this is the logic implemented in + * earlier versions of this plugin, and should be maintained here so + * that apps which were originally deployed with older versions of the + * plugin can continue to provide access to files stored under those + * versions. + */ + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + persistentRoot = Environment.getExternalStorageDirectory().getAbsolutePath(); + tempRoot = Environment.getExternalStorageDirectory().getAbsolutePath() + + "/Android/data/" + packageName + "/cache/"; + } else { + persistentRoot = "/data/data/" + packageName; + } + this.configured = true; + } + + if (this.configured) { + // Create the directories if they don't exist. + File tmpRootFile = new File(tempRoot); + File persistentRootFile = new File(persistentRoot); + tmpRootFile.mkdirs(); + persistentRootFile.mkdirs(); + + // Register initial filesystems + // Note: The temporary and persistent filesystems need to be the first two + // registered, so that they will match window.TEMPORARY and window.PERSISTENT, + // per spec. + this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile)); + this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile)); + this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi())); + this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi())); + + registerExtraFileSystems(getExtraFileSystemsPreference(activity), getAvailableFileSystems(activity)); + + // Initialize static plugin reference for deprecated getEntry method + if (filePlugin == null) { + FileUtils.filePlugin = this; + } + } else { + LOG.e(LOG_TAG, "File plugin configuration error: Please set AndroidPersistentFileLocation in config.xml to one of \"internal\" (for new applications) or \"compatibility\" (for compatibility with previous versions)"); + activity.finish(); + } + } + + public static FileUtils getFilePlugin() { + return filePlugin; + } + + private Filesystem filesystemForURL(LocalFilesystemURL localURL) { + if (localURL == null) return null; + return filesystemForName(localURL.fsName); + } + + @Override + public Uri remapUri(Uri uri) { + // Remap only cdvfile: URLs (not content:). + if (!LocalFilesystemURL.FILESYSTEM_PROTOCOL.equals(uri.getScheme())) { + return null; + } + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + return null; + } + String path = fs.filesystemPathForURL(inputURL); + if (path != null) { + return Uri.parse("file://" + fs.filesystemPathForURL(inputURL)); + } + return null; + } catch (IllegalArgumentException e) { + return null; + } + } + + public boolean execute(String action, final String rawArgs, final CallbackContext callbackContext) { + if (!configured) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "File plugin is not configured. Please see the README.md file for details on how to update config.xml")); + return true; + } + if (action.equals("testSaveLocationExists")) { + threadhelper(new FileOp() { + public void run(JSONArray args) { + boolean b = DirectoryManager.testSaveLocationExists(); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b)); + } + }, rawArgs, callbackContext); + } + else if (action.equals("getFreeDiskSpace")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) { + // The getFreeDiskSpace plugin API is not documented, but some apps call it anyway via exec(). + // For compatibility it always returns free space in the primary external storage, and + // does NOT fallback to internal store if external storage is unavailable. + long l = DirectoryManager.getFreeExternalStorageSpace(); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l)); + } + }, rawArgs, callbackContext); + } + else if (action.equals("testFileExists")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException { + String fname=args.getString(0); + boolean b = DirectoryManager.testFileExists(fname); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b)); + } + }, rawArgs, callbackContext); + } + else if (action.equals("testDirectoryExists")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException { + String fname=args.getString(0); + boolean b = DirectoryManager.testFileExists(fname); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b)); + } + }, rawArgs, callbackContext); + } + else if (action.equals("readAsText")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, MalformedURLException { + String encoding = args.getString(1); + int start = args.getInt(2); + int end = args.getInt(3); + String fname=args.getString(0); + readFileAs(fname, start, end, callbackContext, encoding, PluginResult.MESSAGE_TYPE_STRING); + } + }, rawArgs, callbackContext); + } + else if (action.equals("readAsDataURL")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, MalformedURLException { + int start = args.getInt(1); + int end = args.getInt(2); + String fname=args.getString(0); + readFileAs(fname, start, end, callbackContext, null, -1); + } + }, rawArgs, callbackContext); + } + else if (action.equals("readAsArrayBuffer")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, MalformedURLException { + int start = args.getInt(1); + int end = args.getInt(2); + String fname=args.getString(0); + readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_ARRAYBUFFER); + } + }, rawArgs, callbackContext); + } + else if (action.equals("readAsBinaryString")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, MalformedURLException { + int start = args.getInt(1); + int end = args.getInt(2); + String fname=args.getString(0); + readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_BINARYSTRING); + } + }, rawArgs, callbackContext); + } + else if (action.equals("write")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException { + String fname=args.getString(0); + String nativeURL = resolveLocalFileSystemURI(fname).getString("nativeURL"); + String data=args.getString(1); + int offset=args.getInt(2); + Boolean isBinary=args.getBoolean(3); + + if(needPermission(nativeURL, WRITE)) { + getWritePermission(rawArgs, ACTION_WRITE, callbackContext); + } + else { + long fileSize = write(fname, data, offset, isBinary); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize)); + } + + } + }, rawArgs, callbackContext); + } + else if (action.equals("truncate")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException { + String fname=args.getString(0); + int offset=args.getInt(1); + long fileSize = truncateFile(fname, offset); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize)); + } + }, rawArgs, callbackContext); + } + else if (action.equals("requestAllFileSystems")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws IOException, JSONException { + callbackContext.success(requestAllFileSystems()); + } + }, rawArgs, callbackContext); + } else if (action.equals("requestAllPaths")) { + cordova.getThreadPool().execute( + new Runnable() { + public void run() { + try { + callbackContext.success(requestAllPaths()); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + ); + } else if (action.equals("requestFileSystem")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException { + int fstype = args.getInt(0); + long requiredSize = args.optLong(1); + requestFileSystem(fstype, requiredSize, callbackContext); + } + }, rawArgs, callbackContext); + } + else if (action.equals("resolveLocalFileSystemURI")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws IOException, JSONException { + String fname=args.getString(0); + JSONObject obj = resolveLocalFileSystemURI(fname); + callbackContext.success(obj); + } + }, rawArgs, callbackContext); + } + else if (action.equals("getFileMetadata")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException { + String fname=args.getString(0); + JSONObject obj = getFileMetadata(fname); + callbackContext.success(obj); + } + }, rawArgs, callbackContext); + } + else if (action.equals("getParent")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, IOException { + String fname=args.getString(0); + JSONObject obj = getParent(fname); + callbackContext.success(obj); + } + }, rawArgs, callbackContext); + } + else if (action.equals("getDirectory")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + String dirname = args.getString(0); + String path = args.getString(1); + String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL"); + boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false); + + if(containsCreate && needPermission(nativeURL, WRITE)) { + getWritePermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext); + } + else if(!containsCreate && needPermission(nativeURL, READ)) { + getReadPermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext); + } + else { + JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true); + callbackContext.success(obj); + } + } + }, rawArgs, callbackContext); + } + else if (action.equals("getFile")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + String dirname = args.getString(0); + String path = args.getString(1); + String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL"); + boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false); + + if(containsCreate && needPermission(nativeURL, WRITE)) { + getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext); + } + else if(!containsCreate && needPermission(nativeURL, READ)) { + getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext); + } + else { + JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false); + callbackContext.success(obj); + } + } + }, rawArgs, callbackContext); + } + else if (action.equals("remove")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, NoModificationAllowedException, InvalidModificationException, MalformedURLException { + String fname=args.getString(0); + boolean success = remove(fname); + if (success) { + callbackContext.success(); + } else { + callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR); + } + } + }, rawArgs, callbackContext); + } + else if (action.equals("removeRecursively")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, FileExistsException, MalformedURLException, NoModificationAllowedException { + String fname=args.getString(0); + boolean success = removeRecursively(fname); + if (success) { + callbackContext.success(); + } else { + callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR); + } + } + }, rawArgs, callbackContext); + } + else if (action.equals("moveTo")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException { + String fname=args.getString(0); + String newParent=args.getString(1); + String newName=args.getString(2); + JSONObject entry = transferTo(fname, newParent, newName, true); + callbackContext.success(entry); + } + }, rawArgs, callbackContext); + } + else if (action.equals("copyTo")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException { + String fname=args.getString(0); + String newParent=args.getString(1); + String newName=args.getString(2); + JSONObject entry = transferTo(fname, newParent, newName, false); + callbackContext.success(entry); + } + }, rawArgs, callbackContext); + } + else if (action.equals("readEntries")) { + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException { + String fname=args.getString(0); + JSONArray entries = readEntries(fname); + callbackContext.success(entries); + } + }, rawArgs, callbackContext); + } + else if (action.equals("_getLocalFilesystemPath")) { + // Internal method for testing: Get the on-disk location of a local filesystem url. + // [Currently used for testing file-transfer] + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException { + String localURLstr = args.getString(0); + String fname = filesystemPathForURL(localURLstr); + callbackContext.success(fname); + } + }, rawArgs, callbackContext); + } + else { + return false; + } + return true; + } + + private void getReadPermission(String rawArgs, int action, CallbackContext callbackContext) { + int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext); + PermissionHelper.requestPermission(this, requestCode, Manifest.permission.READ_EXTERNAL_STORAGE); + } + + private void getWritePermission(String rawArgs, int action, CallbackContext callbackContext) { + int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext); + PermissionHelper.requestPermission(this, requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + private boolean hasReadPermission() { + return PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); + } + + private boolean hasWritePermission() { + return PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + private boolean needPermission(String nativeURL, int permissionType) throws JSONException { + JSONObject j = requestAllPaths(); + ArrayList<String> allowedStorageDirectories = new ArrayList<String>(); + allowedStorageDirectories.add(j.getString("applicationDirectory")); + allowedStorageDirectories.add(j.getString("applicationStorageDirectory")); + if(j.has("externalApplicationStorageDirectory")) { + allowedStorageDirectories.add(j.getString("externalApplicationStorageDirectory")); + } + + if(permissionType == READ && hasReadPermission()) { + return false; + } + else if(permissionType == WRITE && hasWritePermission()) { + return false; + } + + // Permission required if the native url lies outside the allowed storage directories + for(String directory : allowedStorageDirectories) { + if(nativeURL.startsWith(directory)) { + return false; + } + } + return true; + } + + + public LocalFilesystemURL resolveNativeUri(Uri nativeUri) { + LocalFilesystemURL localURL = null; + + // Try all installed filesystems. Return the best matching URL + // (determined by the shortest resulting URL) + for (Filesystem fs : filesystems) { + LocalFilesystemURL url = fs.toLocalUri(nativeUri); + if (url != null) { + // A shorter fullPath implies that the filesystem is a better + // match for the local path than the previous best. + if (localURL == null || (url.uri.toString().length() < localURL.toString().length())) { + localURL = url; + } + } + } + return localURL; + } + + /* + * These two native-only methods can be used by other plugins to translate between + * device file system paths and URLs. By design, there is no direct JavaScript + * interface to these methods. + */ + + public String filesystemPathForURL(String localURLstr) throws MalformedURLException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(localURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.filesystemPathForURL(inputURL); + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + public LocalFilesystemURL filesystemURLforLocalPath(String localPath) { + LocalFilesystemURL localURL = null; + int shortestFullPath = 0; + + // Try all installed filesystems. Return the best matching URL + // (determined by the shortest resulting URL) + for (Filesystem fs: filesystems) { + LocalFilesystemURL url = fs.URLforFilesystemPath(localPath); + if (url != null) { + // A shorter fullPath implies that the filesystem is a better + // match for the local path than the previous best. + if (localURL == null || (url.path.length() < shortestFullPath)) { + localURL = url; + shortestFullPath = url.path.length(); + } + } + } + return localURL; + } + + + /* helper to execute functions async and handle the result codes + * + */ + private void threadhelper(final FileOp f, final String rawArgs, final CallbackContext callbackContext){ + cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + JSONArray args = new JSONArray(rawArgs); + f.run(args); + } catch ( Exception e) { + if( e instanceof EncodingException){ + callbackContext.error(FileUtils.ENCODING_ERR); + } else if(e instanceof FileNotFoundException) { + callbackContext.error(FileUtils.NOT_FOUND_ERR); + } else if(e instanceof FileExistsException) { + callbackContext.error(FileUtils.PATH_EXISTS_ERR); + } else if(e instanceof NoModificationAllowedException ) { + callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR); + } else if(e instanceof InvalidModificationException ) { + callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR); + } else if(e instanceof MalformedURLException ) { + callbackContext.error(FileUtils.ENCODING_ERR); + } else if(e instanceof IOException ) { + callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR); + } else if(e instanceof EncodingException ) { + callbackContext.error(FileUtils.ENCODING_ERR); + } else if(e instanceof TypeMismatchException ) { + callbackContext.error(FileUtils.TYPE_MISMATCH_ERR); + } else if(e instanceof JSONException ) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); + } else if (e instanceof SecurityException) { + callbackContext.error(FileUtils.SECURITY_ERR); + } else { + e.printStackTrace(); + callbackContext.error(FileUtils.UNKNOWN_ERR); + } + } + } + }); + } + + /** + * Allows the user to look up the Entry for a file or directory referred to by a local URI. + * + * @param uriString of the file/directory to look up + * @return a JSONObject representing a Entry from the filesystem + * @throws MalformedURLException if the url is not valid + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the user can't read the file + * @throws JSONException + */ + private JSONObject resolveLocalFileSystemURI(String uriString) throws IOException, JSONException { + if (uriString == null) { + throw new MalformedURLException("Unrecognized filesystem URL"); + } + Uri uri = Uri.parse(uriString); + boolean isNativeUri = false; + + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri); + if (inputURL == null) { + /* Check for file://, content:// urls */ + inputURL = resolveNativeUri(uri); + isNativeUri = true; + } + + try { + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + if (fs.exists(inputURL)) { + if (!isNativeUri) { + // If not already resolved as native URI, resolve to a native URI and back to + // fix the terminating slash based on whether the entry is a directory or file. + inputURL = fs.toLocalUri(fs.toNativeUri(inputURL)); + } + + return fs.getEntryForLocalURL(inputURL); + } + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + throw new FileNotFoundException(); + } + + /** + * Read the list of files from this directory. + * + * @return a JSONArray containing JSONObjects that represent Entry objects. + * @throws FileNotFoundException if the directory is not found. + * @throws JSONException + * @throws MalformedURLException + */ + private JSONArray readEntries(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.readEntriesAtLocalURL(inputURL); + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + /** + * A setup method that handles the move/copy of files/directories + * + * @param newName for the file directory to be called, if null use existing file name + * @param move if false do a copy, if true do a move + * @return a Entry object + * @throws NoModificationAllowedException + * @throws IOException + * @throws InvalidModificationException + * @throws EncodingException + * @throws JSONException + * @throws FileExistsException + */ + private JSONObject transferTo(String srcURLstr, String destURLstr, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException { + if (srcURLstr == null || destURLstr == null) { + // either no source or no destination provided + throw new FileNotFoundException(); + } + + LocalFilesystemURL srcURL = LocalFilesystemURL.parse(srcURLstr); + LocalFilesystemURL destURL = LocalFilesystemURL.parse(destURLstr); + + Filesystem srcFs = this.filesystemForURL(srcURL); + Filesystem destFs = this.filesystemForURL(destURL); + + // Check for invalid file name + if (newName != null && newName.contains(":")) { + throw new EncodingException("Bad file name"); + } + + return destFs.copyFileToURL(destURL, newName, srcFs, srcURL, move); + } + + /** + * Deletes a directory and all of its contents, if any. In the event of an error + * [e.g. trying to delete a directory that contains a file that cannot be removed], + * some of the contents of the directory may be deleted. + * It is an error to attempt to delete the root directory of a filesystem. + * + * @return a boolean representing success of failure + * @throws FileExistsException + * @throws NoModificationAllowedException + * @throws MalformedURLException + */ + private boolean removeRecursively(String baseURLstr) throws FileExistsException, NoModificationAllowedException, MalformedURLException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr); + // You can't delete the root directory. + if ("".equals(inputURL.path) || "/".equals(inputURL.path)) { + throw new NoModificationAllowedException("You can't delete the root directory"); + } + + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.recursiveRemoveFileAtLocalURL(inputURL); + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + + /** + * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty. + * It is an error to attempt to delete the root directory of a filesystem. + * + * @return a boolean representing success of failure + * @throws NoModificationAllowedException + * @throws InvalidModificationException + * @throws MalformedURLException + */ + private boolean remove(String baseURLstr) throws NoModificationAllowedException, InvalidModificationException, MalformedURLException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr); + // You can't delete the root directory. + if ("".equals(inputURL.path) || "/".equals(inputURL.path)) { + + throw new NoModificationAllowedException("You can't delete the root directory"); + } + + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.removeFileAtLocalURL(inputURL); + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + /** + * Creates or looks up a file. + * + * @param baseURLstr base directory + * @param path file/directory to lookup or create + * @param options specify whether to create or not + * @param directory if true look up directory, if false look up file + * @return a Entry object + * @throws FileExistsException + * @throws IOException + * @throws TypeMismatchException + * @throws EncodingException + * @throws JSONException + */ + private JSONObject getFile(String baseURLstr, String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.getFileForLocalURL(inputURL, path, options, directory); + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + + } + + /** + * Look up the parent DirectoryEntry containing this Entry. + * If this Entry is the root of its filesystem, its parent is itself. + */ + private JSONObject getParent(String baseURLstr) throws JSONException, IOException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.getParentForLocalURL(inputURL); + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + /** + * Returns a File that represents the current state of the file that this FileEntry represents. + * + * @return returns a JSONObject represent a W3C File object + */ + private JSONObject getFileMetadata(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + return fs.getFileMetadataForLocalURL(inputURL); + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + /** + * Requests a filesystem in which to store application data. + * + * @param type of file system requested + * @param requiredSize required free space in the file system in bytes + * @param callbackContext context for returning the result or error + * @throws JSONException + */ + private void requestFileSystem(int type, long requiredSize, final CallbackContext callbackContext) throws JSONException { + Filesystem rootFs = null; + try { + rootFs = this.filesystems.get(type); + } catch (ArrayIndexOutOfBoundsException e) { + // Pass null through + } + if (rootFs == null) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR)); + } else { + // If a nonzero required size was specified, check that the retrieved filesystem has enough free space. + long availableSize = 0; + if (requiredSize > 0) { + availableSize = rootFs.getFreeSpaceInBytes(); + } + + if (availableSize < requiredSize) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR)); + } else { + JSONObject fs = new JSONObject(); + fs.put("name", rootFs.name); + fs.put("root", rootFs.getRootEntry()); + callbackContext.success(fs); + } + } + } + + /** + * Requests a filesystem in which to store application data. + * + * @return a JSONObject representing the file system + */ + private JSONArray requestAllFileSystems() throws IOException, JSONException { + JSONArray ret = new JSONArray(); + for (Filesystem fs : filesystems) { + ret.put(fs.getRootEntry()); + } + return ret; + } + + private static String toDirUrl(File f) { + return Uri.fromFile(f).toString() + '/'; + } + + private JSONObject requestAllPaths() throws JSONException { + Context context = cordova.getActivity(); + JSONObject ret = new JSONObject(); + ret.put("applicationDirectory", "file:///android_asset/"); + ret.put("applicationStorageDirectory", toDirUrl(context.getFilesDir().getParentFile())); + ret.put("dataDirectory", toDirUrl(context.getFilesDir())); + ret.put("cacheDirectory", toDirUrl(context.getCacheDir())); + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + try { + ret.put("externalApplicationStorageDirectory", toDirUrl(context.getExternalFilesDir(null).getParentFile())); + ret.put("externalDataDirectory", toDirUrl(context.getExternalFilesDir(null))); + ret.put("externalCacheDirectory", toDirUrl(context.getExternalCacheDir())); + ret.put("externalRootDirectory", toDirUrl(Environment.getExternalStorageDirectory())); + } + catch(NullPointerException e) { + /* If external storage is unavailable, context.getExternal* returns null */ + LOG.d(LOG_TAG, "Unable to access these paths, most liklely due to USB storage"); + } + } + return ret; + } + + /** + * Returns a JSON object representing the given File. Internal APIs should be modified + * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin. + * + * @param file the File to convert + * @return a JSON representation of the given File + * @throws JSONException + */ + public JSONObject getEntryForFile(File file) throws JSONException { + JSONObject entry; + + for (Filesystem fs : filesystems) { + entry = fs.makeEntryForFile(file); + if (entry != null) { + return entry; + } + } + return null; + } + + /** + * Returns a JSON object representing the given File. Deprecated, as this is only used by + * FileTransfer, and because it is a static method that should really be an instance method, + * since it depends on the actual filesystem roots in use. Internal APIs should be modified + * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin. + * + * @param file the File to convert + * @return a JSON representation of the given File + * @throws JSONException + */ + @Deprecated + public static JSONObject getEntry(File file) throws JSONException { + if (getFilePlugin() != null) { + return getFilePlugin().getEntryForFile(file); + } + return null; + } + + /** + * Read the contents of a file. + * This is done in a background thread; the result is sent to the callback. + * + * @param start Start position in the file. + * @param end End position to stop at (exclusive). + * @param callbackContext The context through which to send the result. + * @param encoding The encoding to return contents as. Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets) + * @param resultType The desired type of data to send to the callback. + * @return Contents of file. + */ + public void readFileAs(final String srcURLstr, final int start, final int end, final CallbackContext callbackContext, final String encoding, final int resultType) throws MalformedURLException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + + fs.readFileAtURL(inputURL, start, end, new Filesystem.ReadFileCallback() { + public void handleData(InputStream inputStream, String contentType) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + final int BUFFER_SIZE = 8192; + byte[] buffer = new byte[BUFFER_SIZE]; + + for (;;) { + int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE); + + if (bytesRead <= 0) { + break; + } + os.write(buffer, 0, bytesRead); + } + + PluginResult result; + switch (resultType) { + case PluginResult.MESSAGE_TYPE_STRING: + result = new PluginResult(PluginResult.Status.OK, os.toString(encoding)); + break; + case PluginResult.MESSAGE_TYPE_ARRAYBUFFER: + result = new PluginResult(PluginResult.Status.OK, os.toByteArray()); + break; + case PluginResult.MESSAGE_TYPE_BINARYSTRING: + result = new PluginResult(PluginResult.Status.OK, os.toByteArray(), true); + break; + default: // Base64. + byte[] base64 = Base64.encode(os.toByteArray(), Base64.NO_WRAP); + String s = "data:" + contentType + ";base64," + new String(base64, "US-ASCII"); + result = new PluginResult(PluginResult.Status.OK, s); + } + + callbackContext.sendPluginResult(result); + } catch (IOException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR)); + } + } + }); + + + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } catch (FileNotFoundException e) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR)); + } catch (IOException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR)); + } + } + + + /** + * Write contents of file. + * + * @param data The contents of the file. + * @param offset The position to begin writing the file. + * @param isBinary True if the file contents are base64-encoded binary data + */ + /**/ + public long write(String srcURLstr, String data, int offset, boolean isBinary) throws FileNotFoundException, IOException, NoModificationAllowedException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + + long x = fs.writeToFileAtURL(inputURL, data, offset, isBinary); LOG.d("TEST",srcURLstr + ": "+x); return x; + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + + } + + /** + * Truncate the file to size + */ + private long truncateFile(String srcURLstr, long size) throws FileNotFoundException, IOException, NoModificationAllowedException { + try { + LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr); + Filesystem fs = this.filesystemForURL(inputURL); + if (fs == null) { + throw new MalformedURLException("No installed handlers for this URL"); + } + + return fs.truncateFileAtURL(inputURL, size); + } catch (IllegalArgumentException e) { + MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL"); + mue.initCause(e); + throw mue; + } + } + + + /* + * Handle the response + */ + + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException { + + final PendingRequests.Request req = pendingRequests.getAndRemove(requestCode); + if (req != null) { + for(int r:grantResults) + { + if(r == PackageManager.PERMISSION_DENIED) + { + req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.ERROR, SECURITY_ERR)); + return; + } + } + switch(req.getAction()) + { + case ACTION_GET_FILE: + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + String dirname = args.getString(0); + + String path = args.getString(1); + JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false); + req.getCallbackContext().success(obj); + } + }, req.getRawArgs(), req.getCallbackContext()); + break; + case ACTION_GET_DIRECTORY: + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + String dirname = args.getString(0); + + String path = args.getString(1); + JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true); + req.getCallbackContext().success(obj); + } + }, req.getRawArgs(), req.getCallbackContext()); + break; + case ACTION_WRITE: + threadhelper( new FileOp( ){ + public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException { + String fname=args.getString(0); + String data=args.getString(1); + int offset=args.getInt(2); + Boolean isBinary=args.getBoolean(3); + long fileSize = write(fname, data, offset, isBinary); + req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize)); + } + }, req.getRawArgs(), req.getCallbackContext()); + break; + } + } else { + LOG.d(LOG_TAG, "Received permission callback for unknown request code"); + } + } +} diff --git a/src/org/apache/cordova/file/Filesystem.java b/src/org/apache/cordova/file/Filesystem.java new file mode 100644 index 0000000000000000000000000000000000000000..c69d3bdd07a7d3b1a06e67f99f35c6cca883d786 --- /dev/null +++ b/src/org/apache/cordova/file/Filesystem.java @@ -0,0 +1,331 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import android.net.Uri; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.cordova.CordovaResourceApi; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class Filesystem { + + protected final Uri rootUri; + protected final CordovaResourceApi resourceApi; + public final String name; + private JSONObject rootEntry; + + public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) { + this.rootUri = rootUri; + this.name = name; + this.resourceApi = resourceApi; + } + + public interface ReadFileCallback { + public void handleData(InputStream inputStream, String contentType) throws IOException; + } + + public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri nativeURL) { + try { + String path = inputURL.path; + int end = path.endsWith("/") ? 1 : 0; + String[] parts = path.substring(0, path.length() - end).split("/+"); + String fileName = parts[parts.length - 1]; + + JSONObject entry = new JSONObject(); + entry.put("isFile", !inputURL.isDirectory); + entry.put("isDirectory", inputURL.isDirectory); + entry.put("name", fileName); + entry.put("fullPath", path); + // The file system can't be specified, as it would lead to an infinite loop, + // but the filesystem name can be. + entry.put("filesystemName", inputURL.fsName); + // Backwards compatibility + entry.put("filesystem", "temporary".equals(inputURL.fsName) ? 0 : 1); + + String nativeUrlStr = nativeURL.toString(); + if (inputURL.isDirectory && !nativeUrlStr.endsWith("/")) { + nativeUrlStr += "/"; + } + entry.put("nativeURL", nativeUrlStr); + return entry; + } catch (JSONException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public JSONObject makeEntryForURL(LocalFilesystemURL inputURL) { + Uri nativeUri = toNativeUri(inputURL); + return nativeUri == null ? null : makeEntryForURL(inputURL, nativeUri); + } + + public JSONObject makeEntryForNativeUri(Uri nativeUri) { + LocalFilesystemURL inputUrl = toLocalUri(nativeUri); + return inputUrl == null ? null : makeEntryForURL(inputUrl, nativeUri); + } + + public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException { + return makeEntryForURL(inputURL); + } + + public JSONObject makeEntryForFile(File file) { + return makeEntryForNativeUri(Uri.fromFile(file)); + } + + abstract JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String path, + JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException; + + abstract boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException; + + abstract boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException, NoModificationAllowedException; + + abstract LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException; + + public final JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { + LocalFilesystemURL[] children = listChildren(inputURL); + JSONArray entries = new JSONArray(); + if (children != null) { + for (LocalFilesystemURL url : children) { + entries.put(makeEntryForURL(url)); + } + } + return entries; + } + + abstract JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException; + + public Uri getRootUri() { + return rootUri; + } + + public boolean exists(LocalFilesystemURL inputURL) { + try { + getFileMetadataForLocalURL(inputURL); + } catch (FileNotFoundException e) { + return false; + } + return true; + } + + public Uri nativeUriForFullPath(String fullPath) { + Uri ret = null; + if (fullPath != null) { + String encodedPath = Uri.fromFile(new File(fullPath)).getEncodedPath(); + if (encodedPath.startsWith("/")) { + encodedPath = encodedPath.substring(1); + } + ret = rootUri.buildUpon().appendEncodedPath(encodedPath).build(); + } + return ret; + } + + public LocalFilesystemURL localUrlforFullPath(String fullPath) { + Uri nativeUri = nativeUriForFullPath(fullPath); + if (nativeUri != null) { + return toLocalUri(nativeUri); + } + return null; + } + + /** + * Removes multiple repeated //s, and collapses processes ../s. + */ + protected static String normalizePath(String rawPath) { + // If this is an absolute path, trim the leading "/" and replace it later + boolean isAbsolutePath = rawPath.startsWith("/"); + if (isAbsolutePath) { + rawPath = rawPath.replaceFirst("/+", ""); + } + ArrayList<String> components = new ArrayList<String>(Arrays.asList(rawPath.split("/+"))); + for (int index = 0; index < components.size(); ++index) { + if (components.get(index).equals("..")) { + components.remove(index); + if (index > 0) { + components.remove(index-1); + --index; + } + } + } + StringBuilder normalizedPath = new StringBuilder(); + for(String component: components) { + normalizedPath.append("/"); + normalizedPath.append(component); + } + if (isAbsolutePath) { + return normalizedPath.toString(); + } else { + return normalizedPath.toString().substring(1); + } + } + + /** + * Gets the free space in bytes available on this filesystem. + * Subclasses may override this method to return nonzero free space. + */ + public long getFreeSpaceInBytes() { + return 0; + } + + public abstract Uri toNativeUri(LocalFilesystemURL inputURL); + public abstract LocalFilesystemURL toLocalUri(Uri inputURL); + + public JSONObject getRootEntry() { + if (rootEntry == null) { + rootEntry = makeEntryForNativeUri(rootUri); + } + return rootEntry; + } + + public JSONObject getParentForLocalURL(LocalFilesystemURL inputURL) throws IOException { + Uri parentUri = inputURL.uri; + String parentPath = new File(inputURL.uri.getPath()).getParent(); + if (!"/".equals(parentPath)) { + parentUri = inputURL.uri.buildUpon().path(parentPath + '/').build(); + } + return getEntryForLocalURL(LocalFilesystemURL.parse(parentUri)); + } + + protected LocalFilesystemURL makeDestinationURL(String newName, LocalFilesystemURL srcURL, LocalFilesystemURL destURL, boolean isDirectory) { + // I know this looks weird but it is to work around a JSON bug. + if ("null".equals(newName) || "".equals(newName)) { + newName = srcURL.uri.getLastPathSegment();; + } + + String newDest = destURL.uri.toString(); + if (newDest.endsWith("/")) { + newDest = newDest + newName; + } else { + newDest = newDest + "/" + newName; + } + if (isDirectory) { + newDest += '/'; + } + return LocalFilesystemURL.parse(newDest); + } + + /* Read a source URL (possibly from a different filesystem, srcFs,) and copy it to + * the destination URL on this filesystem, optionally with a new filename. + * If move is true, then this method should either perform an atomic move operation + * or remove the source file when finished. + */ + public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName, + Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException { + // First, check to see that we can do it + if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) { + throw new NoModificationAllowedException("Cannot move file at source URL"); + } + final LocalFilesystemURL destination = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory); + + Uri srcNativeUri = srcFs.toNativeUri(srcURL); + + CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(srcNativeUri); + OutputStream os = null; + try { + os = getOutputStreamForURL(destination); + } catch (IOException e) { + ofrr.inputStream.close(); + throw e; + } + // Closes streams. + resourceApi.copyResource(ofrr, os); + + if (move) { + srcFs.removeFileAtLocalURL(srcURL); + } + return getEntryForLocalURL(destination); + } + + public OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws IOException { + return resourceApi.openOutputStream(toNativeUri(inputURL)); + } + + public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end, + ReadFileCallback readFileCallback) throws IOException { + CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(toNativeUri(inputURL)); + if (end < 0) { + end = ofrr.length; + } + long numBytesToRead = end - start; + try { + if (start > 0) { + ofrr.inputStream.skip(start); + } + InputStream inputStream = ofrr.inputStream; + if (end < ofrr.length) { + inputStream = new LimitedInputStream(inputStream, numBytesToRead); + } + readFileCallback.handleData(inputStream, ofrr.mimeType); + } finally { + ofrr.inputStream.close(); + } + } + + abstract long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset, + boolean isBinary) throws NoModificationAllowedException, IOException; + + abstract long truncateFileAtURL(LocalFilesystemURL inputURL, long size) + throws IOException, NoModificationAllowedException; + + // This method should return null if filesystem urls cannot be mapped to paths + abstract String filesystemPathForURL(LocalFilesystemURL url); + + abstract LocalFilesystemURL URLforFilesystemPath(String path); + + abstract boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL); + + protected class LimitedInputStream extends FilterInputStream { + long numBytesToRead; + public LimitedInputStream(InputStream in, long numBytesToRead) { + super(in); + this.numBytesToRead = numBytesToRead; + } + @Override + public int read() throws IOException { + if (numBytesToRead <= 0) { + return -1; + } + numBytesToRead--; + return in.read(); + } + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + if (numBytesToRead <= 0) { + return -1; + } + int bytesToRead = byteCount; + if (byteCount > numBytesToRead) { + bytesToRead = (int)numBytesToRead; // Cast okay; long is less than int here. + } + int numBytesRead = in.read(buffer, byteOffset, bytesToRead); + numBytesToRead -= numBytesRead; + return numBytesRead; + } + } +} diff --git a/src/org/apache/cordova/file/InvalidModificationException.java b/src/org/apache/cordova/file/InvalidModificationException.java new file mode 100644 index 0000000000000000000000000000000000000000..8f6bec59cf30ddba141147ca879050dce9437dda --- /dev/null +++ b/src/org/apache/cordova/file/InvalidModificationException.java @@ -0,0 +1,30 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + + +package org.apache.cordova.file; + +@SuppressWarnings("serial") +public class InvalidModificationException extends Exception { + + public InvalidModificationException(String message) { + super(message); + } + +} diff --git a/src/org/apache/cordova/file/LocalFilesystem.java b/src/org/apache/cordova/file/LocalFilesystem.java new file mode 100644 index 0000000000000000000000000000000000000000..051f99496276254eaa96a05be8dcad4aa7099c2e --- /dev/null +++ b/src/org/apache/cordova/file/LocalFilesystem.java @@ -0,0 +1,513 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import org.apache.cordova.CordovaResourceApi; +import org.json.JSONException; +import org.json.JSONObject; + +import android.os.Build; +import android.os.Environment; +import android.util.Base64; +import android.net.Uri; +import android.content.Context; +import android.content.Intent; + +import java.nio.charset.Charset; + +public class LocalFilesystem extends Filesystem { + private final Context context; + + public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot) { + super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi); + this.context = context; + } + + public String filesystemPathForFullPath(String fullPath) { + return new File(rootUri.getPath(), fullPath).toString(); + } + + @Override + public String filesystemPathForURL(LocalFilesystemURL url) { + return filesystemPathForFullPath(url.path); + } + + private String fullPathForFilesystemPath(String absolutePath) { + if (absolutePath != null && absolutePath.startsWith(rootUri.getPath())) { + return absolutePath.substring(rootUri.getPath().length() - 1); + } + return null; + } + + @Override + public Uri toNativeUri(LocalFilesystemURL inputURL) { + return nativeUriForFullPath(inputURL.path); + } + + @Override + public LocalFilesystemURL toLocalUri(Uri inputURL) { + if (!"file".equals(inputURL.getScheme())) { + return null; + } + File f = new File(inputURL.getPath()); + // Removes and duplicate /s (e.g. file:///a//b/c) + Uri resolvedUri = Uri.fromFile(f); + String rootUriNoTrailingSlash = rootUri.getEncodedPath(); + rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1); + if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) { + return null; + } + String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length()); + // Strip leading slash + if (!subPath.isEmpty()) { + subPath = subPath.substring(1); + } + Uri.Builder b = new Uri.Builder() + .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL) + .authority("localhost") + .path(name); + if (!subPath.isEmpty()) { + b.appendEncodedPath(subPath); + } + if (f.isDirectory()) { + // Add trailing / for directories. + b.appendEncodedPath(""); + } + return LocalFilesystemURL.parse(b.build()); + } + + @Override + public LocalFilesystemURL URLforFilesystemPath(String path) { + return localUrlforFullPath(fullPathForFilesystemPath(path)); + } + + @Override + public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, + String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + boolean create = false; + boolean exclusive = false; + + if (options != null) { + create = options.optBoolean("create"); + if (create) { + exclusive = options.optBoolean("exclusive"); + } + } + + // Check for a ":" character in the file to line up with BB and iOS + if (path.contains(":")) { + throw new EncodingException("This path has an invalid \":\" in it."); + } + + LocalFilesystemURL requestedURL; + + // Check whether the supplied path is absolute or relative + if (directory && !path.endsWith("/")) { + path += "/"; + } + if (path.startsWith("/")) { + requestedURL = localUrlforFullPath(normalizePath(path)); + } else { + requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path)); + } + + File fp = new File(this.filesystemPathForURL(requestedURL)); + + if (create) { + if (exclusive && fp.exists()) { + throw new FileExistsException("create/exclusive fails"); + } + if (directory) { + fp.mkdir(); + } else { + fp.createNewFile(); + } + if (!fp.exists()) { + throw new FileExistsException("create fails"); + } + } + else { + if (!fp.exists()) { + throw new FileNotFoundException("path does not exist"); + } + if (directory) { + if (fp.isFile()) { + throw new TypeMismatchException("path doesn't exist or is file"); + } + } else { + if (fp.isDirectory()) { + throw new TypeMismatchException("path doesn't exist or is directory"); + } + } + } + + // Return the directory + return makeEntryForURL(requestedURL); + } + + @Override + public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException { + + File fp = new File(filesystemPathForURL(inputURL)); + + // You can't delete a directory that is not empty + if (fp.isDirectory() && fp.list().length > 0) { + throw new InvalidModificationException("You can't delete a directory that is not empty."); + } + + return fp.delete(); + } + + @Override + public boolean exists(LocalFilesystemURL inputURL) { + File fp = new File(filesystemPathForURL(inputURL)); + return fp.exists(); + } + + @Override + public long getFreeSpaceInBytes() { + return DirectoryManager.getFreeSpaceInBytes(rootUri.getPath()); + } + + @Override + public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException { + File directory = new File(filesystemPathForURL(inputURL)); + return removeDirRecursively(directory); + } + + protected boolean removeDirRecursively(File directory) throws FileExistsException { + if (directory.isDirectory()) { + for (File file : directory.listFiles()) { + removeDirRecursively(file); + } + } + + if (!directory.delete()) { + throw new FileExistsException("could not delete: " + directory.getName()); + } else { + return true; + } + } + + @Override + public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException { + File fp = new File(filesystemPathForURL(inputURL)); + + if (!fp.exists()) { + // The directory we are listing doesn't exist so we should fail. + throw new FileNotFoundException(); + } + + File[] files = fp.listFiles(); + if (files == null) { + // inputURL is a directory + return null; + } + LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length]; + for (int i = 0; i < files.length; i++) { + entries[i] = URLforFilesystemPath(files[i].getPath()); + } + + return entries; + } + + @Override + public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { + File file = new File(filesystemPathForURL(inputURL)); + + if (!file.exists()) { + throw new FileNotFoundException("File at " + inputURL.uri + " does not exist."); + } + + JSONObject metadata = new JSONObject(); + try { + // Ensure that directories report a size of 0 + metadata.put("size", file.isDirectory() ? 0 : file.length()); + metadata.put("type", resourceApi.getMimeType(Uri.fromFile(file))); + metadata.put("name", file.getName()); + metadata.put("fullPath", inputURL.path); + metadata.put("lastModifiedDate", file.lastModified()); + } catch (JSONException e) { + return null; + } + return metadata; + } + + private void copyFile(Filesystem srcFs, LocalFilesystemURL srcURL, File destFile, boolean move) throws IOException, InvalidModificationException, NoModificationAllowedException { + if (move) { + String realSrcPath = srcFs.filesystemPathForURL(srcURL); + if (realSrcPath != null) { + File srcFile = new File(realSrcPath); + if (srcFile.renameTo(destFile)) { + return; + } + // Trying to rename the file failed. Possibly because we moved across file system on the device. + } + } + + CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(srcFs.toNativeUri(srcURL)); + copyResource(offr, new FileOutputStream(destFile)); + + if (move) { + srcFs.removeFileAtLocalURL(srcURL); + } + } + + private void copyDirectory(Filesystem srcFs, LocalFilesystemURL srcURL, File dstDir, boolean move) throws IOException, NoModificationAllowedException, InvalidModificationException, FileExistsException { + if (move) { + String realSrcPath = srcFs.filesystemPathForURL(srcURL); + if (realSrcPath != null) { + File srcDir = new File(realSrcPath); + // If the destination directory already exists and is empty then delete it. This is according to spec. + if (dstDir.exists()) { + if (dstDir.list().length > 0) { + throw new InvalidModificationException("directory is not empty"); + } + dstDir.delete(); + } + // Try to rename the directory + if (srcDir.renameTo(dstDir)) { + return; + } + // Trying to rename the file failed. Possibly because we moved across file system on the device. + } + } + + if (dstDir.exists()) { + if (dstDir.list().length > 0) { + throw new InvalidModificationException("directory is not empty"); + } + } else { + if (!dstDir.mkdir()) { + // If we can't create the directory then fail + throw new NoModificationAllowedException("Couldn't create the destination directory"); + } + } + + LocalFilesystemURL[] children = srcFs.listChildren(srcURL); + for (LocalFilesystemURL childLocalUrl : children) { + File target = new File(dstDir, new File(childLocalUrl.path).getName()); + if (childLocalUrl.isDirectory) { + copyDirectory(srcFs, childLocalUrl, target, false); + } else { + copyFile(srcFs, childLocalUrl, target, false); + } + } + + if (move) { + srcFs.recursiveRemoveFileAtLocalURL(srcURL); + } + } + + @Override + public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName, + Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException { + + // Check to see if the destination directory exists + String newParent = this.filesystemPathForURL(destURL); + File destinationDir = new File(newParent); + if (!destinationDir.exists()) { + // The destination does not exist so we should fail. + throw new FileNotFoundException("The source does not exist"); + } + + // Figure out where we should be copying to + final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory); + + Uri dstNativeUri = toNativeUri(destinationURL); + Uri srcNativeUri = srcFs.toNativeUri(srcURL); + // Check to see if source and destination are the same file + if (dstNativeUri.equals(srcNativeUri)) { + throw new InvalidModificationException("Can't copy onto itself"); + } + + if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) { + throw new InvalidModificationException("Source URL is read-only (cannot move)"); + } + + File destFile = new File(dstNativeUri.getPath()); + if (destFile.exists()) { + if (!srcURL.isDirectory && destFile.isDirectory()) { + throw new InvalidModificationException("Can't copy/move a file to an existing directory"); + } else if (srcURL.isDirectory && destFile.isFile()) { + throw new InvalidModificationException("Can't copy/move a directory to an existing file"); + } + } + + if (srcURL.isDirectory) { + // E.g. Copy /sdcard/myDir to /sdcard/myDir/backup + if (dstNativeUri.toString().startsWith(srcNativeUri.toString() + '/')) { + throw new InvalidModificationException("Can't copy directory into itself"); + } + copyDirectory(srcFs, srcURL, destFile, move); + } else { + copyFile(srcFs, srcURL, destFile, move); + } + return makeEntryForURL(destinationURL); + } + + @Override + public long writeToFileAtURL(LocalFilesystemURL inputURL, String data, + int offset, boolean isBinary) throws IOException, NoModificationAllowedException { + + boolean append = false; + if (offset > 0) { + this.truncateFileAtURL(inputURL, offset); + append = true; + } + + byte[] rawData; + if (isBinary) { + rawData = Base64.decode(data, Base64.DEFAULT); + } else { + rawData = data.getBytes(Charset.defaultCharset()); + } + ByteArrayInputStream in = new ByteArrayInputStream(rawData); + try + { + byte buff[] = new byte[rawData.length]; + String absolutePath = filesystemPathForURL(inputURL); + FileOutputStream out = new FileOutputStream(absolutePath, append); + try { + in.read(buff, 0, buff.length); + out.write(buff, 0, rawData.length); + out.flush(); + } finally { + // Always close the output + out.close(); + } + if (isPublicDirectory(absolutePath)) { + broadcastNewFile(Uri.fromFile(new File(absolutePath))); + } + } + catch (NullPointerException e) + { + // This is a bug in the Android implementation of the Java Stack + NoModificationAllowedException realException = new NoModificationAllowedException(inputURL.toString()); + realException.initCause(e); + throw realException; + } + + return rawData.length; + } + + private boolean isPublicDirectory(String absolutePath) { + // TODO: should expose a way to scan app's private files (maybe via a flag). + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Lollipop has a bug where SD cards are null. + for (File f : context.getExternalMediaDirs()) { + if(f != null && absolutePath.startsWith(f.getAbsolutePath())) { + return true; + } + } + } + + String extPath = Environment.getExternalStorageDirectory().getAbsolutePath(); + return absolutePath.startsWith(extPath); + } + + /** + * Send broadcast of new file so files appear over MTP + */ + private void broadcastNewFile(Uri nativeUri) { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, nativeUri); + context.sendBroadcast(intent); + } + + @Override + public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException { + File file = new File(filesystemPathForURL(inputURL)); + + if (!file.exists()) { + throw new FileNotFoundException("File at " + inputURL.uri + " does not exist."); + } + + RandomAccessFile raf = new RandomAccessFile(filesystemPathForURL(inputURL), "rw"); + try { + if (raf.length() >= size) { + FileChannel channel = raf.getChannel(); + channel.truncate(size); + return size; + } + + return raf.length(); + } finally { + raf.close(); + } + + + } + + @Override + public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) { + String path = filesystemPathForURL(inputURL); + File file = new File(path); + return file.exists(); + } + + // This is a copy & paste from CordovaResource API that is required since CordovaResourceApi + // has a bug pre-4.0.0. + // TODO: Once cordova-android@4.0.0 is released, delete this copy and make the plugin depend on + // 4.0.0 with an engine tag. + private static void copyResource(CordovaResourceApi.OpenForReadResult input, OutputStream outputStream) throws IOException { + try { + InputStream inputStream = input.inputStream; + if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) { + FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel(); + FileChannel outChannel = ((FileOutputStream)outputStream).getChannel(); + long offset = 0; + long length = input.length; + if (input.assetFd != null) { + offset = input.assetFd.getStartOffset(); + } + // transferFrom()'s 2nd arg is a relative position. Need to set the absolute + // position first. + inChannel.position(offset); + outChannel.transferFrom(inChannel, 0, length); + } else { + final int BUFFER_SIZE = 8192; + byte[] buffer = new byte[BUFFER_SIZE]; + + for (;;) { + int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE); + + if (bytesRead <= 0) { + break; + } + outputStream.write(buffer, 0, bytesRead); + } + } + } finally { + input.inputStream.close(); + if (outputStream != null) { + outputStream.close(); + } + } + } +} diff --git a/src/org/apache/cordova/file/LocalFilesystemURL.java b/src/org/apache/cordova/file/LocalFilesystemURL.java new file mode 100644 index 0000000000000000000000000000000000000000..b96b6ee49b8aadf3490736865be8545aa7623012 --- /dev/null +++ b/src/org/apache/cordova/file/LocalFilesystemURL.java @@ -0,0 +1,64 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import android.net.Uri; + +public class LocalFilesystemURL { + + public static final String FILESYSTEM_PROTOCOL = "cdvfile"; + + public final Uri uri; + public final String fsName; + public final String path; + public final boolean isDirectory; + + private LocalFilesystemURL(Uri uri, String fsName, String fsPath, boolean isDirectory) { + this.uri = uri; + this.fsName = fsName; + this.path = fsPath; + this.isDirectory = isDirectory; + } + + public static LocalFilesystemURL parse(Uri uri) { + if (!FILESYSTEM_PROTOCOL.equals(uri.getScheme())) { + return null; + } + String path = uri.getPath(); + if (path.length() < 1) { + return null; + } + int firstSlashIdx = path.indexOf('/', 1); + if (firstSlashIdx < 0) { + return null; + } + String fsName = path.substring(1, firstSlashIdx); + path = path.substring(firstSlashIdx); + boolean isDirectory = path.charAt(path.length() - 1) == '/'; + return new LocalFilesystemURL(uri, fsName, path, isDirectory); + } + + public static LocalFilesystemURL parse(String uri) { + return parse(Uri.parse(uri)); + } + + public String toString() { + return uri.toString(); + } +} diff --git a/src/org/apache/cordova/file/NoModificationAllowedException.java b/src/org/apache/cordova/file/NoModificationAllowedException.java new file mode 100644 index 0000000000000000000000000000000000000000..627eafb5626aa9f67bc0ad969cb7b4cae01bdd93 --- /dev/null +++ b/src/org/apache/cordova/file/NoModificationAllowedException.java @@ -0,0 +1,29 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +package org.apache.cordova.file; + +@SuppressWarnings("serial") +public class NoModificationAllowedException extends Exception { + + public NoModificationAllowedException(String message) { + super(message); + } + +} diff --git a/src/org/apache/cordova/file/PendingRequests.java b/src/org/apache/cordova/file/PendingRequests.java new file mode 100644 index 0000000000000000000000000000000000000000..4c75f4231866badf8c9b5c67455f5e9d9d6ef8e3 --- /dev/null +++ b/src/org/apache/cordova/file/PendingRequests.java @@ -0,0 +1,94 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.file; + +import android.util.SparseArray; + +import org.apache.cordova.CallbackContext; + +/** + * Holds pending runtime permission requests + */ +class PendingRequests { + private int currentReqId = 0; + private SparseArray<Request> requests = new SparseArray<Request>(); + + /** + * Creates a request and adds it to the array of pending requests. Each created request gets a + * unique result code for use with requestPermission() + * @param rawArgs The raw arguments passed to the plugin + * @param action The action this request corresponds to (get file, etc.) + * @param callbackContext The CallbackContext for this plugin call + * @return The request code that can be used to retrieve the Request object + */ + public synchronized int createRequest(String rawArgs, int action, CallbackContext callbackContext) { + Request req = new Request(rawArgs, action, callbackContext); + requests.put(req.requestCode, req); + return req.requestCode; + } + + /** + * Gets the request corresponding to this request code and removes it from the pending requests + * @param requestCode The request code for the desired request + * @return The request corresponding to the given request code or null if such a + * request is not found + */ + public synchronized Request getAndRemove(int requestCode) { + Request result = requests.get(requestCode); + requests.remove(requestCode); + return result; + } + + /** + * Holds the options and CallbackContext for a call made to the plugin. + */ + public class Request { + + // Unique int used to identify this request in any Android permission callback + private int requestCode; + + // Action to be performed after permission request result + private int action; + + // Raw arguments passed to plugin + private String rawArgs; + + // The callback context for this plugin request + private CallbackContext callbackContext; + + private Request(String rawArgs, int action, CallbackContext callbackContext) { + this.rawArgs = rawArgs; + this.action = action; + this.callbackContext = callbackContext; + this.requestCode = currentReqId ++; + } + + public int getAction() { + return this.action; + } + + public String getRawArgs() { + return rawArgs; + } + + public CallbackContext getCallbackContext() { + return callbackContext; + } + } +} diff --git a/src/org/apache/cordova/file/TypeMismatchException.java b/src/org/apache/cordova/file/TypeMismatchException.java new file mode 100644 index 0000000000000000000000000000000000000000..1315f9a9e5b8d209fed973fda0334fc49d8c4a72 --- /dev/null +++ b/src/org/apache/cordova/file/TypeMismatchException.java @@ -0,0 +1,30 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + + +package org.apache.cordova.file; + +@SuppressWarnings("serial") +public class TypeMismatchException extends Exception { + + public TypeMismatchException(String message) { + super(message); + } + +}