preview doc on chat solved
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
</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.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,50 @@
|
||||
package com.marin.plugin;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.cordova.*;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import com.arthenica.ffmpegkit.FFmpegKit;
|
||||
import com.arthenica.ffmpegkit.FFmpegSession;
|
||||
import com.arthenica.ffmpegkit.FFmpegSessionCompleteCallback;
|
||||
import com.arthenica.ffmpegkit.FFprobeKit;
|
||||
import com.arthenica.ffmpegkit.MediaInformationSession;
|
||||
import com.arthenica.ffmpegkit.ReturnCode;
|
||||
|
||||
public class FFMpeg extends CordovaPlugin {
|
||||
private static final String TAG = "FFMpegPlugin";
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
|
||||
if (action.equals("exec")) {
|
||||
FFmpegKit.executeAsync(data.getString(0), new FFmpegSessionCompleteCallback() {
|
||||
@Override
|
||||
public void apply(FFmpegSession session) {
|
||||
Log.d(TAG, String.format("FFmpeg process exited with state %s and rc %s.%s", session.getState(), session.getReturnCode(), notNull(session.getFailStackTrace())));
|
||||
if (ReturnCode.isSuccess(session.getReturnCode())) {
|
||||
callbackContext.success();
|
||||
} else {
|
||||
callbackContext.error("Error Code: " + session.getReturnCode());
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else if(action.equals("probe")) {
|
||||
MediaInformationSession mediaInformationSession = FFprobeKit.getMediaInformation(data.getString(0));
|
||||
ReturnCode returnCode = mediaInformationSession.getReturnCode();
|
||||
|
||||
if(ReturnCode.isSuccess(returnCode)) {
|
||||
callbackContext.success(mediaInformationSession.getMediaInformation().getAllProperties());
|
||||
} else {
|
||||
callbackContext.error(notNull(mediaInformationSession.getFailStackTrace()));
|
||||
}
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
static String notNull(final String string) {
|
||||
return (string == null) ? "" : String.format("%s%s", "\n", string);
|
||||
}
|
||||
}
|
||||
@@ -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,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 android.support.v4.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,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,63 @@
|
||||
/*
|
||||
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.filetransfer;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Encapsulates in-progress status of uploading or downloading a file to a remote server.
|
||||
*/
|
||||
public class FileProgressResult {
|
||||
|
||||
private boolean lengthComputable = false; // declares whether total is known
|
||||
private long loaded = 0; // bytes sent so far
|
||||
private long total = 0; // bytes total, if known
|
||||
|
||||
public boolean getLengthComputable() {
|
||||
return lengthComputable;
|
||||
}
|
||||
|
||||
public void setLengthComputable(boolean computable) {
|
||||
this.lengthComputable = computable;
|
||||
}
|
||||
|
||||
public long getLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public void setLoaded(long bytes) {
|
||||
this.loaded = bytes;
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long bytes) {
|
||||
this.total = bytes;
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
return new JSONObject(
|
||||
"{loaded:" + loaded +
|
||||
",total:" + total +
|
||||
",lengthComputable:" + (lengthComputable ? "true" : "false") + "}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
/*
|
||||
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.filetransfer;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URLConnection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.CordovaResourceApi;
|
||||
import org.apache.cordova.CordovaResourceApi.OpenForReadResult;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.PluginManager;
|
||||
import org.apache.cordova.PluginResult;
|
||||
import org.apache.cordova.file.FileUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.webkit.CookieManager;
|
||||
|
||||
public class FileTransfer extends CordovaPlugin {
|
||||
|
||||
private static final String LOG_TAG = "FileTransfer";
|
||||
private static final String LINE_START = "--";
|
||||
private static final String LINE_END = "\r\n";
|
||||
private static final String BOUNDARY = "+++++";
|
||||
|
||||
public static int FILE_NOT_FOUND_ERR = 1;
|
||||
public static int INVALID_URL_ERR = 2;
|
||||
public static int CONNECTION_ERR = 3;
|
||||
public static int ABORTED_ERR = 4;
|
||||
public static int NOT_MODIFIED_ERR = 5;
|
||||
|
||||
private static HashMap<String, RequestContext> activeRequests = new HashMap<String, RequestContext>();
|
||||
private static final int MAX_BUFFER_SIZE = 16 * 1024;
|
||||
|
||||
private static final class RequestContext {
|
||||
String source;
|
||||
String target;
|
||||
File targetFile;
|
||||
CallbackContext callbackContext;
|
||||
HttpURLConnection connection;
|
||||
boolean aborted;
|
||||
RequestContext(String source, String target, CallbackContext callbackContext) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.callbackContext = callbackContext;
|
||||
}
|
||||
void sendPluginResult(PluginResult pluginResult) {
|
||||
synchronized (this) {
|
||||
if (!aborted) {
|
||||
callbackContext.sendPluginResult(pluginResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an interface method to an InputStream to return the number of bytes
|
||||
* read from the raw stream. This is used to track total progress against
|
||||
* the HTTP Content-Length header value from the server.
|
||||
*/
|
||||
private static abstract class TrackingInputStream extends FilterInputStream {
|
||||
public TrackingInputStream(final InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
public abstract long getTotalRawBytesRead();
|
||||
}
|
||||
|
||||
private static class ExposedGZIPInputStream extends GZIPInputStream {
|
||||
public ExposedGZIPInputStream(final InputStream in) throws IOException {
|
||||
super(in);
|
||||
}
|
||||
public Inflater getInflater() {
|
||||
return inf;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides raw bytes-read tracking for a GZIP input stream. Reports the
|
||||
* total number of compressed bytes read from the input, rather than the
|
||||
* number of uncompressed bytes.
|
||||
*/
|
||||
private static class TrackingGZIPInputStream extends TrackingInputStream {
|
||||
private ExposedGZIPInputStream gzin;
|
||||
public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {
|
||||
super(gzin);
|
||||
this.gzin = gzin;
|
||||
}
|
||||
public long getTotalRawBytesRead() {
|
||||
return gzin.getInflater().getBytesRead();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides simple total-bytes-read tracking for an existing InputStream
|
||||
*/
|
||||
private static class SimpleTrackingInputStream extends TrackingInputStream {
|
||||
private long bytesRead = 0;
|
||||
public SimpleTrackingInputStream(InputStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
private int updateBytesRead(int newBytesRead) {
|
||||
if (newBytesRead != -1) {
|
||||
bytesRead += newBytesRead;
|
||||
}
|
||||
return newBytesRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return updateBytesRead(super.read());
|
||||
}
|
||||
|
||||
// Note: FilterInputStream delegates read(byte[] bytes) to the below method,
|
||||
// so we don't override it or else double count (CB-5631).
|
||||
@Override
|
||||
public int read(byte[] bytes, int offset, int count) throws IOException {
|
||||
return updateBytesRead(super.read(bytes, offset, count));
|
||||
}
|
||||
|
||||
public long getTotalRawBytesRead() {
|
||||
return bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
|
||||
if (action.equals("upload") || action.equals("download")) {
|
||||
String source = args.getString(0);
|
||||
String target = args.getString(1);
|
||||
|
||||
if (action.equals("upload")) {
|
||||
upload(source, target, args, callbackContext);
|
||||
} else {
|
||||
download(source, target, args, callbackContext);
|
||||
}
|
||||
return true;
|
||||
} else if (action.equals("abort")) {
|
||||
String objectId = args.getString(0);
|
||||
abort(objectId);
|
||||
callbackContext.success();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void addHeadersToRequest(URLConnection connection, JSONObject headers) {
|
||||
try {
|
||||
for (Iterator<?> iter = headers.keys(); iter.hasNext(); ) {
|
||||
/* RFC 2616 says that non-ASCII characters and control
|
||||
* characters are not allowed in header names or values.
|
||||
* Additionally, spaces are not allowed in header names.
|
||||
* RFC 2046 Quoted-printable encoding may be used to encode
|
||||
* arbitrary characters, but we donon- not do that encoding here.
|
||||
*/
|
||||
String headerKey = iter.next().toString();
|
||||
String cleanHeaderKey = headerKey.replaceAll("\\n","")
|
||||
.replaceAll("\\s+","")
|
||||
.replaceAll(":", "")
|
||||
.replaceAll("[^\\x20-\\x7E]+", "");
|
||||
|
||||
JSONArray headerValues = headers.optJSONArray(headerKey);
|
||||
if (headerValues == null) {
|
||||
headerValues = new JSONArray();
|
||||
|
||||
/* RFC 2616 also says that any amount of consecutive linear
|
||||
* whitespace within a header value can be replaced with a
|
||||
* single space character, without affecting the meaning of
|
||||
* that value.
|
||||
*/
|
||||
|
||||
String headerValue = headers.getString(headerKey);
|
||||
String finalValue = headerValue.replaceAll("\\s+", " ").replaceAll("\\n"," ").replaceAll("[^\\x20-\\x7E]+", " ");
|
||||
headerValues.put(finalValue);
|
||||
}
|
||||
|
||||
//Use the clean header key, not the one that we passed in
|
||||
connection.setRequestProperty(cleanHeaderKey, headerValues.getString(0));
|
||||
for (int i = 1; i < headerValues.length(); ++i) {
|
||||
connection.addRequestProperty(headerKey, headerValues.getString(i));
|
||||
}
|
||||
}
|
||||
} catch (JSONException e1) {
|
||||
// No headers to be manipulated!
|
||||
}
|
||||
}
|
||||
|
||||
private String getCookies(final String target) {
|
||||
boolean gotCookie = false;
|
||||
String cookie = null;
|
||||
Class webViewClass = webView.getClass();
|
||||
try {
|
||||
Method gcmMethod = webViewClass.getMethod("getCookieManager");
|
||||
Class iccmClass = gcmMethod.getReturnType();
|
||||
Method gcMethod = iccmClass.getMethod("getCookie", String.class);
|
||||
|
||||
cookie = (String)gcMethod.invoke(
|
||||
iccmClass.cast(
|
||||
gcmMethod.invoke(webView)
|
||||
), target);
|
||||
|
||||
gotCookie = true;
|
||||
} catch (NoSuchMethodException e) {
|
||||
} catch (IllegalAccessException e) {
|
||||
} catch (InvocationTargetException e) {
|
||||
} catch (ClassCastException e) {
|
||||
}
|
||||
|
||||
if (!gotCookie && CookieManager.getInstance() != null) {
|
||||
cookie = CookieManager.getInstance().getCookie(target);
|
||||
}
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the specified file to the server URL provided using an HTTP multipart request.
|
||||
* @param source Full path of the file on the file system
|
||||
* @param target URL of the server to receive the file
|
||||
* @param args JSON Array of args
|
||||
* @param callbackContext callback id for optional progress reports
|
||||
*
|
||||
* args[2] fileKey Name of file request parameter
|
||||
* args[3] fileName File name to be used on server
|
||||
* args[4] mimeType Describes file content type
|
||||
* args[5] params key:value pairs of user-defined parameters
|
||||
* @return FileUploadResult containing result of upload request
|
||||
*/
|
||||
private void upload(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
LOG.d(LOG_TAG, "upload " + source + " to " + target);
|
||||
|
||||
// Setup the options
|
||||
final String fileKey = getArgument(args, 2, "file");
|
||||
final String fileName = getArgument(args, 3, "image.jpg");
|
||||
final String mimeType = getArgument(args, 4, "image/jpeg");
|
||||
final JSONObject params = args.optJSONObject(5) == null ? new JSONObject() : args.optJSONObject(5);
|
||||
// Always use chunked mode unless set to false as per API
|
||||
final boolean chunkedMode = args.optBoolean(7) || args.isNull(7);
|
||||
// Look for headers on the params map for backwards compatibility with older Cordova versions.
|
||||
final JSONObject headers = args.optJSONObject(8) == null ? params.optJSONObject("headers") : args.optJSONObject(8);
|
||||
final String objectId = args.getString(9);
|
||||
final String httpMethod = getArgument(args, 10, "POST");
|
||||
|
||||
final CordovaResourceApi resourceApi = webView.getResourceApi();
|
||||
|
||||
LOG.d(LOG_TAG, "fileKey: " + fileKey);
|
||||
LOG.d(LOG_TAG, "fileName: " + fileName);
|
||||
LOG.d(LOG_TAG, "mimeType: " + mimeType);
|
||||
LOG.d(LOG_TAG, "params: " + params);
|
||||
LOG.d(LOG_TAG, "chunkedMode: " + chunkedMode);
|
||||
LOG.d(LOG_TAG, "headers: " + headers);
|
||||
LOG.d(LOG_TAG, "objectId: " + objectId);
|
||||
LOG.d(LOG_TAG, "httpMethod: " + httpMethod);
|
||||
|
||||
final Uri targetUri = resourceApi.remapUri(Uri.parse(target));
|
||||
|
||||
int uriType = CordovaResourceApi.getUriType(targetUri);
|
||||
final boolean useHttps = uriType == CordovaResourceApi.URI_TYPE_HTTPS;
|
||||
if (uriType != CordovaResourceApi.URI_TYPE_HTTP && !useHttps) {
|
||||
JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, null, 0, null);
|
||||
LOG.e(LOG_TAG, "Unsupported URI: " + targetUri);
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
|
||||
return;
|
||||
}
|
||||
|
||||
final RequestContext context = new RequestContext(source, target, callbackContext);
|
||||
synchronized (activeRequests) {
|
||||
activeRequests.put(objectId, context);
|
||||
}
|
||||
|
||||
cordova.getThreadPool().execute(new Runnable() {
|
||||
public void run() {
|
||||
if (context.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We should call remapUri on background thread otherwise it throws
|
||||
// IllegalStateException when trying to remap 'cdvfile://localhost/content/...' URIs
|
||||
// via ContentFilesystem (see https://issues.apache.org/jira/browse/CB-9022)
|
||||
Uri tmpSrc = Uri.parse(source);
|
||||
final Uri sourceUri = resourceApi.remapUri(
|
||||
tmpSrc.getScheme() != null ? tmpSrc : Uri.fromFile(new File(source)));
|
||||
|
||||
HttpURLConnection conn = null;
|
||||
int totalBytes = 0;
|
||||
int fixedLength = -1;
|
||||
try {
|
||||
// Create return object
|
||||
FileUploadResult result = new FileUploadResult();
|
||||
FileProgressResult progress = new FileProgressResult();
|
||||
|
||||
//------------------ CLIENT REQUEST
|
||||
// Open a HTTP connection to the URL based on protocol
|
||||
conn = resourceApi.createHttpConnection(targetUri);
|
||||
|
||||
// Allow Inputs
|
||||
conn.setDoInput(true);
|
||||
|
||||
// Allow Outputs
|
||||
conn.setDoOutput(true);
|
||||
|
||||
// Don't use a cached copy.
|
||||
conn.setUseCaches(false);
|
||||
|
||||
// Use a post method.
|
||||
conn.setRequestMethod(httpMethod);
|
||||
|
||||
// if we specified a Content-Type header, don't do multipart form upload
|
||||
boolean multipartFormUpload = (headers == null) || !headers.has("Content-Type");
|
||||
if (multipartFormUpload) {
|
||||
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
|
||||
}
|
||||
|
||||
// Set the cookies on the response
|
||||
String cookie = getCookies(target);
|
||||
|
||||
if (cookie != null) {
|
||||
conn.setRequestProperty("Cookie", cookie);
|
||||
}
|
||||
|
||||
// Handle the other headers
|
||||
if (headers != null) {
|
||||
addHeadersToRequest(conn, headers);
|
||||
}
|
||||
|
||||
/*
|
||||
* Store the non-file portions of the multipart data as a string, so that we can add it
|
||||
* to the contentSize, since it is part of the body of the HTTP request.
|
||||
*/
|
||||
StringBuilder beforeData = new StringBuilder();
|
||||
try {
|
||||
for (Iterator<?> iter = params.keys(); iter.hasNext();) {
|
||||
Object key = iter.next();
|
||||
if(!String.valueOf(key).equals("headers"))
|
||||
{
|
||||
beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
|
||||
beforeData.append("Content-Disposition: form-data; name=\"").append(key.toString()).append('"');
|
||||
beforeData.append(LINE_END).append(LINE_END);
|
||||
beforeData.append(params.getString(key.toString()));
|
||||
beforeData.append(LINE_END);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LOG.e(LOG_TAG, e.getMessage(), e);
|
||||
}
|
||||
|
||||
beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
|
||||
beforeData.append("Content-Disposition: form-data; name=\"").append(fileKey).append("\";");
|
||||
beforeData.append(" filename=\"").append(fileName).append('"').append(LINE_END);
|
||||
beforeData.append("Content-Type: ").append(mimeType).append(LINE_END).append(LINE_END);
|
||||
byte[] beforeDataBytes = beforeData.toString().getBytes("UTF-8");
|
||||
byte[] tailParamsBytes = (LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END).getBytes("UTF-8");
|
||||
|
||||
|
||||
// Get a input stream of the file on the phone
|
||||
OpenForReadResult readResult = resourceApi.openForRead(sourceUri);
|
||||
|
||||
int stringLength = beforeDataBytes.length + tailParamsBytes.length;
|
||||
if (readResult.length >= 0) {
|
||||
fixedLength = (int)readResult.length;
|
||||
if (multipartFormUpload)
|
||||
fixedLength += stringLength;
|
||||
progress.setLengthComputable(true);
|
||||
progress.setTotal(fixedLength);
|
||||
}
|
||||
LOG.d(LOG_TAG, "Content Length: " + fixedLength);
|
||||
// setFixedLengthStreamingMode causes and OutOfMemoryException on pre-Froyo devices.
|
||||
// http://code.google.com/p/android/issues/detail?id=3164
|
||||
// It also causes OOM if HTTPS is used, even on newer devices.
|
||||
boolean useChunkedMode = chunkedMode || (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO);
|
||||
useChunkedMode = useChunkedMode || (fixedLength == -1);
|
||||
|
||||
if (useChunkedMode) {
|
||||
conn.setChunkedStreamingMode(MAX_BUFFER_SIZE);
|
||||
// Although setChunkedStreamingMode sets this header, setting it explicitly here works
|
||||
// around an OutOfMemoryException when using https.
|
||||
conn.setRequestProperty("Transfer-Encoding", "chunked");
|
||||
} else {
|
||||
conn.setFixedLengthStreamingMode(fixedLength);
|
||||
|
||||
if (useHttps) {
|
||||
LOG.w(LOG_TAG, "setFixedLengthStreamingMode could cause OutOfMemoryException - switch to chunkedMode=true to avoid it if this is an issue.");
|
||||
}
|
||||
}
|
||||
|
||||
conn.connect();
|
||||
|
||||
OutputStream sendStream = null;
|
||||
try {
|
||||
sendStream = conn.getOutputStream();
|
||||
synchronized (context) {
|
||||
if (context.aborted) {
|
||||
return;
|
||||
}
|
||||
context.connection = conn;
|
||||
}
|
||||
|
||||
if (multipartFormUpload) {
|
||||
//We don't want to change encoding, we just want this to write for all Unicode.
|
||||
sendStream.write(beforeDataBytes);
|
||||
totalBytes += beforeDataBytes.length;
|
||||
}
|
||||
|
||||
// create a buffer of maximum size
|
||||
int bytesAvailable = readResult.inputStream.available();
|
||||
int bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
|
||||
// read file and write it into form...
|
||||
int bytesRead = readResult.inputStream.read(buffer, 0, bufferSize);
|
||||
|
||||
long prevBytesRead = 0;
|
||||
while (bytesRead > 0) {
|
||||
totalBytes += bytesRead;
|
||||
result.setBytesSent(totalBytes);
|
||||
sendStream.write(buffer, 0, bytesRead);
|
||||
if (totalBytes > prevBytesRead + 102400) {
|
||||
prevBytesRead = totalBytes;
|
||||
LOG.d(LOG_TAG, "Uploaded " + totalBytes + " of " + fixedLength + " bytes");
|
||||
}
|
||||
bytesAvailable = readResult.inputStream.available();
|
||||
bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
|
||||
bytesRead = readResult.inputStream.read(buffer, 0, bufferSize);
|
||||
|
||||
// Send a progress event.
|
||||
progress.setLoaded(totalBytes);
|
||||
PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
|
||||
progressResult.setKeepCallback(true);
|
||||
context.sendPluginResult(progressResult);
|
||||
}
|
||||
|
||||
if (multipartFormUpload) {
|
||||
// send multipart form data necessary after file data...
|
||||
sendStream.write(tailParamsBytes);
|
||||
totalBytes += tailParamsBytes.length;
|
||||
}
|
||||
sendStream.flush();
|
||||
} finally {
|
||||
safeClose(readResult.inputStream);
|
||||
safeClose(sendStream);
|
||||
}
|
||||
synchronized (context) {
|
||||
context.connection = null;
|
||||
}
|
||||
LOG.d(LOG_TAG, "Sent " + totalBytes + " of " + fixedLength);
|
||||
|
||||
//------------------ read the SERVER RESPONSE
|
||||
String responseString;
|
||||
int responseCode = conn.getResponseCode();
|
||||
LOG.d(LOG_TAG, "response code: " + responseCode);
|
||||
LOG.d(LOG_TAG, "response headers: " + conn.getHeaderFields());
|
||||
TrackingInputStream inStream = null;
|
||||
try {
|
||||
inStream = getInputStream(conn);
|
||||
synchronized (context) {
|
||||
if (context.aborted) {
|
||||
return;
|
||||
}
|
||||
context.connection = conn;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, conn.getContentLength()));
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead = 0;
|
||||
// write bytes to file
|
||||
while ((bytesRead = inStream.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
responseString = out.toString("UTF-8");
|
||||
} finally {
|
||||
synchronized (context) {
|
||||
context.connection = null;
|
||||
}
|
||||
safeClose(inStream);
|
||||
}
|
||||
|
||||
LOG.d(LOG_TAG, "got response from server");
|
||||
LOG.d(LOG_TAG, responseString.substring(0, Math.min(256, responseString.length())));
|
||||
|
||||
// send request and retrieve response
|
||||
result.setResponseCode(responseCode);
|
||||
result.setResponse(responseString);
|
||||
|
||||
context.sendPluginResult(new PluginResult(PluginResult.Status.OK, result.toJSONObject()));
|
||||
} catch (FileNotFoundException e) {
|
||||
JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, conn, e);
|
||||
LOG.e(LOG_TAG, error.toString(), e);
|
||||
context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
|
||||
} catch (IOException e) {
|
||||
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn, e);
|
||||
LOG.e(LOG_TAG, error.toString(), e);
|
||||
LOG.e(LOG_TAG, "Failed after uploading " + totalBytes + " of " + fixedLength + " bytes.");
|
||||
context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
|
||||
} catch (JSONException e) {
|
||||
LOG.e(LOG_TAG, e.getMessage(), e);
|
||||
context.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
|
||||
} catch (Throwable t) {
|
||||
// Shouldn't happen, but will
|
||||
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn, t);
|
||||
LOG.e(LOG_TAG, error.toString(), t);
|
||||
context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
|
||||
} finally {
|
||||
synchronized (activeRequests) {
|
||||
activeRequests.remove(objectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void safeClose(Closeable stream) {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {
|
||||
String encoding = conn.getContentEncoding();
|
||||
if (encoding != null && encoding.equalsIgnoreCase("gzip")) {
|
||||
return new TrackingGZIPInputStream(new ExposedGZIPInputStream(conn.getInputStream()));
|
||||
}
|
||||
return new SimpleTrackingInputStream(conn.getInputStream());
|
||||
}
|
||||
|
||||
private static JSONObject createFileTransferError(int errorCode, String source, String target, URLConnection connection, Throwable throwable) {
|
||||
|
||||
int httpStatus = 0;
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
String body = null;
|
||||
if (connection != null) {
|
||||
try {
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
httpStatus = ((HttpURLConnection)connection).getResponseCode();
|
||||
InputStream err = ((HttpURLConnection) connection).getErrorStream();
|
||||
if(err != null)
|
||||
{
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(err, "UTF-8"));
|
||||
try {
|
||||
String line = reader.readLine();
|
||||
while(line != null) {
|
||||
bodyBuilder.append(line);
|
||||
line = reader.readLine();
|
||||
if(line != null) {
|
||||
bodyBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
body = bodyBuilder.toString();
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
// IOException can leave connection object in a bad state, so catch all exceptions.
|
||||
} catch (Throwable e) {
|
||||
LOG.w(LOG_TAG, "Error getting HTTP status code from connection.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return createFileTransferError(errorCode, source, target, body, httpStatus, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error object based on the passed in errorCode
|
||||
* @param errorCode the error
|
||||
* @return JSONObject containing the error
|
||||
*/
|
||||
private static JSONObject createFileTransferError(int errorCode, String source, String target, String body, Integer httpStatus, Throwable throwable) {
|
||||
JSONObject error = null;
|
||||
try {
|
||||
error = new JSONObject();
|
||||
error.put("code", errorCode);
|
||||
error.put("source", source);
|
||||
error.put("target", target);
|
||||
if(body != null)
|
||||
{
|
||||
error.put("body", body);
|
||||
}
|
||||
if (httpStatus != null) {
|
||||
error.put("http_status", httpStatus);
|
||||
}
|
||||
if (throwable != null) {
|
||||
String msg = throwable.getMessage();
|
||||
if (msg == null || "".equals(msg)) {
|
||||
msg = throwable.toString();
|
||||
}
|
||||
error.put("exception", msg);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LOG.e(LOG_TAG, e.getMessage(), e);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to read a parameter from the list of JSON args.
|
||||
* @param args the args passed to the Plugin
|
||||
* @param position the position to retrieve the arg from
|
||||
* @param defaultString the default to be used if the arg does not exist
|
||||
* @return String with the retrieved value
|
||||
*/
|
||||
private static String getArgument(JSONArray args, int position, String defaultString) {
|
||||
String arg = defaultString;
|
||||
if (args.length() > position) {
|
||||
arg = args.optString(position);
|
||||
if (arg == null || "null".equals(arg)) {
|
||||
arg = defaultString;
|
||||
}
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file form a given URL and saves it to the specified directory.
|
||||
*
|
||||
* @param source URL of the server to receive the file
|
||||
* @param target Full path of the file on the file system
|
||||
*/
|
||||
private void download(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
|
||||
LOG.d(LOG_TAG, "download " + source + " to " + target);
|
||||
|
||||
final CordovaResourceApi resourceApi = webView.getResourceApi();
|
||||
|
||||
final String objectId = args.getString(3);
|
||||
final JSONObject headers = args.optJSONObject(4);
|
||||
|
||||
final Uri sourceUri = resourceApi.remapUri(Uri.parse(source));
|
||||
int uriType = CordovaResourceApi.getUriType(sourceUri);
|
||||
final boolean useHttps = uriType == CordovaResourceApi.URI_TYPE_HTTPS;
|
||||
final boolean isLocalTransfer = !useHttps && uriType != CordovaResourceApi.URI_TYPE_HTTP;
|
||||
if (uriType == CordovaResourceApi.URI_TYPE_UNKNOWN) {
|
||||
JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, null, 0, null);
|
||||
LOG.e(LOG_TAG, "Unsupported URI: " + sourceUri);
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
|
||||
return;
|
||||
}
|
||||
|
||||
Boolean shouldAllowRequest = null;
|
||||
if (isLocalTransfer) {
|
||||
shouldAllowRequest = true;
|
||||
}
|
||||
|
||||
if (shouldAllowRequest == null) {
|
||||
try {
|
||||
Method gpm = webView.getClass().getMethod("getPluginManager");
|
||||
PluginManager pm = (PluginManager)gpm.invoke(webView);
|
||||
Method san = pm.getClass().getMethod("shouldAllowRequest", String.class);
|
||||
shouldAllowRequest = (Boolean)san.invoke(pm, source);
|
||||
} catch (NoSuchMethodException e) {
|
||||
} catch (IllegalAccessException e) {
|
||||
} catch (InvocationTargetException e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!Boolean.TRUE.equals(shouldAllowRequest)) {
|
||||
LOG.w(LOG_TAG, "The Source URL is not in the Allow List: '" + source + "'");
|
||||
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, null, 401, null);
|
||||
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
|
||||
return;
|
||||
}
|
||||
|
||||
final RequestContext context = new RequestContext(source, target, callbackContext);
|
||||
synchronized (activeRequests) {
|
||||
activeRequests.put(objectId, context);
|
||||
}
|
||||
|
||||
cordova.getThreadPool().execute(new Runnable() {
|
||||
public void run() {
|
||||
if (context.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Accept a path or a URI for the source.
|
||||
Uri tmpTarget = Uri.parse(target);
|
||||
Uri targetUri = resourceApi.remapUri(
|
||||
tmpTarget.getScheme() != null ? tmpTarget : Uri.fromFile(new File(target)));
|
||||
HttpURLConnection connection = null;
|
||||
File file = null;
|
||||
PluginResult result = null;
|
||||
TrackingInputStream inputStream = null;
|
||||
boolean cached = false;
|
||||
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
OpenForReadResult readResult = null;
|
||||
|
||||
file = resourceApi.mapUriToFile(targetUri);
|
||||
context.targetFile = file;
|
||||
|
||||
LOG.d(LOG_TAG, "Download file:" + sourceUri);
|
||||
|
||||
FileProgressResult progress = new FileProgressResult();
|
||||
|
||||
if (isLocalTransfer) {
|
||||
readResult = resourceApi.openForRead(sourceUri);
|
||||
if (readResult.length != -1) {
|
||||
progress.setLengthComputable(true);
|
||||
progress.setTotal(readResult.length);
|
||||
}
|
||||
inputStream = new SimpleTrackingInputStream(readResult.inputStream);
|
||||
} else {
|
||||
// connect to server
|
||||
// Open a HTTP connection to the URL based on protocol
|
||||
connection = resourceApi.createHttpConnection(sourceUri);
|
||||
connection.setRequestMethod("GET");
|
||||
|
||||
// TODO: Make OkHttp use this CookieManager by default.
|
||||
String cookie = getCookies(sourceUri.toString());
|
||||
|
||||
if(cookie != null)
|
||||
{
|
||||
connection.setRequestProperty("cookie", cookie);
|
||||
}
|
||||
|
||||
// This must be explicitly set for gzip progress tracking to work.
|
||||
connection.setRequestProperty("Accept-Encoding", "gzip");
|
||||
|
||||
// Handle the other headers
|
||||
if (headers != null) {
|
||||
addHeadersToRequest(connection, headers);
|
||||
}
|
||||
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
cached = true;
|
||||
connection.disconnect();
|
||||
LOG.d(LOG_TAG, "Resource not modified: " + source);
|
||||
JSONObject error = createFileTransferError(NOT_MODIFIED_ERR, source, target, connection, null);
|
||||
result = new PluginResult(PluginResult.Status.ERROR, error);
|
||||
} else {
|
||||
if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {
|
||||
// Only trust content-length header if we understand
|
||||
// the encoding -- identity or gzip
|
||||
if (connection.getContentLength() != -1) {
|
||||
progress.setLengthComputable(true);
|
||||
progress.setTotal(connection.getContentLength());
|
||||
}
|
||||
}
|
||||
inputStream = getInputStream(connection);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
try {
|
||||
synchronized (context) {
|
||||
if (context.aborted) {
|
||||
return;
|
||||
}
|
||||
context.connection = connection;
|
||||
}
|
||||
|
||||
// write bytes to file
|
||||
byte[] buffer = new byte[MAX_BUFFER_SIZE];
|
||||
int bytesRead = 0;
|
||||
outputStream = new FileOutputStream(file);
|
||||
while ((bytesRead = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
// Send a progress event.
|
||||
progress.setLoaded(inputStream.getTotalRawBytesRead());
|
||||
PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
|
||||
progressResult.setKeepCallback(true);
|
||||
context.sendPluginResult(progressResult);
|
||||
}
|
||||
} finally {
|
||||
synchronized (context) {
|
||||
context.connection = null;
|
||||
}
|
||||
safeClose(inputStream);
|
||||
safeClose(outputStream);
|
||||
}
|
||||
|
||||
LOG.d(LOG_TAG, "Saved file: " + target);
|
||||
|
||||
|
||||
// create FileEntry object
|
||||
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) {
|
||||
}
|
||||
}
|
||||
file = resourceApi.mapUriToFile(targetUri);
|
||||
context.targetFile = file;
|
||||
FileUtils filePlugin = (FileUtils) pm.getPlugin("File");
|
||||
if (filePlugin != null) {
|
||||
JSONObject fileEntry = filePlugin.getEntryForFile(file);
|
||||
if (fileEntry != null) {
|
||||
result = new PluginResult(PluginResult.Status.OK, fileEntry);
|
||||
} else {
|
||||
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, null);
|
||||
LOG.e(LOG_TAG, "File plugin cannot represent download path");
|
||||
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
|
||||
}
|
||||
} else {
|
||||
LOG.e(LOG_TAG, "File plugin not found; cannot save downloaded file");
|
||||
result = new PluginResult(PluginResult.Status.ERROR, "File plugin not found; cannot save downloaded file");
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, connection, e);
|
||||
LOG.e(LOG_TAG, error.toString(), e);
|
||||
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
|
||||
} catch (IOException e) {
|
||||
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, e);
|
||||
LOG.e(LOG_TAG, error.toString(), e);
|
||||
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
|
||||
} catch (JSONException e) {
|
||||
LOG.e(LOG_TAG, e.getMessage(), e);
|
||||
result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
|
||||
} catch (Throwable e) {
|
||||
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, e);
|
||||
LOG.e(LOG_TAG, error.toString(), e);
|
||||
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
|
||||
} finally {
|
||||
synchronized (activeRequests) {
|
||||
activeRequests.remove(objectId);
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
result = new PluginResult(PluginResult.Status.ERROR, createFileTransferError(CONNECTION_ERR, source, target, connection, null));
|
||||
}
|
||||
// Remove incomplete download.
|
||||
if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {
|
||||
file.delete();
|
||||
}
|
||||
context.sendPluginResult(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort an ongoing upload or download.
|
||||
*/
|
||||
private void abort(String objectId) {
|
||||
final RequestContext context;
|
||||
synchronized (activeRequests) {
|
||||
context = activeRequests.remove(objectId);
|
||||
}
|
||||
if (context != null) {
|
||||
// Closing the streams can block, so execute on a background thread.
|
||||
cordova.getThreadPool().execute(new Runnable() {
|
||||
public void run() {
|
||||
synchronized (context) {
|
||||
File file = context.targetFile;
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
// Trigger the abort callback immediately to minimize latency between it and abort() being called.
|
||||
JSONObject error = createFileTransferError(ABORTED_ERR, context.source, context.target, null, -1, null);
|
||||
context.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, error));
|
||||
context.aborted = true;
|
||||
if (context.connection != null) {
|
||||
try {
|
||||
context.connection.disconnect();
|
||||
} catch (Exception e) {
|
||||
LOG.e(LOG_TAG, "CB-8431 Catch workaround for fatal exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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.filetransfer;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Encapsulates the result and/or status of uploading a file to a remote server.
|
||||
*/
|
||||
public class FileUploadResult {
|
||||
|
||||
private long bytesSent = 0; // bytes sent
|
||||
private int responseCode = -1; // HTTP response code
|
||||
private String response = null; // HTTP response
|
||||
private String objectId = null; // FileTransfer object id
|
||||
|
||||
public long getBytesSent() {
|
||||
return bytesSent;
|
||||
}
|
||||
|
||||
public void setBytesSent(long bytes) {
|
||||
this.bytesSent = bytes;
|
||||
}
|
||||
|
||||
public int getResponseCode() {
|
||||
return responseCode;
|
||||
}
|
||||
|
||||
public void setResponseCode(int responseCode) {
|
||||
this.responseCode = responseCode;
|
||||
}
|
||||
|
||||
public String getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
public void setResponse(String response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public String getObjectId() {
|
||||
return objectId;
|
||||
}
|
||||
|
||||
public void setObjectId(String objectId) {
|
||||
this.objectId = objectId;
|
||||
}
|
||||
|
||||
public JSONObject toJSONObject() throws JSONException {
|
||||
return new JSONObject(
|
||||
"{bytesSent:" + bytesSent +
|
||||
",responseCode:" + responseCode +
|
||||
",response:" + JSONObject.quote(response) +
|
||||
",objectId:" + JSONObject.quote(objectId) + "}");
|
||||
}
|
||||
}
|
||||
@@ -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,571 @@
|
||||
/*
|
||||
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>();
|
||||
}
|
||||
|
||||
public Context getApplicationContext() {
|
||||
return cordova.getActivity().getApplicationContext();
|
||||
}
|
||||
|
||||
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,768 @@
|
||||
/*
|
||||
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.content.Context;
|
||||
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 Context context; // The Application Context 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;
|
||||
context = handler.getApplicationContext();
|
||||
this.id = id;
|
||||
this.audioFile = file;
|
||||
this.tempFiles = new LinkedList<String>();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio file path from the provided fileName or creates a new temporary file path.
|
||||
*
|
||||
* @param fileName the audio file name, if null a temporary 3gp file name is provided
|
||||
* @return String
|
||||
*/
|
||||
private String createAudioFilePath(String fileName) {
|
||||
File dir = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
|
||||
? context.getExternalFilesDir(null)
|
||||
: context.getCacheDir();
|
||||
|
||||
fileName = (fileName == null || fileName.isEmpty())
|
||||
? String.format("tmprecording-%d.3gp", System.currentTimeMillis())
|
||||
: fileName;
|
||||
|
||||
return dir.getAbsolutePath() + File.separator + fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = createAudioFilePath(null);
|
||||
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("/")) {
|
||||
file = createAudioFilePath(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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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,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>
|
||||