Merge with Peter branch_2
@@ -0,0 +1,27 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest package="capacitor.android.plugins"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:amazon="http://schemas.amazon.com/apk/res/android">
|
||||
<application >
|
||||
<activity android:name="com.soundcloud.android.crop.CropImageActivity"/>
|
||||
<provider android:name="de.sitewaerts.cordova.documentviewer.FileProvider" android:authorities="${applicationId}.DocumentViewerPlugin.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/documentviewer_file_paths"/>
|
||||
</provider>
|
||||
<provider android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider" android:authorities="${applicationId}.fileOpener2.provider" android:exported="false" android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/opener_paths"/>
|
||||
</provider>
|
||||
<activity android:name="com.sarriaroman.PhotoViewer.PhotoActivity" android:theme="@android:style/Theme.Holo.NoActionBar.Fullscreen"/>
|
||||
<activity android:name="de.niklasmerz.cordova.biometric.BiometricActivity" android:theme="@style/TransparentTheme" android:exported="true"/>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.akeo.cordova.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
// Cordova-required packages
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class MultipleDocumentsPicker extends CordovaPlugin {
|
||||
private CallbackContext callback;
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args,
|
||||
final CallbackContext callbackContext) {
|
||||
/* Verify that the user sent a 'pick' action */
|
||||
if (!action.equals("pick")) {
|
||||
callbackContext.error("\"" + action + "\" is not a recognized action.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer type;
|
||||
this.callback = callbackContext;
|
||||
try {
|
||||
JSONObject options = args.getJSONObject(0);
|
||||
type = options.getInt("type");
|
||||
chooseFile(callbackContext, type);
|
||||
return true;
|
||||
} catch (JSONException e) {
|
||||
callbackContext.error("Error encountered: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void chooseFile (CallbackContext callbackContext, Integer type) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
if(type == 1) {
|
||||
intent.setType("image/*");
|
||||
} else {
|
||||
intent.setType("*/*");
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
Intent chooser = Intent.createChooser(intent, "Select File");
|
||||
cordova.startActivityForResult(this, chooser, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult (int requestCode, int resultCode, Intent data) {
|
||||
JSONArray results = new JSONArray();
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Uri uri = null;
|
||||
ClipData clipData = null;
|
||||
|
||||
if (data != null) {
|
||||
uri = data.getData();
|
||||
clipData = data.getClipData();
|
||||
}
|
||||
if (uri != null) {
|
||||
results.put(getMetadata(uri));
|
||||
} else if (clipData != null && clipData.getItemCount() > 0) {
|
||||
final int length = clipData.getItemCount();
|
||||
for (int i = 0; i < length; ++i) {
|
||||
ClipData.Item item = clipData.getItemAt(i);
|
||||
results.put(getMetadata(item.getUri()));
|
||||
}
|
||||
}
|
||||
this.callback.success(results.toString());
|
||||
} else {
|
||||
this.callback.error("Execute failed");
|
||||
}
|
||||
}
|
||||
|
||||
private Object getMetadata(Uri uri) {
|
||||
try {
|
||||
JSONObject result = new JSONObject();
|
||||
|
||||
result.put("uri", uri.toString());
|
||||
|
||||
ContentResolver contentResolver = this.cordova.getActivity().getContentResolver();
|
||||
|
||||
result.put("type", contentResolver.getType(uri));
|
||||
Cursor cursor = contentResolver.query(uri, null, null, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (!cursor.isNull(displayNameIndex)) {
|
||||
result.put("name", cursor.getString(displayNameIndex));
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
int mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE);
|
||||
if (!cursor.isNull(mimeIndex)) {
|
||||
result.put("type", cursor.getString(mimeIndex));
|
||||
}
|
||||
}
|
||||
|
||||
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
|
||||
if (!cursor.isNull(sizeIndex)) {
|
||||
result.put("size", cursor.getInt(sizeIndex));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (JSONException err) {
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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 com.google.cordova;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.OkUrlFactory;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class OkHttpPlugin extends CordovaPlugin {
|
||||
public static final OkHttpClient client = new OkHttpClient();
|
||||
static {
|
||||
URL.setURLStreamHandlerFactory(new OkUrlFactory(client));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
package com.hiddentao.cordova.filepath;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.Manifest;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.apache.cordova.PermissionHelper;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.io.File;
|
||||
|
||||
public class FilePath extends CordovaPlugin {
|
||||
|
||||
private static final String TAG = "[FilePath plugin]: ";
|
||||
|
||||
private static final int INVALID_ACTION_ERROR_CODE = -1;
|
||||
|
||||
private static final int GET_PATH_ERROR_CODE = 0;
|
||||
private static final String GET_PATH_ERROR_ID = null;
|
||||
|
||||
private static final int GET_CLOUD_PATH_ERROR_CODE = 1;
|
||||
private static final String GET_CLOUD_PATH_ERROR_ID = "cloud";
|
||||
|
||||
private static final int RC_READ_EXTERNAL_STORAGE = 5;
|
||||
|
||||
private static CallbackContext callback;
|
||||
private static String uriStr;
|
||||
|
||||
public static final int READ_REQ_CODE = 0;
|
||||
|
||||
public static final String READ = Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
|
||||
protected void getReadPermission(int requestCode) {
|
||||
PermissionHelper.requestPermission(this, requestCode, READ);
|
||||
}
|
||||
|
||||
public void initialize(CordovaInterface cordova, final CordovaWebView webView) {
|
||||
super.initialize(cordova, webView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
*
|
||||
* @param action The action to execute.
|
||||
* @param args JSONArry of arguments for the plugin.
|
||||
* @param callbackContext The callback context through which to return stuff to caller.
|
||||
* @return A PluginResult object with a status and message.
|
||||
*/
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
this.callback = callbackContext;
|
||||
this.uriStr = args.getString(0);
|
||||
|
||||
if (action.equals("resolveNativePath")) {
|
||||
if (PermissionHelper.hasPermission(this, READ)) {
|
||||
resolveNativePath();
|
||||
}
|
||||
else {
|
||||
getReadPermission(READ_REQ_CODE);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
JSONObject resultObj = new JSONObject();
|
||||
|
||||
resultObj.put("code", INVALID_ACTION_ERROR_CODE);
|
||||
resultObj.put("message", "Invalid action.");
|
||||
|
||||
callbackContext.error(resultObj);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void resolveNativePath() throws JSONException {
|
||||
JSONObject resultObj = new JSONObject();
|
||||
/* content:///... */
|
||||
Uri pvUrl = Uri.parse(this.uriStr);
|
||||
|
||||
Log.d(TAG, "URI: " + this.uriStr);
|
||||
|
||||
Context appContext = this.cordova.getActivity().getApplicationContext();
|
||||
String filePath = getPath(appContext, pvUrl);
|
||||
|
||||
//check result; send error/success callback
|
||||
if (filePath == GET_PATH_ERROR_ID) {
|
||||
resultObj.put("code", GET_PATH_ERROR_CODE);
|
||||
resultObj.put("message", "Unable to resolve filesystem path.");
|
||||
|
||||
this.callback.error(resultObj);
|
||||
}
|
||||
else if (filePath.equals(GET_CLOUD_PATH_ERROR_ID)) {
|
||||
resultObj.put("code", GET_CLOUD_PATH_ERROR_CODE);
|
||||
resultObj.put("message", "Files from cloud cannot be resolved to filesystem, download is required.");
|
||||
|
||||
this.callback.error(resultObj);
|
||||
}
|
||||
else {
|
||||
Log.d(TAG, "Filepath: " + filePath);
|
||||
|
||||
this.callback.success("file://" + filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
|
||||
for (int r : grantResults) {
|
||||
if (r == PackageManager.PERMISSION_DENIED) {
|
||||
JSONObject resultObj = new JSONObject();
|
||||
resultObj.put("code", 3);
|
||||
resultObj.put("message", "Filesystem permission was denied.");
|
||||
|
||||
this.callback.error(resultObj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCode == READ_REQ_CODE) {
|
||||
resolveNativePath();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||
*/
|
||||
private static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is DownloadsProvider.
|
||||
*/
|
||||
private static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is MediaProvider.
|
||||
*/
|
||||
private static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is Google Photos.
|
||||
*/
|
||||
private static boolean isGooglePhotosUri(Uri uri) {
|
||||
return ("com.google.android.apps.photos.content".equals(uri.getAuthority())
|
||||
|| "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is Google Drive.
|
||||
*/
|
||||
private static boolean isGoogleDriveUri(Uri uri) {
|
||||
return "com.google.android.apps.docs.storage".equals(uri.getAuthority()) || "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is One Drive.
|
||||
*/
|
||||
private static boolean isOneDriveUri(Uri uri) {
|
||||
return "com.microsoft.skydrive.content.external".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the data column for this Uri. This is useful for
|
||||
* MediaStore Uris, and other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @param selection (Optional) Filter used in the query.
|
||||
* @param selectionArgs (Optional) Selection arguments used in the query.
|
||||
* @return The value of the _data column, which is typically a file path.
|
||||
*/
|
||||
private static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int column_index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content:// from segment list
|
||||
* In the new Uri Authority of Google Photos, the last segment is not the content:// anymore
|
||||
* So let's iterate through all segments and find the content uri!
|
||||
*
|
||||
* @param segments The list of segment
|
||||
*/
|
||||
private static String getContentFromSegments(List<String> segments) {
|
||||
String contentPath = "";
|
||||
|
||||
for (String item : segments) {
|
||||
if (item.startsWith("content://")) {
|
||||
contentPath = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return contentPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists on device
|
||||
*
|
||||
* @param filePath The absolute file path
|
||||
*/
|
||||
private static boolean fileExists(String filePath) {
|
||||
File file = new File(filePath);
|
||||
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full file path from external storage
|
||||
*
|
||||
* @param pathData The storage type and the relative path
|
||||
*/
|
||||
private static String getPathFromExtSD(String[] pathData) {
|
||||
final String type = pathData[0];
|
||||
final String relativePath = "/" + pathData[1];
|
||||
String fullPath = "";
|
||||
|
||||
// on my Sony devices (4.4.4 & 5.1.1), `type` is a dynamic string
|
||||
// something like "71F8-2C0A", some kind of unique id per storage
|
||||
// don't know any API that can get the root path of that storage based on its id.
|
||||
//
|
||||
// so no "primary" type, but let the check here for other devices
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
fullPath = Environment.getExternalStorageDirectory() + relativePath;
|
||||
if (fileExists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
//fix some devices(Android Q),'type' like "71F8-2C0A"
|
||||
//but "primary".equalsIgnoreCase(type) is false
|
||||
fullPath = "/storage/" + type + "/" + relativePath;
|
||||
if (fileExists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
// Environment.isExternalStorageRemovable() is `true` for external and internal storage
|
||||
// so we cannot relay on it.
|
||||
//
|
||||
// instead, for each possible path, check if file exists
|
||||
// we'll start with secondary storage as this could be our (physically) removable sd card
|
||||
fullPath = System.getenv("SECONDARY_STORAGE") + relativePath;
|
||||
if (fileExists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
fullPath = System.getenv("EXTERNAL_STORAGE") + relativePath;
|
||||
if (fileExists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file path from a Uri. This will get the the path for Storage Access
|
||||
* Framework Documents, as well as the _data field for the MediaStore and
|
||||
* other file-based ContentProviders.<br>
|
||||
* <br>
|
||||
* Callers should check whether the path is local before assuming it
|
||||
* represents a local file.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
*/
|
||||
private static String getPath(final Context context, final Uri uri) {
|
||||
|
||||
Log.d(TAG, "File - " +
|
||||
"Authority: " + uri.getAuthority() +
|
||||
", Fragment: " + uri.getFragment() +
|
||||
", Port: " + uri.getPort() +
|
||||
", Query: " + uri.getQuery() +
|
||||
", Scheme: " + uri.getScheme() +
|
||||
", Host: " + uri.getHost() +
|
||||
", Segments: " + uri.getPathSegments().toString()
|
||||
);
|
||||
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
String fullPath = getPathFromExtSD(split);
|
||||
if (fullPath != "") {
|
||||
return fullPath;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DownloadsProvider
|
||||
else if (isDownloadsDocument(uri)) {
|
||||
// thanks to https://github.com/hiddentao/cordova-plugin-filepath/issues/34#issuecomment-430129959
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String fileName = cursor.getString(0);
|
||||
String path = Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName;
|
||||
if (fileExists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
//
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
String[] contentUriPrefixesToTry = new String[]{
|
||||
"content://downloads/public_downloads",
|
||||
"content://downloads/my_downloads"
|
||||
};
|
||||
|
||||
for (String contentUriPrefix : contentUriPrefixesToTry) {
|
||||
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
|
||||
try {
|
||||
String path = getDataColumn(context, contentUri, null, null);
|
||||
if (path != null) {
|
||||
return path;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return getDriveFilePath(uri, context);
|
||||
} catch (Exception e) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
}
|
||||
// MediaProvider
|
||||
else if (isMediaDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
} else {
|
||||
contentUri = MediaStore.Files.getContentUri("external");
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[]{
|
||||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
} else if (isGoogleDriveUri(uri)) {
|
||||
return getDriveFilePath(uri, context);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri)) {
|
||||
if (uri.toString().contains("mediakey")) {
|
||||
return getDriveFilePath(uri, context);
|
||||
} else {
|
||||
String contentPath = getContentFromSegments(uri.getPathSegments());
|
||||
if (contentPath != "") {
|
||||
return getPath(context, Uri.parse(contentPath));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isGoogleDriveUri(uri) || isOneDriveUri(uri)) {
|
||||
return getDriveFilePath(uri, context);
|
||||
}
|
||||
|
||||
return getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getDriveFilePath(Uri uri, Context context) {
|
||||
Uri returnUri = uri;
|
||||
Cursor returnCursor = context.getContentResolver().query(returnUri, null, null, null, null);
|
||||
/*
|
||||
* Get the column indexes of the data in the Cursor,
|
||||
* * move to the first row in the Cursor, get the data,
|
||||
* * and display it.
|
||||
* */
|
||||
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
|
||||
returnCursor.moveToFirst();
|
||||
String name = (returnCursor.getString(nameIndex));
|
||||
String size = (Long.toString(returnCursor.getLong(sizeIndex)));
|
||||
File file = new File(context.getCacheDir(), name);
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
int read = 0;
|
||||
int maxBufferSize = 1 * 1024 * 1024;
|
||||
int bytesAvailable = inputStream.available();
|
||||
|
||||
//int bufferSize = 1024;
|
||||
int bufferSize = Math.min(bytesAvailable, maxBufferSize);
|
||||
|
||||
final byte[] buffers = new byte[bufferSize];
|
||||
while ((read = inputStream.read(buffers)) != -1) {
|
||||
outputStream.write(buffers, 0, read);
|
||||
}
|
||||
Log.e("File Size", "Size " + file.length());
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
Log.e("File Path", "Path " + file.getPath());
|
||||
Log.e("File Size", "Size " + file.length());
|
||||
} catch (Exception e) {
|
||||
Log.e("Exception", e.getMessage());
|
||||
}
|
||||
return file.getPath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.jeduan.crop;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
|
||||
import com.soundcloud.android.crop.Crop;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class CropPlugin extends CordovaPlugin {
|
||||
private CallbackContext callbackContext;
|
||||
private Uri inputUri;
|
||||
private Uri outputUri;
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
|
||||
if (action.equals("cropImage")) {
|
||||
String imagePath = args.getString(0);
|
||||
|
||||
this.inputUri = Uri.parse(imagePath);
|
||||
this.outputUri = Uri.fromFile(new File(getTempDirectoryPath() + "/" + System.currentTimeMillis()+ "-cropped.jpg"));
|
||||
|
||||
PluginResult pr = new PluginResult(PluginResult.Status.NO_RESULT);
|
||||
pr.setKeepCallback(true);
|
||||
callbackContext.sendPluginResult(pr);
|
||||
this.callbackContext = callbackContext;
|
||||
|
||||
cordova.setActivityResultCallback(this);
|
||||
Crop.of(this.inputUri, this.outputUri)
|
||||
.asSquare()
|
||||
.start(cordova.getActivity());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
if (requestCode == Crop.REQUEST_CROP) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Uri imageUri = Crop.getOutput(intent);
|
||||
this.callbackContext.success("file://" + imageUri.getPath() + "?" + System.currentTimeMillis());
|
||||
this.callbackContext = null;
|
||||
} else if (resultCode == Crop.RESULT_ERROR) {
|
||||
try {
|
||||
JSONObject err = new JSONObject();
|
||||
err.put("message", "Error on cropping");
|
||||
err.put("code", String.valueOf(resultCode));
|
||||
this.callbackContext.error(err);
|
||||
this.callbackContext = null;
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else if (resultCode == Activity.RESULT_CANCELED) {
|
||||
try {
|
||||
JSONObject err = new JSONObject();
|
||||
err.put("message", "User cancelled");
|
||||
err.put("code", "userCancelled");
|
||||
this.callbackContext.error(err);
|
||||
this.callbackContext = null;
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
}
|
||||
|
||||
private String getTempDirectoryPath() {
|
||||
File cache = null;
|
||||
|
||||
// SD Card Mounted
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
cache = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
|
||||
"/Android/data/" + cordova.getActivity().getPackageName() + "/cache/");
|
||||
}
|
||||
// Use internal storage
|
||||
else {
|
||||
cache = cordova.getActivity().getCacheDir();
|
||||
}
|
||||
|
||||
// Create the cache directory if it doesn't exist
|
||||
cache.mkdirs();
|
||||
return cache.getAbsolutePath();
|
||||
}
|
||||
|
||||
public Bundle onSaveInstanceState() {
|
||||
Bundle state = new Bundle();
|
||||
|
||||
if (this.inputUri != null) {
|
||||
state.putString("inputUri", this.inputUri.toString());
|
||||
}
|
||||
|
||||
if (this.outputUri != null) {
|
||||
state.putString("outputUri", this.outputUri.toString());
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
|
||||
|
||||
if (state.containsKey("inputUri")) {
|
||||
this.inputUri = Uri.parse(state.getString("inputUri"));
|
||||
}
|
||||
|
||||
if (state.containsKey("outputUri")) {
|
||||
this.inputUri = Uri.parse(state.getString("outputUri"));
|
||||
}
|
||||
|
||||
this.callbackContext = callbackContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package com.sarriaroman.PhotoViewer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Base64;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.picasso.Callback;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.RequestCreator;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import uk.co.senab.photoview.PhotoViewAttacher;
|
||||
|
||||
public class PhotoActivity extends Activity {
|
||||
private PhotoViewAttacher mAttacher;
|
||||
|
||||
private ImageView photo;
|
||||
|
||||
private ImageButton closeBtn;
|
||||
private ImageButton shareBtn;
|
||||
private ProgressBar loadingBar;
|
||||
|
||||
private TextView titleTxt;
|
||||
|
||||
private String mImage;
|
||||
private String mTitle;
|
||||
private boolean mShare;
|
||||
private JSONObject mHeaders;
|
||||
private JSONObject pOptions;
|
||||
private File mTempImage;
|
||||
private int shareBtnVisibility;
|
||||
|
||||
public static JSONArray mArgs = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(getApplication().getResources().getIdentifier("activity_photo", "layout", getApplication().getPackageName()));
|
||||
|
||||
// Load the Views
|
||||
findViews();
|
||||
|
||||
try {
|
||||
this.mImage = mArgs.getString(0);
|
||||
this.mTitle = mArgs.getString(1);
|
||||
this.mShare = mArgs.getBoolean(2);
|
||||
this.mHeaders = parseHeaders(mArgs.optString(5));
|
||||
this.pOptions = mArgs.optJSONObject(6);
|
||||
|
||||
if( pOptions == null ) {
|
||||
pOptions = new JSONObject();
|
||||
pOptions.put("fit", true);
|
||||
pOptions.put("centerInside", true);
|
||||
pOptions.put("centerCrop", false);
|
||||
}
|
||||
|
||||
//Set the share button visibility
|
||||
shareBtnVisibility = this.mShare ? View.VISIBLE : View.INVISIBLE;
|
||||
|
||||
|
||||
} catch (JSONException exception) {
|
||||
shareBtnVisibility = View.INVISIBLE;
|
||||
}
|
||||
shareBtn.setVisibility(shareBtnVisibility);
|
||||
//Change the activity title
|
||||
if (!mTitle.equals("")) {
|
||||
titleTxt.setText(mTitle);
|
||||
}
|
||||
|
||||
try {
|
||||
loadImage();
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Set Button Listeners
|
||||
closeBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
shareBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
try {
|
||||
Method m = StrictMode.class.getMethod("disableDeathOnFileUriExposure");
|
||||
m.invoke(null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
Uri imageUri;
|
||||
if (mTempImage == null) {
|
||||
mTempImage = getLocalBitmapFileFromView(photo);
|
||||
}
|
||||
|
||||
imageUri = Uri.fromFile(mTempImage);
|
||||
|
||||
if (imageUri != null) {
|
||||
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
|
||||
|
||||
sharingIntent.setType("image/*");
|
||||
sharingIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
|
||||
|
||||
startActivity(Intent.createChooser(sharingIntent, "Share"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and Connect Views
|
||||
*/
|
||||
private void findViews() {
|
||||
// Buttons first
|
||||
closeBtn = (ImageButton) findViewById(getApplication().getResources().getIdentifier("closeBtn", "id", getApplication().getPackageName()));
|
||||
shareBtn = (ImageButton) findViewById(getApplication().getResources().getIdentifier("shareBtn", "id", getApplication().getPackageName()));
|
||||
|
||||
//ProgressBar
|
||||
loadingBar = (ProgressBar) findViewById(getApplication().getResources().getIdentifier("loadingBar", "id", getApplication().getPackageName()));
|
||||
// Photo Container
|
||||
photo = (ImageView) findViewById(getApplication().getResources().getIdentifier("photoView", "id", getApplication().getPackageName()));
|
||||
mAttacher = new PhotoViewAttacher(photo);
|
||||
|
||||
// Title TextView
|
||||
titleTxt = (TextView) findViewById(getApplication().getResources().getIdentifier("titleTxt", "id", getApplication().getPackageName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Activity
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide Loading when showing the photo. Update the PhotoView Attacher
|
||||
*/
|
||||
private void hideLoadingAndUpdate() {
|
||||
photo.setVisibility(View.VISIBLE);
|
||||
loadingBar.setVisibility(View.INVISIBLE);
|
||||
shareBtn.setVisibility(shareBtnVisibility);
|
||||
|
||||
mAttacher.update();
|
||||
}
|
||||
|
||||
private RequestCreator setOptions(RequestCreator picasso) throws JSONException {
|
||||
if(this.pOptions.has("fit") && this.pOptions.optBoolean("fit")) {
|
||||
picasso.fit();
|
||||
}
|
||||
|
||||
if(this.pOptions.has("centerInside") && this.pOptions.optBoolean("centerInside")) {
|
||||
picasso.centerInside();
|
||||
}
|
||||
|
||||
if(this.pOptions.has("centerCrop") && this.pOptions.optBoolean("centerCrop")) {
|
||||
picasso.centerCrop();
|
||||
}
|
||||
|
||||
return picasso;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the image using Picasso
|
||||
*/
|
||||
private void loadImage() throws JSONException {
|
||||
if (mImage.startsWith("http") || mImage.startsWith("file")) {
|
||||
this.setOptions(Picasso.get().load(mImage)).into(photo, new Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
hideLoadingAndUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Toast.makeText(getActivity(), "Error loading image.", Toast.LENGTH_LONG).show();
|
||||
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else if (mImage.startsWith("data:image")) {
|
||||
|
||||
new AsyncTask<Void, Void, File>() {
|
||||
|
||||
protected File doInBackground(Void... params) {
|
||||
String base64Image = mImage.substring(mImage.indexOf(",") + 1);
|
||||
return getLocalBitmapFileFromString(base64Image);
|
||||
}
|
||||
|
||||
protected void onPostExecute(File file) {
|
||||
mTempImage = file;
|
||||
|
||||
try {
|
||||
setOptions(Picasso.get().load(mTempImage))
|
||||
.into(photo, new Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
hideLoadingAndUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Toast.makeText(getActivity(), "Error loading image.", Toast.LENGTH_LONG).show();
|
||||
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
|
||||
} else {
|
||||
photo.setImageURI(Uri.parse(mImage));
|
||||
|
||||
hideLoadingAndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
if (mTempImage != null) {
|
||||
mTempImage.delete();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
||||
public File getLocalBitmapFileFromString(String base64) {
|
||||
File file;
|
||||
try {
|
||||
file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"share_image_" + System.currentTimeMillis() + ".png");
|
||||
file.getParentFile().mkdirs();
|
||||
FileOutputStream output = new FileOutputStream(file);
|
||||
byte[] decoded = Base64.decode(base64, Base64.DEFAULT);
|
||||
output.write(decoded);
|
||||
output.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
file = null;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Local Image due to Restrictions
|
||||
*
|
||||
* @param imageView
|
||||
* @return
|
||||
*/
|
||||
public File getLocalBitmapFileFromView(ImageView imageView) {
|
||||
Drawable drawable = imageView.getDrawable();
|
||||
Bitmap bmp;
|
||||
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store image to default external storage directory
|
||||
File file;
|
||||
try {
|
||||
file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"share_image_" + System.currentTimeMillis() + ".png");
|
||||
file.getParentFile().mkdirs();
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
bmp.compress(Bitmap.CompressFormat.PNG, 90, out);
|
||||
out.close();
|
||||
|
||||
} catch (IOException e) {
|
||||
file = null;
|
||||
e.printStackTrace();
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private JSONObject parseHeaders(String headerString) {
|
||||
JSONObject headers = null;
|
||||
|
||||
// Short circuit if headers is empty
|
||||
if (headerString == null || headerString.length() == 0) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// headers should never be a JSON array, only a JSON object
|
||||
try {
|
||||
headers = new JSONObject(headerString);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.sarriaroman.PhotoViewer;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
/**
|
||||
* Class to Open PhotoViewer with the Required Parameters from Cordova
|
||||
* <p>
|
||||
* - URL
|
||||
* - Title
|
||||
*/
|
||||
public class PhotoViewer extends CordovaPlugin {
|
||||
|
||||
public static final int PERMISSION_DENIED_ERROR = 20;
|
||||
|
||||
public static final String WRITE = Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||
public static final String READ = Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
|
||||
public static final int REQ_CODE = 0;
|
||||
|
||||
protected JSONArray args;
|
||||
protected CallbackContext callbackContext;
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
if (action.equals("show")) {
|
||||
this.args = args;
|
||||
this.callbackContext = callbackContext;
|
||||
|
||||
if (cordova.hasPermission(READ) && cordova.hasPermission(WRITE)) {
|
||||
this.launchActivity();
|
||||
} else {
|
||||
this.getPermission();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void getPermission() {
|
||||
cordova.requestPermissions(this, REQ_CODE, new String[]{WRITE, READ});
|
||||
}
|
||||
|
||||
//
|
||||
protected void launchActivity() throws JSONException {
|
||||
Intent i = new Intent(this.cordova.getActivity(), com.sarriaroman.PhotoViewer.PhotoActivity.class);
|
||||
PhotoActivity.mArgs = this.args;
|
||||
|
||||
this.cordova.getActivity().startActivity(i);
|
||||
this.callbackContext.success("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) throws JSONException {
|
||||
for (int r : grantResults) {
|
||||
if (r == PackageManager.PERMISSION_DENIED) {
|
||||
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case REQ_CODE:
|
||||
launchActivity();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
*
|
||||
* 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 cordova.plugins.screenorientation;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.util.Log;
|
||||
|
||||
public class CDVOrientation extends CordovaPlugin {
|
||||
|
||||
private static final String TAG = "YoikScreenOrientation";
|
||||
|
||||
/**
|
||||
* Screen Orientation Constants
|
||||
*/
|
||||
|
||||
private static final String ANY = "any";
|
||||
private static final String PORTRAIT_PRIMARY = "portrait-primary";
|
||||
private static final String PORTRAIT_SECONDARY = "portrait-secondary";
|
||||
private static final String LANDSCAPE_PRIMARY = "landscape-primary";
|
||||
private static final String LANDSCAPE_SECONDARY = "landscape-secondary";
|
||||
private static final String PORTRAIT = "portrait";
|
||||
private static final String LANDSCAPE = "landscape";
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
|
||||
|
||||
Log.d(TAG, "execute action: " + action);
|
||||
|
||||
// Route the Action
|
||||
if (action.equals("screenOrientation")) {
|
||||
return routeScreenOrientation(args, callbackContext);
|
||||
}
|
||||
|
||||
// Action not found
|
||||
callbackContext.error("action not recognised");
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean routeScreenOrientation(JSONArray args, CallbackContext callbackContext) {
|
||||
|
||||
String action = args.optString(0);
|
||||
|
||||
|
||||
|
||||
String orientation = args.optString(1);
|
||||
|
||||
Log.d(TAG, "Requested ScreenOrientation: " + orientation);
|
||||
|
||||
Activity activity = cordova.getActivity();
|
||||
|
||||
if (orientation.equals(ANY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
} else if (orientation.equals(LANDSCAPE_PRIMARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else if (orientation.equals(PORTRAIT_PRIMARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
} else if (orientation.equals(LANDSCAPE)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
} else if (orientation.equals(PORTRAIT)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
} else if (orientation.equals(LANDSCAPE_SECONDARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
|
||||
} else if (orientation.equals(PORTRAIT_SECONDARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
|
||||
}
|
||||
|
||||
callbackContext.success();
|
||||
return true;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Args {
|
||||
private static final String TAG = "ARGS";
|
||||
private JSONArray jsonArray;
|
||||
private JSONObject argsObject;
|
||||
|
||||
Args(JSONArray jsonArray) {
|
||||
this.jsonArray = jsonArray;
|
||||
}
|
||||
|
||||
public Boolean getBoolean(String name, Boolean defaultValue) {
|
||||
try {
|
||||
if (getArgsObject().has(name)){
|
||||
return getArgsObject().getBoolean(name);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Can't parse '" + name + "'. Default will be used.", e);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public String getString(String name, String defaultValue) {
|
||||
try {
|
||||
if (getArgsObject().optString(name) != null
|
||||
&& !getArgsObject().optString(name).isEmpty()){
|
||||
return getArgsObject().getString(name);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Can't parse '" + name + "'. Default will be used.", e);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private JSONObject getArgsObject() throws JSONException {
|
||||
if (this.argsObject != null) {
|
||||
return this.argsObject;
|
||||
}
|
||||
this.argsObject = jsonArray.getJSONObject(0);
|
||||
return this.argsObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
public class BiometricActivity extends AppCompatActivity {
|
||||
|
||||
private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 2;
|
||||
private PromptInfo mPromptInfo;
|
||||
private CryptographyManager mCryptographyManager;
|
||||
private static final String SECRET_KEY = "__aio_secret_key";
|
||||
private BiometricPrompt mBiometricPrompt;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(null);
|
||||
int layout = getResources()
|
||||
.getIdentifier("biometric_activity", "layout", getPackageName());
|
||||
setContentView(layout);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCryptographyManager = new CryptographyManagerImpl();
|
||||
mPromptInfo = new PromptInfo.Builder(getIntent().getExtras()).build();
|
||||
final Handler handler = new Handler(Looper.getMainLooper());
|
||||
Executor executor = handler::post;
|
||||
mBiometricPrompt = new BiometricPrompt(this, executor, mAuthenticationCallback);
|
||||
try {
|
||||
authenticate();
|
||||
} catch (CryptoException e) {
|
||||
finishWithError(e);
|
||||
} catch (Exception e) {
|
||||
finishWithError(PluginError.BIOMETRIC_UNKNOWN_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void authenticate() throws CryptoException {
|
||||
switch (mPromptInfo.getType()) {
|
||||
case JUST_AUTHENTICATE:
|
||||
justAuthenticate();
|
||||
return;
|
||||
case REGISTER_SECRET:
|
||||
authenticateToEncrypt(mPromptInfo.invalidateOnEnrollment());
|
||||
return;
|
||||
case LOAD_SECRET:
|
||||
authenticateToDecrypt();
|
||||
return;
|
||||
}
|
||||
throw new CryptoException(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
|
||||
}
|
||||
|
||||
private void authenticateToEncrypt(boolean invalidateOnEnrollment) throws CryptoException {
|
||||
if (mPromptInfo.getSecret() == null) {
|
||||
throw new CryptoException(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
|
||||
}
|
||||
Cipher cipher = mCryptographyManager
|
||||
.getInitializedCipherForEncryption(SECRET_KEY, invalidateOnEnrollment, this);
|
||||
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
|
||||
}
|
||||
|
||||
private void justAuthenticate() {
|
||||
mBiometricPrompt.authenticate(createPromptInfo());
|
||||
}
|
||||
|
||||
private void authenticateToDecrypt() throws CryptoException {
|
||||
byte[] initializationVector = EncryptedData.loadInitializationVector(this);
|
||||
Cipher cipher = mCryptographyManager
|
||||
.getInitializedCipherForDecryption(SECRET_KEY, initializationVector, this);
|
||||
mBiometricPrompt.authenticate(createPromptInfo(), new BiometricPrompt.CryptoObject(cipher));
|
||||
}
|
||||
|
||||
private BiometricPrompt.PromptInfo createPromptInfo() {
|
||||
BiometricPrompt.PromptInfo.Builder promptInfoBuilder = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(mPromptInfo.getTitle())
|
||||
.setSubtitle(mPromptInfo.getSubtitle())
|
||||
.setConfirmationRequired(mPromptInfo.getConfirmationRequired())
|
||||
.setDescription(mPromptInfo.getDescription());
|
||||
|
||||
if (mPromptInfo.isDeviceCredentialAllowed()
|
||||
&& mPromptInfo.getType() == BiometricActivityType.JUST_AUTHENTICATE
|
||||
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // TODO: remove after fix https://issuetracker.google.com/issues/142740104
|
||||
promptInfoBuilder.setDeviceCredentialAllowed(true);
|
||||
} else {
|
||||
promptInfoBuilder.setNegativeButtonText(mPromptInfo.getCancelButtonTitle());
|
||||
}
|
||||
return promptInfoBuilder.build();
|
||||
}
|
||||
|
||||
private BiometricPrompt.AuthenticationCallback mAuthenticationCallback =
|
||||
new BiometricPrompt.AuthenticationCallback() {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
onError(errorCode, errString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
try {
|
||||
finishWithSuccess(result.getCryptoObject());
|
||||
} catch (CryptoException e) {
|
||||
finishWithError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
onError(PluginError.BIOMETRIC_AUTHENTICATION_FAILED.getValue(), PluginError.BIOMETRIC_AUTHENTICATION_FAILED.getMessage());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// TODO: remove after fix https://issuetracker.google.com/issues/142740104
|
||||
private void showAuthenticationScreen() {
|
||||
KeyguardManager keyguardManager = ContextCompat
|
||||
.getSystemService(this, KeyguardManager.class);
|
||||
if (keyguardManager == null
|
||||
|| android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
return;
|
||||
}
|
||||
if (keyguardManager.isKeyguardSecure()) {
|
||||
Intent intent = keyguardManager
|
||||
.createConfirmDeviceCredentialIntent(mPromptInfo.getTitle(), mPromptInfo.getDescription());
|
||||
this.startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
|
||||
} else {
|
||||
// Show a message that the user hasn't set up a lock screen.
|
||||
finishWithError(PluginError.BIOMETRIC_SCREEN_GUARD_UNSECURED);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove after fix https://issuetracker.google.com/issues/142740104
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
finishWithSuccess();
|
||||
} else {
|
||||
finishWithError(PluginError.BIOMETRIC_PIN_OR_PATTERN_DISMISSED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onError(int errorCode, @NonNull CharSequence errString) {
|
||||
|
||||
switch (errorCode)
|
||||
{
|
||||
case BiometricPrompt.ERROR_USER_CANCELED:
|
||||
case BiometricPrompt.ERROR_CANCELED:
|
||||
finishWithError(PluginError.BIOMETRIC_DISMISSED);
|
||||
return;
|
||||
case BiometricPrompt.ERROR_NEGATIVE_BUTTON:
|
||||
// TODO: remove after fix https://issuetracker.google.com/issues/142740104
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P && mPromptInfo.isDeviceCredentialAllowed()) {
|
||||
showAuthenticationScreen();
|
||||
return;
|
||||
}
|
||||
finishWithError(PluginError.BIOMETRIC_DISMISSED);
|
||||
break;
|
||||
case BiometricPrompt.ERROR_LOCKOUT:
|
||||
finishWithError(PluginError.BIOMETRIC_LOCKED_OUT.getValue(), errString.toString());
|
||||
break;
|
||||
case BiometricPrompt.ERROR_LOCKOUT_PERMANENT:
|
||||
finishWithError(PluginError.BIOMETRIC_LOCKED_OUT_PERMANENT.getValue(), errString.toString());
|
||||
break;
|
||||
default:
|
||||
finishWithError(errorCode, errString.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void finishWithSuccess() {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void finishWithSuccess(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
|
||||
Intent intent = null;
|
||||
switch (mPromptInfo.getType()) {
|
||||
case REGISTER_SECRET:
|
||||
encrypt(cryptoObject);
|
||||
break;
|
||||
case LOAD_SECRET:
|
||||
intent = getDecryptedIntent(cryptoObject);
|
||||
break;
|
||||
}
|
||||
if (intent == null) {
|
||||
setResult(RESULT_OK);
|
||||
} else {
|
||||
setResult(RESULT_OK, intent);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
private void encrypt(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
|
||||
String text = mPromptInfo.getSecret();
|
||||
EncryptedData encryptedData = mCryptographyManager.encryptData(text, cryptoObject.getCipher());
|
||||
encryptedData.save(this);
|
||||
}
|
||||
|
||||
private Intent getDecryptedIntent(BiometricPrompt.CryptoObject cryptoObject) throws CryptoException {
|
||||
byte[] ciphertext = EncryptedData.loadCiphertext(this);
|
||||
String secret = mCryptographyManager.decryptData(ciphertext, cryptoObject.getCipher());
|
||||
if (secret != null) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(PromptInfo.SECRET_EXTRA, secret);
|
||||
return intent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void finishWithError(CryptoException e) {
|
||||
finishWithError(e.getError().getValue(), e.getMessage());
|
||||
}
|
||||
|
||||
private void finishWithError(PluginError error) {
|
||||
finishWithError(error.getValue(), error.getMessage());
|
||||
}
|
||||
|
||||
private void finishWithError(PluginError error, String message) {
|
||||
finishWithError(error.getValue(), message);
|
||||
}
|
||||
|
||||
private void finishWithError(int code, String message) {
|
||||
Intent data = new Intent();
|
||||
data.putExtra("code", code);
|
||||
data.putExtra("message", message);
|
||||
setResult(RESULT_CANCELED, data);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
public enum BiometricActivityType {
|
||||
JUST_AUTHENTICATE(1),
|
||||
REGISTER_SECRET(2),
|
||||
LOAD_SECRET(3);
|
||||
|
||||
private int value;
|
||||
|
||||
BiometricActivityType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static BiometricActivityType fromValue(int val) {
|
||||
for (BiometricActivityType type : values()) {
|
||||
if (type.getValue() == val) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
class CryptoException extends Exception {
|
||||
private PluginError error;
|
||||
|
||||
CryptoException(String message, Exception cause) {
|
||||
this(PluginError.BIOMETRIC_UNKNOWN_ERROR, message, cause);
|
||||
}
|
||||
|
||||
CryptoException(PluginError error) {
|
||||
this(error, error.getMessage(), null);
|
||||
}
|
||||
|
||||
CryptoException(PluginError error, Exception cause) {
|
||||
this(error, error.getMessage(), cause);
|
||||
}
|
||||
|
||||
private CryptoException(PluginError error, String message, Exception cause) {
|
||||
super(message, cause);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public PluginError getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
interface CryptographyManager {
|
||||
|
||||
/**
|
||||
* This method first gets or generates an instance of SecretKey and then initializes the Cipher
|
||||
* with the key. The secret key uses [ENCRYPT_MODE][Cipher.ENCRYPT_MODE] is used.
|
||||
*/
|
||||
Cipher getInitializedCipherForEncryption(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException;
|
||||
|
||||
/**
|
||||
* This method first gets or generates an instance of SecretKey and then initializes the Cipher
|
||||
* with the key. The secret key uses [DECRYPT_MODE][Cipher.DECRYPT_MODE] is used.
|
||||
*/
|
||||
Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector, Context context) throws CryptoException;
|
||||
|
||||
/**
|
||||
* The Cipher created with [getInitializedCipherForEncryption] is used here
|
||||
*/
|
||||
EncryptedData encryptData(String plaintext, Cipher cipher) throws CryptoException;
|
||||
|
||||
/**
|
||||
* The Cipher created with [getInitializedCipherForDecryption] is used here
|
||||
*/
|
||||
String decryptData(byte[] ciphertext, Cipher cipher) throws CryptoException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.security.KeyPairGeneratorSpec;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Calendar;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
class CryptographyManagerImpl implements CryptographyManager {
|
||||
|
||||
private static final int KEY_SIZE = 256;
|
||||
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
|
||||
private static final String ENCRYPTION_PADDING = "NoPadding"; // KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
private static final String ENCRYPTION_ALGORITHM = "AES"; // KeyProperties.KEY_ALGORITHM_AES
|
||||
private static final String KEY_ALGORITHM_AES = "AES"; // KeyProperties.KEY_ALGORITHM_AES
|
||||
private static final String ENCRYPTION_BLOCK_MODE = "GCM"; // KeyProperties.BLOCK_MODE_GCM
|
||||
|
||||
private Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
String transformation = ENCRYPTION_ALGORITHM + "/" + ENCRYPTION_BLOCK_MODE + "/" + ENCRYPTION_PADDING;
|
||||
return Cipher.getInstance(transformation);
|
||||
}
|
||||
|
||||
private SecretKey getOrCreateSecretKey(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return getOrCreateSecretKeyNew(keyName, invalidateOnEnrollment);
|
||||
} else {
|
||||
return getOrCreateSecretKeyOld(keyName, context);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKey getOrCreateSecretKeyOld(String keyName, Context context) throws CryptoException {
|
||||
Calendar start = Calendar.getInstance();
|
||||
Calendar end = Calendar.getInstance();
|
||||
end.add(Calendar.YEAR, 1);
|
||||
try {
|
||||
KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(context)
|
||||
.setAlias(keyName)
|
||||
.setSubject(new X500Principal("CN=FINGERPRINT_AIO ," +
|
||||
" O=FINGERPRINT_AIO" +
|
||||
" C=World"))
|
||||
.setSerialNumber(BigInteger.ONE)
|
||||
.setStartDate(start.getTime())
|
||||
.setEndDate(end.getTime())
|
||||
.build();
|
||||
KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
|
||||
kg.init(keySpec);
|
||||
return kg.generateKey();
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private SecretKey getOrCreateSecretKeyNew(String keyName, boolean invalidateOnEnrollment) throws CryptoException {
|
||||
try {
|
||||
// If Secretkey was previously created for that keyName, then grab and return it.
|
||||
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
|
||||
keyStore.load(null); // Keystore must be loaded before it can be accessed
|
||||
|
||||
|
||||
SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// if you reach here, then a new SecretKey must be generated for that keyName
|
||||
KeyGenParameterSpec.Builder keyGenParamsBuilder = new KeyGenParameterSpec.Builder(keyName,
|
||||
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setKeySize(KEY_SIZE)
|
||||
.setUserAuthenticationRequired(true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
keyGenParamsBuilder.setInvalidatedByBiometricEnrollment(invalidateOnEnrollment);
|
||||
}
|
||||
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES,
|
||||
ANDROID_KEYSTORE);
|
||||
keyGenerator.init(keyGenParamsBuilder.build());
|
||||
|
||||
return keyGenerator.generateKey();
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cipher getInitializedCipherForEncryption(String keyName, boolean invalidateOnEnrollment, Context context) throws CryptoException {
|
||||
try {
|
||||
Cipher cipher = getCipher();
|
||||
SecretKey secretKey = getOrCreateSecretKey(keyName, invalidateOnEnrollment, context);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher;
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
handleException(e, keyName);
|
||||
} catch (KeyInvalidatedException kie) {
|
||||
return getInitializedCipherForEncryption(keyName, invalidateOnEnrollment, context);
|
||||
}
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleException(Exception e, String keyName) throws CryptoException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& e instanceof KeyPermanentlyInvalidatedException) {
|
||||
removeKey(keyName);
|
||||
throw new KeyInvalidatedException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector, Context context) throws CryptoException {
|
||||
try {
|
||||
Cipher cipher = getCipher();
|
||||
SecretKey secretKey = getOrCreateSecretKey(keyName, true, context);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, initializationVector));
|
||||
return cipher;
|
||||
} catch (Exception e) {
|
||||
handleException(e, keyName);
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeKey(String keyName) throws CryptoException {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
|
||||
keyStore.load(null); // Keystore must be loaded before it can be accessed
|
||||
keyStore.deleteEntry(keyName);
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptedData encryptData(String plaintext, Cipher cipher) throws CryptoException {
|
||||
try {
|
||||
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||
return new EncryptedData(ciphertext, cipher.getIV());
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptData(byte[] ciphertext, Cipher cipher) throws CryptoException {
|
||||
try {
|
||||
byte[] plaintext = cipher.doFinal(ciphertext);
|
||||
return new String(plaintext, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Base64;
|
||||
|
||||
class EncryptedData {
|
||||
|
||||
private static final String CIPHERTEXT_KEY_NAME = "__biometric-aio-ciphertext";
|
||||
private static final String IV_KEY_NAME = "__biometric-aio-iv";
|
||||
|
||||
private byte[] ciphertext;
|
||||
private byte[] initializationVector;
|
||||
|
||||
EncryptedData(byte[] ciphertext, byte[] initializationVector) {
|
||||
this.ciphertext = ciphertext;
|
||||
this.initializationVector = initializationVector;
|
||||
}
|
||||
|
||||
static byte[] loadInitializationVector(Context context) throws CryptoException {
|
||||
return load(IV_KEY_NAME, context);
|
||||
}
|
||||
|
||||
static byte[] loadCiphertext(Context context) throws CryptoException {
|
||||
return load(CIPHERTEXT_KEY_NAME, context);
|
||||
}
|
||||
|
||||
void save(Context context) {
|
||||
save(IV_KEY_NAME, initializationVector, context);
|
||||
save(CIPHERTEXT_KEY_NAME, ciphertext, context);
|
||||
}
|
||||
|
||||
private void save(String key, byte[] value, Context context) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
preferences.edit()
|
||||
.putString(key, Base64.encodeToString(value, Base64.DEFAULT))
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static byte[] load(String key, Context context) throws CryptoException {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String res = preferences.getString(key, null);
|
||||
if (res == null) throw new CryptoException(PluginError.BIOMETRIC_NO_SECRET_FOUND);
|
||||
return Base64.decode(res, Base64.DEFAULT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
import androidx.biometric.BiometricManager;
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Fingerprint extends CordovaPlugin {
|
||||
|
||||
private static final String TAG = "Fingerprint";
|
||||
private static final int REQUEST_CODE_BIOMETRIC = 1;
|
||||
|
||||
private CallbackContext mCallbackContext = null;
|
||||
private PromptInfo.Builder mPromptInfoBuilder;
|
||||
|
||||
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
|
||||
super.initialize(cordova, webView);
|
||||
Log.v(TAG, "Init Fingerprint");
|
||||
mPromptInfoBuilder = new PromptInfo.Builder(
|
||||
this.getApplicationLabel(cordova.getActivity())
|
||||
);
|
||||
}
|
||||
|
||||
public boolean execute(final String action, JSONArray args, CallbackContext callbackContext) {
|
||||
|
||||
this.mCallbackContext = callbackContext;
|
||||
Log.v(TAG, "Fingerprint action: " + action);
|
||||
|
||||
if ("authenticate".equals(action)) {
|
||||
executeAuthenticate(args);
|
||||
return true;
|
||||
|
||||
} else if ("registerBiometricSecret".equals(action)) {
|
||||
executeRegisterBiometricSecret(args);
|
||||
return true;
|
||||
|
||||
} else if ("loadBiometricSecret".equals(action)) {
|
||||
executeLoadBiometricSecret(args);
|
||||
return true;
|
||||
|
||||
} else if ("isAvailable".equals(action)) {
|
||||
executeIsAvailable();
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void executeIsAvailable() {
|
||||
PluginError error = canAuthenticate();
|
||||
if (error != null) {
|
||||
sendError(error);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
|
||||
sendSuccess("biometric");
|
||||
} else {
|
||||
sendSuccess("finger");
|
||||
}
|
||||
}
|
||||
private void executeRegisterBiometricSecret(JSONArray args) {
|
||||
// should at least contains the secret
|
||||
if (args == null) {
|
||||
sendError(PluginError.BIOMETRIC_ARGS_PARSING_FAILED);
|
||||
return;
|
||||
}
|
||||
this.runBiometricActivity(args, BiometricActivityType.REGISTER_SECRET);
|
||||
}
|
||||
|
||||
private void executeLoadBiometricSecret(JSONArray args) {
|
||||
this.runBiometricActivity(args, BiometricActivityType.LOAD_SECRET);
|
||||
}
|
||||
|
||||
private void executeAuthenticate(JSONArray args) {
|
||||
this.runBiometricActivity(args, BiometricActivityType.JUST_AUTHENTICATE);
|
||||
}
|
||||
|
||||
private void runBiometricActivity(JSONArray args, BiometricActivityType type) {
|
||||
PluginError error = canAuthenticate();
|
||||
if (error != null) {
|
||||
sendError(error);
|
||||
return;
|
||||
}
|
||||
cordova.getActivity().runOnUiThread(() -> {
|
||||
mPromptInfoBuilder.parseArgs(args, type);
|
||||
Intent intent = new Intent(cordova.getActivity().getApplicationContext(), BiometricActivity.class);
|
||||
intent.putExtras(mPromptInfoBuilder.build().getBundle());
|
||||
this.cordova.startActivityForResult(this, intent, REQUEST_CODE_BIOMETRIC);
|
||||
});
|
||||
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
|
||||
pluginResult.setKeepCallback(true);
|
||||
this.mCallbackContext.sendPluginResult(pluginResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
if (requestCode != REQUEST_CODE_BIOMETRIC) {
|
||||
return;
|
||||
}
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
sendError(intent);
|
||||
return;
|
||||
}
|
||||
sendSuccess(intent);
|
||||
}
|
||||
|
||||
private void sendSuccess(Intent intent) {
|
||||
if (intent != null && intent.getExtras() != null) {
|
||||
sendSuccess(intent.getExtras().getString(PromptInfo.SECRET_EXTRA));
|
||||
} else {
|
||||
sendSuccess("biometric_success");
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(Intent intent) {
|
||||
if (intent != null) {
|
||||
Bundle extras = intent.getExtras();
|
||||
sendError(extras.getInt("code"), extras.getString("message"));
|
||||
} else {
|
||||
sendError(PluginError.BIOMETRIC_DISMISSED);
|
||||
}
|
||||
}
|
||||
|
||||
private PluginError canAuthenticate() {
|
||||
int error = BiometricManager.from(cordova.getContext()).canAuthenticate();
|
||||
switch (error) {
|
||||
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
|
||||
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
|
||||
return PluginError.BIOMETRIC_HARDWARE_NOT_SUPPORTED;
|
||||
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
|
||||
return PluginError.BIOMETRIC_NOT_ENROLLED;
|
||||
case BiometricManager.BIOMETRIC_SUCCESS:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(int code, String message) {
|
||||
JSONObject resultJson = new JSONObject();
|
||||
try {
|
||||
resultJson.put("code", code);
|
||||
resultJson.put("message", message);
|
||||
|
||||
PluginResult result = new PluginResult(PluginResult.Status.ERROR, resultJson);
|
||||
result.setKeepCallback(true);
|
||||
cordova.getActivity().runOnUiThread(() ->
|
||||
Fingerprint.this.mCallbackContext.sendPluginResult(result));
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(PluginError error) {
|
||||
sendError(error.getValue(), error.getMessage());
|
||||
}
|
||||
|
||||
private void sendSuccess(String message) {
|
||||
cordova.getActivity().runOnUiThread(() ->
|
||||
this.mCallbackContext.success(message));
|
||||
}
|
||||
|
||||
private String getApplicationLabel(Context context) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
ApplicationInfo app = packageManager
|
||||
.getApplicationInfo(context.getPackageName(), 0);
|
||||
return packageManager.getApplicationLabel(app).toString();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
class KeyInvalidatedException extends CryptoException {
|
||||
KeyInvalidatedException() {
|
||||
super(PluginError.BIOMETRIC_NO_SECRET_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
public enum PluginError {
|
||||
|
||||
BIOMETRIC_UNKNOWN_ERROR(-100),
|
||||
BIOMETRIC_AUTHENTICATION_FAILED(-102, "Authentication failed"),
|
||||
BIOMETRIC_HARDWARE_NOT_SUPPORTED(-104),
|
||||
BIOMETRIC_NOT_ENROLLED(-106),
|
||||
BIOMETRIC_DISMISSED(-108),
|
||||
BIOMETRIC_PIN_OR_PATTERN_DISMISSED(-109),
|
||||
BIOMETRIC_SCREEN_GUARD_UNSECURED(-110,
|
||||
"Go to 'Settings -> Security -> Screenlock' to set up a lock screen"),
|
||||
BIOMETRIC_LOCKED_OUT(-111),
|
||||
BIOMETRIC_LOCKED_OUT_PERMANENT(-112),
|
||||
BIOMETRIC_NO_SECRET_FOUND(-113),
|
||||
BIOMETRIC_ARGS_PARSING_FAILED(-115);
|
||||
|
||||
private int value;
|
||||
private String message;
|
||||
|
||||
PluginError(int value) {
|
||||
this.value = value;
|
||||
this.message = this.name();
|
||||
}
|
||||
|
||||
PluginError(int value, String message) {
|
||||
this.value = value;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package de.niklasmerz.cordova.biometric;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
||||
class PromptInfo {
|
||||
|
||||
private static final String DISABLE_BACKUP = "disableBackup";
|
||||
private static final String TITLE = "title";
|
||||
private static final String SUBTITLE = "subtitle";
|
||||
private static final String DESCRIPTION = "description";
|
||||
private static final String FALLBACK_BUTTON_TITLE = "fallbackButtonTitle";
|
||||
private static final String CANCEL_BUTTON_TITLE = "cancelButtonTitle";
|
||||
private static final String CONFIRMATION_REQUIRED = "confirmationRequired";
|
||||
private static final String INVALIDATE_ON_ENROLLMENT = "invalidateOnEnrollment";
|
||||
private static final String SECRET = "secret";
|
||||
private static final String BIOMETRIC_ACTIVITY_TYPE = "biometricActivityType";
|
||||
|
||||
static final String SECRET_EXTRA = "secret";
|
||||
|
||||
private Bundle bundle = new Bundle();
|
||||
|
||||
Bundle getBundle() {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
return bundle.getString(TITLE);
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return bundle.getString(SUBTITLE);
|
||||
}
|
||||
|
||||
String getDescription() {
|
||||
return bundle.getString(DESCRIPTION);
|
||||
}
|
||||
|
||||
boolean isDeviceCredentialAllowed() {
|
||||
return !bundle.getBoolean(DISABLE_BACKUP);
|
||||
}
|
||||
|
||||
String getFallbackButtonTitle() {
|
||||
return bundle.getString(FALLBACK_BUTTON_TITLE);
|
||||
}
|
||||
|
||||
String getCancelButtonTitle() {
|
||||
return bundle.getString(CANCEL_BUTTON_TITLE);
|
||||
}
|
||||
|
||||
boolean getConfirmationRequired() {
|
||||
return bundle.getBoolean(CONFIRMATION_REQUIRED);
|
||||
}
|
||||
|
||||
String getSecret() {
|
||||
return bundle.getString(SECRET);
|
||||
}
|
||||
|
||||
boolean invalidateOnEnrollment() {
|
||||
return bundle.getBoolean(INVALIDATE_ON_ENROLLMENT);
|
||||
}
|
||||
|
||||
BiometricActivityType getType() {
|
||||
return BiometricActivityType.fromValue(bundle.getInt(BIOMETRIC_ACTIVITY_TYPE));
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private static final String TAG = "PromptInfo.Builder";
|
||||
private Bundle bundle;
|
||||
private boolean disableBackup = false;
|
||||
private String title;
|
||||
private String subtitle = null;
|
||||
private String description = null;
|
||||
private String fallbackButtonTitle = "Use backup";
|
||||
private String cancelButtonTitle = "Cancel";
|
||||
private boolean confirmationRequired = true;
|
||||
private boolean invalidateOnEnrollment = false;
|
||||
private String secret = null;
|
||||
private BiometricActivityType type = null;
|
||||
|
||||
Builder(String applicationLabel) {
|
||||
if (applicationLabel == null) {
|
||||
title = "Biometric Sign On";
|
||||
} else {
|
||||
title = applicationLabel + " Biometric Sign On";
|
||||
}
|
||||
}
|
||||
|
||||
Builder(Bundle bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public PromptInfo build() {
|
||||
PromptInfo promptInfo = new PromptInfo();
|
||||
|
||||
if (this.bundle != null) {
|
||||
promptInfo.bundle = bundle;
|
||||
return promptInfo;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(SUBTITLE, this.subtitle);
|
||||
bundle.putString(TITLE, this.title);
|
||||
bundle.putString(DESCRIPTION, this.description);
|
||||
bundle.putString(FALLBACK_BUTTON_TITLE, this.fallbackButtonTitle);
|
||||
bundle.putString(CANCEL_BUTTON_TITLE, this.cancelButtonTitle);
|
||||
bundle.putString(SECRET, this.secret);
|
||||
bundle.putBoolean(DISABLE_BACKUP, this.disableBackup);
|
||||
bundle.putBoolean(CONFIRMATION_REQUIRED, this.confirmationRequired);
|
||||
bundle.putBoolean(INVALIDATE_ON_ENROLLMENT, this.invalidateOnEnrollment);
|
||||
bundle.putInt(BIOMETRIC_ACTIVITY_TYPE, this.type.getValue());
|
||||
promptInfo.bundle = bundle;
|
||||
|
||||
return promptInfo;
|
||||
}
|
||||
|
||||
void parseArgs(JSONArray jsonArgs, BiometricActivityType type) {
|
||||
this.type = type;
|
||||
|
||||
Args args = new Args(jsonArgs);
|
||||
disableBackup = args.getBoolean(DISABLE_BACKUP, disableBackup);
|
||||
title = args.getString(TITLE, title);
|
||||
subtitle = args.getString(SUBTITLE, subtitle);
|
||||
description = args.getString(DESCRIPTION, description);
|
||||
fallbackButtonTitle = args.getString(FALLBACK_BUTTON_TITLE, "Use Backup");
|
||||
cancelButtonTitle = args.getString(CANCEL_BUTTON_TITLE, "Cancel");
|
||||
confirmationRequired = args.getBoolean(CONFIRMATION_REQUIRED, confirmationRequired);
|
||||
invalidateOnEnrollment = args.getBoolean(INVALIDATE_ON_ENROLLMENT, false);
|
||||
secret = args.getString(SECRET, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,926 @@
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2017 sitewaerts GmbH. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package de.sitewaerts.cordova.documentviewer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import org.apache.cordova.*;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
//15: Android 4.0.3
|
||||
//19: Android 4.4.2
|
||||
//25: Android 7
|
||||
@TargetApi(29)
|
||||
public final class DocumentViewerPlugin
|
||||
extends CordovaPlugin
|
||||
{
|
||||
|
||||
private static final String TAG = "DocumentViewerPlugin";
|
||||
|
||||
public static final class Actions
|
||||
{
|
||||
|
||||
static final String GET_SUPPORT_INFO = "getSupportInfo";
|
||||
|
||||
static final String CAN_VIEW = "canViewDocument";
|
||||
|
||||
static final String VIEW_DOCUMENT = "viewDocument";
|
||||
|
||||
static final String CLOSE = "close";
|
||||
|
||||
static final String APP_PAUSED = "appPaused";
|
||||
|
||||
static final String APP_RESUMED = "appResumed";
|
||||
|
||||
static final String INSTALL_VIEWER_APP = "install";
|
||||
|
||||
}
|
||||
|
||||
public static final class Args
|
||||
{
|
||||
public static final String URL = "url";
|
||||
|
||||
static final String CONTENT_TYPE = "contentType";
|
||||
|
||||
static final String OPTIONS = "options";
|
||||
}
|
||||
|
||||
private static final String ANDROID_OPTIONS = "android";
|
||||
private static final String DOCUMENTVIEW_OPTIONS = "documentView";
|
||||
private static final String NAVIGATIONVIEW_OPTIONS = "navigationView";
|
||||
private static final String EMAIL_OPTIONS = "email";
|
||||
private static final String PRINT_OPTIONS = "print";
|
||||
private static final String OPENWITH_OPTIONS = "openWith";
|
||||
private static final String BOOKMARKS_OPTIONS = "bookmarks";
|
||||
private static final String SEARCH_OPTIONS = "search";
|
||||
private static final String TITLE_OPTIONS = "title";
|
||||
|
||||
public static final class Options
|
||||
{
|
||||
static final String VIEWER_APP_PACKAGE_ID = "viewerAppPackage";
|
||||
|
||||
static final String VIEWER_APP_ACTIVITY = "viewerAppActivity";
|
||||
|
||||
static final String CLOSE_LABEL = "closeLabel";
|
||||
|
||||
static final String ENABLED = "enabled";
|
||||
}
|
||||
|
||||
public static final class AutoCloseOptions
|
||||
{
|
||||
static final String NAME = "autoClose";
|
||||
|
||||
static final String OPTION_ON_PAUSE = "onPause";
|
||||
|
||||
}
|
||||
|
||||
public static final String PDF = "application/pdf";
|
||||
|
||||
public static final class Result
|
||||
{
|
||||
static final String SUPPORTED = "supported";
|
||||
|
||||
static final String STATUS = "status";
|
||||
|
||||
static final String MESSAGE = "message";
|
||||
|
||||
static final String DETAILS = "details";
|
||||
|
||||
static final String MISSING_APP_ID = "missingAppId";
|
||||
}
|
||||
|
||||
private static final int REQUEST_CODE_OPEN = 1000;
|
||||
|
||||
private static final int REQUEST_CODE_INSTALL = 1001;
|
||||
|
||||
private CallbackContext callbackContext;
|
||||
|
||||
|
||||
public void initialize(CordovaInterface cordova, CordovaWebView webView)
|
||||
{
|
||||
super.initialize(cordova, webView);
|
||||
clearTempFiles();
|
||||
}
|
||||
|
||||
public void onDestroy()
|
||||
{
|
||||
clearTempFiles();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public void onReset()
|
||||
{
|
||||
clearTempFiles();
|
||||
super.onReset();
|
||||
}
|
||||
|
||||
private final class Current
|
||||
{
|
||||
private final String packageId;
|
||||
private final String activity;
|
||||
private final String url;
|
||||
|
||||
public Current(String packageId, String activity, String url)
|
||||
{
|
||||
this.packageId = packageId;
|
||||
this.activity = activity;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
private Current current;
|
||||
|
||||
/**
|
||||
* Executes the request and returns a boolean.
|
||||
*
|
||||
* @param action The action to execute.
|
||||
* @param argsArray JSONArray of arguments for the plugin.
|
||||
* @param callbackContext The callback context used when calling back into JavaScript.
|
||||
* @return boolean.
|
||||
*/
|
||||
public boolean execute(final String action, final JSONArray argsArray, final CallbackContext callbackContext)
|
||||
{
|
||||
cordova.getThreadPool().execute(new Runnable()
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
doExecute(action, argsArray, callbackContext);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
handleException(e, action, argsArray, callbackContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void handleException(Exception e, String action, JSONArray argsArray, CallbackContext callbackContext)
|
||||
{
|
||||
e.printStackTrace();
|
||||
|
||||
try
|
||||
{
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put(Result.STATUS,
|
||||
PluginResult.Status.ERROR.ordinal()
|
||||
);
|
||||
errorObj.put(Result.MESSAGE, e.getMessage());
|
||||
errorObj.put(Result.DETAILS, getStackTrace(e));
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
catch (JSONException e1)
|
||||
{
|
||||
// should never happen
|
||||
e1.printStackTrace();
|
||||
callbackContext.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getStackTrace(Throwable t)
|
||||
{
|
||||
if (t == null)
|
||||
return "";
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
t.printStackTrace(pw);
|
||||
|
||||
try
|
||||
{
|
||||
pw.close();
|
||||
sw.flush();
|
||||
sw.close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignorieren
|
||||
}
|
||||
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
private void doExecute(String action, JSONArray argsArray, CallbackContext callbackContext)
|
||||
throws JSONException
|
||||
{
|
||||
JSONObject args;
|
||||
JSONObject options;
|
||||
if (argsArray.length() > 0)
|
||||
{
|
||||
args = argsArray.getJSONObject(0);
|
||||
options = args.getJSONObject(Args.OPTIONS);
|
||||
}
|
||||
else
|
||||
{
|
||||
//no arguments passed, initialize with empty JSON Objects
|
||||
args = new JSONObject();
|
||||
options = new JSONObject();
|
||||
}
|
||||
|
||||
if (action.equals(Actions.VIEW_DOCUMENT))
|
||||
{
|
||||
String url = args.getString(Args.URL);
|
||||
String contentType = args.getString(Args.CONTENT_TYPE);
|
||||
|
||||
JSONObject androidOptions = options.getJSONObject(ANDROID_OPTIONS);
|
||||
|
||||
String packageId = androidOptions.getString(
|
||||
Options.VIEWER_APP_PACKAGE_ID
|
||||
);
|
||||
String activity = androidOptions.getString(
|
||||
Options.VIEWER_APP_ACTIVITY
|
||||
);
|
||||
//put cordova arguments into Android Bundle in order to pass them to the external Activity
|
||||
Bundle viewerOptions = new Bundle();
|
||||
//exec
|
||||
viewerOptions
|
||||
.putString(DOCUMENTVIEW_OPTIONS + "." + Options.CLOSE_LABEL,
|
||||
options.getJSONObject(DOCUMENTVIEW_OPTIONS)
|
||||
.getString(Options.CLOSE_LABEL)
|
||||
);
|
||||
viewerOptions.putString(
|
||||
NAVIGATIONVIEW_OPTIONS + "." + Options.CLOSE_LABEL,
|
||||
options.getJSONObject(NAVIGATIONVIEW_OPTIONS)
|
||||
.getString(Options.CLOSE_LABEL)
|
||||
);
|
||||
viewerOptions.putBoolean(EMAIL_OPTIONS + "." + Options.ENABLED,
|
||||
options.getJSONObject(EMAIL_OPTIONS)
|
||||
.optBoolean(Options.ENABLED, false)
|
||||
);
|
||||
viewerOptions.putBoolean(PRINT_OPTIONS + "." + Options.ENABLED,
|
||||
options.getJSONObject(PRINT_OPTIONS)
|
||||
.optBoolean(Options.ENABLED, false)
|
||||
);
|
||||
viewerOptions.putBoolean(OPENWITH_OPTIONS + "." + Options.ENABLED,
|
||||
options.getJSONObject(OPENWITH_OPTIONS)
|
||||
.optBoolean(Options.ENABLED, false)
|
||||
);
|
||||
viewerOptions.putBoolean(BOOKMARKS_OPTIONS + "." + Options.ENABLED,
|
||||
options.getJSONObject(BOOKMARKS_OPTIONS)
|
||||
.optBoolean(Options.ENABLED, false)
|
||||
);
|
||||
viewerOptions.putBoolean(SEARCH_OPTIONS + "." + Options.ENABLED,
|
||||
options.getJSONObject(SEARCH_OPTIONS)
|
||||
.optBoolean(Options.ENABLED, false)
|
||||
);
|
||||
viewerOptions.putBoolean(AutoCloseOptions.NAME + "."
|
||||
+ AutoCloseOptions.OPTION_ON_PAUSE,
|
||||
options.getJSONObject(AutoCloseOptions.NAME)
|
||||
.optBoolean(AutoCloseOptions.OPTION_ON_PAUSE, false)
|
||||
);
|
||||
viewerOptions
|
||||
.putString(TITLE_OPTIONS, options.getString(TITLE_OPTIONS));
|
||||
|
||||
this._open(url, contentType, packageId, activity,
|
||||
callbackContext,
|
||||
viewerOptions
|
||||
);
|
||||
}
|
||||
else if (action.equals(Actions.CLOSE))
|
||||
{
|
||||
this._close(callbackContext);
|
||||
}
|
||||
else if (action.equals(Actions.APP_PAUSED))
|
||||
{
|
||||
this._ignore(callbackContext);
|
||||
}
|
||||
else if (action.equals(Actions.APP_RESUMED))
|
||||
{
|
||||
this._ignore(callbackContext);
|
||||
}
|
||||
else if (action.equals(Actions.INSTALL_VIEWER_APP))
|
||||
{
|
||||
String packageId = options.getJSONObject(ANDROID_OPTIONS).getString(
|
||||
Options.VIEWER_APP_PACKAGE_ID
|
||||
);
|
||||
|
||||
this._install(packageId, callbackContext);
|
||||
}
|
||||
else if (action.equals(Actions.CAN_VIEW))
|
||||
{
|
||||
String url = args.getString(Args.URL);
|
||||
|
||||
String contentType = args.getString(Args.CONTENT_TYPE);
|
||||
|
||||
JSONObject androidOptions = options.getJSONObject(ANDROID_OPTIONS);
|
||||
|
||||
String packageId = androidOptions.getString(
|
||||
Options.VIEWER_APP_PACKAGE_ID
|
||||
);
|
||||
|
||||
final JSONObject successObj = new JSONObject();
|
||||
if (PDF.equals(contentType))
|
||||
{
|
||||
if (canGetFile(url))
|
||||
{
|
||||
if (!this._appIsInstalled(packageId))
|
||||
{
|
||||
successObj.put(Result.STATUS,
|
||||
PluginResult.Status.NO_RESULT.ordinal()
|
||||
);
|
||||
successObj.put(Result.MISSING_APP_ID, packageId);
|
||||
}
|
||||
else
|
||||
{
|
||||
successObj.put(Result.STATUS,
|
||||
PluginResult.Status.OK.ordinal()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String message = "File '" + url + "' is not available (cannot access file)";
|
||||
Log.d(TAG, message);
|
||||
successObj.put(Result.STATUS,
|
||||
PluginResult.Status.NO_RESULT.ordinal()
|
||||
);
|
||||
successObj.put(Result.MESSAGE, message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String message =
|
||||
"Content type '" + contentType + "' is not supported";
|
||||
Log.d(TAG, message);
|
||||
successObj.put(Result.STATUS,
|
||||
PluginResult.Status.NO_RESULT.ordinal()
|
||||
);
|
||||
successObj.put(Result.MESSAGE, message);
|
||||
}
|
||||
|
||||
callbackContext.success(successObj);
|
||||
}
|
||||
else if (action.equals(Actions.GET_SUPPORT_INFO))
|
||||
{
|
||||
JSONObject successObj = new JSONObject();
|
||||
JSONArray supported = new JSONArray();
|
||||
supported.put(PDF);
|
||||
successObj.put(Result.SUPPORTED, supported);
|
||||
callbackContext.success(successObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put(Result.STATUS,
|
||||
PluginResult.Status.INVALID_ACTION.ordinal()
|
||||
);
|
||||
errorObj.put(Result.MESSAGE, "Invalid action '" + action + "'");
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when a previously started Activity ends
|
||||
*
|
||||
* @param requestCode The request code originally supplied to startActivityForResult(),
|
||||
* allowing you to identify who this result came from.
|
||||
* @param resultCode The integer result code returned by the child activity through its setResult().
|
||||
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
|
||||
*/
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent intent)
|
||||
{
|
||||
if (this.callbackContext == null)
|
||||
return;
|
||||
|
||||
if (requestCode == REQUEST_CODE_OPEN)
|
||||
{
|
||||
this.current = null;
|
||||
//remove tmp file
|
||||
clearTempFiles();
|
||||
try
|
||||
{
|
||||
// send closed event
|
||||
JSONObject successObj = new JSONObject();
|
||||
successObj.put(Result.STATUS,
|
||||
PluginResult.Status.NO_RESULT.ordinal()
|
||||
);
|
||||
this.callbackContext.success(successObj);
|
||||
}
|
||||
catch (JSONException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.callbackContext = null;
|
||||
}
|
||||
else if (requestCode == REQUEST_CODE_INSTALL)
|
||||
{
|
||||
this.current = null;
|
||||
// send success event
|
||||
this.callbackContext.success();
|
||||
this.callbackContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void _ignore(CallbackContext callbackContext)
|
||||
{
|
||||
// ignore
|
||||
callbackContext.success();
|
||||
}
|
||||
|
||||
private void _close(CallbackContext callbackContext)
|
||||
{
|
||||
if (current == null)
|
||||
{
|
||||
callbackContext.success();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.cordova.getActivity().finishActivity(REQUEST_CODE_OPEN);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
this.current = null;
|
||||
callbackContext.success();
|
||||
|
||||
}
|
||||
|
||||
private void _open(String url, String contentType, String packageId, String activity, CallbackContext callbackContext, Bundle viewerOptions)
|
||||
throws JSONException
|
||||
{
|
||||
clearTempFiles();
|
||||
|
||||
File file = getAccessibleFile(url);
|
||||
|
||||
if (file != null && file.exists() && file.isFile())
|
||||
{
|
||||
try
|
||||
{
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
||||
// @see http://stackoverflow.com/questions/2780102/open-another-application-from-your-own-intent
|
||||
intent.addCategory(Intent.CATEGORY_EMBED);
|
||||
|
||||
|
||||
if (newApi())
|
||||
{
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
Uri contentUri = FileProvider.getUriForFile(
|
||||
webView.getContext(),
|
||||
cordova.getActivity().getPackageName() + "." + TAG
|
||||
+ ".fileprovider",
|
||||
file
|
||||
);
|
||||
intent.setDataAndType(contentUri, contentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
intent.setDataAndType(Uri.fromFile(file), contentType);
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.putExtra(this.getClass().getName(), viewerOptions);
|
||||
//activity needs fully qualified name here
|
||||
intent.setComponent(
|
||||
new ComponentName(packageId, packageId + "." + activity)
|
||||
);
|
||||
|
||||
this.callbackContext = callbackContext;
|
||||
this.cordova.startActivityForResult(this, intent,
|
||||
REQUEST_CODE_OPEN
|
||||
);
|
||||
|
||||
this.current = new Current(packageId, activity, url);
|
||||
|
||||
// send shown event
|
||||
JSONObject successObj = new JSONObject();
|
||||
successObj.put(Result.STATUS, PluginResult.Status.OK.ordinal());
|
||||
PluginResult result = new PluginResult(PluginResult.Status.OK,
|
||||
successObj
|
||||
);
|
||||
// need to keep callback for close event
|
||||
result.setKeepCallback(true);
|
||||
callbackContext.sendPluginResult(result);
|
||||
}
|
||||
catch (android.content.ActivityNotFoundException e)
|
||||
{
|
||||
this.current = null;
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put(Result.STATUS, PluginResult.Status.ERROR.ordinal()
|
||||
);
|
||||
errorObj.put(Result.MESSAGE,
|
||||
"Activity not found: " + e.getMessage()
|
||||
);
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.current = null;
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put(Result.STATUS, PluginResult.Status.ERROR.ordinal());
|
||||
errorObj.put(Result.MESSAGE, "File '" + url + "' is not available (Cannot create accessible file).");
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFile(File src, File target)
|
||||
throws IOException
|
||||
{
|
||||
// Log.d(TAG, "Creating temp file for " + src.getAbsolutePath()
|
||||
// + " at " + target.getAbsolutePath());
|
||||
copyFile(new FileInputStream(src), target);
|
||||
}
|
||||
|
||||
private void copyFile(InputStream in, File target)
|
||||
throws IOException
|
||||
{
|
||||
OutputStream out = null;
|
||||
//create tmp folder if not present
|
||||
if (!target.getParentFile().exists()
|
||||
&& !target.getParentFile().mkdirs())
|
||||
throw new IOException("Cannot create path " + target.getParentFile()
|
||||
.getAbsolutePath()
|
||||
);
|
||||
try
|
||||
{
|
||||
out = new FileOutputStream(target);
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1)
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e(TAG, "Failed to copy stream to "
|
||||
+ target.getAbsolutePath(), e
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (in != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
in.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// NOOP
|
||||
}
|
||||
}
|
||||
if (out != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
out.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// NOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private int tempCounter = 0;
|
||||
|
||||
private File getSharedTempFile(String name)
|
||||
{
|
||||
return new File(getSharedTempDir(), (tempCounter++) + "." + name);
|
||||
}
|
||||
|
||||
private File getSharedTempDir()
|
||||
{
|
||||
if (newApi())
|
||||
{
|
||||
return new File(
|
||||
new File(cordova.getActivity().getCacheDir(), "tmp"), TAG
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new File(
|
||||
new File(cordova.getActivity().getExternalFilesDir(null),
|
||||
"tmp"
|
||||
), TAG
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearTempFiles()
|
||||
{
|
||||
File dir = getSharedTempDir();
|
||||
if (!dir.exists())
|
||||
return;
|
||||
|
||||
//Log.d(TAG, "clearing temp files below " + dir.getAbsolutePath());
|
||||
deleteRecursive(dir, false);
|
||||
}
|
||||
|
||||
private void deleteRecursive(File f, boolean self)
|
||||
{
|
||||
if (!f.exists())
|
||||
return;
|
||||
|
||||
if (f.isDirectory())
|
||||
{
|
||||
File[] files = f.listFiles();
|
||||
for (File file : files)
|
||||
deleteRecursive(file, true);
|
||||
}
|
||||
|
||||
if (self && !f.delete())
|
||||
Log.e(TAG, "Failed to delete file " + f.getAbsoluteFile());
|
||||
|
||||
}
|
||||
|
||||
private static final String ASSETS = "file:///android_asset/";
|
||||
|
||||
private boolean canGetFile(String fileArg)
|
||||
{
|
||||
// TODO: better check for assets files ...
|
||||
if(fileArg.startsWith(ASSETS))
|
||||
return true;
|
||||
File f = getFile(fileArg);
|
||||
return f != null && f.exists();
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private boolean newApi()
|
||||
{
|
||||
/*
|
||||
|
||||
see https://github.com/sitewaerts/cordova-plugin-document-viewer/issues/76
|
||||
|
||||
This is due to backwards compatibility with the android viewer app (https://github.com/sitewaerts/android-document-viewer and https://play.google.com/store/apps/details?id=de.sitewaerts.cleverdox.viewer).
|
||||
|
||||
- The outdated version 1.1.2 (minSDK 15 = 4.0.3/Ice Cream Sandwich) will be still be delivered to some (probably old) devices.
|
||||
+ supports old style access via public files
|
||||
- The current version 1.2.0 (minSDK 16 = 4.2/Jelly Bean) will be delivered to all other devices.
|
||||
+ supports the FileProvider API.
|
||||
+ supports old style access via public files
|
||||
+ Starting with Nougat the usage of the FileProvider API is obligatory, the old style access via public files won't work anymore.
|
||||
*/
|
||||
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
|
||||
}
|
||||
|
||||
private File getAccessibleFile(String fileArg)
|
||||
throws JSONException
|
||||
{
|
||||
if (newApi())
|
||||
return getAccessibleFileNew(fileArg);
|
||||
else
|
||||
return getAccessibleFileOld(fileArg);
|
||||
}
|
||||
|
||||
private void close(Closeable c)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (c != null)
|
||||
c.close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private File getAccessibleFileNew(String fileArg)
|
||||
throws JSONException
|
||||
{
|
||||
CordovaResourceApi cra = webView.getResourceApi();
|
||||
Uri uri = Uri.parse(fileArg);
|
||||
OutputStream os = null;
|
||||
try
|
||||
{
|
||||
String fileName = new File(uri.getPath()).getName();
|
||||
File tmpFile = getSharedTempFile(fileName);
|
||||
if(!tmpFile.getParentFile().exists()
|
||||
&& !tmpFile.getParentFile().mkdirs())
|
||||
throw new IOException("mkdirs "
|
||||
+ tmpFile.getParentFile().getAbsolutePath()
|
||||
+ " failed.");
|
||||
os = new FileOutputStream(tmpFile);
|
||||
cra.copyResource(uri, os);
|
||||
tmpFile.deleteOnExit();
|
||||
return tmpFile;
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
return null; // not found
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e(TAG, "Failed to copy file: " + fileArg, e);
|
||||
JSONException je = new JSONException(e.getMessage());
|
||||
je.initCause(e);
|
||||
throw je;
|
||||
}
|
||||
finally
|
||||
{
|
||||
close(os);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private File getAccessibleFileOld(String fileArg)
|
||||
throws JSONException
|
||||
{
|
||||
if (fileArg.startsWith(ASSETS))
|
||||
{
|
||||
String filePath = fileArg.substring(ASSETS.length());
|
||||
String fileName = filePath.substring(
|
||||
filePath.lastIndexOf(File.pathSeparator) + 1);
|
||||
|
||||
//Log.d(TAG, "Handling assets file: fileArg: " + fileArg + ", filePath: " + filePath + ", fileName: " + fileName);
|
||||
|
||||
try
|
||||
{
|
||||
File tmpFile = getSharedTempFile(fileName);
|
||||
InputStream in;
|
||||
try
|
||||
{
|
||||
in = this.cordova.getActivity().getAssets().open(filePath);
|
||||
if (in == null)
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// not found
|
||||
return null;
|
||||
}
|
||||
copyFile(in, tmpFile);
|
||||
tmpFile.deleteOnExit();
|
||||
return tmpFile;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e(TAG, "Failed to copy file: " + filePath, e);
|
||||
JSONException je = new JSONException(e.getMessage());
|
||||
je.initCause(e);
|
||||
throw je;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
File file = getFile(fileArg);
|
||||
if (file == null || !file.exists() || !file.isFile())
|
||||
return null;
|
||||
|
||||
// detect private files, copy to accessible tmp dir if necessary
|
||||
// XXX does this condition cover all cases?
|
||||
if (file.getAbsolutePath().contains(
|
||||
cordova.getActivity().getFilesDir().getAbsolutePath()
|
||||
))
|
||||
{
|
||||
// XXX this is the "official" way to share private files with other apps: with a content:// URI. Unfortunately, MuPDF does not swallow the generated URI. :(
|
||||
// path = FileProvider.getUriForFile(cordova.getActivity(), "de.sitewaerts.cordova.fileprovider", file);
|
||||
// cordova.getActivity().grantUriPermission(packageId, path, Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
|
||||
try
|
||||
{
|
||||
File tmpFile = getSharedTempFile(file.getName());
|
||||
copyFile(file, tmpFile);
|
||||
tmpFile.deleteOnExit();
|
||||
return tmpFile;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e(TAG, "Failed to copy file: " + file.getName(), e);
|
||||
JSONException je = new JSONException(e.getMessage());
|
||||
je.initCause(e);
|
||||
throw je;
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private File getFile(String fileArg)
|
||||
{
|
||||
if (newApi())
|
||||
return getFileNew(fileArg);
|
||||
else
|
||||
return getFileOld(fileArg);
|
||||
}
|
||||
|
||||
private File getFileNew(String fileArg)
|
||||
{
|
||||
CordovaResourceApi cra = webView.getResourceApi();
|
||||
Uri uri = Uri.parse(fileArg);
|
||||
return cra.mapUriToFile(uri);
|
||||
}
|
||||
|
||||
private File getFileOld(String fileArg)
|
||||
{
|
||||
String filePath;
|
||||
try
|
||||
{
|
||||
CordovaResourceApi resourceApi = webView.getResourceApi();
|
||||
Uri fileUri = resourceApi.remapUri(Uri.parse(fileArg));
|
||||
filePath = this.stripFileProtocol(fileUri.toString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
filePath = fileArg;
|
||||
}
|
||||
return new File(filePath);
|
||||
}
|
||||
|
||||
private void _install(String packageId, CallbackContext callbackContext)
|
||||
throws JSONException
|
||||
{
|
||||
if (!this._appIsInstalled(packageId))
|
||||
{
|
||||
this.callbackContext = callbackContext;
|
||||
|
||||
try
|
||||
{
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=" + packageId)
|
||||
);
|
||||
this.cordova.startActivityForResult(this, intent,
|
||||
REQUEST_CODE_INSTALL
|
||||
);
|
||||
}
|
||||
catch (android.content.ActivityNotFoundException e)
|
||||
{
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse(
|
||||
"https://play.google.com/store/apps/details?id="
|
||||
+ packageId)
|
||||
);
|
||||
this.cordova.startActivityForResult(this, intent,
|
||||
REQUEST_CODE_INSTALL
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put(Result.STATUS, PluginResult.Status.ERROR.ordinal());
|
||||
errorObj.put(Result.MESSAGE,
|
||||
"Package " + packageId + " already installed"
|
||||
);
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean _appIsInstalled(String packageId)
|
||||
{
|
||||
PackageManager pm = cordova.getActivity().getPackageManager();
|
||||
try
|
||||
{
|
||||
pm.getPackageInfo(packageId, PackageManager.GET_ACTIVITIES);
|
||||
return true;
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String stripFileProtocol(String uriString)
|
||||
{
|
||||
if (uriString.startsWith("file://"))
|
||||
uriString = uriString.substring(7);
|
||||
return uriString;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package de.sitewaerts.cordova.documentviewer;
|
||||
|
||||
public class FileProvider extends androidx.core.content.FileProvider {
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 pwlin - pwlin05@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package io.github.pwlin.cordova.plugins.fileopener2;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import io.github.pwlin.cordova.plugins.fileopener2.FileProvider;
|
||||
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.apache.cordova.CordovaResourceApi;
|
||||
|
||||
public class FileOpener2 extends CordovaPlugin {
|
||||
|
||||
/**
|
||||
* Executes the request and returns a boolean.
|
||||
*
|
||||
* @param action
|
||||
* The action to execute.
|
||||
* @param args
|
||||
* JSONArry of arguments for the plugin.
|
||||
* @param callbackContext
|
||||
* The callback context used when calling back into JavaScript.
|
||||
* @return boolean.
|
||||
*/
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
if (action.equals("open")) {
|
||||
String fileUrl = args.getString(0);
|
||||
String contentType = args.getString(1);
|
||||
Boolean openWithDefault = true;
|
||||
if(args.length() > 2){
|
||||
openWithDefault = args.getBoolean(2);
|
||||
}
|
||||
this._open(fileUrl, contentType, openWithDefault, callbackContext);
|
||||
}
|
||||
else if (action.equals("uninstall")) {
|
||||
this._uninstall(args.getString(0), callbackContext);
|
||||
}
|
||||
else if (action.equals("appIsInstalled")) {
|
||||
JSONObject successObj = new JSONObject();
|
||||
if (this._appIsInstalled(args.getString(0))) {
|
||||
successObj.put("status", PluginResult.Status.OK.ordinal());
|
||||
successObj.put("message", "Installed");
|
||||
}
|
||||
else {
|
||||
successObj.put("status", PluginResult.Status.NO_RESULT.ordinal());
|
||||
successObj.put("message", "Not installed");
|
||||
}
|
||||
callbackContext.success(successObj);
|
||||
}
|
||||
else {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("status", PluginResult.Status.INVALID_ACTION.ordinal());
|
||||
errorObj.put("message", "Invalid action");
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void _open(String fileArg, String contentType, Boolean openWithDefault, CallbackContext callbackContext) throws JSONException {
|
||||
String fileName = "";
|
||||
try {
|
||||
CordovaResourceApi resourceApi = webView.getResourceApi();
|
||||
Uri fileUri = resourceApi.remapUri(Uri.parse(fileArg));
|
||||
fileName = fileUri.getPath();
|
||||
} catch (Exception e) {
|
||||
fileName = fileArg;
|
||||
}
|
||||
File file = new File(fileName);
|
||||
if (file.exists()) {
|
||||
try {
|
||||
if (contentType == null || contentType.trim().equals("")) {
|
||||
contentType = _getMimeType(fileName);
|
||||
}
|
||||
|
||||
Intent intent;
|
||||
if (contentType.equals("application/vnd.android.package-archive")) {
|
||||
// https://stackoverflow.com/questions/9637629/can-we-install-an-apk-from-a-contentprovider/9672282#9672282
|
||||
intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
Uri path;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
path = Uri.fromFile(file);
|
||||
} else {
|
||||
Context context = cordova.getActivity().getApplicationContext();
|
||||
path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".fileOpener2.provider", file);
|
||||
}
|
||||
intent.setDataAndType(path, contentType);
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
} else {
|
||||
intent = new Intent(Intent.ACTION_VIEW);
|
||||
Context context = cordova.getActivity().getApplicationContext();
|
||||
Uri path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".fileOpener2.provider", file);
|
||||
intent.setDataAndType(path, contentType);
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* @see
|
||||
* http://stackoverflow.com/questions/14321376/open-an-activity-from-a-cordovaplugin
|
||||
*/
|
||||
if(openWithDefault){
|
||||
cordova.getActivity().startActivity(intent);
|
||||
}
|
||||
else{
|
||||
cordova.getActivity().startActivity(Intent.createChooser(intent, "Open File in..."));
|
||||
}
|
||||
|
||||
callbackContext.success();
|
||||
} catch (android.content.ActivityNotFoundException e) {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("status", PluginResult.Status.ERROR.ordinal());
|
||||
errorObj.put("message", "Activity not found: " + e.getMessage());
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
} else {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("status", PluginResult.Status.ERROR.ordinal());
|
||||
errorObj.put("message", "File not found");
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
private String _getMimeType(String url) {
|
||||
String mimeType = "*/*";
|
||||
int extensionIndex = url.lastIndexOf('.');
|
||||
if (extensionIndex > 0) {
|
||||
String extMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(url.substring(extensionIndex+1));
|
||||
if (extMimeType != null) {
|
||||
mimeType = extMimeType;
|
||||
}
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
private void _uninstall(String packageId, CallbackContext callbackContext) throws JSONException {
|
||||
if (this._appIsInstalled(packageId)) {
|
||||
Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
|
||||
intent.setData(Uri.parse("package:" + packageId));
|
||||
cordova.getActivity().startActivity(intent);
|
||||
callbackContext.success();
|
||||
}
|
||||
else {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("status", PluginResult.Status.ERROR.ordinal());
|
||||
errorObj.put("message", "This package is not installed");
|
||||
callbackContext.error(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean _appIsInstalled(String packageId) {
|
||||
PackageManager pm = cordova.getActivity().getPackageManager();
|
||||
boolean appInstalled = false;
|
||||
try {
|
||||
pm.getPackageInfo(packageId, PackageManager.GET_ACTIVITIES);
|
||||
appInstalled = true;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
appInstalled = false;
|
||||
}
|
||||
return appInstalled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 pwlin - pwlin05@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package io.github.pwlin.cordova.plugins.fileopener2;
|
||||
|
||||
/*
|
||||
* http://stackoverflow.com/questions/40746144/error-with-duplicated-fileprovider-in-manifest-xml-with-cordova/41550634#41550634
|
||||
*/
|
||||
public class FileProvider extends androidx.core.content.FileProvider {
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
|
||||
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||
* Copyright (c) 2010, IBM Corporation
|
||||
*/
|
||||
|
||||
package io.sqlc;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWindow;
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
// no longer needed - for pre-Honeycomb NO LONGER SUPPORTED:
|
||||
// import android.database.sqlite.SQLiteCursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.lang.IllegalArgumentException;
|
||||
import java.lang.Number;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Android Database helper class
|
||||
*/
|
||||
class SQLiteAndroidDatabase
|
||||
{
|
||||
private static final Pattern FIRST_WORD = Pattern.compile("^[\\s;]*([^\\s;]+)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern WHERE_CLAUSE = Pattern.compile("\\s+WHERE\\s+(.+)$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern UPDATE_TABLE_NAME = Pattern.compile("^\\s*UPDATE\\s+(\\S+)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern DELETE_TABLE_NAME = Pattern.compile("^\\s*DELETE\\s+FROM\\s+(\\S+)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final boolean isPostHoneycomb = android.os.Build.VERSION.SDK_INT >= 11;
|
||||
|
||||
File dbFile;
|
||||
|
||||
SQLiteDatabase mydb;
|
||||
|
||||
boolean isTransactionActive = false;
|
||||
|
||||
/**
|
||||
* NOTE: Using default constructor, no explicit constructor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Open a database.
|
||||
*
|
||||
* @param dbfile The database File specification
|
||||
*/
|
||||
void open(File dbfile) throws Exception {
|
||||
if (!isPostHoneycomb) {
|
||||
Log.v("SQLiteAndroidDatabase.open",
|
||||
"INTERNAL PLUGIN ERROR: deprecated android.os.Build.VERSION not supported: " +
|
||||
android.os.Build.VERSION.SDK_INT);
|
||||
throw new RuntimeException(
|
||||
"INTERNAL PLUGIN ERROR: deprecated android.os.Build.VERSION not supported: " +
|
||||
android.os.Build.VERSION.SDK_INT);
|
||||
}
|
||||
dbFile = dbfile; // for possible bug workaround
|
||||
mydb = SQLiteDatabase.openOrCreateDatabase(dbfile, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a database (in the current thread).
|
||||
*/
|
||||
void closeDatabaseNow() {
|
||||
if (mydb != null) {
|
||||
if (isTransactionActive) {
|
||||
try {
|
||||
mydb.endTransaction();
|
||||
} catch (Exception ex) {
|
||||
Log.v("closeDatabaseNow", "INTERNAL PLUGIN ERROR IGNORED: Not able to end active transaction before closing database: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
isTransactionActive = false;
|
||||
}
|
||||
mydb.close();
|
||||
mydb = null;
|
||||
}
|
||||
}
|
||||
|
||||
void bugWorkaround() throws Exception {
|
||||
this.closeDatabaseNow();
|
||||
this.open(dbFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a batch request and sends the results via cbc.
|
||||
*
|
||||
* @param queryarr Array of query strings
|
||||
* @param jsonparamsArr Array of JSON query parameters
|
||||
* @param cbc Callback context from Cordova API
|
||||
*/
|
||||
void executeSqlBatch(String[] queryarr, JSONArray[] jsonparamsArr, CallbackContext cbc) {
|
||||
|
||||
if (mydb == null) {
|
||||
// not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database
|
||||
// (internal plugin error)
|
||||
cbc.error("INTERNAL PLUGIN ERROR: database not open");
|
||||
return;
|
||||
}
|
||||
|
||||
int len = queryarr.length;
|
||||
JSONArray batchResults = new JSONArray();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
executeSqlBatchStatement(queryarr[i], jsonparamsArr[i], batchResults);
|
||||
}
|
||||
|
||||
cbc.success(batchResults);
|
||||
}
|
||||
|
||||
private void executeSqlBatchStatement(String query, JSONArray json_params, JSONArray batchResults) {
|
||||
|
||||
if (mydb == null) {
|
||||
// Should not happen here
|
||||
return;
|
||||
|
||||
} else {
|
||||
|
||||
int rowsAffectedCompat = 0;
|
||||
boolean needRowsAffectedCompat = false;
|
||||
|
||||
JSONObject queryResult = null;
|
||||
|
||||
String errorMessage = "unknown";
|
||||
int code = 0; // SQLException.UNKNOWN_ERR
|
||||
|
||||
try {
|
||||
boolean needRawQuery = true;
|
||||
|
||||
//Log.v("executeSqlBatch", "get query type");
|
||||
QueryType queryType = getQueryType(query);
|
||||
//Log.v("executeSqlBatch", "query type: " + queryType);
|
||||
|
||||
if (queryType == QueryType.update || queryType == queryType.delete) {
|
||||
// if (isPostHoneycomb) {
|
||||
SQLiteStatement myStatement = mydb.compileStatement(query);
|
||||
|
||||
if (json_params != null) {
|
||||
bindArgsToStatement(myStatement, json_params);
|
||||
}
|
||||
|
||||
int rowsAffected = -1; // (assuming invalid)
|
||||
|
||||
// Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
|
||||
// (Catch SQLiteException here to avoid extra retry)
|
||||
try {
|
||||
rowsAffected = myStatement.executeUpdateDelete();
|
||||
// Indicate valid results:
|
||||
needRawQuery = false;
|
||||
} catch (SQLiteConstraintException ex) {
|
||||
// Indicate problem & stop this query:
|
||||
ex.printStackTrace();
|
||||
errorMessage = "constraint failure: " + ex.getMessage();
|
||||
code = 6; // SQLException.CONSTRAINT_ERR
|
||||
Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): Error=" + errorMessage);
|
||||
needRawQuery = false;
|
||||
} catch (SQLiteException ex) {
|
||||
// Indicate problem & stop this query:
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): Error=" + errorMessage);
|
||||
needRawQuery = false;
|
||||
} catch (Exception ex) {
|
||||
// Assuming SDK_INT was lying & method not found:
|
||||
// do nothing here & try again with raw query.
|
||||
ex.printStackTrace();
|
||||
// Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): runtime error (fallback to old API): " + errorMessage);
|
||||
Log.v("SQLiteAndroidDatabase.executeSqlBatchStatement",
|
||||
"INTERNAL PLUGIN ERROR: could not do myStatement.executeUpdateDelete(): " + ex.getMessage());
|
||||
throw(ex);
|
||||
}
|
||||
|
||||
// "finally" cleanup myStatement
|
||||
myStatement.close();
|
||||
|
||||
if (rowsAffected != -1) {
|
||||
queryResult = new JSONObject();
|
||||
queryResult.put("rowsAffected", rowsAffected);
|
||||
}
|
||||
// }
|
||||
|
||||
if (needRawQuery) { // for pre-honeycomb behavior
|
||||
rowsAffectedCompat = countRowsAffectedCompat(queryType, query, json_params, mydb);
|
||||
needRowsAffectedCompat = true;
|
||||
}
|
||||
}
|
||||
|
||||
// INSERT:
|
||||
if (queryType == QueryType.insert && json_params != null) {
|
||||
needRawQuery = false;
|
||||
|
||||
SQLiteStatement myStatement = mydb.compileStatement(query);
|
||||
|
||||
bindArgsToStatement(myStatement, json_params);
|
||||
|
||||
long insertId = -1; // (invalid)
|
||||
|
||||
try {
|
||||
insertId = myStatement.executeInsert();
|
||||
|
||||
// statement has finished with no constraint violation:
|
||||
queryResult = new JSONObject();
|
||||
if (insertId != -1) {
|
||||
queryResult.put("insertId", insertId);
|
||||
queryResult.put("rowsAffected", 1);
|
||||
} else {
|
||||
queryResult.put("rowsAffected", 0);
|
||||
}
|
||||
} catch (SQLiteConstraintException ex) {
|
||||
// report constraint violation error result with the error message
|
||||
ex.printStackTrace();
|
||||
errorMessage = "constraint failure: " + ex.getMessage();
|
||||
code = 6; // SQLException.CONSTRAINT_ERR
|
||||
Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
|
||||
} catch (SQLiteException ex) {
|
||||
// report some other error result with the error message
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
|
||||
}
|
||||
|
||||
// "finally" cleanup myStatement
|
||||
myStatement.close();
|
||||
}
|
||||
|
||||
if (queryType == QueryType.begin) {
|
||||
needRawQuery = false;
|
||||
try {
|
||||
mydb.beginTransaction();
|
||||
isTransactionActive = true;
|
||||
|
||||
queryResult = new JSONObject();
|
||||
queryResult.put("rowsAffected", 0);
|
||||
} catch (SQLiteException ex) {
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteDatabase.beginTransaction(): Error=" + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (queryType == QueryType.commit) {
|
||||
needRawQuery = false;
|
||||
try {
|
||||
mydb.setTransactionSuccessful();
|
||||
mydb.endTransaction();
|
||||
isTransactionActive = false;
|
||||
|
||||
queryResult = new JSONObject();
|
||||
queryResult.put("rowsAffected", 0);
|
||||
} catch (SQLiteException ex) {
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteDatabase.setTransactionSuccessful/endTransaction(): Error=" + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (queryType == QueryType.rollback) {
|
||||
needRawQuery = false;
|
||||
try {
|
||||
mydb.endTransaction();
|
||||
isTransactionActive = false;
|
||||
|
||||
queryResult = new JSONObject();
|
||||
queryResult.put("rowsAffected", 0);
|
||||
} catch (SQLiteException ex) {
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteDatabase.endTransaction(): Error=" + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// raw query for other statements:
|
||||
if (needRawQuery) {
|
||||
try {
|
||||
queryResult = this.executeSqlStatementQuery(mydb, query, json_params);
|
||||
|
||||
} catch (SQLiteConstraintException ex) {
|
||||
// report constraint violation error result with the error message
|
||||
ex.printStackTrace();
|
||||
errorMessage = "constraint failure: " + ex.getMessage();
|
||||
code = 6; // SQLException.CONSTRAINT_ERR
|
||||
Log.v("executeSqlBatch", "Raw query error=" + errorMessage);
|
||||
} catch (SQLiteException ex) {
|
||||
// report some other error result with the error message
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "Raw query error=" + errorMessage);
|
||||
}
|
||||
|
||||
if (needRowsAffectedCompat) {
|
||||
queryResult.put("rowsAffected", rowsAffectedCompat);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + errorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
if (queryResult != null) {
|
||||
JSONObject r = new JSONObject();
|
||||
|
||||
r.put("type", "success");
|
||||
r.put("result", queryResult);
|
||||
|
||||
batchResults.put(r);
|
||||
} else {
|
||||
JSONObject r = new JSONObject();
|
||||
r.put("type", "error");
|
||||
|
||||
JSONObject er = new JSONObject();
|
||||
er.put("message", errorMessage);
|
||||
er.put("code", code);
|
||||
r.put("result", er);
|
||||
|
||||
batchResults.put(r);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
ex.printStackTrace();
|
||||
Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + ex.getMessage());
|
||||
// TODO what to do?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final int countRowsAffectedCompat(QueryType queryType, String query, JSONArray json_params,
|
||||
SQLiteDatabase mydb) throws JSONException {
|
||||
// quick and dirty way to calculate the rowsAffected in pre-Honeycomb. just do a SELECT
|
||||
// beforehand using the same WHERE clause. might not be perfect, but it's better than nothing
|
||||
Matcher whereMatcher = WHERE_CLAUSE.matcher(query);
|
||||
|
||||
String where = "";
|
||||
|
||||
int pos = 0;
|
||||
while (whereMatcher.find(pos)) {
|
||||
where = " WHERE " + whereMatcher.group(1);
|
||||
pos = whereMatcher.start(1);
|
||||
}
|
||||
// WHERE clause may be omitted, and also be sure to find the last one,
|
||||
// e.g. for cases where there's a subquery
|
||||
|
||||
// bindings may be in the update clause, so only take the last n
|
||||
int numQuestionMarks = 0;
|
||||
for (int j = 0; j < where.length(); j++) {
|
||||
if (where.charAt(j) == '?') {
|
||||
numQuestionMarks++;
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray subParams = null;
|
||||
|
||||
if (json_params != null) {
|
||||
// only take the last n of every array of sqlArgs
|
||||
JSONArray origArray = json_params;
|
||||
subParams = new JSONArray();
|
||||
int startPos = origArray.length() - numQuestionMarks;
|
||||
for (int j = startPos; j < origArray.length(); j++) {
|
||||
subParams.put(j - startPos, origArray.get(j));
|
||||
}
|
||||
}
|
||||
|
||||
if (queryType == QueryType.update) {
|
||||
Matcher tableMatcher = UPDATE_TABLE_NAME.matcher(query);
|
||||
if (tableMatcher.find()) {
|
||||
String table = tableMatcher.group(1);
|
||||
try {
|
||||
SQLiteStatement statement = mydb.compileStatement(
|
||||
"SELECT count(*) FROM " + table + where);
|
||||
|
||||
if (subParams != null) {
|
||||
bindArgsToStatement(statement, subParams);
|
||||
}
|
||||
|
||||
return (int)statement.simpleQueryForLong();
|
||||
} catch (Exception e) {
|
||||
// assume we couldn't count for whatever reason, keep going
|
||||
Log.e(SQLiteAndroidDatabase.class.getSimpleName(), "uncaught", e);
|
||||
}
|
||||
}
|
||||
} else { // delete
|
||||
Matcher tableMatcher = DELETE_TABLE_NAME.matcher(query);
|
||||
if (tableMatcher.find()) {
|
||||
String table = tableMatcher.group(1);
|
||||
try {
|
||||
SQLiteStatement statement = mydb.compileStatement(
|
||||
"SELECT count(*) FROM " + table + where);
|
||||
bindArgsToStatement(statement, subParams);
|
||||
|
||||
return (int)statement.simpleQueryForLong();
|
||||
} catch (Exception e) {
|
||||
// assume we couldn't count for whatever reason, keep going
|
||||
Log.e(SQLiteAndroidDatabase.class.getSimpleName(), "uncaught", e);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void bindArgsToStatement(SQLiteStatement myStatement, JSONArray sqlArgs) throws JSONException {
|
||||
for (int i = 0; i < sqlArgs.length(); i++) {
|
||||
if (sqlArgs.get(i) instanceof Float || sqlArgs.get(i) instanceof Double) {
|
||||
myStatement.bindDouble(i + 1, sqlArgs.getDouble(i));
|
||||
} else if (sqlArgs.get(i) instanceof Number) {
|
||||
myStatement.bindLong(i + 1, sqlArgs.getLong(i));
|
||||
} else if (sqlArgs.isNull(i)) {
|
||||
myStatement.bindNull(i + 1);
|
||||
} else {
|
||||
myStatement.bindString(i + 1, sqlArgs.getString(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rows results from query cursor.
|
||||
*
|
||||
* @param cur Cursor into query results
|
||||
* @return results in string form
|
||||
*/
|
||||
private JSONObject executeSqlStatementQuery(SQLiteDatabase mydb, String query,
|
||||
JSONArray paramsAsJson) throws Exception {
|
||||
JSONObject rowsResult = new JSONObject();
|
||||
|
||||
Cursor cur = null;
|
||||
try {
|
||||
String[] params = null;
|
||||
|
||||
params = new String[paramsAsJson.length()];
|
||||
|
||||
for (int j = 0; j < paramsAsJson.length(); j++) {
|
||||
if (paramsAsJson.isNull(j))
|
||||
params[j] = "";
|
||||
else
|
||||
params[j] = paramsAsJson.getString(j);
|
||||
}
|
||||
|
||||
cur = mydb.rawQuery(query, params);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
String errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + errorMessage);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// If query result has rows
|
||||
if (cur != null && cur.moveToFirst()) {
|
||||
JSONArray rowsArrayResult = new JSONArray();
|
||||
String key = "";
|
||||
int colCount = cur.getColumnCount();
|
||||
|
||||
// Build up JSON result object for each row
|
||||
do {
|
||||
JSONObject row = new JSONObject();
|
||||
try {
|
||||
for (int i = 0; i < colCount; ++i) {
|
||||
key = cur.getColumnName(i);
|
||||
|
||||
if (isPostHoneycomb) {
|
||||
// Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
|
||||
try {
|
||||
bindPostHoneycomb(row, key, cur, i);
|
||||
} catch (Exception ex) {
|
||||
// bindPreHoneycomb(row, key, cur, i);
|
||||
Log.v("SQLiteAndroidDatabase.executeSqlStatementQuery",
|
||||
"INTERNAL PLUGIN ERROR: could not bindPostHoneycomb: " + ex.getMessage());
|
||||
throw(ex);
|
||||
}
|
||||
} else {
|
||||
// NOT EXPECTED:
|
||||
// bindPreHoneycomb(row, key, cur, i);
|
||||
Log.v("SQLiteAndroidDatabase.executeSqlStatementQuery",
|
||||
"INTERNAL PLUGIN ERROR: deprecated android.os.Build.VERSION not supported: " + android.os.Build.VERSION.SDK_INT);
|
||||
throw new RuntimeException(
|
||||
"INTERNAL PLUGIN ERROR: deprecated android.os.Build.VERSION not supported: " +
|
||||
android.os.Build.VERSION.SDK_INT);
|
||||
}
|
||||
}
|
||||
|
||||
rowsArrayResult.put(row);
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} while (cur.moveToNext());
|
||||
|
||||
try {
|
||||
rowsResult.put("rows", rowsArrayResult);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (cur != null) {
|
||||
cur.close();
|
||||
}
|
||||
|
||||
return rowsResult;
|
||||
}
|
||||
|
||||
private void bindPostHoneycomb(JSONObject row, String key, Cursor cur, int i) throws JSONException {
|
||||
int curType = cur.getType(i);
|
||||
|
||||
switch (curType) {
|
||||
case Cursor.FIELD_TYPE_NULL:
|
||||
row.put(key, JSONObject.NULL);
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_INTEGER:
|
||||
row.put(key, cur.getLong(i));
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
row.put(key, cur.getDouble(i));
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
default: /* (BLOB) */
|
||||
row.put(key, cur.getString(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ** NO LONGER SUPPORTED:
|
||||
private void bindPreHoneycomb(JSONObject row, String key, Cursor cursor, int i) throws JSONException {
|
||||
// Since cursor.getType() is not available pre-honeycomb, this is
|
||||
// a workaround so we don't have to bind everything as a string
|
||||
// Details here: http://stackoverflow.com/q/11658239
|
||||
SQLiteCursor sqLiteCursor = (SQLiteCursor) cursor;
|
||||
CursorWindow cursorWindow = sqLiteCursor.getWindow();
|
||||
int pos = cursor.getPosition();
|
||||
if (cursorWindow.isNull(pos, i)) {
|
||||
row.put(key, JSONObject.NULL);
|
||||
} else if (cursorWindow.isLong(pos, i)) {
|
||||
row.put(key, cursor.getLong(i));
|
||||
} else if (cursorWindow.isFloat(pos, i)) {
|
||||
row.put(key, cursor.getDouble(i));
|
||||
} else {
|
||||
// STRING or BLOB:
|
||||
row.put(key, cursor.getString(i));
|
||||
}
|
||||
}
|
||||
// */
|
||||
|
||||
static QueryType getQueryType(String query) {
|
||||
Matcher matcher = FIRST_WORD.matcher(query);
|
||||
|
||||
// FIND & return query type, or throw:
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
String first = matcher.group(1);
|
||||
|
||||
// explictly reject if blank
|
||||
// (needed for SQLCipher version)
|
||||
if (first.length() == 0) throw new RuntimeException("query not found");
|
||||
|
||||
return QueryType.valueOf(first.toLowerCase(Locale.ENGLISH));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
// unknown verb (NOT blank)
|
||||
return QueryType.other;
|
||||
}
|
||||
} else {
|
||||
// explictly reject if blank
|
||||
// (needed for SQLCipher version)
|
||||
throw new RuntimeException("query not found");
|
||||
}
|
||||
}
|
||||
|
||||
static enum QueryType {
|
||||
update,
|
||||
insert,
|
||||
delete,
|
||||
select,
|
||||
begin,
|
||||
commit,
|
||||
rollback,
|
||||
other
|
||||
}
|
||||
} /* vim: set expandtab : */
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
|
||||
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||
* Copyright (c) 2010, IBM Corporation
|
||||
*/
|
||||
|
||||
package io.sqlc;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.lang.Number;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import io.liteglue.SQLCode;
|
||||
import io.liteglue.SQLColumnType;
|
||||
import io.liteglue.SQLiteConnector;
|
||||
import io.liteglue.SQLiteConnection;
|
||||
import io.liteglue.SQLiteOpenFlags;
|
||||
import io.liteglue.SQLiteStatement;
|
||||
|
||||
/**
|
||||
* Android SQLite-Connector Database helper class
|
||||
*/
|
||||
class SQLiteConnectorDatabase extends SQLiteAndroidDatabase
|
||||
{
|
||||
static SQLiteConnector connector = new SQLiteConnector();
|
||||
|
||||
SQLiteConnection mydb;
|
||||
|
||||
/**
|
||||
* NOTE: Using default constructor, no explicit constructor.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Open a database.
|
||||
*
|
||||
* @param dbFile The database File specification
|
||||
*/
|
||||
@Override
|
||||
void open(File dbFile) throws Exception {
|
||||
mydb = connector.newSQLiteConnection(dbFile.getAbsolutePath(),
|
||||
SQLiteOpenFlags.READWRITE | SQLiteOpenFlags.CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a database (in the current thread).
|
||||
*/
|
||||
@Override
|
||||
void closeDatabaseNow() {
|
||||
try {
|
||||
if (mydb != null)
|
||||
mydb.dispose();
|
||||
} catch (Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database, ignoring", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore Android bug workaround for NDK version
|
||||
*/
|
||||
@Override
|
||||
void bugWorkaround() { }
|
||||
|
||||
/**
|
||||
* Executes a batch request and sends the results via cbc.
|
||||
*
|
||||
* @param dbname The name of the database.
|
||||
* @param queryarr Array of query strings
|
||||
* @param jsonparams Array of JSON query parameters
|
||||
* @param cbc Callback context from Cordova API
|
||||
*/
|
||||
@Override
|
||||
void executeSqlBatch( String[] queryarr, JSONArray[] jsonparams, CallbackContext cbc) {
|
||||
|
||||
if (mydb == null) {
|
||||
// not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database
|
||||
cbc.error("database has been closed");
|
||||
return;
|
||||
}
|
||||
|
||||
int len = queryarr.length;
|
||||
JSONArray batchResults = new JSONArray();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
int rowsAffectedCompat = 0;
|
||||
boolean needRowsAffectedCompat = false;
|
||||
|
||||
JSONObject queryResult = null;
|
||||
|
||||
String errorMessage = "unknown";
|
||||
int sqliteErrorCode = -1;
|
||||
int code = 0; // SQLException.UNKNOWN_ERR
|
||||
|
||||
try {
|
||||
String query = queryarr[i];
|
||||
|
||||
long lastTotal = mydb.getTotalChanges();
|
||||
queryResult = this.executeSQLiteStatement(query, jsonparams[i], cbc);
|
||||
long newTotal = mydb.getTotalChanges();
|
||||
long rowsAffected = newTotal - lastTotal;
|
||||
|
||||
queryResult.put("rowsAffected", rowsAffected);
|
||||
if (rowsAffected > 0) {
|
||||
long insertId = mydb.getLastInsertRowid();
|
||||
if (insertId > 0) {
|
||||
queryResult.put("insertId", insertId);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
ex.printStackTrace();
|
||||
sqliteErrorCode = ex.getErrorCode();
|
||||
errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): SQL Error code = " + sqliteErrorCode + " message = " + errorMessage);
|
||||
|
||||
switch(sqliteErrorCode) {
|
||||
case SQLCode.ERROR:
|
||||
code = 5; // SQLException.SYNTAX_ERR
|
||||
break;
|
||||
case 13: // SQLITE_FULL
|
||||
code = 4; // SQLException.QUOTA_ERR
|
||||
break;
|
||||
case SQLCode.CONSTRAINT:
|
||||
code = 6; // SQLException.CONSTRAINT_ERR
|
||||
break;
|
||||
default:
|
||||
/* do nothing */
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
// NOT expected:
|
||||
ex.printStackTrace();
|
||||
errorMessage = ex.getMessage();
|
||||
code = 0; // SQLException.UNKNOWN_ERR
|
||||
Log.e("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): UNEXPECTED JSON Error=" + errorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
if (queryResult != null) {
|
||||
JSONObject r = new JSONObject();
|
||||
|
||||
r.put("type", "success");
|
||||
r.put("result", queryResult);
|
||||
|
||||
batchResults.put(r);
|
||||
} else {
|
||||
JSONObject r = new JSONObject();
|
||||
r.put("type", "error");
|
||||
|
||||
JSONObject er = new JSONObject();
|
||||
er.put("message", errorMessage);
|
||||
er.put("code", code);
|
||||
r.put("result", er);
|
||||
|
||||
batchResults.put(r);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
ex.printStackTrace();
|
||||
Log.e("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + ex.getMessage());
|
||||
// TODO what to do?
|
||||
}
|
||||
}
|
||||
|
||||
cbc.success(batchResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rows results from query cursor.
|
||||
*
|
||||
* @param cur Cursor into query results
|
||||
* @return results in string form
|
||||
*/
|
||||
private JSONObject executeSQLiteStatement(String query, JSONArray paramsAsJson,
|
||||
CallbackContext cbc) throws JSONException, SQLException {
|
||||
JSONObject rowsResult = new JSONObject();
|
||||
|
||||
boolean hasRows = false;
|
||||
|
||||
SQLiteStatement myStatement = mydb.prepareStatement(query);
|
||||
|
||||
try {
|
||||
String[] params = null;
|
||||
|
||||
params = new String[paramsAsJson.length()];
|
||||
|
||||
for (int i = 0; i < paramsAsJson.length(); ++i) {
|
||||
if (paramsAsJson.isNull(i)) {
|
||||
myStatement.bindNull(i + 1);
|
||||
} else {
|
||||
Object p = paramsAsJson.get(i);
|
||||
if (p instanceof Float || p instanceof Double)
|
||||
myStatement.bindDouble(i + 1, paramsAsJson.getDouble(i));
|
||||
else if (p instanceof Number)
|
||||
myStatement.bindLong(i + 1, paramsAsJson.getLong(i));
|
||||
else
|
||||
myStatement.bindTextNativeString(i + 1, paramsAsJson.getString(i));
|
||||
}
|
||||
}
|
||||
|
||||
hasRows = myStatement.step();
|
||||
} catch (SQLException ex) {
|
||||
ex.printStackTrace();
|
||||
String errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + errorMessage);
|
||||
|
||||
// cleanup statement and throw the exception:
|
||||
myStatement.dispose();
|
||||
throw ex;
|
||||
} catch (JSONException ex) {
|
||||
ex.printStackTrace();
|
||||
String errorMessage = ex.getMessage();
|
||||
Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + errorMessage);
|
||||
|
||||
// cleanup statement and throw the exception:
|
||||
myStatement.dispose();
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// If query result has rows
|
||||
if (hasRows) {
|
||||
JSONArray rowsArrayResult = new JSONArray();
|
||||
String key = "";
|
||||
int colCount = myStatement.getColumnCount();
|
||||
|
||||
// Build up JSON result object for each row
|
||||
do {
|
||||
JSONObject row = new JSONObject();
|
||||
try {
|
||||
for (int i = 0; i < colCount; ++i) {
|
||||
key = myStatement.getColumnName(i);
|
||||
|
||||
switch (myStatement.getColumnType(i)) {
|
||||
case SQLColumnType.NULL:
|
||||
row.put(key, JSONObject.NULL);
|
||||
break;
|
||||
|
||||
case SQLColumnType.REAL:
|
||||
row.put(key, myStatement.getColumnDouble(i));
|
||||
break;
|
||||
|
||||
case SQLColumnType.INTEGER:
|
||||
row.put(key, myStatement.getColumnLong(i));
|
||||
break;
|
||||
|
||||
case SQLColumnType.BLOB:
|
||||
case SQLColumnType.TEXT:
|
||||
default: // (just in case)
|
||||
row.put(key, myStatement.getColumnTextNativeString(i));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rowsArrayResult.put(row);
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} while (myStatement.step());
|
||||
|
||||
try {
|
||||
rowsResult.put("rows", rowsArrayResult);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
myStatement.dispose();
|
||||
|
||||
return rowsResult;
|
||||
}
|
||||
|
||||
} /* vim: set expandtab : */
|
||||
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
* Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
|
||||
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||
* Copyright (c) 2010, IBM Corporation
|
||||
*/
|
||||
|
||||
package io.sqlc;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.lang.IllegalArgumentException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class SQLitePlugin extends CordovaPlugin {
|
||||
|
||||
/**
|
||||
* Concurrent database runner map.
|
||||
*
|
||||
* NOTE: no public static accessor to db (runner) map since it is not
|
||||
* expected to work properly with db threading.
|
||||
*
|
||||
* FUTURE TBD put DBRunner into a public class that can provide external accessor.
|
||||
*
|
||||
* ADDITIONAL NOTE: Storing as Map<String, DBRunner> to avoid portabiity issue
|
||||
* between Java 6/7/8 as discussed in:
|
||||
* https://gist.github.com/AlainODea/1375759b8720a3f9f094
|
||||
*
|
||||
* THANKS to @NeoLSN (Jason Yang/楊朝傑) for giving the pointer in:
|
||||
* https://github.com/litehelpers/Cordova-sqlite-storage/issues/727
|
||||
*/
|
||||
private Map<String, DBRunner> dbrmap = new ConcurrentHashMap<String, DBRunner>();
|
||||
|
||||
/**
|
||||
* NOTE: Using default constructor, no explicit constructor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
*
|
||||
* @param actionAsString The action to execute.
|
||||
* @param args JSONArry of arguments for the plugin.
|
||||
* @param cbc Callback context from Cordova API
|
||||
* @return Whether the action was valid.
|
||||
*/
|
||||
@Override
|
||||
public boolean execute(String actionAsString, JSONArray args, CallbackContext cbc) {
|
||||
|
||||
Action action;
|
||||
try {
|
||||
action = Action.valueOf(actionAsString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// shouldn't ever happen
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return executeAndPossiblyThrow(action, args, cbc);
|
||||
} catch (JSONException e) {
|
||||
// TODO: signal JSON problem to JS
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackContext cbc)
|
||||
throws JSONException {
|
||||
|
||||
boolean status = true;
|
||||
JSONObject o;
|
||||
String echo_value;
|
||||
String dbname;
|
||||
|
||||
switch (action) {
|
||||
case echoStringValue:
|
||||
o = args.getJSONObject(0);
|
||||
echo_value = o.getString("value");
|
||||
cbc.success(echo_value);
|
||||
break;
|
||||
|
||||
case open:
|
||||
o = args.getJSONObject(0);
|
||||
dbname = o.getString("name");
|
||||
// open database and start reading its queue
|
||||
this.startDatabase(dbname, o, cbc);
|
||||
break;
|
||||
|
||||
case close:
|
||||
o = args.getJSONObject(0);
|
||||
dbname = o.getString("path");
|
||||
// put request in the q to close the db
|
||||
this.closeDatabase(dbname, cbc);
|
||||
break;
|
||||
|
||||
case delete:
|
||||
o = args.getJSONObject(0);
|
||||
dbname = o.getString("path");
|
||||
|
||||
deleteDatabase(dbname, cbc);
|
||||
|
||||
break;
|
||||
|
||||
case executeSqlBatch:
|
||||
case backgroundExecuteSqlBatch:
|
||||
JSONObject allargs = args.getJSONObject(0);
|
||||
JSONObject dbargs = allargs.getJSONObject("dbargs");
|
||||
dbname = dbargs.getString("dbname");
|
||||
JSONArray txargs = allargs.getJSONArray("executes");
|
||||
|
||||
if (txargs.isNull(0)) {
|
||||
cbc.error("INTERNAL PLUGIN ERROR: missing executes list");
|
||||
} else {
|
||||
int len = txargs.length();
|
||||
String[] queries = new String[len];
|
||||
JSONArray[] jsonparams = new JSONArray[len];
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
JSONObject a = txargs.getJSONObject(i);
|
||||
queries[i] = a.getString("sql");
|
||||
jsonparams[i] = a.getJSONArray("params");
|
||||
}
|
||||
|
||||
// put db query in the queue to be executed in the db thread:
|
||||
DBQuery q = new DBQuery(queries, jsonparams, cbc);
|
||||
DBRunner r = dbrmap.get(dbname);
|
||||
if (r != null) {
|
||||
try {
|
||||
r.q.put(q);
|
||||
} catch(Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't add to queue", e);
|
||||
cbc.error("INTERNAL PLUGIN ERROR: couldn't add to queue");
|
||||
}
|
||||
} else {
|
||||
cbc.error("INTERNAL PLUGIN ERROR: database not open");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up and close all open databases.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
while (!dbrmap.isEmpty()) {
|
||||
String dbname = dbrmap.keySet().iterator().next();
|
||||
|
||||
this.closeDatabaseNow(dbname);
|
||||
|
||||
DBRunner r = dbrmap.get(dbname);
|
||||
try {
|
||||
// stop the db runner thread:
|
||||
r.q.put(new DBQuery());
|
||||
} catch(Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "INTERNAL PLUGIN CLEANUP ERROR: could not stop db thread due to exception", e);
|
||||
}
|
||||
dbrmap.remove(dbname);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// LOCAL METHODS
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
private void startDatabase(String dbname, JSONObject options, CallbackContext cbc) {
|
||||
DBRunner r = dbrmap.get(dbname);
|
||||
|
||||
if (r != null) {
|
||||
// NO LONGER EXPECTED due to BUG 666 workaround solution:
|
||||
cbc.error("INTERNAL ERROR: database already open for db name: " + dbname);
|
||||
} else {
|
||||
r = new DBRunner(dbname, options, cbc);
|
||||
dbrmap.put(dbname, r);
|
||||
this.cordova.getThreadPool().execute(r);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Open a database.
|
||||
*
|
||||
* @param dbName The name of the database file
|
||||
*/
|
||||
private SQLiteAndroidDatabase openDatabase(String dbname, CallbackContext cbc, boolean old_impl) throws Exception {
|
||||
try {
|
||||
// ASSUMPTION: no db (connection/handle) is already stored in the map
|
||||
// [should be true according to the code in DBRunner.run()]
|
||||
|
||||
File dbfile = this.cordova.getActivity().getDatabasePath(dbname);
|
||||
|
||||
if (!dbfile.exists()) {
|
||||
dbfile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
Log.v("info", "Open sqlite db: " + dbfile.getAbsolutePath());
|
||||
|
||||
SQLiteAndroidDatabase mydb = old_impl ? new SQLiteAndroidDatabase() : new SQLiteConnectorDatabase();
|
||||
mydb.open(dbfile);
|
||||
|
||||
if (cbc != null) // XXX Android locking/closing BUG workaround
|
||||
cbc.success();
|
||||
|
||||
return mydb;
|
||||
} catch (Exception e) {
|
||||
if (cbc != null) // XXX Android locking/closing BUG workaround
|
||||
cbc.error("can't open database " + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a database (in another thread).
|
||||
*
|
||||
* @param dbName The name of the database file
|
||||
*/
|
||||
private void closeDatabase(String dbname, CallbackContext cbc) {
|
||||
DBRunner r = dbrmap.get(dbname);
|
||||
if (r != null) {
|
||||
try {
|
||||
r.q.put(new DBQuery(false, cbc));
|
||||
} catch(Exception e) {
|
||||
if (cbc != null) {
|
||||
cbc.error("couldn't close database" + e);
|
||||
}
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
|
||||
}
|
||||
} else {
|
||||
if (cbc != null) {
|
||||
cbc.success();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a database (in the current thread).
|
||||
*
|
||||
* @param dbname The name of the database file
|
||||
*/
|
||||
private void closeDatabaseNow(String dbname) {
|
||||
DBRunner r = dbrmap.get(dbname);
|
||||
|
||||
if (r != null) {
|
||||
SQLiteAndroidDatabase mydb = r.mydb;
|
||||
|
||||
if (mydb != null)
|
||||
mydb.closeDatabaseNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDatabase(String dbname, CallbackContext cbc) {
|
||||
DBRunner r = dbrmap.get(dbname);
|
||||
if (r != null) {
|
||||
try {
|
||||
r.q.put(new DBQuery(true, cbc));
|
||||
} catch(Exception e) {
|
||||
if (cbc != null) {
|
||||
cbc.error("couldn't close database" + e);
|
||||
}
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
|
||||
}
|
||||
} else {
|
||||
boolean deleteResult = this.deleteDatabaseNow(dbname);
|
||||
if (deleteResult) {
|
||||
cbc.success();
|
||||
} else {
|
||||
cbc.error("couldn't delete database");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a database.
|
||||
*
|
||||
* @param dbName The name of the database file
|
||||
*
|
||||
* @return true if successful or false if an exception was encountered
|
||||
*/
|
||||
private boolean deleteDatabaseNow(String dbname) {
|
||||
File dbfile = this.cordova.getActivity().getDatabasePath(dbname);
|
||||
|
||||
try {
|
||||
return cordova.getActivity().deleteDatabase(dbfile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class DBRunner implements Runnable {
|
||||
final String dbname;
|
||||
private boolean oldImpl;
|
||||
private boolean bugWorkaround;
|
||||
|
||||
final BlockingQueue<DBQuery> q;
|
||||
final CallbackContext openCbc;
|
||||
|
||||
SQLiteAndroidDatabase mydb;
|
||||
|
||||
DBRunner(final String dbname, JSONObject options, CallbackContext cbc) {
|
||||
this.dbname = dbname;
|
||||
this.oldImpl = options.has("androidOldDatabaseImplementation");
|
||||
Log.v(SQLitePlugin.class.getSimpleName(), "Android db implementation: built-in android.database.sqlite package");
|
||||
this.bugWorkaround = this.oldImpl && options.has("androidBugWorkaround");
|
||||
if (this.bugWorkaround)
|
||||
Log.v(SQLitePlugin.class.getSimpleName(), "Android db closing/locking workaround applied");
|
||||
|
||||
this.q = new LinkedBlockingQueue<DBQuery>();
|
||||
this.openCbc = cbc;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
this.mydb = openDatabase(dbname, this.openCbc, this.oldImpl);
|
||||
} catch (Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error, stopping db thread", e);
|
||||
dbrmap.remove(dbname);
|
||||
return;
|
||||
}
|
||||
|
||||
DBQuery dbq = null;
|
||||
|
||||
try {
|
||||
dbq = q.take();
|
||||
|
||||
while (!dbq.stop) {
|
||||
mydb.executeSqlBatch(dbq.queries, dbq.jsonparams, dbq.cbc);
|
||||
|
||||
if (this.bugWorkaround && dbq.queries.length == 1 && dbq.queries[0] == "COMMIT")
|
||||
mydb.bugWorkaround();
|
||||
|
||||
dbq = q.take();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
|
||||
}
|
||||
|
||||
if (dbq != null && dbq.close) {
|
||||
try {
|
||||
closeDatabaseNow(dbname);
|
||||
|
||||
dbrmap.remove(dbname); // (should) remove ourself
|
||||
|
||||
if (!dbq.delete) {
|
||||
dbq.cbc.success();
|
||||
} else {
|
||||
try {
|
||||
boolean deleteResult = deleteDatabaseNow(dbname);
|
||||
if (deleteResult) {
|
||||
dbq.cbc.success();
|
||||
} else {
|
||||
dbq.cbc.error("couldn't delete database");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e);
|
||||
dbq.cbc.error("couldn't delete database: " + e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
|
||||
if (dbq.cbc != null) {
|
||||
dbq.cbc.error("couldn't close database: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class DBQuery {
|
||||
// XXX TODO replace with DBRunner action enum:
|
||||
final boolean stop;
|
||||
final boolean close;
|
||||
final boolean delete;
|
||||
final String[] queries;
|
||||
final JSONArray[] jsonparams;
|
||||
final CallbackContext cbc;
|
||||
|
||||
DBQuery(String[] myqueries, JSONArray[] params, CallbackContext c) {
|
||||
this.stop = false;
|
||||
this.close = false;
|
||||
this.delete = false;
|
||||
this.queries = myqueries;
|
||||
this.jsonparams = params;
|
||||
this.cbc = c;
|
||||
}
|
||||
|
||||
DBQuery(boolean delete, CallbackContext cbc) {
|
||||
this.stop = true;
|
||||
this.close = true;
|
||||
this.delete = delete;
|
||||
this.queries = null;
|
||||
this.jsonparams = null;
|
||||
this.cbc = cbc;
|
||||
}
|
||||
|
||||
// signal the DBRunner thread to stop:
|
||||
DBQuery() {
|
||||
this.stop = true;
|
||||
this.close = false;
|
||||
this.delete = false;
|
||||
this.queries = null;
|
||||
this.jsonparams = null;
|
||||
this.cbc = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static enum Action {
|
||||
echoStringValue,
|
||||
open,
|
||||
close,
|
||||
delete,
|
||||
executeSqlBatch,
|
||||
backgroundExecuteSqlBatch,
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set expandtab : */
|
||||
@@ -0,0 +1,127 @@
|
||||
package me.rahul.plugins.sqlDB;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import android.os.Build;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.Log;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private Context myContext;
|
||||
private SQLiteDatabase database;
|
||||
private String databasePath;
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
super(context, sqlDB.dbname, null, 1);
|
||||
this.myContext = context;
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public void createdatabase(File dbPath, String source, final CallbackContext callbackContext) throws IOException {
|
||||
|
||||
Log.d("CordovaLog","Inside CreateDatabase getAbsolutePath= "+dbPath.getAbsolutePath());
|
||||
Log.d("CordovaLog","Inside CreateDatabase path = "+dbPath.getPath());
|
||||
databasePath = dbPath.getAbsolutePath();
|
||||
this.getReadableDatabase();
|
||||
if(Build.VERSION.SDK_INT > 26) {
|
||||
this.close();
|
||||
}
|
||||
try {
|
||||
copyDatabase(dbPath, source, callbackContext);
|
||||
} catch (IOException e) {
|
||||
throw new Error(
|
||||
"Create Database Exception ============================ "
|
||||
+ e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void copyDatabase(File database, String source, final CallbackContext callbackContext) throws IOException {
|
||||
InputStream myInput = null;
|
||||
JSONObject response = new JSONObject();
|
||||
PluginResult plresult = new PluginResult(PluginResult.Status.NO_RESULT);
|
||||
try {
|
||||
if(source.indexOf("www") != -1) {
|
||||
myInput = myContext.getAssets().open("www/" + sqlDB.dbname);
|
||||
} else {
|
||||
File src = new File(source);
|
||||
myInput = new FileInputStream(src);
|
||||
}
|
||||
OutputStream myOutput = new FileOutputStream(database);
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((myInput.read(buffer)) > -1) {
|
||||
myOutput.write(buffer);
|
||||
}
|
||||
|
||||
myOutput.flush();
|
||||
myOutput.close();
|
||||
myInput.close();
|
||||
try {
|
||||
this.openDataBase();
|
||||
response.put("message", "Db Copied");
|
||||
response.put("code", 200);
|
||||
plresult = new PluginResult(PluginResult.Status.OK,response);
|
||||
this.close();
|
||||
callbackContext.sendPluginResult(plresult);
|
||||
} catch (Exception err) {
|
||||
throw new Error(err.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d("CordovaLog",e.getMessage());
|
||||
try {
|
||||
response.put("message", e.getMessage());
|
||||
response.put("code", 400);
|
||||
plresult = new PluginResult(PluginResult.Status.ERROR, response);
|
||||
callbackContext.sendPluginResult(plresult);
|
||||
} catch (JSONException err1) {
|
||||
throw new Error(err1.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void openDataBase() throws SQLException {
|
||||
|
||||
//Open the database
|
||||
database = SQLiteDatabase.openDatabase(databasePath, null, SQLiteDatabase.OPEN_READONLY);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
|
||||
if(database != null)
|
||||
database.close();
|
||||
|
||||
super.close();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package me.rahul.plugins.sqlDB;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* This class echoes a string called from JavaScript.
|
||||
*/
|
||||
public class sqlDB extends CordovaPlugin {
|
||||
|
||||
public static String dbname = "dbname";
|
||||
PluginResult plresult = new PluginResult(PluginResult.Status.NO_RESULT);
|
||||
|
||||
private void sendPluginResponse(int code, String msg, boolean error, CallbackContext callbackContext) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put("message", msg);
|
||||
response.put("code", code);
|
||||
if (error) {
|
||||
plresult = new PluginResult(PluginResult.Status.ERROR,
|
||||
response);
|
||||
} else {
|
||||
plresult = new PluginResult(PluginResult.Status.OK, response);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
plresult = new PluginResult(PluginResult.Status.ERROR,
|
||||
e.getMessage());
|
||||
}
|
||||
callbackContext.sendPluginResult(plresult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args,
|
||||
CallbackContext callbackContext) throws JSONException {
|
||||
|
||||
if (action.equalsIgnoreCase("copy")) {
|
||||
this.copyDB(args.getString(0), "www", callbackContext);
|
||||
return true;
|
||||
} else if (action.equalsIgnoreCase("remove")) {
|
||||
String db = args.getString(0);
|
||||
File path = cordova.getActivity().getDatabasePath(db);
|
||||
Boolean fileExists = path.exists();
|
||||
if (fileExists) {
|
||||
boolean deleted = path.delete();
|
||||
if (deleted) {
|
||||
sendPluginResponse(200, "Database Deleted", false, callbackContext);
|
||||
} else {
|
||||
sendPluginResponse(400, "Unable to Delete", true, callbackContext);
|
||||
}
|
||||
} else {
|
||||
sendPluginResponse(404, "Invalid DB Location or DB Doesn't Exists", true, callbackContext);
|
||||
}
|
||||
return true;
|
||||
} else if (action.equalsIgnoreCase("copyDbToStorage")) {
|
||||
String db = args.getString(0);
|
||||
String dest = args.getString(2);
|
||||
boolean overwrite = args.getBoolean(3);
|
||||
this.copyDbToStorage(db, dest, overwrite, callbackContext);
|
||||
return true;
|
||||
} else if (action.equalsIgnoreCase("copyDbFromStorage")) {
|
||||
String db = args.getString(0);
|
||||
String src = args.getString(2);
|
||||
boolean deleteolddb = args.getBoolean(3);
|
||||
this.copyDbFromStorage(db, src, deleteolddb, callbackContext);
|
||||
return true;
|
||||
} else if (action.equalsIgnoreCase("checkDbOnStorage")) {
|
||||
String db = args.getString(0);
|
||||
String src = args.getString(1);
|
||||
this.checkDbOnStorage(db, src, callbackContext);
|
||||
return true;
|
||||
} else {
|
||||
plresult = new PluginResult(PluginResult.Status.INVALID_ACTION);
|
||||
callbackContext.sendPluginResult(plresult);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDB(final String dbName, final String src, final CallbackContext callbackContext) {
|
||||
|
||||
final File dbpath;
|
||||
dbname = dbName;
|
||||
JSONObject response = new JSONObject();
|
||||
final DatabaseHelper dbhelper = new DatabaseHelper(this.cordova
|
||||
.getActivity().getApplicationContext());
|
||||
dbpath = this.cordova.getActivity().getDatabasePath(dbname);
|
||||
Boolean dbexists = dbpath.exists();
|
||||
Log.d("CordovaLog", "DatabasePath = " + dbpath + "&&&& dbname = " + dbname + "&&&&DB Exists =" + dbexists);
|
||||
|
||||
if (dbexists) {
|
||||
sendPluginResponse(516, "DB Already Exists", true, callbackContext);
|
||||
} else {
|
||||
cordova.getThreadPool().execute(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
PluginResult plresult;
|
||||
|
||||
// TODO Auto-generated method stub
|
||||
// try {
|
||||
// dbhelper.createdatabase(dbpath, src, callbackContext);
|
||||
//// plResult = new PluginResult(PluginResult.Status.OK);
|
||||
//// callbackContext.sendPluginResult(plResult);
|
||||
// //sendPluginResponse(200,"Db Copied", false, callbackContext);
|
||||
// } catch (Exception e) {
|
||||
//
|
||||
//// plResult = new PluginResult(PluginResult.Status.ERROR,
|
||||
//// e.getMessage());
|
||||
//// callbackContext.sendPluginResult(plResult);
|
||||
// sendPluginResponse(400, e.getMessage(), true, callbackContext);
|
||||
// }
|
||||
InputStream myInput = null;
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
myInput = cordova.getActivity().getAssets().open("www/" + dbName);
|
||||
|
||||
OutputStream myOutput = new FileOutputStream(dbpath);
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((myInput.read(buffer)) > -1) {
|
||||
myOutput.write(buffer);
|
||||
}
|
||||
|
||||
myOutput.flush();
|
||||
myOutput.close();
|
||||
myInput.close();
|
||||
try {
|
||||
response.put("message", "Db Copied");
|
||||
response.put("code", 200);
|
||||
plresult = new PluginResult(PluginResult.Status.OK,response);
|
||||
callbackContext.sendPluginResult(plresult);
|
||||
} catch (JSONException err) {
|
||||
throw new Error(err.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d("Cordova", "Try Error = "+e.getMessage());
|
||||
try {
|
||||
dbhelper.createdatabase(dbpath, src, callbackContext);
|
||||
} catch (Exception err) {
|
||||
sendPluginResponse(400, e.getMessage(), true, callbackContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void newCopyDB(File source, File destination, CallbackContext callbackContext) {
|
||||
try {
|
||||
InputStream myInput = new FileInputStream(source);
|
||||
OutputStream myOutput = new FileOutputStream(destination);
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((myInput.read(buffer)) > -1) {
|
||||
myOutput.write(buffer);
|
||||
}
|
||||
|
||||
myOutput.flush();
|
||||
myOutput.close();
|
||||
myInput.close();
|
||||
sendPluginResponse(200, "DB Copied Successfully", false, callbackContext);
|
||||
} catch (IOException e) {
|
||||
sendPluginResponse(400, e.getMessage(), true, callbackContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDbFromStorage(String db, String src, boolean deleteolddb, final CallbackContext callbackContext) {
|
||||
File source;
|
||||
if (src.indexOf("file://") != -1) {
|
||||
source = new File(src.replace("file://", ""));
|
||||
} else {
|
||||
source = new File(src);
|
||||
}
|
||||
|
||||
if (source.exists()) {
|
||||
if (deleteolddb) {
|
||||
File path = cordova.getActivity().getDatabasePath(db);
|
||||
Boolean fileExists = path.exists();
|
||||
if (fileExists) {
|
||||
boolean deleted = path.delete();
|
||||
if (deleted) {
|
||||
this.copyDB(db, source.getAbsolutePath(), callbackContext);
|
||||
} else {
|
||||
sendPluginResponse(400, "Unable to Delete", true, callbackContext);
|
||||
}
|
||||
} else {
|
||||
sendPluginResponse(404, "Old DB Doesn't Exists", true, callbackContext);
|
||||
}
|
||||
} else {
|
||||
this.copyDB(db, source.getAbsolutePath(), callbackContext);
|
||||
}
|
||||
} else {
|
||||
sendPluginResponse(404, "Invalid DB Source Location", true, callbackContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDbOnStorage(String db, String src, final CallbackContext callbackContext) {
|
||||
File source;
|
||||
if (src.indexOf("file://") != -1) {
|
||||
source = new File(src.replace("file://", "")+db);
|
||||
} else {
|
||||
source = new File(src+db);
|
||||
}
|
||||
if (source.exists()) {
|
||||
sendPluginResponse(200, "DB File Exists At Source Location", false, callbackContext);
|
||||
} else {
|
||||
sendPluginResponse(404, "Invalid DB Source Location", true, callbackContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void copyDbToStorage(String dbname, String dest, boolean overwrite, final CallbackContext callbackContext) {
|
||||
|
||||
File source = cordova.getActivity().getDatabasePath(dbname);
|
||||
File destFolder;
|
||||
File destination;
|
||||
|
||||
if (dest.indexOf("file://") != -1) {
|
||||
destination = new File(dest.replace("file://", "") + dbname);
|
||||
destFolder = new File(dest.replace("file://", ""));
|
||||
} else {
|
||||
destination = new File(dest + dbname);
|
||||
destFolder = new File(dest);
|
||||
}
|
||||
|
||||
|
||||
if (!destFolder.exists()) {
|
||||
destFolder.mkdirs();
|
||||
}
|
||||
|
||||
if (!destFolder.exists()) {
|
||||
sendPluginResponse(404, "Invalid output DB Location", true, callbackContext);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if the db exists at source
|
||||
if (source.exists()) {
|
||||
//check if the db file is already present at destination and overwrite flag is false
|
||||
if(destination.exists() && !overwrite) {
|
||||
sendPluginResponse(400, "DB already exists at destination", true, callbackContext);
|
||||
} else {
|
||||
this.newCopyDB(source, destination, callbackContext);
|
||||
}
|
||||
} else {
|
||||
sendPluginResponse(404, "Invalid DB Location or DB Doesn't Exists", true, callbackContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
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.device;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.provider.Settings;
|
||||
|
||||
public class Device extends CordovaPlugin {
|
||||
public static final String TAG = "Device";
|
||||
|
||||
public static String platform; // Device OS
|
||||
public static String uuid; // Device UUID
|
||||
|
||||
private static final String ANDROID_PLATFORM = "Android";
|
||||
private static final String AMAZON_PLATFORM = "amazon-fireos";
|
||||
private static final String AMAZON_DEVICE = "Amazon";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public Device() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context of the Command. This can then be used to do things like
|
||||
* get file paths associated with the Activity.
|
||||
*
|
||||
* @param cordova The context of the main Activity.
|
||||
* @param webView The CordovaWebView Cordova is running in.
|
||||
*/
|
||||
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
|
||||
super.initialize(cordova, webView);
|
||||
Device.uuid = getUuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
*
|
||||
* @param action The action to execute.
|
||||
* @param args JSONArry of arguments for the plugin.
|
||||
* @param callbackContext The callback id used when calling back into JavaScript.
|
||||
* @return True if the action was valid, false if not.
|
||||
*/
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
if ("getDeviceInfo".equals(action)) {
|
||||
JSONObject r = new JSONObject();
|
||||
r.put("uuid", Device.uuid);
|
||||
r.put("version", this.getOSVersion());
|
||||
r.put("platform", this.getPlatform());
|
||||
r.put("model", this.getModel());
|
||||
r.put("manufacturer", this.getManufacturer());
|
||||
r.put("isVirtual", this.isVirtual());
|
||||
r.put("serial", this.getSerialNumber());
|
||||
callbackContext.success(r);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// LOCAL METHODS
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the OS name.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getPlatform() {
|
||||
String platform;
|
||||
if (isAmazonDevice()) {
|
||||
platform = AMAZON_PLATFORM;
|
||||
} else {
|
||||
platform = ANDROID_PLATFORM;
|
||||
}
|
||||
return platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device's Universally Unique Identifier (UUID).
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getUuid() {
|
||||
String uuid = Settings.Secure.getString(this.cordova.getActivity().getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
String model = android.os.Build.MODEL;
|
||||
return model;
|
||||
}
|
||||
|
||||
public String getProductName() {
|
||||
String productname = android.os.Build.PRODUCT;
|
||||
return productname;
|
||||
}
|
||||
|
||||
public String getManufacturer() {
|
||||
String manufacturer = android.os.Build.MANUFACTURER;
|
||||
return manufacturer;
|
||||
}
|
||||
|
||||
public String getSerialNumber() {
|
||||
String serial = android.os.Build.SERIAL;
|
||||
return serial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OS version.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getOSVersion() {
|
||||
String osversion = android.os.Build.VERSION.RELEASE;
|
||||
return osversion;
|
||||
}
|
||||
|
||||
public String getSDKVersion() {
|
||||
@SuppressWarnings("deprecation")
|
||||
String sdkversion = android.os.Build.VERSION.SDK;
|
||||
return sdkversion;
|
||||
}
|
||||
|
||||
public String getTimeZoneID() {
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
return (tz.getID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check if the device is manufactured by Amazon
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isAmazonDevice() {
|
||||
if (android.os.Build.MANUFACTURER.equals(AMAZON_DEVICE)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isVirtual() {
|
||||
return android.os.Build.FINGERPRINT.contains("generic") ||
|
||||
android.os.Build.PRODUCT.contains("sdk");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
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.dialogs;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.AlertDialog.Builder;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Resources;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
/**
|
||||
* This class provides access to notifications on the device.
|
||||
*
|
||||
* Be aware that this implementation gets called on
|
||||
* navigator.notification.{alert|confirm|prompt}, and that there is a separate
|
||||
* implementation in org.apache.cordova.CordovaChromeClient that gets
|
||||
* called on a simple window.{alert|confirm|prompt}.
|
||||
*/
|
||||
public class Notification extends CordovaPlugin {
|
||||
|
||||
private static final String LOG_TAG = "Notification";
|
||||
|
||||
private static final String ACTION_BEEP = "beep";
|
||||
private static final String ACTION_ALERT = "alert";
|
||||
private static final String ACTION_CONFIRM = "confirm";
|
||||
private static final String ACTION_PROMPT = "prompt";
|
||||
private static final String ACTION_ACTIVITY_START = "activityStart";
|
||||
private static final String ACTION_ACTIVITY_STOP = "activityStop";
|
||||
private static final String ACTION_PROGRESS_START = "progressStart";
|
||||
private static final String ACTION_PROGRESS_VALUE = "progressValue";
|
||||
private static final String ACTION_PROGRESS_STOP = "progressStop";
|
||||
|
||||
private static final long BEEP_TIMEOUT = 5000;
|
||||
private static final long BEEP_WAIT_TINE = 100;
|
||||
|
||||
public int confirmResult = -1;
|
||||
public ProgressDialog spinnerDialog = null;
|
||||
public ProgressDialog progressDialog = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public Notification() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
*
|
||||
* @param action The action to execute.
|
||||
* @param args JSONArray of arguments for the plugin.
|
||||
* @param callbackContext The callback context used when calling back into JavaScript.
|
||||
* @return True when the action was valid, false otherwise.
|
||||
*/
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
/*
|
||||
* Don't run any of these if the current activity is finishing
|
||||
* in order to avoid android.view.WindowManager$BadTokenException
|
||||
* crashing the app. Just return true here since false should only
|
||||
* be returned in the event of an invalid action.
|
||||
*/
|
||||
if (this.cordova.getActivity().isFinishing()) return true;
|
||||
|
||||
if (action.equals(ACTION_BEEP)) {
|
||||
this.beep(args.getLong(0));
|
||||
}
|
||||
else if (action.equals(ACTION_ALERT)) {
|
||||
this.alert(args.getString(0), args.getString(1), args.getString(2), callbackContext);
|
||||
return true;
|
||||
}
|
||||
else if (action.equals(ACTION_CONFIRM)) {
|
||||
this.confirm(args.getString(0), args.getString(1), args.getJSONArray(2), callbackContext);
|
||||
return true;
|
||||
}
|
||||
else if (action.equals(ACTION_PROMPT)) {
|
||||
this.prompt(args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), callbackContext);
|
||||
return true;
|
||||
}
|
||||
else if (action.equals(ACTION_ACTIVITY_START)) {
|
||||
this.activityStart(args.getString(0), args.getString(1));
|
||||
}
|
||||
else if (action.equals(ACTION_ACTIVITY_STOP)) {
|
||||
this.activityStop();
|
||||
}
|
||||
else if (action.equals(ACTION_PROGRESS_START)) {
|
||||
this.progressStart(args.getString(0), args.getString(1));
|
||||
}
|
||||
else if (action.equals(ACTION_PROGRESS_VALUE)) {
|
||||
this.progressValue(args.getInt(0));
|
||||
}
|
||||
else if (action.equals(ACTION_PROGRESS_STOP)) {
|
||||
this.progressStop();
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only alert and confirm are async.
|
||||
callbackContext.success();
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// LOCAL METHODS
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Beep plays the default notification ringtone.
|
||||
*
|
||||
* @param count Number of times to play notification
|
||||
*/
|
||||
public void beep(final long count) {
|
||||
cordova.getThreadPool().execute(new Runnable() {
|
||||
public void run() {
|
||||
Uri ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
Ringtone notification = RingtoneManager.getRingtone(cordova.getActivity().getBaseContext(), ringtone);
|
||||
|
||||
// If phone is not set to silent mode
|
||||
if (notification != null) {
|
||||
for (long i = 0; i < count; ++i) {
|
||||
notification.play();
|
||||
long timeout = BEEP_TIMEOUT;
|
||||
while (notification.isPlaying() && (timeout > 0)) {
|
||||
timeout = timeout - BEEP_WAIT_TINE;
|
||||
try {
|
||||
Thread.sleep(BEEP_WAIT_TINE);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and shows a native Android alert with given Strings
|
||||
* @param message The message the alert should display
|
||||
* @param title The title of the alert
|
||||
* @param buttonLabel The label of the button
|
||||
* @param callbackContext The callback context
|
||||
*/
|
||||
public synchronized void alert(final String message, final String title, final String buttonLabel, final CallbackContext callbackContext) {
|
||||
final CordovaInterface cordova = this.cordova;
|
||||
|
||||
Runnable runnable = new Runnable() {
|
||||
public void run() {
|
||||
|
||||
Builder dlg = createDialog(cordova); // new AlertDialog.Builder(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
dlg.setMessage(message);
|
||||
dlg.setTitle(title);
|
||||
dlg.setCancelable(true);
|
||||
dlg.setPositiveButton(buttonLabel,
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, 0));
|
||||
}
|
||||
});
|
||||
dlg.setOnCancelListener(new AlertDialog.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog)
|
||||
{
|
||||
dialog.dismiss();
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, 0));
|
||||
}
|
||||
});
|
||||
|
||||
changeTextDirection(dlg);
|
||||
};
|
||||
};
|
||||
this.cordova.getActivity().runOnUiThread(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and shows a native Android confirm dialog with given title, message, buttons.
|
||||
* This dialog only shows up to 3 buttons. Any labels after that will be ignored.
|
||||
* The index of the button pressed will be returned to the JavaScript callback identified by callbackId.
|
||||
*
|
||||
* @param message The message the dialog should display
|
||||
* @param title The title of the dialog
|
||||
* @param buttonLabels A comma separated list of button labels (Up to 3 buttons)
|
||||
* @param callbackContext The callback context.
|
||||
*/
|
||||
public synchronized void confirm(final String message, final String title, final JSONArray buttonLabels, final CallbackContext callbackContext) {
|
||||
final CordovaInterface cordova = this.cordova;
|
||||
|
||||
Runnable runnable = new Runnable() {
|
||||
public void run() {
|
||||
Builder dlg = createDialog(cordova); // new AlertDialog.Builder(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
dlg.setMessage(message);
|
||||
dlg.setTitle(title);
|
||||
dlg.setCancelable(true);
|
||||
|
||||
// First button
|
||||
if (buttonLabels.length() > 0) {
|
||||
try {
|
||||
dlg.setNegativeButton(buttonLabels.getString(0),
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, 1));
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on first button.");
|
||||
}
|
||||
}
|
||||
|
||||
// Second button
|
||||
if (buttonLabels.length() > 1) {
|
||||
try {
|
||||
dlg.setNeutralButton(buttonLabels.getString(1),
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, 2));
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on second button.");
|
||||
}
|
||||
}
|
||||
|
||||
// Third button
|
||||
if (buttonLabels.length() > 2) {
|
||||
try {
|
||||
dlg.setPositiveButton(buttonLabels.getString(2),
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, 3));
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on third button.");
|
||||
}
|
||||
}
|
||||
dlg.setOnCancelListener(new AlertDialog.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog)
|
||||
{
|
||||
dialog.dismiss();
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, 0));
|
||||
}
|
||||
});
|
||||
|
||||
changeTextDirection(dlg);
|
||||
};
|
||||
};
|
||||
this.cordova.getActivity().runOnUiThread(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and shows a native Android prompt dialog with given title, message, buttons.
|
||||
* This dialog only shows up to 3 buttons. Any labels after that will be ignored.
|
||||
* The following results are returned to the JavaScript callback identified by callbackId:
|
||||
* buttonIndex Index number of the button selected
|
||||
* input1 The text entered in the prompt dialog box
|
||||
*
|
||||
* @param message The message the dialog should display
|
||||
* @param title The title of the dialog
|
||||
* @param buttonLabels A comma separated list of button labels (Up to 3 buttons)
|
||||
* @param callbackContext The callback context.
|
||||
*/
|
||||
public synchronized void prompt(final String message, final String title, final JSONArray buttonLabels, final String defaultText, final CallbackContext callbackContext) {
|
||||
|
||||
final CordovaInterface cordova = this.cordova;
|
||||
|
||||
Runnable runnable = new Runnable() {
|
||||
public void run() {
|
||||
final EditText promptInput = new EditText(cordova.getActivity());
|
||||
|
||||
/* CB-11677 - By default, prompt input text color is set according current theme.
|
||||
But for some android versions is not visible (for example 5.1.1).
|
||||
android.R.color.primary_text_light will make text visible on all versions. */
|
||||
Resources resources = cordova.getActivity().getResources();
|
||||
int promptInputTextColor = resources.getColor(android.R.color.primary_text_light);
|
||||
promptInput.setTextColor(promptInputTextColor);
|
||||
promptInput.setText(defaultText);
|
||||
Builder dlg = createDialog(cordova); // new AlertDialog.Builder(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
dlg.setMessage(message);
|
||||
dlg.setTitle(title);
|
||||
dlg.setCancelable(true);
|
||||
|
||||
dlg.setView(promptInput);
|
||||
|
||||
final JSONObject result = new JSONObject();
|
||||
|
||||
// First button
|
||||
if (buttonLabels.length() > 0) {
|
||||
try {
|
||||
dlg.setNegativeButton(buttonLabels.getString(0),
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
try {
|
||||
result.put("buttonIndex",1);
|
||||
result.put("input1", promptInput.getText().toString().trim().length()==0 ? defaultText : promptInput.getText());
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on first button.", e);
|
||||
}
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on first button.");
|
||||
}
|
||||
}
|
||||
|
||||
// Second button
|
||||
if (buttonLabels.length() > 1) {
|
||||
try {
|
||||
dlg.setNeutralButton(buttonLabels.getString(1),
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
try {
|
||||
result.put("buttonIndex",2);
|
||||
result.put("input1", promptInput.getText().toString().trim().length()==0 ? defaultText : promptInput.getText());
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on second button.", e);
|
||||
}
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on second button.");
|
||||
}
|
||||
}
|
||||
|
||||
// Third button
|
||||
if (buttonLabels.length() > 2) {
|
||||
try {
|
||||
dlg.setPositiveButton(buttonLabels.getString(2),
|
||||
new AlertDialog.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
try {
|
||||
result.put("buttonIndex",3);
|
||||
result.put("input1", promptInput.getText().toString().trim().length()==0 ? defaultText : promptInput.getText());
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on third button.", e);
|
||||
}
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LOG.d(LOG_TAG,"JSONException on third button.");
|
||||
}
|
||||
}
|
||||
dlg.setOnCancelListener(new AlertDialog.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog){
|
||||
dialog.dismiss();
|
||||
try {
|
||||
result.put("buttonIndex",0);
|
||||
result.put("input1", promptInput.getText().toString().trim().length()==0 ? defaultText : promptInput.getText());
|
||||
} catch (JSONException e) { e.printStackTrace(); }
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
|
||||
}
|
||||
});
|
||||
|
||||
changeTextDirection(dlg);
|
||||
};
|
||||
};
|
||||
this.cordova.getActivity().runOnUiThread(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the spinner.
|
||||
*
|
||||
* @param title Title of the dialog
|
||||
* @param message The message of the dialog
|
||||
*/
|
||||
public synchronized void activityStart(final String title, final String message) {
|
||||
if (this.spinnerDialog != null) {
|
||||
this.spinnerDialog.dismiss();
|
||||
this.spinnerDialog = null;
|
||||
}
|
||||
final Notification notification = this;
|
||||
final CordovaInterface cordova = this.cordova;
|
||||
Runnable runnable = new Runnable() {
|
||||
public void run() {
|
||||
notification.spinnerDialog = createProgressDialog(cordova); // new ProgressDialog(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
notification.spinnerDialog.setTitle(title);
|
||||
notification.spinnerDialog.setMessage(message);
|
||||
notification.spinnerDialog.setCancelable(true);
|
||||
notification.spinnerDialog.setIndeterminate(true);
|
||||
notification.spinnerDialog.setOnCancelListener(
|
||||
new DialogInterface.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
notification.spinnerDialog = null;
|
||||
}
|
||||
});
|
||||
notification.spinnerDialog.show();
|
||||
}
|
||||
};
|
||||
this.cordova.getActivity().runOnUiThread(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop spinner.
|
||||
*/
|
||||
public synchronized void activityStop() {
|
||||
if (this.spinnerDialog != null) {
|
||||
this.spinnerDialog.dismiss();
|
||||
this.spinnerDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the progress dialog.
|
||||
*
|
||||
* @param title Title of the dialog
|
||||
* @param message The message of the dialog
|
||||
*/
|
||||
public synchronized void progressStart(final String title, final String message) {
|
||||
if (this.progressDialog != null) {
|
||||
this.progressDialog.dismiss();
|
||||
this.progressDialog = null;
|
||||
}
|
||||
final Notification notification = this;
|
||||
final CordovaInterface cordova = this.cordova;
|
||||
Runnable runnable = new Runnable() {
|
||||
public void run() {
|
||||
notification.progressDialog = createProgressDialog(cordova); // new ProgressDialog(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
notification.progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
notification.progressDialog.setTitle(title);
|
||||
notification.progressDialog.setMessage(message);
|
||||
notification.progressDialog.setCancelable(true);
|
||||
notification.progressDialog.setMax(100);
|
||||
notification.progressDialog.setProgress(0);
|
||||
notification.progressDialog.setOnCancelListener(
|
||||
new DialogInterface.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
notification.progressDialog = null;
|
||||
}
|
||||
});
|
||||
notification.progressDialog.show();
|
||||
}
|
||||
};
|
||||
this.cordova.getActivity().runOnUiThread(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value of progress bar.
|
||||
*
|
||||
* @param value 0-100
|
||||
*/
|
||||
public synchronized void progressValue(int value) {
|
||||
if (this.progressDialog != null) {
|
||||
this.progressDialog.setProgress(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop progress dialog.
|
||||
*/
|
||||
public synchronized void progressStop() {
|
||||
if (this.progressDialog != null) {
|
||||
this.progressDialog.dismiss();
|
||||
this.progressDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private Builder createDialog(CordovaInterface cordova) {
|
||||
int currentapiVersion = android.os.Build.VERSION.SDK_INT;
|
||||
if (currentapiVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) {
|
||||
return new Builder(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
} else {
|
||||
return new Builder(cordova.getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private ProgressDialog createProgressDialog(CordovaInterface cordova) {
|
||||
int currentapiVersion = android.os.Build.VERSION.SDK_INT;
|
||||
if (currentapiVersion >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
return new ProgressDialog(cordova.getActivity(), AlertDialog.THEME_DEVICE_DEFAULT_LIGHT);
|
||||
} else {
|
||||
return new ProgressDialog(cordova.getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private void changeTextDirection(Builder dlg){
|
||||
int currentapiVersion = android.os.Build.VERSION.SDK_INT;
|
||||
dlg.create();
|
||||
AlertDialog dialog = dlg.show();
|
||||
if (currentapiVersion >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
TextView messageview = (TextView)dialog.findViewById(android.R.id.message);
|
||||
messageview.setTextDirection(android.view.View.TEXT_DIRECTION_LOCALE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
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.globalization;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.text.format.Time;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class Globalization extends CordovaPlugin {
|
||||
//GlobalizationCommand Plugin Actions
|
||||
public static final String GETLOCALENAME = "getLocaleName";
|
||||
public static final String DATETOSTRING = "dateToString";
|
||||
public static final String STRINGTODATE = "stringToDate";
|
||||
public static final String GETDATEPATTERN = "getDatePattern";
|
||||
public static final String GETDATENAMES = "getDateNames";
|
||||
public static final String ISDAYLIGHTSAVINGSTIME = "isDayLightSavingsTime";
|
||||
public static final String GETFIRSTDAYOFWEEK = "getFirstDayOfWeek";
|
||||
public static final String NUMBERTOSTRING = "numberToString";
|
||||
public static final String STRINGTONUMBER = "stringToNumber";
|
||||
public static final String GETNUMBERPATTERN = "getNumberPattern";
|
||||
public static final String GETCURRENCYPATTERN = "getCurrencyPattern";
|
||||
public static final String GETPREFERREDLANGUAGE = "getPreferredLanguage";
|
||||
|
||||
//GlobalizationCommand Option Parameters
|
||||
public static final String OPTIONS = "options";
|
||||
public static final String FORMATLENGTH = "formatLength";
|
||||
//public static final String SHORT = "short"; //default for dateToString format
|
||||
public static final String MEDIUM = "medium";
|
||||
public static final String LONG = "long";
|
||||
public static final String FULL = "full";
|
||||
public static final String SELECTOR = "selector";
|
||||
//public static final String DATEANDTIME = "date and time"; //default for dateToString
|
||||
public static final String DATE = "date";
|
||||
public static final String TIME = "time";
|
||||
public static final String DATESTRING = "dateString";
|
||||
public static final String TYPE = "type";
|
||||
public static final String ITEM = "item";
|
||||
public static final String NARROW = "narrow";
|
||||
public static final String WIDE = "wide";
|
||||
public static final String MONTHS = "months";
|
||||
public static final String DAYS = "days";
|
||||
//public static final String DECMIAL = "wide"; //default for numberToString
|
||||
public static final String NUMBER = "number";
|
||||
public static final String NUMBERSTRING = "numberString";
|
||||
public static final String PERCENT = "percent";
|
||||
public static final String CURRENCY = "currency";
|
||||
public static final String CURRENCYCODE = "currencyCode";
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try{
|
||||
if (action.equals(GETLOCALENAME)){
|
||||
obj = getLocaleName();
|
||||
}else if (action.equals(GETPREFERREDLANGUAGE)){
|
||||
obj = getPreferredLanguage();
|
||||
} else if (action.equalsIgnoreCase(DATETOSTRING)) {
|
||||
obj = getDateToString(data);
|
||||
}else if(action.equalsIgnoreCase(STRINGTODATE)){
|
||||
obj = getStringtoDate(data);
|
||||
}else if(action.equalsIgnoreCase(GETDATEPATTERN)){
|
||||
obj = getDatePattern(data);
|
||||
}else if(action.equalsIgnoreCase(GETDATENAMES)){
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.GINGERBREAD) {
|
||||
throw new GlobalizationError(GlobalizationError.UNKNOWN_ERROR);
|
||||
} else {
|
||||
obj = getDateNames(data);
|
||||
}
|
||||
}else if(action.equalsIgnoreCase(ISDAYLIGHTSAVINGSTIME)){
|
||||
obj = getIsDayLightSavingsTime(data);
|
||||
}else if(action.equalsIgnoreCase(GETFIRSTDAYOFWEEK)){
|
||||
obj = getFirstDayOfWeek(data);
|
||||
}else if(action.equalsIgnoreCase(NUMBERTOSTRING)){
|
||||
obj = getNumberToString(data);
|
||||
}else if(action.equalsIgnoreCase(STRINGTONUMBER)){
|
||||
obj = getStringToNumber(data);
|
||||
}else if(action.equalsIgnoreCase(GETNUMBERPATTERN)){
|
||||
obj = getNumberPattern(data);
|
||||
}else if(action.equalsIgnoreCase(GETCURRENCYPATTERN)){
|
||||
obj = getCurrencyPattern(data);
|
||||
}else {
|
||||
return false;
|
||||
}
|
||||
|
||||
callbackContext.success(obj);
|
||||
}catch (GlobalizationError ge){
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, ge.toJson()));
|
||||
}catch (Exception e){
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
* @Description: Returns a well-formed ITEF BCP 47 language tag representing
|
||||
* the locale identifier for the client's current locale
|
||||
*
|
||||
* @Return: String: The BCP 47 language tag for the current locale
|
||||
*/
|
||||
private String toBcp47Language(Locale loc){
|
||||
final char SEP = '-'; // we will use a dash as per BCP 47
|
||||
String language = loc.getLanguage();
|
||||
String region = loc.getCountry();
|
||||
String variant = loc.getVariant();
|
||||
|
||||
// special case for Norwegian Nynorsk since "NY" cannot be a variant as per BCP 47
|
||||
// this goes before the string matching since "NY" wont pass the variant checks
|
||||
if( language.equals("no") && region.equals("NO") && variant.equals("NY")){
|
||||
language = "nn";
|
||||
region = "NO";
|
||||
variant = "";
|
||||
}
|
||||
|
||||
if( language.isEmpty() || !language.matches("\\p{Alpha}{2,8}")){
|
||||
language = "und"; // Follow the Locale#toLanguageTag() implementation
|
||||
// which says to return "und" for Undetermined
|
||||
}else if(language.equals("iw")){
|
||||
language = "he"; // correct deprecated "Hebrew"
|
||||
}else if(language.equals("in")){
|
||||
language = "id"; // correct deprecated "Indonesian"
|
||||
}else if(language.equals("ji")){
|
||||
language = "yi"; // correct deprecated "Yiddish"
|
||||
}
|
||||
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!region.matches("\\p{Alpha}{2}|\\p{Digit}{3}")) {
|
||||
region = "";
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}")) {
|
||||
variant = "";
|
||||
}
|
||||
|
||||
StringBuilder bcp47Tag = new StringBuilder(language);
|
||||
if (!region.isEmpty()) {
|
||||
bcp47Tag.append(SEP).append(region);
|
||||
}
|
||||
if (!variant.isEmpty()) {
|
||||
bcp47Tag.append(SEP).append(variant);
|
||||
}
|
||||
|
||||
return bcp47Tag.toString();
|
||||
}
|
||||
/*
|
||||
* @Description: Returns the BCP 47 Unicode locale identifier for current locale setting
|
||||
* The locale is defined by a language code, a country code, and a variant, separated
|
||||
* by a hyphen, for example, "en-US", "fr-CA", etc.,
|
||||
*
|
||||
* @Return: JSONObject
|
||||
* Object.value {String}: The locale identifier
|
||||
*
|
||||
* @throws: GlobalizationError.UNKNOWN_ERROR
|
||||
*/
|
||||
private JSONObject getLocaleName() throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
try{
|
||||
obj.put("value", toBcp47Language(Locale.getDefault()));
|
||||
return obj;
|
||||
}catch(Exception e){
|
||||
throw new GlobalizationError(GlobalizationError.UNKNOWN_ERROR);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* @Description: Returns the BCP 47 language tag for the client's
|
||||
* current language. Currently in Android this is the same as locale,
|
||||
* since Java does not distinguish between locale and language.
|
||||
*
|
||||
* @Return: JSONObject
|
||||
* Object.value {String}: The language identifier
|
||||
*
|
||||
* @throws: GlobalizationError.UNKNOWN_ERROR
|
||||
*/
|
||||
private JSONObject getPreferredLanguage() throws GlobalizationError {
|
||||
JSONObject obj = new JSONObject();
|
||||
try {
|
||||
obj.put("value", toBcp47Language(Locale.getDefault()));
|
||||
return obj;
|
||||
} catch (Exception e) {
|
||||
throw new GlobalizationError(GlobalizationError.UNKNOWN_ERROR);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* @Description: Returns a date formatted as a string according to the client's user preferences and
|
||||
* calendar using the time zone of the client.
|
||||
*
|
||||
* @Return: JSONObject
|
||||
* Object.value {String}: The localized date string
|
||||
*
|
||||
* @throws: GlobalizationError.FORMATTING_ERROR
|
||||
*/
|
||||
private JSONObject getDateToString(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
try{
|
||||
Date date = new Date((Long)options.getJSONObject(0).get(DATE));
|
||||
|
||||
//get formatting pattern from android device (Will only have device specific formatting for short form of date) or options supplied
|
||||
JSONObject datePattern = getDatePattern(options);
|
||||
SimpleDateFormat fmt = new SimpleDateFormat(datePattern.getString("pattern"));
|
||||
|
||||
//return formatted date
|
||||
return obj.put("value",fmt.format(date));
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.FORMATTING_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Parses a date formatted as a string according to the client's user
|
||||
* preferences and calendar using the time zone of the client and returns
|
||||
* the corresponding date object
|
||||
* @Return: JSONObject
|
||||
* Object.year {Number}: The four digit year
|
||||
* Object.month {Number}: The month from (0 - 11)
|
||||
* Object.day {Number}: The day from (1 - 31)
|
||||
* Object.hour {Number}: The hour from (0 - 23)
|
||||
* Object.minute {Number}: The minute from (0 - 59)
|
||||
* Object.second {Number}: The second from (0 - 59)
|
||||
* Object.millisecond {Number}: The milliseconds (from 0 - 999), not available on all platforms
|
||||
*
|
||||
* @throws: GlobalizationError.PARSING_ERROR
|
||||
*/
|
||||
private JSONObject getStringtoDate(JSONArray options)throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
Date date;
|
||||
try{
|
||||
//get format pattern from android device (Will only have device specific formatting for short form of date) or options supplied
|
||||
DateFormat fmt = new SimpleDateFormat(getDatePattern(options).getString("pattern"));
|
||||
|
||||
//attempt parsing string based on user preferences
|
||||
date = fmt.parse(options.getJSONObject(0).get(DATESTRING).toString());
|
||||
|
||||
//set Android Time object
|
||||
Time time = new Time();
|
||||
time.set(date.getTime());
|
||||
|
||||
//return properties;
|
||||
obj.put("year", time.year);
|
||||
obj.put("month", time.month);
|
||||
obj.put("day", time.monthDay);
|
||||
obj.put("hour", time.hour);
|
||||
obj.put("minute", time.minute);
|
||||
obj.put("second", time.second);
|
||||
obj.put("millisecond", Long.valueOf(0));
|
||||
return obj;
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.PARSING_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns a pattern string for formatting and parsing dates according to the client's
|
||||
* user preferences.
|
||||
* @Return: JSONObject
|
||||
*
|
||||
* Object.pattern {String}: The date and time pattern for formatting and parsing dates.
|
||||
* The patterns follow Unicode Technical Standard #35
|
||||
* http://unicode.org/reports/tr35/tr35-4.html
|
||||
* Object.timezone {String}: The abbreviated name of the time zone on the client
|
||||
* Object.utc_offset {Number}: The current difference in seconds between the client's
|
||||
* time zone and coordinated universal time.
|
||||
* Object.dst_offset {Number}: The current daylight saving time offset in seconds
|
||||
* between the client's non-daylight saving's time zone
|
||||
* and the client's daylight saving's time zone.
|
||||
*
|
||||
* @throws: GlobalizationError.PATTERN_ERROR
|
||||
*/
|
||||
private JSONObject getDatePattern(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try{
|
||||
SimpleDateFormat fmtDate = (SimpleDateFormat)android.text.format.DateFormat.getDateFormat(this.cordova.getActivity()); //default user preference for date
|
||||
SimpleDateFormat fmtTime = (SimpleDateFormat)android.text.format.DateFormat.getTimeFormat(this.cordova.getActivity()); //default user preference for time
|
||||
|
||||
String fmt = fmtDate.toLocalizedPattern() + " " + fmtTime.toLocalizedPattern(); //default SHORT date/time format. ex. dd/MM/yyyy h:mm a
|
||||
|
||||
//get Date value + options (if available)
|
||||
if (options.getJSONObject(0).has(OPTIONS)){
|
||||
//options were included
|
||||
|
||||
JSONObject innerOptions = options.getJSONObject(0).getJSONObject(OPTIONS);
|
||||
//get formatLength option
|
||||
if (!innerOptions.isNull(FORMATLENGTH)){
|
||||
String fmtOpt = innerOptions.getString(FORMATLENGTH);
|
||||
if (fmtOpt.equalsIgnoreCase(MEDIUM)){//medium
|
||||
fmtDate = (SimpleDateFormat)android.text.format.DateFormat.getMediumDateFormat(this.cordova.getActivity());
|
||||
}else if (fmtOpt.equalsIgnoreCase(LONG) || fmtOpt.equalsIgnoreCase(FULL)){ //long/full
|
||||
fmtDate = (SimpleDateFormat)android.text.format.DateFormat.getLongDateFormat(this.cordova.getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
//return pattern type
|
||||
fmt = fmtDate.toLocalizedPattern() + " " + fmtTime.toLocalizedPattern();
|
||||
if (!innerOptions.isNull(SELECTOR)){
|
||||
String selOpt = innerOptions.getString(SELECTOR);
|
||||
if (selOpt.equalsIgnoreCase(DATE)){
|
||||
fmt = fmtDate.toLocalizedPattern();
|
||||
}else if (selOpt.equalsIgnoreCase(TIME)){
|
||||
fmt = fmtTime.toLocalizedPattern();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TimeZone from users device
|
||||
//TimeZone tz = Calendar.getInstance(Locale.getDefault()).getTimeZone(); //substitute method
|
||||
TimeZone tz = TimeZone.getTimeZone(Time.getCurrentTimezone());
|
||||
|
||||
obj.put("pattern", fmt);
|
||||
obj.put("timezone", tz.getDisplayName(tz.inDaylightTime(Calendar.getInstance().getTime()),TimeZone.SHORT));
|
||||
obj.put("iana_timezone", tz.getID());
|
||||
obj.put("utc_offset", tz.getRawOffset()/1000);
|
||||
obj.put("dst_offset", tz.getDSTSavings()/1000);
|
||||
return obj;
|
||||
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.PATTERN_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns an array of either the names of the months or days of the week
|
||||
* according to the client's user preferences and calendar
|
||||
* @Return: JSONObject
|
||||
* Object.value {Array{String}}: The array of names starting from either
|
||||
* the first month in the year or the
|
||||
* first day of the week.
|
||||
*
|
||||
* @throws: GlobalizationError.UNKNOWN_ERROR
|
||||
*/
|
||||
@TargetApi(9)
|
||||
private JSONObject getDateNames(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
//String[] value;
|
||||
JSONArray value = new JSONArray();
|
||||
List<String> namesList = new ArrayList<String>();
|
||||
final Map<String,Integer> namesMap; // final needed for sorting with anonymous comparator
|
||||
try{
|
||||
int type = 0; //default wide
|
||||
int item = 0; //default months
|
||||
|
||||
//get options if available
|
||||
if (options.getJSONObject(0).length() > 0){
|
||||
//get type if available
|
||||
if (!((JSONObject)options.getJSONObject(0).get(OPTIONS)).isNull(TYPE)){
|
||||
String t = (String)((JSONObject)options.getJSONObject(0).get(OPTIONS)).get(TYPE);
|
||||
if (t.equalsIgnoreCase(NARROW)){type++;} //DateUtils.LENGTH_MEDIUM
|
||||
}
|
||||
//get item if available
|
||||
if (!((JSONObject)options.getJSONObject(0).get(OPTIONS)).isNull(ITEM)){
|
||||
String t = (String)((JSONObject)options.getJSONObject(0).get(OPTIONS)).get(ITEM);
|
||||
if (t.equalsIgnoreCase(DAYS)){item += 10;} //Days of week start at 1
|
||||
}
|
||||
}
|
||||
//determine return value
|
||||
int method = item + type;
|
||||
if (method == 1) { //months and narrow
|
||||
namesMap = Calendar.getInstance().getDisplayNames(Calendar.MONTH, Calendar.SHORT, Locale.getDefault());
|
||||
} else if (method == 10) { //days and wide
|
||||
namesMap = Calendar.getInstance().getDisplayNames(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault());
|
||||
} else if (method == 11) { //days and narrow
|
||||
namesMap = Calendar.getInstance().getDisplayNames(Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.getDefault());
|
||||
} else { //default: months and wide
|
||||
namesMap = Calendar.getInstance().getDisplayNames(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
|
||||
}
|
||||
|
||||
// save names as a list
|
||||
for(String name : namesMap.keySet()) {
|
||||
namesList.add(name);
|
||||
}
|
||||
|
||||
// sort the list according to values in namesMap
|
||||
Collections.sort(namesList, new Comparator<String>() {
|
||||
public int compare(String arg0, String arg1) {
|
||||
return namesMap.get(arg0).compareTo(namesMap.get(arg1));
|
||||
}
|
||||
});
|
||||
|
||||
// convert nameList into JSONArray of String objects
|
||||
for (int i = 0; i < namesList.size(); i ++){
|
||||
value.put(namesList.get(i));
|
||||
}
|
||||
|
||||
//return array of names
|
||||
return obj.put("value", value);
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.UNKNOWN_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns whether daylight savings time is in effect for a given date using the client's
|
||||
* time zone and calendar.
|
||||
* @Return: JSONObject
|
||||
* Object.dst {Boolean}: The value "true" indicates that daylight savings time is
|
||||
* in effect for the given date and "false" indicate that it is not. *
|
||||
*
|
||||
* @throws: GlobalizationError.UNKNOWN_ERROR
|
||||
*/
|
||||
private JSONObject getIsDayLightSavingsTime(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
boolean dst = false;
|
||||
try{
|
||||
Date date = new Date((Long)options.getJSONObject(0).get(DATE));
|
||||
//TimeZone tz = Calendar.getInstance(Locale.getDefault()).getTimeZone();
|
||||
TimeZone tz = TimeZone.getTimeZone(Time.getCurrentTimezone());
|
||||
dst = tz.inDaylightTime(date); //get daylight savings data from date object and user timezone settings
|
||||
|
||||
return obj.put("dst",dst);
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.UNKNOWN_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns the first day of the week according to the client's user preferences and calendar.
|
||||
* The days of the week are numbered starting from 1 where 1 is considered to be Sunday.
|
||||
* @Return: JSONObject
|
||||
* Object.value {Number}: The number of the first day of the week.
|
||||
*
|
||||
* @throws: GlobalizationError.UNKNOWN_ERROR
|
||||
*/
|
||||
private JSONObject getFirstDayOfWeek(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
try{
|
||||
int value = Calendar.getInstance(Locale.getDefault()).getFirstDayOfWeek(); //get first day of week based on user locale settings
|
||||
return obj.put("value", value);
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.UNKNOWN_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns a number formatted as a string according to the client's user preferences.
|
||||
* @Return: JSONObject
|
||||
* Object.value {String}: The formatted number string.
|
||||
*
|
||||
* @throws: GlobalizationError.FORMATTING_ERROR
|
||||
*/
|
||||
private JSONObject getNumberToString(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
String value = "";
|
||||
try{
|
||||
DecimalFormat fmt = getNumberFormatInstance(options);//returns Decimal/Currency/Percent instance
|
||||
value = fmt.format(options.getJSONObject(0).get(NUMBER));
|
||||
return obj.put("value", value);
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.FORMATTING_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Parses a number formatted as a string according to the client's user preferences and
|
||||
* returns the corresponding number.
|
||||
* @Return: JSONObject
|
||||
* Object.value {Number}: The parsed number.
|
||||
*
|
||||
* @throws: GlobalizationError.PARSING_ERROR
|
||||
*/
|
||||
private JSONObject getStringToNumber(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
Number value;
|
||||
try{
|
||||
DecimalFormat fmt = getNumberFormatInstance(options); //returns Decimal/Currency/Percent instance
|
||||
value = fmt.parse((String)options.getJSONObject(0).get(NUMBERSTRING));
|
||||
return obj.put("value", value);
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.PARSING_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns a pattern string for formatting and parsing numbers according to the client's user
|
||||
* preferences.
|
||||
* @Return: JSONObject
|
||||
* Object.pattern {String}: The number pattern for formatting and parsing numbers.
|
||||
* The patterns follow Unicode Technical Standard #35.
|
||||
* http://unicode.org/reports/tr35/tr35-4.html
|
||||
* Object.symbol {String}: The symbol to be used when formatting and parsing
|
||||
* e.g., percent or currency symbol.
|
||||
* Object.fraction {Number}: The number of fractional digits to use when parsing and
|
||||
* formatting numbers.
|
||||
* Object.rounding {Number}: The rounding increment to use when parsing and formatting.
|
||||
* Object.positive {String}: The symbol to use for positive numbers when parsing and formatting.
|
||||
* Object.negative: {String}: The symbol to use for negative numbers when parsing and formatting.
|
||||
* Object.decimal: {String}: The decimal symbol to use for parsing and formatting.
|
||||
* Object.grouping: {String}: The grouping symbol to use for parsing and formatting.
|
||||
*
|
||||
* @throws: GlobalizationError.PATTERN_ERROR
|
||||
*/
|
||||
private JSONObject getNumberPattern(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
try{
|
||||
//uses java.text.DecimalFormat to format value
|
||||
DecimalFormat fmt = (DecimalFormat) DecimalFormat.getInstance(Locale.getDefault()); //default format
|
||||
String symbol = String.valueOf(fmt.getDecimalFormatSymbols().getDecimalSeparator());
|
||||
//get Date value + options (if available)
|
||||
if (options.getJSONObject(0).length() > 0){
|
||||
//options were included
|
||||
if (!((JSONObject)options.getJSONObject(0).get(OPTIONS)).isNull(TYPE)){
|
||||
String fmtOpt = (String)((JSONObject)options.getJSONObject(0).get(OPTIONS)).get(TYPE);
|
||||
if (fmtOpt.equalsIgnoreCase(CURRENCY)){
|
||||
fmt = (DecimalFormat) DecimalFormat.getCurrencyInstance(Locale.getDefault());
|
||||
symbol = fmt.getDecimalFormatSymbols().getCurrencySymbol();
|
||||
}else if(fmtOpt.equalsIgnoreCase(PERCENT)){
|
||||
fmt = (DecimalFormat) DecimalFormat.getPercentInstance(Locale.getDefault());
|
||||
symbol = String.valueOf(fmt.getDecimalFormatSymbols().getPercent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return properties
|
||||
obj.put("pattern", fmt.toPattern());
|
||||
obj.put("symbol", symbol);
|
||||
obj.put("fraction", fmt.getMinimumFractionDigits());
|
||||
obj.put("rounding", Integer.valueOf(0));
|
||||
obj.put("positive", fmt.getPositivePrefix());
|
||||
obj.put("negative", fmt.getNegativePrefix());
|
||||
obj.put("decimal", String.valueOf(fmt.getDecimalFormatSymbols().getDecimalSeparator()));
|
||||
obj.put("grouping", String.valueOf(fmt.getDecimalFormatSymbols().getGroupingSeparator()));
|
||||
|
||||
return obj;
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.PATTERN_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Returns a pattern string for formatting and parsing currency values according to the client's
|
||||
* user preferences and ISO 4217 currency code.
|
||||
* @Return: JSONObject
|
||||
* Object.pattern {String}: The currency pattern for formatting and parsing currency values.
|
||||
* The patterns follow Unicode Technical Standard #35
|
||||
* http://unicode.org/reports/tr35/tr35-4.html
|
||||
* Object.code {String}: The ISO 4217 currency code for the pattern.
|
||||
* Object.fraction {Number}: The number of fractional digits to use when parsing and
|
||||
* formatting currency.
|
||||
* Object.rounding {Number}: The rounding increment to use when parsing and formatting.
|
||||
* Object.decimal: {String}: The decimal symbol to use for parsing and formatting.
|
||||
* Object.grouping: {String}: The grouping symbol to use for parsing and formatting.
|
||||
*
|
||||
* @throws: GlobalizationError.FORMATTING_ERROR
|
||||
*/
|
||||
private JSONObject getCurrencyPattern(JSONArray options) throws GlobalizationError{
|
||||
JSONObject obj = new JSONObject();
|
||||
try{
|
||||
//get ISO 4217 currency code
|
||||
String code = options.getJSONObject(0).getString(CURRENCYCODE);
|
||||
|
||||
//uses java.text.DecimalFormat to format value
|
||||
DecimalFormat fmt = (DecimalFormat) DecimalFormat.getCurrencyInstance(Locale.getDefault());
|
||||
|
||||
//set currency format
|
||||
Currency currency = Currency.getInstance(code);
|
||||
fmt.setCurrency(currency);
|
||||
|
||||
//return properties
|
||||
obj.put("pattern", fmt.toPattern());
|
||||
obj.put("code", currency.getCurrencyCode());
|
||||
obj.put("fraction", fmt.getMinimumFractionDigits());
|
||||
obj.put("rounding", Integer.valueOf(0));
|
||||
obj.put("decimal", String.valueOf(fmt.getDecimalFormatSymbols().getDecimalSeparator()));
|
||||
obj.put("grouping", String.valueOf(fmt.getDecimalFormatSymbols().getGroupingSeparator()));
|
||||
|
||||
return obj;
|
||||
}catch(Exception ge){
|
||||
throw new GlobalizationError(GlobalizationError.FORMATTING_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Description: Parses a JSONArray from user options and returns the correct Instance of Decimal/Percent/Currency.
|
||||
* @Return: DecimalFormat : The Instance to use.
|
||||
*
|
||||
* @throws: JSONException
|
||||
*/
|
||||
private DecimalFormat getNumberFormatInstance(JSONArray options) throws JSONException{
|
||||
DecimalFormat fmt = (DecimalFormat)DecimalFormat.getInstance(Locale.getDefault()); //default format
|
||||
try{
|
||||
if (options.getJSONObject(0).length() > 1){
|
||||
//options were included
|
||||
if (!((JSONObject)options.getJSONObject(0).get(OPTIONS)).isNull(TYPE)){
|
||||
String fmtOpt = (String)((JSONObject)options.getJSONObject(0).get(OPTIONS)).get(TYPE);
|
||||
if (fmtOpt.equalsIgnoreCase(CURRENCY)){
|
||||
fmt = (DecimalFormat)DecimalFormat.getCurrencyInstance(Locale.getDefault());
|
||||
}else if(fmtOpt.equalsIgnoreCase(PERCENT)){
|
||||
fmt = (DecimalFormat)DecimalFormat.getPercentInstance(Locale.getDefault());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}catch (JSONException je){}
|
||||
return fmt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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.globalization;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* @description Exception class representing defined Globalization error codes
|
||||
* @Globalization error codes:
|
||||
* GlobalizationError.UNKNOWN_ERROR = 0;
|
||||
* GlobalizationError.FORMATTING_ERROR = 1;
|
||||
* GlobalizationError.PARSING_ERROR = 2;
|
||||
* GlobalizationError.PATTERN_ERROR = 3;
|
||||
*/
|
||||
public class GlobalizationError extends Exception{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
||||
public static final String FORMATTING_ERROR = "FORMATTING_ERROR";
|
||||
public static final String PARSING_ERROR = "PARSING_ERROR";
|
||||
public static final String PATTERN_ERROR = "PATTERN_ERROR";
|
||||
|
||||
int error = 0; //default unknown error thrown
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public GlobalizationError() {}
|
||||
/**
|
||||
* Create an exception returning an error code
|
||||
*
|
||||
* @param s
|
||||
*/
|
||||
public GlobalizationError(String s) {
|
||||
if (s.equalsIgnoreCase(FORMATTING_ERROR)){
|
||||
error = 1;
|
||||
}else if (s.equalsIgnoreCase(PARSING_ERROR)){
|
||||
error = 2;
|
||||
}else if (s.equalsIgnoreCase(PATTERN_ERROR)){
|
||||
error = 3;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* get error string based on error code
|
||||
*
|
||||
* @param String msg
|
||||
*/
|
||||
public String getErrorString(){
|
||||
String msg = "";
|
||||
switch (error){
|
||||
case 0:
|
||||
msg = UNKNOWN_ERROR;
|
||||
break;
|
||||
case 1:
|
||||
msg = FORMATTING_ERROR;
|
||||
break;
|
||||
case 2:
|
||||
msg = PARSING_ERROR;
|
||||
break;
|
||||
case 3:
|
||||
msg = PATTERN_ERROR;
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
/**
|
||||
* get error code
|
||||
*
|
||||
* @param String msg
|
||||
*/
|
||||
public int getErrorCode(){
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the json version of this object to return to javascript
|
||||
* @return
|
||||
*/
|
||||
public JSONObject toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
try {
|
||||
obj.put("code", getErrorCode());
|
||||
obj.put("message", getErrorString());
|
||||
} catch (JSONException e) {
|
||||
// never happens
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
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.inappbrowser;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Created by Oliver on 22/11/2013.
|
||||
*/
|
||||
public class InAppBrowserDialog extends Dialog {
|
||||
Context context;
|
||||
InAppBrowser inAppBrowser = null;
|
||||
|
||||
public InAppBrowserDialog(Context context, int theme) {
|
||||
super(context, theme);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setInAppBroswer(InAppBrowser browser) {
|
||||
this.inAppBrowser = browser;
|
||||
}
|
||||
|
||||
public void onBackPressed () {
|
||||
if (this.inAppBrowser == null) {
|
||||
this.dismiss();
|
||||
} else {
|
||||
// better to go through the in inAppBrowser
|
||||
// because it does a clean up
|
||||
if (this.inAppBrowser.hardwareBack() && this.inAppBrowser.canGoBack()) {
|
||||
this.inAppBrowser.goBack();
|
||||
} else {
|
||||
this.inAppBrowser.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
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.inappbrowser;
|
||||
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.Message;
|
||||
import android.webkit.JsPromptResult;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebStorage;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.webkit.GeolocationPermissions.Callback;
|
||||
|
||||
public class InAppChromeClient extends WebChromeClient {
|
||||
|
||||
private CordovaWebView webView;
|
||||
private String LOG_TAG = "InAppChromeClient";
|
||||
private long MAX_QUOTA = 100 * 1024 * 1024;
|
||||
|
||||
public InAppChromeClient(CordovaWebView webView) {
|
||||
super();
|
||||
this.webView = webView;
|
||||
}
|
||||
/**
|
||||
* Handle database quota exceeded notification.
|
||||
*
|
||||
* @param url
|
||||
* @param databaseIdentifier
|
||||
* @param currentQuota
|
||||
* @param estimatedSize
|
||||
* @param totalUsedQuota
|
||||
* @param quotaUpdater
|
||||
*/
|
||||
@Override
|
||||
public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
|
||||
long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
|
||||
{
|
||||
LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
|
||||
quotaUpdater.updateQuota(MAX_QUOTA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin.
|
||||
*
|
||||
* @param origin
|
||||
* @param callback
|
||||
*/
|
||||
@Override
|
||||
public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
|
||||
super.onGeolocationPermissionsShowPrompt(origin, callback);
|
||||
callback.invoke(origin, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the client to display a prompt dialog to the user.
|
||||
* If the client returns true, WebView will assume that the client will
|
||||
* handle the prompt dialog and call the appropriate JsPromptResult method.
|
||||
*
|
||||
* The prompt bridge provided for the InAppBrowser is capable of executing any
|
||||
* oustanding callback belonging to the InAppBrowser plugin. Care has been
|
||||
* taken that other callbacks cannot be triggered, and that no other code
|
||||
* execution is possible.
|
||||
*
|
||||
* To trigger the bridge, the prompt default value should be of the form:
|
||||
*
|
||||
* gap-iab://<callbackId>
|
||||
*
|
||||
* where <callbackId> is the string id of the callback to trigger (something
|
||||
* like "InAppBrowser0123456789")
|
||||
*
|
||||
* If present, the prompt message is expected to be a JSON-encoded value to
|
||||
* pass to the callback. A JSON_EXCEPTION is returned if the JSON is invalid.
|
||||
*
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param defaultValue
|
||||
* @param result
|
||||
*/
|
||||
@Override
|
||||
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
|
||||
// See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute.
|
||||
if (defaultValue != null && defaultValue.startsWith("gap")) {
|
||||
if(defaultValue.startsWith("gap-iab://")) {
|
||||
PluginResult scriptResult;
|
||||
String scriptCallbackId = defaultValue.substring(10);
|
||||
if (scriptCallbackId.matches("^InAppBrowser[0-9]{1,10}$")) {
|
||||
if(message == null || message.length() == 0) {
|
||||
scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray());
|
||||
} else {
|
||||
try {
|
||||
scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message));
|
||||
} catch(JSONException e) {
|
||||
scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
|
||||
}
|
||||
}
|
||||
this.webView.sendPluginResult(scriptResult, scriptCallbackId);
|
||||
result.confirm("");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// Anything else that doesn't look like InAppBrowser0123456789 should end up here
|
||||
LOG.w(LOG_TAG, "InAppBrowser callback called with invalid callbackId : "+ scriptCallbackId);
|
||||
result.cancel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Anything else with a gap: prefix should get this message
|
||||
LOG.w(LOG_TAG, "InAppBrowser does not support Cordova API calls: " + url + " " + defaultValue);
|
||||
result.cancel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The InAppWebBrowser WebView is configured to MultipleWindow mode to mitigate a security
|
||||
* bug found in Chromium prior to version 83.0.4103.106.
|
||||
* See https://bugs.chromium.org/p/chromium/issues/detail?id=1083819
|
||||
*
|
||||
* Valid Urls set to open in new window will be routed back to load in the original WebView.
|
||||
*
|
||||
* @param view
|
||||
* @param isDialog
|
||||
* @param isUserGesture
|
||||
* @param resultMsg
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
|
||||
WebView inAppWebView = view;
|
||||
final WebViewClient webViewClient =
|
||||
new WebViewClient() {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
inAppWebView.loadUrl(request.getUrl().toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
inAppWebView.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
final WebView newWebView = new WebView(view.getContext());
|
||||
newWebView.setWebViewClient(webViewClient);
|
||||
|
||||
final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
|
||||
transport.setWebView(newWebView);
|
||||
resultMsg.sendToTarget();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
/*
|
||||
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.media;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CordovaResourceApi;
|
||||
import org.apache.cordova.PermissionHelper;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import java.security.Permission;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* This class called by CordovaActivity to play and record audio.
|
||||
* The file can be local or over a network using http.
|
||||
*
|
||||
* Audio formats supported (tested):
|
||||
* .mp3, .wav
|
||||
*
|
||||
* Local audio files must reside in one of two places:
|
||||
* android_asset: file name must start with /android_asset/sound.mp3
|
||||
* sdcard: file name is just sound.mp3
|
||||
*/
|
||||
public class AudioHandler extends CordovaPlugin {
|
||||
|
||||
public static String TAG = "AudioHandler";
|
||||
HashMap<String, AudioPlayer> players; // Audio player object
|
||||
ArrayList<AudioPlayer> pausedForPhone; // Audio players that were paused when phone call came in
|
||||
ArrayList<AudioPlayer> pausedForFocus; // Audio players that were paused when focus was lost
|
||||
private int origVolumeStream = -1;
|
||||
private CallbackContext messageChannel;
|
||||
|
||||
|
||||
public static String [] permissions = { Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE};
|
||||
public static int RECORD_AUDIO = 0;
|
||||
public static int WRITE_EXTERNAL_STORAGE = 1;
|
||||
|
||||
public static final int PERMISSION_DENIED_ERROR = 20;
|
||||
|
||||
private String recordId;
|
||||
private String fileUriStr;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public AudioHandler() {
|
||||
this.players = new HashMap<String, AudioPlayer>();
|
||||
this.pausedForPhone = new ArrayList<AudioPlayer>();
|
||||
this.pausedForFocus = new ArrayList<AudioPlayer>();
|
||||
}
|
||||
|
||||
|
||||
protected void getWritePermission(int requestCode)
|
||||
{
|
||||
PermissionHelper.requestPermission(this, requestCode, permissions[WRITE_EXTERNAL_STORAGE]);
|
||||
}
|
||||
|
||||
|
||||
protected void getMicPermission(int requestCode)
|
||||
{
|
||||
PermissionHelper.requestPermission(this, requestCode, permissions[RECORD_AUDIO]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
* @param action The action to execute.
|
||||
* @param args JSONArry of arguments for the plugin.
|
||||
* @param callbackContext The callback context used when calling back into JavaScript.
|
||||
* @return A PluginResult object with a status and message.
|
||||
*/
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
CordovaResourceApi resourceApi = webView.getResourceApi();
|
||||
PluginResult.Status status = PluginResult.Status.OK;
|
||||
String result = "";
|
||||
|
||||
if (action.equals("startRecordingAudio")) {
|
||||
recordId = args.getString(0);
|
||||
String target = args.getString(1);
|
||||
try {
|
||||
Uri targetUri = resourceApi.remapUri(Uri.parse(target));
|
||||
fileUriStr = targetUri.toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
fileUriStr = target;
|
||||
}
|
||||
promptForRecord();
|
||||
}
|
||||
else if (action.equals("stopRecordingAudio")) {
|
||||
this.stopRecordingAudio(args.getString(0), true);
|
||||
}
|
||||
else if (action.equals("pauseRecordingAudio")) {
|
||||
this.stopRecordingAudio(args.getString(0), false);
|
||||
}
|
||||
else if (action.equals("resumeRecordingAudio")) {
|
||||
this.resumeRecordingAudio(args.getString(0));
|
||||
}
|
||||
else if (action.equals("startPlayingAudio")) {
|
||||
String target = args.getString(1);
|
||||
String fileUriStr;
|
||||
try {
|
||||
Uri targetUri = resourceApi.remapUri(Uri.parse(target));
|
||||
fileUriStr = targetUri.toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
fileUriStr = target;
|
||||
}
|
||||
this.startPlayingAudio(args.getString(0), FileHelper.stripFileProtocol(fileUriStr));
|
||||
}
|
||||
else if (action.equals("seekToAudio")) {
|
||||
this.seekToAudio(args.getString(0), args.getInt(1));
|
||||
}
|
||||
else if (action.equals("pausePlayingAudio")) {
|
||||
this.pausePlayingAudio(args.getString(0));
|
||||
}
|
||||
else if (action.equals("stopPlayingAudio")) {
|
||||
this.stopPlayingAudio(args.getString(0));
|
||||
} else if (action.equals("setVolume")) {
|
||||
try {
|
||||
this.setVolume(args.getString(0), Float.parseFloat(args.getString(1)));
|
||||
} catch (NumberFormatException nfe) {
|
||||
//no-op
|
||||
}
|
||||
} else if (action.equals("getCurrentPositionAudio")) {
|
||||
float f = this.getCurrentPositionAudio(args.getString(0));
|
||||
callbackContext.sendPluginResult(new PluginResult(status, f));
|
||||
return true;
|
||||
}
|
||||
else if (action.equals("getDurationAudio")) {
|
||||
float f = this.getDurationAudio(args.getString(0), args.getString(1));
|
||||
callbackContext.sendPluginResult(new PluginResult(status, f));
|
||||
return true;
|
||||
}
|
||||
else if (action.equals("create")) {
|
||||
String id = args.getString(0);
|
||||
String src = FileHelper.stripFileProtocol(args.getString(1));
|
||||
getOrCreatePlayer(id, src);
|
||||
}
|
||||
else if (action.equals("release")) {
|
||||
boolean b = this.release(args.getString(0));
|
||||
callbackContext.sendPluginResult(new PluginResult(status, b));
|
||||
return true;
|
||||
}
|
||||
else if (action.equals("messageChannel")) {
|
||||
messageChannel = callbackContext;
|
||||
return true;
|
||||
} else if (action.equals("getCurrentAmplitudeAudio")) {
|
||||
float f = this.getCurrentAmplitudeAudio(args.getString(0));
|
||||
callbackContext.sendPluginResult(new PluginResult(status, f));
|
||||
return true;
|
||||
}
|
||||
else { // Unrecognized action.
|
||||
return false;
|
||||
}
|
||||
|
||||
callbackContext.sendPluginResult(new PluginResult(status, result));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all audio players and recorders.
|
||||
*/
|
||||
public void onDestroy() {
|
||||
if (!players.isEmpty()) {
|
||||
onLastPlayerReleased();
|
||||
}
|
||||
for (AudioPlayer audio : this.players.values()) {
|
||||
audio.destroy();
|
||||
}
|
||||
this.players.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all audio players and recorders on navigate.
|
||||
*/
|
||||
@Override
|
||||
public void onReset() {
|
||||
onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a message is sent to plugin.
|
||||
*
|
||||
* @param id The message id
|
||||
* @param data The message data
|
||||
* @return Object to stop propagation or null
|
||||
*/
|
||||
public Object onMessage(String id, Object data) {
|
||||
|
||||
// If phone message
|
||||
if (id.equals("telephone")) {
|
||||
|
||||
// If phone ringing, then pause playing
|
||||
if ("ringing".equals(data) || "offhook".equals(data)) {
|
||||
|
||||
// Get all audio players and pause them
|
||||
for (AudioPlayer audio : this.players.values()) {
|
||||
if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) {
|
||||
this.pausedForPhone.add(audio);
|
||||
audio.pausePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If phone idle, then resume playing those players we paused
|
||||
else if ("idle".equals(data)) {
|
||||
for (AudioPlayer audio : this.pausedForPhone) {
|
||||
audio.startPlaying(null);
|
||||
}
|
||||
this.pausedForPhone.clear();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// LOCAL METHODS
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
private AudioPlayer getOrCreatePlayer(String id, String file) {
|
||||
AudioPlayer ret = players.get(id);
|
||||
if (ret == null) {
|
||||
if (players.isEmpty()) {
|
||||
onFirstPlayerCreated();
|
||||
}
|
||||
ret = new AudioPlayer(this, id, file);
|
||||
players.put(id, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the audio player instance to save memory.
|
||||
* @param id The id of the audio player
|
||||
*/
|
||||
private boolean release(String id) {
|
||||
AudioPlayer audio = players.remove(id);
|
||||
if (audio == null) {
|
||||
return false;
|
||||
}
|
||||
if (players.isEmpty()) {
|
||||
onLastPlayerReleased();
|
||||
}
|
||||
audio.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start recording and save the specified file.
|
||||
* @param id The id of the audio player
|
||||
* @param file The name of the file
|
||||
*/
|
||||
public void startRecordingAudio(String id, String file) {
|
||||
AudioPlayer audio = getOrCreatePlayer(id, file);
|
||||
audio.startRecording(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop/Pause recording and save to the file specified when recording started.
|
||||
* @param id The id of the audio player
|
||||
* @param stop If true stop recording, if false pause recording
|
||||
*/
|
||||
public void stopRecordingAudio(String id, boolean stop) {
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
audio.stopRecording(stop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume recording
|
||||
* @param id The id of the audio player
|
||||
*/
|
||||
public void resumeRecordingAudio(String id) {
|
||||
AudioPlayer audio = players.get(id);
|
||||
if (audio != null) {
|
||||
audio.resumeRecording();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or resume playing audio file.
|
||||
* @param id The id of the audio player
|
||||
* @param file The name of the audio file.
|
||||
*/
|
||||
public void startPlayingAudio(String id, String file) {
|
||||
AudioPlayer audio = getOrCreatePlayer(id, file);
|
||||
audio.startPlaying(file);
|
||||
getAudioFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a location.
|
||||
* @param id The id of the audio player
|
||||
* @param milliseconds int: number of milliseconds to skip 1000 = 1 second
|
||||
*/
|
||||
public void seekToAudio(String id, int milliseconds) {
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
audio.seekToPlaying(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause playing.
|
||||
* @param id The id of the audio player
|
||||
*/
|
||||
public void pausePlayingAudio(String id) {
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
audio.pausePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop playing the audio file.
|
||||
* @param id The id of the audio player
|
||||
*/
|
||||
public void stopPlayingAudio(String id) {
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
audio.stopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current position of playback.
|
||||
* @param id The id of the audio player
|
||||
* @return position in msec
|
||||
*/
|
||||
public float getCurrentPositionAudio(String id) {
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
return (audio.getCurrentPosition() / 1000.0f);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration of the audio file.
|
||||
* @param id The id of the audio player
|
||||
* @param file The name of the audio file.
|
||||
* @return The duration in msec.
|
||||
*/
|
||||
public float getDurationAudio(String id, String file) {
|
||||
AudioPlayer audio = getOrCreatePlayer(id, file);
|
||||
return audio.getDuration(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio device to be used for playback.
|
||||
*
|
||||
* @param output 1=earpiece, 2=speaker
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void setAudioOutputDevice(int output) {
|
||||
String TAG1 = "AudioHandler.setAudioOutputDevice(): Error : ";
|
||||
|
||||
AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE);
|
||||
if (output == 2) {
|
||||
audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_SPEAKER, AudioManager.ROUTE_ALL);
|
||||
}
|
||||
else if (output == 1) {
|
||||
audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_EARPIECE, AudioManager.ROUTE_ALL);
|
||||
}
|
||||
else {
|
||||
LOG.e(TAG1," Unknown output device");
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseAllLostFocus() {
|
||||
for (AudioPlayer audio : this.players.values()) {
|
||||
if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) {
|
||||
this.pausedForFocus.add(audio);
|
||||
audio.pausePlaying();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void resumeAllGainedFocus() {
|
||||
for (AudioPlayer audio : this.pausedForFocus) {
|
||||
audio.resumePlaying();
|
||||
}
|
||||
this.pausedForFocus.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the the audio focus
|
||||
*/
|
||||
private OnAudioFocusChangeListener focusChangeListener = new OnAudioFocusChangeListener() {
|
||||
public void onAudioFocusChange(int focusChange) {
|
||||
switch (focusChange) {
|
||||
case (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) :
|
||||
case (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) :
|
||||
case (AudioManager.AUDIOFOCUS_LOSS) :
|
||||
pauseAllLostFocus();
|
||||
break;
|
||||
case (AudioManager.AUDIOFOCUS_GAIN):
|
||||
resumeAllGainedFocus();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void getAudioFocus() {
|
||||
String TAG2 = "AudioHandler.getAudioFocus(): Error : ";
|
||||
|
||||
AudioManager am = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE);
|
||||
int result = am.requestAudioFocus(focusChangeListener,
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN);
|
||||
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
LOG.e(TAG2,result + " instead of " + AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the audio device to be used for playback.
|
||||
*
|
||||
* @return 1=earpiece, 2=speaker
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public int getAudioOutputDevice() {
|
||||
AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE);
|
||||
if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_EARPIECE) {
|
||||
return 1;
|
||||
}
|
||||
else if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_SPEAKER) {
|
||||
return 2;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the volume for an audio device
|
||||
*
|
||||
* @param id The id of the audio player
|
||||
* @param volume Volume to adjust to 0.0f - 1.0f
|
||||
*/
|
||||
public void setVolume(String id, float volume) {
|
||||
String TAG3 = "AudioHandler.setVolume(): Error : ";
|
||||
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
audio.setVolume(volume);
|
||||
} else {
|
||||
LOG.e(TAG3,"Unknown Audio Player " + id);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFirstPlayerCreated() {
|
||||
origVolumeStream = cordova.getActivity().getVolumeControlStream();
|
||||
cordova.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
}
|
||||
|
||||
private void onLastPlayerReleased() {
|
||||
if (origVolumeStream != -1) {
|
||||
cordova.getActivity().setVolumeControlStream(origVolumeStream);
|
||||
origVolumeStream = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void sendEventMessage(String action, JSONObject actionData) {
|
||||
JSONObject message = new JSONObject();
|
||||
try {
|
||||
message.put("action", action);
|
||||
if (actionData != null) {
|
||||
message.put(action, actionData);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LOG.e(TAG, "Failed to create event message", e);
|
||||
}
|
||||
|
||||
PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, message);
|
||||
pluginResult.setKeepCallback(true);
|
||||
if (messageChannel != null) {
|
||||
messageChannel.sendPluginResult(pluginResult);
|
||||
}
|
||||
}
|
||||
|
||||
public void onRequestPermissionResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) throws JSONException
|
||||
{
|
||||
for(int r:grantResults)
|
||||
{
|
||||
if(r == PackageManager.PERMISSION_DENIED)
|
||||
{
|
||||
this.messageChannel.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
|
||||
return;
|
||||
}
|
||||
}
|
||||
promptForRecord();
|
||||
}
|
||||
|
||||
/*
|
||||
* This little utility method catch-all work great for multi-permission stuff.
|
||||
*
|
||||
*/
|
||||
|
||||
private void promptForRecord()
|
||||
{
|
||||
if(PermissionHelper.hasPermission(this, permissions[WRITE_EXTERNAL_STORAGE]) &&
|
||||
PermissionHelper.hasPermission(this, permissions[RECORD_AUDIO])) {
|
||||
this.startRecordingAudio(recordId, FileHelper.stripFileProtocol(fileUriStr));
|
||||
}
|
||||
else if(PermissionHelper.hasPermission(this, permissions[RECORD_AUDIO]))
|
||||
{
|
||||
getWritePermission(WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
else
|
||||
{
|
||||
getMicPermission(RECORD_AUDIO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current amplitude of recording.
|
||||
* @param id The id of the audio player
|
||||
* @return amplitude
|
||||
*/
|
||||
public float getCurrentAmplitudeAudio(String id) {
|
||||
AudioPlayer audio = this.players.get(id);
|
||||
if (audio != null) {
|
||||
return (audio.getCurrentAmplitude());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
/*
|
||||
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.media;
|
||||
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.MediaPlayer.OnCompletionListener;
|
||||
import android.media.MediaPlayer.OnErrorListener;
|
||||
import android.media.MediaPlayer.OnPreparedListener;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Environment;
|
||||
|
||||
import org.apache.cordova.LOG;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* This class implements the audio playback and recording capabilities used by Cordova.
|
||||
* It is called by the AudioHandler Cordova class.
|
||||
* Only one file can be played or recorded per class instance.
|
||||
*
|
||||
* Local audio files must reside in one of two places:
|
||||
* android_asset: file name must start with /android_asset/sound.mp3
|
||||
* sdcard: file name is just sound.mp3
|
||||
*/
|
||||
public class AudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener {
|
||||
|
||||
// AudioPlayer modes
|
||||
public enum MODE { NONE, PLAY, RECORD };
|
||||
|
||||
// AudioPlayer states
|
||||
public enum STATE { MEDIA_NONE,
|
||||
MEDIA_STARTING,
|
||||
MEDIA_RUNNING,
|
||||
MEDIA_PAUSED,
|
||||
MEDIA_STOPPED,
|
||||
MEDIA_LOADING
|
||||
};
|
||||
|
||||
private static final String LOG_TAG = "AudioPlayer";
|
||||
|
||||
// AudioPlayer message ids
|
||||
private static int MEDIA_STATE = 1;
|
||||
private static int MEDIA_DURATION = 2;
|
||||
private static int MEDIA_POSITION = 3;
|
||||
private static int MEDIA_ERROR = 9;
|
||||
|
||||
// Media error codes
|
||||
private static int MEDIA_ERR_NONE_ACTIVE = 0;
|
||||
private static int MEDIA_ERR_ABORTED = 1;
|
||||
// private static int MEDIA_ERR_NETWORK = 2;
|
||||
// private static int MEDIA_ERR_DECODE = 3;
|
||||
// private static int MEDIA_ERR_NONE_SUPPORTED = 4;
|
||||
|
||||
private AudioHandler handler; // The AudioHandler object
|
||||
private String id; // The id of this player (used to identify Media object in JavaScript)
|
||||
private MODE mode = MODE.NONE; // Playback or Recording mode
|
||||
private STATE state = STATE.MEDIA_NONE; // State of recording or playback
|
||||
|
||||
private String audioFile = null; // File name to play or record to
|
||||
private float duration = -1; // Duration of audio
|
||||
|
||||
private MediaRecorder recorder = null; // Audio recording object
|
||||
private LinkedList<String> tempFiles = null; // Temporary recording file name
|
||||
private String tempFile = null;
|
||||
|
||||
private MediaPlayer player = null; // Audio player object
|
||||
private boolean prepareOnly = true; // playback after file prepare flag
|
||||
private int seekOnPrepared = 0; // seek to this location once media is prepared
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param handler The audio handler object
|
||||
* @param id The id of this audio player
|
||||
*/
|
||||
public AudioPlayer(AudioHandler handler, String id, String file) {
|
||||
this.handler = handler;
|
||||
this.id = id;
|
||||
this.audioFile = file;
|
||||
this.tempFiles = new LinkedList<String>();
|
||||
}
|
||||
|
||||
private String generateTempFile() {
|
||||
String tempFileName = null;
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
tempFileName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording-" + System.currentTimeMillis() + ".3gp";
|
||||
} else {
|
||||
tempFileName = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/tmprecording-" + System.currentTimeMillis() + ".3gp";
|
||||
}
|
||||
return tempFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy player and stop audio playing or recording.
|
||||
*/
|
||||
public void destroy() {
|
||||
// Stop any play or record
|
||||
if (this.player != null) {
|
||||
if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
|
||||
this.player.stop();
|
||||
this.setState(STATE.MEDIA_STOPPED);
|
||||
}
|
||||
this.player.release();
|
||||
this.player = null;
|
||||
}
|
||||
if (this.recorder != null) {
|
||||
if (this.state != STATE.MEDIA_STOPPED) {
|
||||
this.stopRecording(true);
|
||||
}
|
||||
this.recorder.release();
|
||||
this.recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start recording the specified file.
|
||||
*
|
||||
* @param file The name of the file
|
||||
*/
|
||||
public void startRecording(String file) {
|
||||
switch (this.mode) {
|
||||
case PLAY:
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: Can't record in play mode.");
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
break;
|
||||
case NONE:
|
||||
this.audioFile = file;
|
||||
this.recorder = new MediaRecorder();
|
||||
this.recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
||||
this.recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS); // RAW_AMR);
|
||||
this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); //AMR_NB);
|
||||
this.tempFile = generateTempFile();
|
||||
this.recorder.setOutputFile(this.tempFile);
|
||||
try {
|
||||
this.recorder.prepare();
|
||||
this.recorder.start();
|
||||
this.setState(STATE.MEDIA_RUNNING);
|
||||
return;
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
break;
|
||||
case RECORD:
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: Already recording.");
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save temporary recorded file to specified name
|
||||
*
|
||||
* @param file
|
||||
*/
|
||||
public void moveFile(String file) {
|
||||
/* this is a hack to save the file as the specified name */
|
||||
|
||||
if (!file.startsWith("/")) {
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file;
|
||||
} else {
|
||||
file = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/" + file;
|
||||
}
|
||||
}
|
||||
|
||||
int size = this.tempFiles.size();
|
||||
LOG.d(LOG_TAG, "size = " + size);
|
||||
|
||||
// only one file so just copy it
|
||||
if (size == 1) {
|
||||
String logMsg = "renaming " + this.tempFile + " to " + file;
|
||||
LOG.d(LOG_TAG, logMsg);
|
||||
|
||||
File f = new File(this.tempFile);
|
||||
if (!f.renameTo(new File(file))) {
|
||||
|
||||
FileOutputStream outputStream = null;
|
||||
File outputFile = null;
|
||||
try {
|
||||
outputFile = new File(file);
|
||||
outputStream = new FileOutputStream(outputFile);
|
||||
FileInputStream inputStream = null;
|
||||
File inputFile = null;
|
||||
try {
|
||||
inputFile = new File(this.tempFile);
|
||||
LOG.d(LOG_TAG, "INPUT FILE LENGTH: " + String.valueOf(inputFile.length()) );
|
||||
inputStream = new FileInputStream(inputFile);
|
||||
copy(inputStream, outputStream, false);
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, e.getLocalizedMessage(), e);
|
||||
} finally {
|
||||
if (inputStream != null) try {
|
||||
inputStream.close();
|
||||
inputFile.delete();
|
||||
inputFile = null;
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, e.getLocalizedMessage(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (outputStream != null) try {
|
||||
outputStream.close();
|
||||
LOG.d(LOG_TAG, "OUTPUT FILE LENGTH: " + String.valueOf(outputFile.length()) );
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, e.getLocalizedMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// more than one file so the user must have pause recording. We'll need to concat files.
|
||||
else {
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = new FileOutputStream(new File(file));
|
||||
FileInputStream inputStream = null;
|
||||
File inputFile = null;
|
||||
for (int i = 0; i < size; i++) {
|
||||
try {
|
||||
inputFile = new File(this.tempFiles.get(i));
|
||||
inputStream = new FileInputStream(inputFile);
|
||||
copy(inputStream, outputStream, (i>0));
|
||||
} catch(Exception e) {
|
||||
LOG.e(LOG_TAG, e.getLocalizedMessage(), e);
|
||||
} finally {
|
||||
if (inputStream != null) try {
|
||||
inputStream.close();
|
||||
inputFile.delete();
|
||||
inputFile = null;
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, e.getLocalizedMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (outputStream != null) try {
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, e.getLocalizedMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long copy(InputStream from, OutputStream to, boolean skipHeader)
|
||||
throws IOException {
|
||||
byte[] buf = new byte[8096];
|
||||
long total = 0;
|
||||
if (skipHeader) {
|
||||
from.skip(6);
|
||||
}
|
||||
while (true) {
|
||||
int r = from.read(buf);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
to.write(buf, 0, r);
|
||||
total += r;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop/Pause recording and save to the file specified when recording started.
|
||||
*/
|
||||
public void stopRecording(boolean stop) {
|
||||
if (this.recorder != null) {
|
||||
try{
|
||||
if (this.state == STATE.MEDIA_RUNNING) {
|
||||
this.recorder.stop();
|
||||
}
|
||||
this.recorder.reset();
|
||||
if (!this.tempFiles.contains(this.tempFile)) {
|
||||
this.tempFiles.add(this.tempFile);
|
||||
}
|
||||
if (stop) {
|
||||
LOG.d(LOG_TAG, "stopping recording");
|
||||
this.setState(STATE.MEDIA_STOPPED);
|
||||
this.moveFile(this.audioFile);
|
||||
} else {
|
||||
LOG.d(LOG_TAG, "pause recording");
|
||||
this.setState(STATE.MEDIA_PAUSED);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume recording and save to the file specified when recording started.
|
||||
*/
|
||||
public void resumeRecording() {
|
||||
startRecording(this.audioFile);
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
// Playback
|
||||
//==========================================================================
|
||||
|
||||
/**
|
||||
* Start or resume playing audio file.
|
||||
*
|
||||
* @param file The name of the audio file.
|
||||
*/
|
||||
public void startPlaying(String file) {
|
||||
if (this.readyPlayer(file) && this.player != null) {
|
||||
this.player.start();
|
||||
this.setState(STATE.MEDIA_RUNNING);
|
||||
this.seekOnPrepared = 0; //insures this is always reset
|
||||
} else {
|
||||
this.prepareOnly = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek or jump to a new time in the track.
|
||||
*/
|
||||
public void seekToPlaying(int milliseconds) {
|
||||
if (this.readyPlayer(this.audioFile)) {
|
||||
if (milliseconds > 0) {
|
||||
this.player.seekTo(milliseconds);
|
||||
}
|
||||
LOG.d(LOG_TAG, "Send a onStatus update for the new seek");
|
||||
sendStatusChange(MEDIA_POSITION, null, (milliseconds / 1000.0f));
|
||||
}
|
||||
else {
|
||||
this.seekOnPrepared = milliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause playing.
|
||||
*/
|
||||
public void pausePlaying() {
|
||||
|
||||
// If playing, then pause
|
||||
if (this.state == STATE.MEDIA_RUNNING && this.player != null) {
|
||||
this.player.pause();
|
||||
this.setState(STATE.MEDIA_PAUSED);
|
||||
}
|
||||
else {
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal());
|
||||
sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop playing the audio file.
|
||||
*/
|
||||
public void stopPlaying() {
|
||||
if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
|
||||
this.player.pause();
|
||||
this.player.seekTo(0);
|
||||
LOG.d(LOG_TAG, "stopPlaying is calling stopped");
|
||||
this.setState(STATE.MEDIA_STOPPED);
|
||||
}
|
||||
else {
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal());
|
||||
sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume playing.
|
||||
*/
|
||||
public void resumePlaying() {
|
||||
this.startPlaying(this.audioFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when playback of a media source has completed.
|
||||
*
|
||||
* @param player The MediaPlayer that reached the end of the file
|
||||
*/
|
||||
public void onCompletion(MediaPlayer player) {
|
||||
LOG.d(LOG_TAG, "on completion is calling stopped");
|
||||
this.setState(STATE.MEDIA_STOPPED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current position of playback.
|
||||
*
|
||||
* @return position in msec or -1 if not playing
|
||||
*/
|
||||
public long getCurrentPosition() {
|
||||
if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
|
||||
int curPos = this.player.getCurrentPosition();
|
||||
sendStatusChange(MEDIA_POSITION, null, (curPos / 1000.0f));
|
||||
return curPos;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if playback file is streaming or local.
|
||||
* It is streaming if file name starts with "http://"
|
||||
*
|
||||
* @param file The file name
|
||||
* @return T=streaming, F=local
|
||||
*/
|
||||
public boolean isStreaming(String file) {
|
||||
if (file.contains("http://") || file.contains("https://") || file.contains("rtsp://")) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration of the audio file.
|
||||
*
|
||||
* @param file The name of the audio file.
|
||||
* @return The duration in msec.
|
||||
* -1=can't be determined
|
||||
* -2=not allowed
|
||||
*/
|
||||
public float getDuration(String file) {
|
||||
|
||||
// Can't get duration of recording
|
||||
if (this.recorder != null) {
|
||||
return (-2); // not allowed
|
||||
}
|
||||
|
||||
// If audio file already loaded and started, then return duration
|
||||
if (this.player != null) {
|
||||
return this.duration;
|
||||
}
|
||||
|
||||
// If no player yet, then create one
|
||||
else {
|
||||
this.prepareOnly = true;
|
||||
this.startPlaying(file);
|
||||
|
||||
// This will only return value for local, since streaming
|
||||
// file hasn't been read yet.
|
||||
return this.duration;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the media source is ready for playback.
|
||||
*
|
||||
* @param player The MediaPlayer that is ready for playback
|
||||
*/
|
||||
public void onPrepared(MediaPlayer player) {
|
||||
// Listen for playback completion
|
||||
this.player.setOnCompletionListener(this);
|
||||
// seek to any location received while not prepared
|
||||
this.seekToPlaying(this.seekOnPrepared);
|
||||
// If start playing after prepared
|
||||
if (!this.prepareOnly) {
|
||||
this.player.start();
|
||||
this.setState(STATE.MEDIA_RUNNING);
|
||||
this.seekOnPrepared = 0; //reset only when played
|
||||
} else {
|
||||
this.setState(STATE.MEDIA_STARTING);
|
||||
}
|
||||
// Save off duration
|
||||
this.duration = getDurationInSeconds();
|
||||
// reset prepare only flag
|
||||
this.prepareOnly = true;
|
||||
|
||||
// Send status notification to JavaScript
|
||||
sendStatusChange(MEDIA_DURATION, null, this.duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* By default Android returns the length of audio in mills but we want seconds
|
||||
*
|
||||
* @return length of clip in seconds
|
||||
*/
|
||||
private float getDurationInSeconds() {
|
||||
return (this.player.getDuration() / 1000.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when there has been an error during an asynchronous operation
|
||||
* (other errors will throw exceptions at method call time).
|
||||
*
|
||||
* @param player the MediaPlayer the error pertains to
|
||||
* @param arg1 the type of error that has occurred: (MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_SERVER_DIED)
|
||||
* @param arg2 an extra code, specific to the error.
|
||||
*/
|
||||
public boolean onError(MediaPlayer player, int arg1, int arg2) {
|
||||
LOG.d(LOG_TAG, "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")");
|
||||
|
||||
// we don't want to send success callback
|
||||
// so we don't call setState() here
|
||||
this.state = STATE.MEDIA_STOPPED;
|
||||
this.destroy();
|
||||
// Send error notification to JavaScript
|
||||
sendErrorStatus(arg1);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state and send it to JavaScript.
|
||||
*
|
||||
* @param state
|
||||
*/
|
||||
private void setState(STATE state) {
|
||||
if (this.state != state) {
|
||||
sendStatusChange(MEDIA_STATE, null, (float)state.ordinal());
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mode and send it to JavaScript.
|
||||
*
|
||||
* @param mode
|
||||
*/
|
||||
private void setMode(MODE mode) {
|
||||
if (this.mode != mode) {
|
||||
//mode is not part of the expected behavior, so no notification
|
||||
//this.handler.webView.sendJavascript("cordova.require('cordova-plugin-media.Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");");
|
||||
}
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio state.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public int getState() {
|
||||
return this.state.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the volume for audio player
|
||||
*
|
||||
* @param volume
|
||||
*/
|
||||
public void setVolume(float volume) {
|
||||
if (this.player != null) {
|
||||
this.player.setVolume(volume, volume);
|
||||
} else {
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: Cannot set volume until the audio file is initialized.");
|
||||
sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* attempts to put the player in play mode
|
||||
* @return true if in playmode, false otherwise
|
||||
*/
|
||||
private boolean playMode() {
|
||||
switch(this.mode) {
|
||||
case NONE:
|
||||
this.setMode(MODE.PLAY);
|
||||
break;
|
||||
case PLAY:
|
||||
break;
|
||||
case RECORD:
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: Can't play in record mode.");
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
return false; //player is not ready
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* attempts to initialize the media player for playback
|
||||
* @param file the file to play
|
||||
* @return false if player not ready, reports if in wrong mode or state
|
||||
*/
|
||||
private boolean readyPlayer(String file) {
|
||||
if (playMode()) {
|
||||
switch (this.state) {
|
||||
case MEDIA_NONE:
|
||||
if (this.player == null) {
|
||||
this.player = new MediaPlayer();
|
||||
this.player.setOnErrorListener(this);
|
||||
}
|
||||
try {
|
||||
this.loadAudioFile(file);
|
||||
} catch (Exception e) {
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
}
|
||||
return false;
|
||||
case MEDIA_LOADING:
|
||||
//cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING instead
|
||||
LOG.d(LOG_TAG, "AudioPlayer Loading: startPlaying() called during media preparation: " + STATE.MEDIA_STARTING.ordinal());
|
||||
this.prepareOnly = false;
|
||||
return false;
|
||||
case MEDIA_STARTING:
|
||||
case MEDIA_RUNNING:
|
||||
case MEDIA_PAUSED:
|
||||
return true;
|
||||
case MEDIA_STOPPED:
|
||||
//if we are readying the same file
|
||||
if (file!=null && this.audioFile.compareTo(file) == 0) {
|
||||
//maybe it was recording?
|
||||
if (player == null) {
|
||||
this.player = new MediaPlayer();
|
||||
this.player.setOnErrorListener(this);
|
||||
this.prepareOnly = false;
|
||||
|
||||
try {
|
||||
this.loadAudioFile(file);
|
||||
} catch (Exception e) {
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
}
|
||||
return false;//we´re not ready yet
|
||||
}
|
||||
else {
|
||||
//reset the audio file
|
||||
player.seekTo(0);
|
||||
player.pause();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
//reset the player
|
||||
this.player.reset();
|
||||
try {
|
||||
this.loadAudioFile(file);
|
||||
} catch (Exception e) {
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
}
|
||||
//if we had to prepare the file, we won't be in the correct state for playback
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
LOG.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: " + this.state);
|
||||
sendErrorStatus(MEDIA_ERR_ABORTED);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* load audio file
|
||||
* @throws IOException
|
||||
* @throws IllegalStateException
|
||||
* @throws SecurityException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private void loadAudioFile(String file) throws IllegalArgumentException, SecurityException, IllegalStateException, IOException {
|
||||
if (this.isStreaming(file)) {
|
||||
this.player.setDataSource(file);
|
||||
this.player.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||
//if it's a streaming file, play mode is implied
|
||||
this.setMode(MODE.PLAY);
|
||||
this.setState(STATE.MEDIA_STARTING);
|
||||
this.player.setOnPreparedListener(this);
|
||||
this.player.prepareAsync();
|
||||
}
|
||||
else {
|
||||
if (file.startsWith("/android_asset/")) {
|
||||
String f = file.substring(15);
|
||||
android.content.res.AssetFileDescriptor fd = this.handler.cordova.getActivity().getAssets().openFd(f);
|
||||
this.player.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
|
||||
}
|
||||
else {
|
||||
File fp = new File(file);
|
||||
if (fp.exists()) {
|
||||
FileInputStream fileInputStream = new FileInputStream(file);
|
||||
this.player.setDataSource(fileInputStream.getFD());
|
||||
fileInputStream.close();
|
||||
}
|
||||
else {
|
||||
this.player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/" + file);
|
||||
}
|
||||
}
|
||||
this.setState(STATE.MEDIA_STARTING);
|
||||
this.player.setOnPreparedListener(this);
|
||||
this.player.prepare();
|
||||
|
||||
// Get duration
|
||||
this.duration = getDurationInSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorStatus(int errorCode) {
|
||||
sendStatusChange(MEDIA_ERROR, errorCode, null);
|
||||
}
|
||||
|
||||
private void sendStatusChange(int messageType, Integer additionalCode, Float value) {
|
||||
|
||||
if (additionalCode != null && value != null) {
|
||||
throw new IllegalArgumentException("Only one of additionalCode or value can be specified, not both");
|
||||
}
|
||||
|
||||
JSONObject statusDetails = new JSONObject();
|
||||
try {
|
||||
statusDetails.put("id", this.id);
|
||||
statusDetails.put("msgType", messageType);
|
||||
if (additionalCode != null) {
|
||||
JSONObject code = new JSONObject();
|
||||
code.put("code", additionalCode.intValue());
|
||||
statusDetails.put("value", code);
|
||||
}
|
||||
else if (value != null) {
|
||||
statusDetails.put("value", value.floatValue());
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LOG.e(LOG_TAG, "Failed to create status details", e);
|
||||
}
|
||||
|
||||
this.handler.sendEventMessage("status", statusDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current amplitude of recording.
|
||||
*
|
||||
* @return amplitude or 0 if not recording
|
||||
*/
|
||||
public float getCurrentAmplitude() {
|
||||
if (this.recorder != null) {
|
||||
try{
|
||||
if (this.state == STATE.MEDIA_RUNNING) {
|
||||
return (float) this.recorder.getMaxAmplitude() / 32762;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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.media;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class FileHelper {
|
||||
|
||||
/**
|
||||
* Removes the "file://" prefix from the given URI string, if applicable.
|
||||
* If the given URI string doesn't have a "file://" prefix, it is returned unchanged.
|
||||
*
|
||||
* @param uriString the URI string to operate on
|
||||
* @return a path without the "file://" prefix
|
||||
*/
|
||||
public static String stripFileProtocol(String uriString) {
|
||||
if (uriString.startsWith("file://")) {
|
||||
return Uri.parse(uriString).getPath();
|
||||
}
|
||||
return uriString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
/*
|
||||
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.mediacapture;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.apache.cordova.file.FileUtils;
|
||||
import org.apache.cordova.file.LocalFilesystemURL;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PermissionHelper;
|
||||
import org.apache.cordova.PluginManager;
|
||||
import org.apache.cordova.mediacapture.PendingRequests.Request;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
public class Capture extends CordovaPlugin {
|
||||
|
||||
private static final String VIDEO_3GPP = "video/3gpp";
|
||||
private static final String VIDEO_MP4 = "video/mp4";
|
||||
private static final String AUDIO_3GPP = "audio/3gpp";
|
||||
private static final String[] AUDIO_TYPES = new String[] {"audio/3gpp", "audio/aac", "audio/amr", "audio/wav"};
|
||||
private static final String IMAGE_JPEG = "image/jpeg";
|
||||
|
||||
private static final int CAPTURE_AUDIO = 0; // Constant for capture audio
|
||||
private static final int CAPTURE_IMAGE = 1; // Constant for capture image
|
||||
private static final int CAPTURE_VIDEO = 2; // Constant for capture video
|
||||
private static final String LOG_TAG = "Capture";
|
||||
|
||||
private static final int CAPTURE_INTERNAL_ERR = 0;
|
||||
// private static final int CAPTURE_APPLICATION_BUSY = 1;
|
||||
// private static final int CAPTURE_INVALID_ARGUMENT = 2;
|
||||
private static final int CAPTURE_NO_MEDIA_FILES = 3;
|
||||
private static final int CAPTURE_PERMISSION_DENIED = 4;
|
||||
private static final int CAPTURE_NOT_SUPPORTED = 20;
|
||||
|
||||
private boolean cameraPermissionInManifest; // Whether or not the CAMERA permission is declared in AndroidManifest.xml
|
||||
|
||||
private final PendingRequests pendingRequests = new PendingRequests();
|
||||
|
||||
private int numPics; // Number of pictures before capture activity
|
||||
private Uri imageUri;
|
||||
|
||||
// public void setContext(Context mCtx)
|
||||
// {
|
||||
// if (CordovaInterface.class.isInstance(mCtx))
|
||||
// cordova = (CordovaInterface) mCtx;
|
||||
// else
|
||||
// LOG.d(LOG_TAG, "ERROR: You must use the CordovaInterface for this to work correctly. Please implement it in your activity");
|
||||
// }
|
||||
|
||||
@Override
|
||||
protected void pluginInitialize() {
|
||||
super.pluginInitialize();
|
||||
|
||||
// CB-10670: The CAMERA permission does not need to be requested unless it is declared
|
||||
// in AndroidManifest.xml. This plugin does not declare it, but others may and so we must
|
||||
// check the package info to determine if the permission is present.
|
||||
|
||||
cameraPermissionInManifest = false;
|
||||
try {
|
||||
PackageManager packageManager = this.cordova.getActivity().getPackageManager();
|
||||
String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
|
||||
if (permissionsInPackage != null) {
|
||||
for (String permission : permissionsInPackage) {
|
||||
if (permission.equals(Manifest.permission.CAMERA)) {
|
||||
cameraPermissionInManifest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NameNotFoundException e) {
|
||||
// We are requesting the info for our package, so this should
|
||||
// never be caught
|
||||
LOG.e(LOG_TAG, "Failed checking for CAMERA permission in manifest", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
if (action.equals("getFormatData")) {
|
||||
JSONObject obj = getFormatData(args.getString(0), args.getString(1));
|
||||
callbackContext.success(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
JSONObject options = args.optJSONObject(0);
|
||||
|
||||
if (action.equals("captureAudio")) {
|
||||
this.captureAudio(pendingRequests.createRequest(CAPTURE_AUDIO, options, callbackContext));
|
||||
}
|
||||
else if (action.equals("captureImage")) {
|
||||
this.captureImage(pendingRequests.createRequest(CAPTURE_IMAGE, options, callbackContext));
|
||||
}
|
||||
else if (action.equals("captureVideo")) {
|
||||
this.captureVideo(pendingRequests.createRequest(CAPTURE_VIDEO, options, callbackContext));
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the media data file data depending on it's mime type
|
||||
*
|
||||
* @param filePath path to the file
|
||||
* @param mimeType of the file
|
||||
* @return a MediaFileData object
|
||||
*/
|
||||
private JSONObject getFormatData(String filePath, String mimeType) throws JSONException {
|
||||
Uri fileUrl = filePath.startsWith("file:") ? Uri.parse(filePath) : Uri.fromFile(new File(filePath));
|
||||
JSONObject obj = new JSONObject();
|
||||
// setup defaults
|
||||
obj.put("height", 0);
|
||||
obj.put("width", 0);
|
||||
obj.put("bitrate", 0);
|
||||
obj.put("duration", 0);
|
||||
obj.put("codecs", "");
|
||||
|
||||
// If the mimeType isn't set the rest will fail
|
||||
// so let's see if we can determine it.
|
||||
if (mimeType == null || mimeType.equals("") || "null".equals(mimeType)) {
|
||||
mimeType = FileHelper.getMimeType(fileUrl, cordova);
|
||||
}
|
||||
LOG.d(LOG_TAG, "Mime type = " + mimeType);
|
||||
|
||||
if (mimeType.equals(IMAGE_JPEG) || filePath.endsWith(".jpg")) {
|
||||
obj = getImageData(fileUrl, obj);
|
||||
}
|
||||
else if (Arrays.asList(AUDIO_TYPES).contains(mimeType)) {
|
||||
obj = getAudioVideoData(filePath, obj, false);
|
||||
}
|
||||
else if (mimeType.equals(VIDEO_3GPP) || mimeType.equals(VIDEO_MP4)) {
|
||||
obj = getAudioVideoData(filePath, obj, true);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Image specific attributes
|
||||
*
|
||||
* @param filePath path to the file
|
||||
* @param obj represents the Media File Data
|
||||
* @return a JSONObject that represents the Media File Data
|
||||
* @throws JSONException
|
||||
*/
|
||||
private JSONObject getImageData(Uri fileUrl, JSONObject obj) throws JSONException {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(fileUrl.getPath(), options);
|
||||
obj.put("height", options.outHeight);
|
||||
obj.put("width", options.outWidth);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Image specific attributes
|
||||
*
|
||||
* @param filePath path to the file
|
||||
* @param obj represents the Media File Data
|
||||
* @param video if true get video attributes as well
|
||||
* @return a JSONObject that represents the Media File Data
|
||||
* @throws JSONException
|
||||
*/
|
||||
private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException {
|
||||
MediaPlayer player = new MediaPlayer();
|
||||
try {
|
||||
player.setDataSource(filePath);
|
||||
player.prepare();
|
||||
obj.put("duration", player.getDuration() / 1000);
|
||||
if (video) {
|
||||
obj.put("height", player.getVideoHeight());
|
||||
obj.put("width", player.getVideoWidth());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.d(LOG_TAG, "Error: loading video file");
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an intent to capture audio. Result handled by onActivityResult()
|
||||
*/
|
||||
private void captureAudio(Request req) {
|
||||
if (!PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
} else {
|
||||
try {
|
||||
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
|
||||
|
||||
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
|
||||
} catch (ActivityNotFoundException ex) {
|
||||
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NOT_SUPPORTED, "No Activity found to handle Audio Capture."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getTempDirectoryPath() {
|
||||
File cache = null;
|
||||
|
||||
// Use internal storage
|
||||
cache = cordova.getActivity().getCacheDir();
|
||||
|
||||
// Create the cache directory if it doesn't exist
|
||||
cache.mkdirs();
|
||||
return cache.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an intent to capture images. Result handled by onActivityResult()
|
||||
*/
|
||||
private void captureImage(Request req) {
|
||||
boolean needExternalStoragePermission =
|
||||
!PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
|
||||
boolean needCameraPermission = cameraPermissionInManifest &&
|
||||
!PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
|
||||
|
||||
if (needExternalStoragePermission || needCameraPermission) {
|
||||
if (needExternalStoragePermission && needCameraPermission) {
|
||||
PermissionHelper.requestPermissions(this, req.requestCode, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA});
|
||||
} else if (needExternalStoragePermission) {
|
||||
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
} else {
|
||||
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA);
|
||||
}
|
||||
} else {
|
||||
// Save the number of images currently on disk for later
|
||||
this.numPics = queryImgDB(whichContentStore()).getCount();
|
||||
|
||||
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
|
||||
ContentResolver contentResolver = this.cordova.getActivity().getContentResolver();
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(MediaStore.Images.Media.MIME_TYPE, IMAGE_JPEG);
|
||||
imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv);
|
||||
LOG.d(LOG_TAG, "Taking a picture and saving to: " + imageUri.toString());
|
||||
|
||||
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri);
|
||||
|
||||
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createWritableFile(File file) throws IOException {
|
||||
file.createNewFile();
|
||||
file.setWritable(true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an intent to capture video. Result handled by onActivityResult()
|
||||
*/
|
||||
private void captureVideo(Request req) {
|
||||
if(cameraPermissionInManifest && !PermissionHelper.hasPermission(this, Manifest.permission.CAMERA)) {
|
||||
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA);
|
||||
} else {
|
||||
Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
|
||||
if(Build.VERSION.SDK_INT > 7){
|
||||
intent.putExtra("android.intent.extra.durationLimit", req.duration);
|
||||
intent.putExtra("android.intent.extra.videoQuality", req.quality);
|
||||
}
|
||||
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the video view exits.
|
||||
*
|
||||
* @param requestCode The request code originally supplied to startActivityForResult(),
|
||||
* allowing you to identify who this result came from.
|
||||
* @param resultCode The integer result code returned by the child activity through its setResult().
|
||||
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
|
||||
* @throws JSONException
|
||||
*/
|
||||
public void onActivityResult(int requestCode, int resultCode, final Intent intent) {
|
||||
final Request req = pendingRequests.get(requestCode);
|
||||
|
||||
// Result received okay
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Runnable processActivityResult = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
switch(req.action) {
|
||||
case CAPTURE_AUDIO:
|
||||
onAudioActivityResult(req, intent);
|
||||
break;
|
||||
case CAPTURE_IMAGE:
|
||||
onImageActivityResult(req);
|
||||
break;
|
||||
case CAPTURE_VIDEO:
|
||||
onVideoActivityResult(req, intent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.cordova.getThreadPool().execute(processActivityResult);
|
||||
}
|
||||
// If canceled
|
||||
else if (resultCode == Activity.RESULT_CANCELED) {
|
||||
// If we have partial results send them back to the user
|
||||
if (req.results.length() > 0) {
|
||||
pendingRequests.resolveWithSuccess(req);
|
||||
}
|
||||
// user canceled the action
|
||||
else {
|
||||
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Canceled."));
|
||||
}
|
||||
}
|
||||
// If something else
|
||||
else {
|
||||
// If we have partial results send them back to the user
|
||||
if (req.results.length() > 0) {
|
||||
pendingRequests.resolveWithSuccess(req);
|
||||
}
|
||||
// something bad happened
|
||||
else {
|
||||
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Did not complete!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void onAudioActivityResult(Request req, Intent intent) {
|
||||
// Get the uri of the audio clip
|
||||
Uri data = intent.getData();
|
||||
// create a file object from the uri
|
||||
req.results.put(createMediaFile(data));
|
||||
|
||||
if (req.results.length() >= req.limit) {
|
||||
// Send Uri back to JavaScript for listening to audio
|
||||
pendingRequests.resolveWithSuccess(req);
|
||||
} else {
|
||||
// still need to capture more audio clips
|
||||
captureAudio(req);
|
||||
}
|
||||
}
|
||||
|
||||
public void onImageActivityResult(Request req) {
|
||||
// Add image to results
|
||||
req.results.put(createMediaFile(imageUri));
|
||||
|
||||
checkForDuplicateImage();
|
||||
|
||||
if (req.results.length() >= req.limit) {
|
||||
// Send Uri back to JavaScript for viewing image
|
||||
pendingRequests.resolveWithSuccess(req);
|
||||
} else {
|
||||
// still need to capture more images
|
||||
captureImage(req);
|
||||
}
|
||||
}
|
||||
|
||||
public void onVideoActivityResult(Request req, Intent intent) {
|
||||
Uri data = null;
|
||||
|
||||
if (intent != null){
|
||||
// Get the uri of the video clip
|
||||
data = intent.getData();
|
||||
}
|
||||
|
||||
if( data == null){
|
||||
File movie = new File(getTempDirectoryPath(), "Capture.avi");
|
||||
data = Uri.fromFile(movie);
|
||||
}
|
||||
|
||||
// create a file object from the uri
|
||||
if(data == null) {
|
||||
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null"));
|
||||
}
|
||||
else {
|
||||
req.results.put(createMediaFile(data));
|
||||
|
||||
if (req.results.length() >= req.limit) {
|
||||
// Send Uri back to JavaScript for viewing video
|
||||
pendingRequests.resolveWithSuccess(req);
|
||||
} else {
|
||||
// still need to capture more video clips
|
||||
captureVideo(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSONObject that represents a File from the Uri
|
||||
*
|
||||
* @param data the Uri of the audio/image/video
|
||||
* @return a JSONObject that represents a File
|
||||
* @throws IOException
|
||||
*/
|
||||
private JSONObject createMediaFile(Uri data) {
|
||||
File fp = webView.getResourceApi().mapUriToFile(data);
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
Class webViewClass = webView.getClass();
|
||||
PluginManager pm = null;
|
||||
try {
|
||||
Method gpm = webViewClass.getMethod("getPluginManager");
|
||||
pm = (PluginManager) gpm.invoke(webView);
|
||||
} catch (NoSuchMethodException e) {
|
||||
} catch (IllegalAccessException e) {
|
||||
} catch (InvocationTargetException e) {
|
||||
}
|
||||
if (pm == null) {
|
||||
try {
|
||||
Field pmf = webViewClass.getField("pluginManager");
|
||||
pm = (PluginManager)pmf.get(webView);
|
||||
} catch (NoSuchFieldException e) {
|
||||
} catch (IllegalAccessException e) {
|
||||
}
|
||||
}
|
||||
FileUtils filePlugin = (FileUtils) pm.getPlugin("File");
|
||||
LocalFilesystemURL url = filePlugin.filesystemURLforLocalPath(fp.getAbsolutePath());
|
||||
|
||||
try {
|
||||
// File properties
|
||||
obj.put("name", fp.getName());
|
||||
obj.put("fullPath", Uri.fromFile(fp));
|
||||
if (url != null) {
|
||||
obj.put("localURL", url.toString());
|
||||
}
|
||||
// Because of an issue with MimeTypeMap.getMimeTypeFromExtension() all .3gpp files
|
||||
// are reported as video/3gpp. I'm doing this hacky check of the URI to see if it
|
||||
// is stored in the audio or video content store.
|
||||
if (fp.getAbsoluteFile().toString().endsWith(".3gp") || fp.getAbsoluteFile().toString().endsWith(".3gpp")) {
|
||||
if (data.toString().contains("/audio/")) {
|
||||
obj.put("type", AUDIO_3GPP);
|
||||
} else {
|
||||
obj.put("type", VIDEO_3GPP);
|
||||
}
|
||||
} else {
|
||||
obj.put("type", FileHelper.getMimeType(Uri.fromFile(fp), cordova));
|
||||
}
|
||||
|
||||
obj.put("lastModifiedDate", fp.lastModified());
|
||||
obj.put("size", fp.length());
|
||||
} catch (JSONException e) {
|
||||
// this will never happen
|
||||
e.printStackTrace();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private JSONObject createErrorObject(int code, String message) {
|
||||
JSONObject obj = new JSONObject();
|
||||
try {
|
||||
obj.put("code", code);
|
||||
obj.put("message", message);
|
||||
} catch (JSONException e) {
|
||||
// This will never happen
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cursor that can be used to determine how many images we have.
|
||||
*
|
||||
* @return a cursor
|
||||
*/
|
||||
private Cursor queryImgDB(Uri contentStore) {
|
||||
return this.cordova.getActivity().getContentResolver().query(
|
||||
contentStore,
|
||||
new String[] { MediaStore.Images.Media._ID },
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to find out if we are in a situation where the Camera Intent adds to images
|
||||
* to the content store.
|
||||
*/
|
||||
private void checkForDuplicateImage() {
|
||||
Uri contentStore = whichContentStore();
|
||||
Cursor cursor = queryImgDB(contentStore);
|
||||
int currentNumOfImages = cursor.getCount();
|
||||
|
||||
// delete the duplicate file if the difference is 2
|
||||
if ((currentNumOfImages - numPics) == 2) {
|
||||
cursor.moveToLast();
|
||||
int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID))) - 1;
|
||||
Uri uri = Uri.parse(contentStore + "/" + id);
|
||||
this.cordova.getActivity().getContentResolver().delete(uri, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we are storing the images in internal or external storage
|
||||
* @return Uri
|
||||
*/
|
||||
private Uri whichContentStore() {
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
return android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else {
|
||||
return android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI;
|
||||
}
|
||||
}
|
||||
|
||||
private void executeRequest(Request req) {
|
||||
switch (req.action) {
|
||||
case CAPTURE_AUDIO:
|
||||
this.captureAudio(req);
|
||||
break;
|
||||
case CAPTURE_IMAGE:
|
||||
this.captureImage(req);
|
||||
break;
|
||||
case CAPTURE_VIDEO:
|
||||
this.captureVideo(req);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void onRequestPermissionResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) throws JSONException {
|
||||
Request req = pendingRequests.get(requestCode);
|
||||
|
||||
if (req != null) {
|
||||
boolean success = true;
|
||||
for(int r:grantResults) {
|
||||
if (r == PackageManager.PERMISSION_DENIED) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
executeRequest(req);
|
||||
} else {
|
||||
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_PERMISSION_DENIED, "Permission denied."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Bundle onSaveInstanceState() {
|
||||
return pendingRequests.toBundle();
|
||||
}
|
||||
|
||||
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
|
||||
pendingRequests.setLastSavedState(state, callbackContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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.mediacapture;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
// TODO: Replace with CordovaResourceApi.getMimeType() post 3.1.
|
||||
public class FileHelper {
|
||||
public static String getMimeTypeForExtension(String path) {
|
||||
String extension = path;
|
||||
int lastDot = extension.lastIndexOf('.');
|
||||
if (lastDot != -1) {
|
||||
extension = extension.substring(lastDot + 1);
|
||||
}
|
||||
// Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
|
||||
extension = extension.toLowerCase(Locale.getDefault());
|
||||
if (extension.equals("3ga")) {
|
||||
return "audio/3gpp";
|
||||
}
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of the data specified by the given URI string.
|
||||
*
|
||||
* @param uriString the URI string of the data
|
||||
* @return the mime type of the specified data
|
||||
*/
|
||||
public static String getMimeType(Uri uri, CordovaInterface cordova) {
|
||||
String mimeType = null;
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
mimeType = cordova.getActivity().getContentResolver().getType(uri);
|
||||
} else {
|
||||
mimeType = getMimeTypeForExtension(uri.getPath());
|
||||
}
|
||||
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
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.mediacapture;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Holds the pending javascript requests for the plugin
|
||||
*/
|
||||
public class PendingRequests {
|
||||
private static final String LOG_TAG = "PendingCaptureRequests";
|
||||
|
||||
private static final String CURRENT_ID_KEY = "currentReqId";
|
||||
private static final String REQUEST_KEY_PREFIX = "request_";
|
||||
|
||||
private int currentReqId = 0;
|
||||
private SparseArray<Request> requests = new SparseArray<Request>();
|
||||
|
||||
private Bundle lastSavedState;
|
||||
private CallbackContext resumeContext;
|
||||
|
||||
/**
|
||||
* Creates a request and adds it to the array of pending requests. Each created request gets a
|
||||
* unique result code for use with startActivityForResult() and requestPermission()
|
||||
* @param action The action this request corresponds to (capture image, capture audio, etc.)
|
||||
* @param options The options for this request passed from the javascript
|
||||
* @param callbackContext The CallbackContext to return the result to
|
||||
* @return The newly created Request object with a unique result code
|
||||
* @throws JSONException
|
||||
*/
|
||||
public synchronized Request createRequest(int action, JSONObject options, CallbackContext callbackContext) throws JSONException {
|
||||
Request req = new Request(action, options, callbackContext);
|
||||
requests.put(req.requestCode, req);
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request corresponding to this request code
|
||||
* @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 get(int requestCode) {
|
||||
// Check to see if this request was saved
|
||||
if (lastSavedState != null && lastSavedState.containsKey(REQUEST_KEY_PREFIX + requestCode)) {
|
||||
Request r = new Request(lastSavedState.getBundle(REQUEST_KEY_PREFIX + requestCode), this.resumeContext, requestCode);
|
||||
requests.put(requestCode, r);
|
||||
|
||||
// Only one of the saved requests will get restored, because that's all cordova-android
|
||||
// supports. Having more than one is an extremely unlikely scenario anyway
|
||||
this.lastSavedState = null;
|
||||
this.resumeContext = null;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
return requests.get(requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the request from the array of pending requests and sends an error plugin result
|
||||
* to the CallbackContext that contains the given error object
|
||||
* @param req The request to be resolved
|
||||
* @param error The error to be returned to the CallbackContext
|
||||
*/
|
||||
public synchronized void resolveWithFailure(Request req, JSONObject error) {
|
||||
req.callbackContext.error(error);
|
||||
requests.remove(req.requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the request from the array of pending requests and sends a successful plugin result
|
||||
* to the CallbackContext that contains the result of the request
|
||||
* @param req The request to be resolved
|
||||
*/
|
||||
public synchronized void resolveWithSuccess(Request req) {
|
||||
req.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, req.results));
|
||||
requests.remove(req.requestCode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Each request gets a unique ID that represents its request code when calls are made to
|
||||
* Activities and for permission requests
|
||||
* @return A unique request code
|
||||
*/
|
||||
private synchronized int incrementCurrentReqId() {
|
||||
return currentReqId ++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore state saved by calling toBundle along with a callbackContext to be used in
|
||||
* delivering the results of a pending callback
|
||||
*
|
||||
* @param lastSavedState The bundle received from toBundle()
|
||||
* @param resumeContext The callbackContext to return results to
|
||||
*/
|
||||
public synchronized void setLastSavedState(Bundle lastSavedState, CallbackContext resumeContext) {
|
||||
this.lastSavedState = lastSavedState;
|
||||
this.resumeContext = resumeContext;
|
||||
this.currentReqId = lastSavedState.getInt(CURRENT_ID_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current pending requests to a bundle for saving when the Activity gets destroyed.
|
||||
*
|
||||
* @return A Bundle that can be used to restore state using setLastSavedState()
|
||||
*/
|
||||
public synchronized Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(CURRENT_ID_KEY, currentReqId);
|
||||
|
||||
for (int i = 0; i < requests.size(); i++) {
|
||||
Request r = requests.valueAt(i);
|
||||
int requestCode = requests.keyAt(i);
|
||||
bundle.putBundle(REQUEST_KEY_PREFIX + requestCode, r.toBundle());
|
||||
}
|
||||
|
||||
if (requests.size() > 1) {
|
||||
// This scenario is hopefully very unlikely because there isn't much that can be
|
||||
// done about it. Should only occur if an external Activity is launched while
|
||||
// there is a pending permission request and the device is on low memory
|
||||
LOG.w(LOG_TAG, "More than one media capture request pending on Activity destruction. Some requests will be dropped!");
|
||||
}
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the options and CallbackContext for a capture request made to the plugin.
|
||||
*/
|
||||
public class Request {
|
||||
|
||||
// Keys for use in saving requests to a bundle
|
||||
private static final String ACTION_KEY = "action";
|
||||
private static final String LIMIT_KEY = "limit";
|
||||
private static final String DURATION_KEY = "duration";
|
||||
private static final String QUALITY_KEY = "quality";
|
||||
private static final String RESULTS_KEY = "results";
|
||||
|
||||
// Unique int used to identify this request in any Android Permission or Activity callbacks
|
||||
public int requestCode;
|
||||
|
||||
// The action that this request is performing
|
||||
public int action;
|
||||
|
||||
// The number of pics/vids/audio clips to take (CAPTURE_IMAGE, CAPTURE_VIDEO, CAPTURE_AUDIO)
|
||||
public long limit = 1;
|
||||
|
||||
// Optional max duration of recording in seconds (CAPTURE_VIDEO only)
|
||||
public int duration = 0;
|
||||
|
||||
// Quality level for video capture 0 low, 1 high (CAPTURE_VIDEO only)
|
||||
public int quality = 1;
|
||||
|
||||
// The array of results to be returned to the javascript callback on success
|
||||
public JSONArray results = new JSONArray();
|
||||
|
||||
// The callback context for this plugin request
|
||||
private CallbackContext callbackContext;
|
||||
|
||||
private Request(int action, JSONObject options, CallbackContext callbackContext) throws JSONException {
|
||||
this.callbackContext = callbackContext;
|
||||
this.action = action;
|
||||
|
||||
if (options != null) {
|
||||
this.limit = options.optLong("limit", 1);
|
||||
this.duration = options.optInt("duration", 0);
|
||||
this.quality = options.optInt("quality", 1);
|
||||
}
|
||||
|
||||
this.requestCode = incrementCurrentReqId();
|
||||
}
|
||||
|
||||
private Request(Bundle bundle, CallbackContext callbackContext, int requestCode) {
|
||||
this.callbackContext = callbackContext;
|
||||
this.requestCode = requestCode;
|
||||
this.action = bundle.getInt(ACTION_KEY);
|
||||
this.limit = bundle.getLong(LIMIT_KEY);
|
||||
this.duration = bundle.getInt(DURATION_KEY);
|
||||
this.quality = bundle.getInt(QUALITY_KEY);
|
||||
|
||||
try {
|
||||
this.results = new JSONArray(bundle.getString(RESULTS_KEY));
|
||||
} catch(JSONException e) {
|
||||
// This should never be caught
|
||||
LOG.e(LOG_TAG, "Error parsing results for request from saved bundle", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
bundle.putInt(ACTION_KEY, this.action);
|
||||
bundle.putLong(LIMIT_KEY, this.limit);
|
||||
bundle.putInt(DURATION_KEY, this.duration);
|
||||
bundle.putInt(QUALITY_KEY, this.quality);
|
||||
bundle.putString(RESULTS_KEY, this.results.toString());
|
||||
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
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.networkinformation;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class NetworkManager extends CordovaPlugin {
|
||||
|
||||
public static int NOT_REACHABLE = 0;
|
||||
public static int REACHABLE_VIA_CARRIER_DATA_NETWORK = 1;
|
||||
public static int REACHABLE_VIA_WIFI_NETWORK = 2;
|
||||
|
||||
public static final String WIFI = "wifi";
|
||||
public static final String WIMAX = "wimax";
|
||||
// mobile
|
||||
public static final String MOBILE = "mobile";
|
||||
|
||||
// Android L calls this Cellular, because I have no idea!
|
||||
public static final String CELLULAR = "cellular";
|
||||
// 2G network types
|
||||
public static final String TWO_G = "2g";
|
||||
public static final String GSM = "gsm";
|
||||
public static final String GPRS = "gprs";
|
||||
public static final String EDGE = "edge";
|
||||
// 3G network types
|
||||
public static final String THREE_G = "3g";
|
||||
public static final String CDMA = "cdma";
|
||||
public static final String UMTS = "umts";
|
||||
public static final String HSPA = "hspa";
|
||||
public static final String HSUPA = "hsupa";
|
||||
public static final String HSDPA = "hsdpa";
|
||||
public static final String ONEXRTT = "1xrtt";
|
||||
public static final String EHRPD = "ehrpd";
|
||||
// 4G network types
|
||||
public static final String FOUR_G = "4g";
|
||||
public static final String LTE = "lte";
|
||||
public static final String UMB = "umb";
|
||||
public static final String HSPA_PLUS = "hspa+";
|
||||
// return type
|
||||
public static final String TYPE_UNKNOWN = "unknown";
|
||||
public static final String TYPE_ETHERNET = "ethernet";
|
||||
public static final String TYPE_ETHERNET_SHORT = "eth";
|
||||
public static final String TYPE_WIFI = "wifi";
|
||||
public static final String TYPE_2G = "2g";
|
||||
public static final String TYPE_3G = "3g";
|
||||
public static final String TYPE_4G = "4g";
|
||||
public static final String TYPE_NONE = "none";
|
||||
|
||||
private static final String LOG_TAG = "NetworkManager";
|
||||
|
||||
private CallbackContext connectionCallbackContext;
|
||||
|
||||
ConnectivityManager sockMan;
|
||||
BroadcastReceiver receiver;
|
||||
private String lastTypeOfNetwork;
|
||||
|
||||
/**
|
||||
* Sets the context of the Command. This can then be used to do things like
|
||||
* get file paths associated with the Activity.
|
||||
*
|
||||
* @param cordova The context of the main Activity.
|
||||
* @param webView The CordovaWebView Cordova is running in.
|
||||
*/
|
||||
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
|
||||
super.initialize(cordova, webView);
|
||||
this.sockMan = (ConnectivityManager) cordova.getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
this.connectionCallbackContext = null;
|
||||
|
||||
this.registerConnectivityActionReceiver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
*
|
||||
* @param action The action to execute.
|
||||
* @param args JSONArry of arguments for the plugin.
|
||||
* @param callbackContext The callback id used when calling back into JavaScript.
|
||||
* @return True if the action was valid, false otherwise.
|
||||
*/
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
|
||||
if (action.equals("getConnectionInfo")) {
|
||||
this.connectionCallbackContext = callbackContext;
|
||||
NetworkInfo info = sockMan.getActiveNetworkInfo();
|
||||
String connectionType = this.getTypeOfNetworkFallbackToTypeNoneIfNotConnected(info);
|
||||
PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, connectionType);
|
||||
pluginResult.setKeepCallback(true);
|
||||
callbackContext.sendPluginResult(pluginResult);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop network receiver.
|
||||
*/
|
||||
public void onDestroy() {
|
||||
this.unregisterReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(boolean multitasking) {
|
||||
this.unregisterReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(boolean multitasking) {
|
||||
super.onResume(multitasking);
|
||||
|
||||
this.unregisterReceiver();
|
||||
this.registerConnectivityActionReceiver();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// LOCAL METHODS
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
private void registerConnectivityActionReceiver() {
|
||||
// We need to listen to connectivity events to update navigator.connection
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
if (this.receiver == null) {
|
||||
this.receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// (The null check is for the ARM Emulator, please use Intel Emulator for better results)
|
||||
if (NetworkManager.this.webView != null) {
|
||||
updateConnectionInfo(sockMan.getActiveNetworkInfo());
|
||||
}
|
||||
|
||||
String connectionType;
|
||||
if (NetworkManager.this.lastTypeOfNetwork == null) {
|
||||
connectionType = TYPE_NONE;
|
||||
} else {
|
||||
connectionType = NetworkManager.this.lastTypeOfNetwork;
|
||||
}
|
||||
|
||||
// Lollipop always returns false for the EXTRA_NO_CONNECTIVITY flag => fix for Android M and above.
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && TYPE_NONE.equals(connectionType)) {
|
||||
boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
|
||||
LOG.d(LOG_TAG, "Intent no connectivity: " + noConnectivity);
|
||||
if(noConnectivity) {
|
||||
LOG.d(LOG_TAG, "Really no connectivity");
|
||||
} else {
|
||||
LOG.d(LOG_TAG, "!!! Switching to unknown, Intent states there is a connectivity.");
|
||||
sendUpdate(TYPE_UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
webView.getContext().registerReceiver(this.receiver, intentFilter);
|
||||
}
|
||||
|
||||
private void unregisterReceiver() {
|
||||
if (this.receiver != null) {
|
||||
try {
|
||||
webView.getContext().unregisterReceiver(this.receiver);
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, "Error unregistering network receiver: " + e.getMessage(), e);
|
||||
} finally {
|
||||
receiver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the JavaScript side whenever the connection changes
|
||||
*
|
||||
* @param info the current active network info
|
||||
* @return
|
||||
*/
|
||||
private void updateConnectionInfo(NetworkInfo info) {
|
||||
// send update to javascript "navigator.connection"
|
||||
// Jellybean sends its own info
|
||||
String currentNetworkType = this.getTypeOfNetworkFallbackToTypeNoneIfNotConnected(info);
|
||||
if (currentNetworkType.equals(this.lastTypeOfNetwork)) {
|
||||
LOG.d(LOG_TAG, "Networkinfo state didn't change, there is no event propagated to the JavaScript side.");
|
||||
} else {
|
||||
sendUpdate(currentNetworkType);
|
||||
this.lastTypeOfNetwork = currentNetworkType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of network connection of the NetworkInfo input
|
||||
*
|
||||
* @param info the current active network info
|
||||
* @return type the type of network
|
||||
*/
|
||||
private String getTypeOfNetworkFallbackToTypeNoneIfNotConnected(NetworkInfo info) {
|
||||
// the info might still be null in this part of the code
|
||||
String type;
|
||||
if (info != null) {
|
||||
// If we are not connected to any network set type to none
|
||||
if (!info.isConnected()) {
|
||||
type = TYPE_NONE;
|
||||
}
|
||||
else {
|
||||
type = getType(info);
|
||||
}
|
||||
} else {
|
||||
type = TYPE_NONE;
|
||||
}
|
||||
|
||||
LOG.d(LOG_TAG, "Connection Type: " + type);
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new plugin result and send it back to JavaScript
|
||||
*
|
||||
* @param connection the network info to set as navigator.connection
|
||||
*/
|
||||
private void sendUpdate(String type) {
|
||||
if (connectionCallbackContext != null) {
|
||||
PluginResult result = new PluginResult(PluginResult.Status.OK, type);
|
||||
result.setKeepCallback(true);
|
||||
connectionCallbackContext.sendPluginResult(result);
|
||||
}
|
||||
webView.postMessage("networkconnection", type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the type of connection
|
||||
*
|
||||
* @param info the network info so we can determine connection type.
|
||||
* @return the type of mobile network we are on
|
||||
*/
|
||||
private String getType(NetworkInfo info) {
|
||||
String type = info.getTypeName().toLowerCase(Locale.US);
|
||||
|
||||
LOG.d(LOG_TAG, "toLower : " + type);
|
||||
if (type.equals(WIFI)) {
|
||||
return TYPE_WIFI;
|
||||
} else if (type.toLowerCase().equals(TYPE_ETHERNET) || type.toLowerCase().startsWith(TYPE_ETHERNET_SHORT)) {
|
||||
return TYPE_ETHERNET;
|
||||
} else if (type.equals(MOBILE) || type.equals(CELLULAR)) {
|
||||
type = info.getSubtypeName().toLowerCase(Locale.US);
|
||||
if (type.equals(GSM) ||
|
||||
type.equals(GPRS) ||
|
||||
type.equals(EDGE) ||
|
||||
type.equals(TWO_G)) {
|
||||
return TYPE_2G;
|
||||
} else if (type.startsWith(CDMA) ||
|
||||
type.equals(UMTS) ||
|
||||
type.equals(ONEXRTT) ||
|
||||
type.equals(EHRPD) ||
|
||||
type.equals(HSUPA) ||
|
||||
type.equals(HSDPA) ||
|
||||
type.equals(HSPA) ||
|
||||
type.equals(THREE_G)) {
|
||||
return TYPE_3G;
|
||||
} else if (type.equals(LTE) ||
|
||||
type.equals(UMB) ||
|
||||
type.equals(HSPA_PLUS) ||
|
||||
type.equals(FOUR_G)) {
|
||||
return TYPE_4G;
|
||||
}
|
||||
}
|
||||
return TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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.statusbar;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaArgs;
|
||||
import org.apache.cordova.CordovaInterface;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CordovaWebView;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.json.JSONException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class StatusBar extends CordovaPlugin {
|
||||
private static final String TAG = "StatusBar";
|
||||
|
||||
/**
|
||||
* Sets the context of the Command. This can then be used to do things like
|
||||
* get file paths associated with the Activity.
|
||||
*
|
||||
* @param cordova The context of the main Activity.
|
||||
* @param webView The CordovaWebView Cordova is running in.
|
||||
*/
|
||||
@Override
|
||||
public void initialize(final CordovaInterface cordova, CordovaWebView webView) {
|
||||
LOG.v(TAG, "StatusBar: initialization");
|
||||
super.initialize(cordova, webView);
|
||||
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Clear flag FLAG_FORCE_NOT_FULLSCREEN which is set initially
|
||||
// by the Cordova.
|
||||
Window window = cordova.getActivity().getWindow();
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
|
||||
// Read 'StatusBarBackgroundColor' from config.xml, default is #000000.
|
||||
setStatusBarBackgroundColor(preferences.getString("StatusBarBackgroundColor", "#000000"));
|
||||
|
||||
// Read 'StatusBarStyle' from config.xml, default is 'lightcontent'.
|
||||
setStatusBarStyle(preferences.getString("StatusBarStyle", "lightcontent"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request and returns PluginResult.
|
||||
*
|
||||
* @param action The action to execute.
|
||||
* @param args JSONArry of arguments for the plugin.
|
||||
* @param callbackContext The callback id used when calling back into JavaScript.
|
||||
* @return True if the action was valid, false otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
|
||||
LOG.v(TAG, "Executing action: " + action);
|
||||
final Activity activity = this.cordova.getActivity();
|
||||
final Window window = activity.getWindow();
|
||||
|
||||
if ("_ready".equals(action)) {
|
||||
boolean statusBarVisible = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0;
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, statusBarVisible));
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("show".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we
|
||||
// use KitKat here to be aligned with "Fullscreen" preference
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
int uiOptions = window.getDecorView().getSystemUiVisibility();
|
||||
uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
uiOptions &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
|
||||
window.getDecorView().setSystemUiVisibility(uiOptions);
|
||||
}
|
||||
|
||||
// CB-11197 We still need to update LayoutParams to force status bar
|
||||
// to be hidden when entering e.g. text fields
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("hide".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we
|
||||
// use KitKat here to be aligned with "Fullscreen" preference
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
int uiOptions = window.getDecorView().getSystemUiVisibility()
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
|
||||
window.getDecorView().setSystemUiVisibility(uiOptions);
|
||||
}
|
||||
|
||||
// CB-11197 We still need to update LayoutParams to force status bar
|
||||
// to be hidden when entering e.g. text fields
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("backgroundColorByHexString".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
setStatusBarBackgroundColor(args.getString(0));
|
||||
} catch (JSONException ignore) {
|
||||
LOG.e(TAG, "Invalid hexString argument, use f.i. '#777777'");
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("overlaysWebView".equals(action)) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
setStatusBarTransparent(args.getBoolean(0));
|
||||
} catch (JSONException ignore) {
|
||||
LOG.e(TAG, "Invalid boolean argument");
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else return args.getBoolean(0) == false;
|
||||
}
|
||||
|
||||
if ("styleDefault".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatusBarStyle("default");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("styleLightContent".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatusBarStyle("lightcontent");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("styleBlackTranslucent".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatusBarStyle("blacktranslucent");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("styleBlackOpaque".equals(action)) {
|
||||
this.cordova.getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setStatusBarStyle("blackopaque");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setStatusBarBackgroundColor(final String colorPref) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (colorPref != null && !colorPref.isEmpty()) {
|
||||
final Window window = cordova.getActivity().getWindow();
|
||||
// Method and constants not available on all SDKs but we want to be able to compile this code with any SDK
|
||||
window.clearFlags(0x04000000); // SDK 19: WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
window.addFlags(0x80000000); // SDK 21: WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||
try {
|
||||
// Using reflection makes sure any 5.0+ device will work without having to compile with SDK level 21
|
||||
window.getClass().getMethod("setStatusBarColor", int.class).invoke(window, Color.parseColor(colorPref));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
LOG.e(TAG, "Invalid hexString argument, use f.i. '#999999'");
|
||||
} catch (Exception ignore) {
|
||||
// this should not happen, only in case Android removes this method in a version > 21
|
||||
LOG.w(TAG, "Method window.setStatusBarColor not found for SDK level " + Build.VERSION.SDK_INT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStatusBarTransparent(final boolean transparent) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
final Window window = cordova.getActivity().getWindow();
|
||||
if (transparent) {
|
||||
window.getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
else {
|
||||
window.getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStatusBarStyle(final String style) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (style != null && !style.isEmpty()) {
|
||||
View decorView = cordova.getActivity().getWindow().getDecorView();
|
||||
int uiOptions = decorView.getSystemUiVisibility();
|
||||
|
||||
String[] darkContentStyles = {
|
||||
"default",
|
||||
};
|
||||
|
||||
String[] lightContentStyles = {
|
||||
"lightcontent",
|
||||
"blacktranslucent",
|
||||
"blackopaque",
|
||||
};
|
||||
|
||||
if (Arrays.asList(darkContentStyles).contains(style.toLowerCase())) {
|
||||
decorView.setSystemUiVisibility(uiOptions | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Arrays.asList(lightContentStyles).contains(style.toLowerCase())) {
|
||||
decorView.setSystemUiVisibility(uiOptions & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.e(TAG, "Invalid style, must be either 'default', 'lightcontent' or the deprecated 'blacktranslucent' and 'blackopaque'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
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.whitelist;
|
||||
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.ConfigXmlParser;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.Whitelist;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class WhitelistPlugin extends CordovaPlugin {
|
||||
private static final String LOG_TAG = "WhitelistPlugin";
|
||||
private Whitelist allowedNavigations;
|
||||
private Whitelist allowedIntents;
|
||||
private Whitelist allowedRequests;
|
||||
|
||||
// Used when instantiated via reflection by PluginManager
|
||||
public WhitelistPlugin() {
|
||||
}
|
||||
// These can be used by embedders to allow Java-configuration of whitelists.
|
||||
public WhitelistPlugin(Context context) {
|
||||
this(new Whitelist(), new Whitelist(), null);
|
||||
new CustomConfigXmlParser().parse(context);
|
||||
}
|
||||
public WhitelistPlugin(XmlPullParser xmlParser) {
|
||||
this(new Whitelist(), new Whitelist(), null);
|
||||
new CustomConfigXmlParser().parse(xmlParser);
|
||||
}
|
||||
public WhitelistPlugin(Whitelist allowedNavigations, Whitelist allowedIntents, Whitelist allowedRequests) {
|
||||
if (allowedRequests == null) {
|
||||
allowedRequests = new Whitelist();
|
||||
allowedRequests.addWhiteListEntry("file:///*", false);
|
||||
allowedRequests.addWhiteListEntry("data:*", false);
|
||||
}
|
||||
this.allowedNavigations = allowedNavigations;
|
||||
this.allowedIntents = allowedIntents;
|
||||
this.allowedRequests = allowedRequests;
|
||||
}
|
||||
@Override
|
||||
public void pluginInitialize() {
|
||||
if (allowedNavigations == null) {
|
||||
allowedNavigations = new Whitelist();
|
||||
allowedIntents = new Whitelist();
|
||||
allowedRequests = new Whitelist();
|
||||
new CustomConfigXmlParser().parse(webView.getContext());
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomConfigXmlParser extends ConfigXmlParser {
|
||||
@Override
|
||||
public void handleStartTag(XmlPullParser xml) {
|
||||
String strNode = xml.getName();
|
||||
if (strNode.equals("content")) {
|
||||
String startPage = xml.getAttributeValue(null, "src");
|
||||
allowedNavigations.addWhiteListEntry(startPage, false);
|
||||
} else if (strNode.equals("allow-navigation")) {
|
||||
String origin = xml.getAttributeValue(null, "href");
|
||||
if ("*".equals(origin)) {
|
||||
allowedNavigations.addWhiteListEntry("http://*/*", false);
|
||||
allowedNavigations.addWhiteListEntry("https://*/*", false);
|
||||
allowedNavigations.addWhiteListEntry("data:*", false);
|
||||
} else {
|
||||
allowedNavigations.addWhiteListEntry(origin, false);
|
||||
}
|
||||
} else if (strNode.equals("allow-intent")) {
|
||||
String origin = xml.getAttributeValue(null, "href");
|
||||
allowedIntents.addWhiteListEntry(origin, false);
|
||||
} else if (strNode.equals("access")) {
|
||||
String origin = xml.getAttributeValue(null, "origin");
|
||||
String subdomains = xml.getAttributeValue(null, "subdomains");
|
||||
boolean external = (xml.getAttributeValue(null, "launch-external") != null);
|
||||
if (origin != null) {
|
||||
if (external) {
|
||||
LOG.w(LOG_TAG, "Found <access launch-external> within config.xml. Please use <allow-intent> instead.");
|
||||
allowedIntents.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
|
||||
} else {
|
||||
if ("*".equals(origin)) {
|
||||
allowedRequests.addWhiteListEntry("http://*/*", false);
|
||||
allowedRequests.addWhiteListEntry("https://*/*", false);
|
||||
} else {
|
||||
allowedRequests.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void handleEndTag(XmlPullParser xml) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean shouldAllowNavigation(String url) {
|
||||
if (allowedNavigations.isUrlWhiteListed(url)) {
|
||||
return true;
|
||||
}
|
||||
return null; // Default policy
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean shouldAllowRequest(String url) {
|
||||
if (Boolean.TRUE == shouldAllowNavigation(url)) {
|
||||
return true;
|
||||
}
|
||||
if (allowedRequests.isUrlWhiteListed(url)) {
|
||||
return true;
|
||||
}
|
||||
return null; // Default policy
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean shouldOpenExternalUrl(String url) {
|
||||
if (allowedIntents.isUrlWhiteListed(url)) {
|
||||
return true;
|
||||
}
|
||||
return null; // Default policy
|
||||
}
|
||||
|
||||
public Whitelist getAllowedNavigations() {
|
||||
return allowedNavigations;
|
||||
}
|
||||
|
||||
public void setAllowedNavigations(Whitelist allowedNavigations) {
|
||||
this.allowedNavigations = allowedNavigations;
|
||||
}
|
||||
|
||||
public Whitelist getAllowedIntents() {
|
||||
return allowedIntents;
|
||||
}
|
||||
|
||||
public void setAllowedIntents(Whitelist allowedIntents) {
|
||||
this.allowedIntents = allowedIntents;
|
||||
}
|
||||
|
||||
public Whitelist getAllowedRequests() {
|
||||
return allowedRequests;
|
||||
}
|
||||
|
||||
public void setAllowedRequests(Whitelist allowedRequests) {
|
||||
this.allowedRequests = allowedRequests;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 593 B |
|
After Width: | Height: | Size: 599 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 328 B |
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 744 B |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 1021 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 681 B |
@@ -0,0 +1,66 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000000"
|
||||
tools:context=".PhotoActivity" >
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loadingBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/photoView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:scaleType="centerInside"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true" >
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/fullscreen_content_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:background="#50000000"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UselessParent" >
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/closeBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:padding="8dp"
|
||||
android:text="Close" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text=""
|
||||
android:id="@+id/titleTxt"
|
||||
android:layout_weight="1"
|
||||
android:textColor="@android:color/primary_text_dark"
|
||||
android:gravity="center_vertical|center_horizontal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/shareBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@android:drawable/ic_menu_share"
|
||||
android:padding="8dp"
|
||||
android:text="Close" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019 The Android Open Source Project
|
||||
|
||||
Licensed 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.
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent" android:layout_height="match_parent">
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019 The Android Open Source Project
|
||||
|
||||
Licensed 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<style name="TransparentTheme" parent="Theme.AppCompat">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path path="tmp/DocumentViewerPlugin" name="plugin_tmp_files" />
|
||||
<!--<external-path path="Android/data/com.your.package/files/tmp/DocumentViewerPlugin/" name="plugin_tmp_files" />-->
|
||||
</paths>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://developer.android.com/reference/android/support/v4/content/FileProvider.html#SpecifyFiles -->
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- cordova.file.dataDirectory -->
|
||||
<files-path name="files" path="." />
|
||||
<!-- cordova.file.cacheDirectory -->
|
||||
<cache-path name="cache" path="." />
|
||||
<!-- cordova.file.externalDataDirectory -->
|
||||
<external-files-path name="external-files" path="." />
|
||||
<!-- cordova.file.externalCacheDirectory -->
|
||||
<external-cache-path name="external-cache" path="." />
|
||||
<!-- cordova.file.externalRootDirectory -->
|
||||
<external-path name="external" path="." />
|
||||
</paths>
|
||||