Merge with developer-c
@@ -1,458 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!TextUtils.isEmpty(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
//
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
try {
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
} catch(NumberFormatException e) {
|
||||
//In Android 8 and Android P the id is not a number
|
||||
return uri.getPath().replaceFirst("^/document/raw:", "").replaceFirst("^raw:", "");
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
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)) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,599 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
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 : */
|
||||
@@ -1,284 +0,0 @@
|
||||
/*
|
||||
* 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.lang.Number;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
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;
|
||||
|
||||
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 : */
|
||||
@@ -1,431 +0,0 @@
|
||||
/*
|
||||
* 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.lang.Number;
|
||||
|
||||
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 : */
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
/*
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
/*
|
||||
* 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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cordova.whitelist;
|
||||
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.apache.cordova.ConfigXmlParser;
|
||||
import org.apache.cordova.LOG;
|
||||
import org.apache.cordova.Whitelist;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class WhitelistPlugin extends CordovaPlugin {
|
||||
private static final String LOG_TAG = "WhitelistPlugin";
|
||||
private Whitelist allowedNavigations;
|
||||
private Whitelist allowedIntents;
|
||||
private Whitelist allowedRequests;
|
||||
|
||||
// Used when instantiated via reflection by PluginManager
|
||||
public WhitelistPlugin() {
|
||||
}
|
||||
// These can be used by embedders to allow Java-configuration of whitelists.
|
||||
public WhitelistPlugin(Context context) {
|
||||
this(new Whitelist(), new Whitelist(), null);
|
||||
new CustomConfigXmlParser().parse(context);
|
||||
}
|
||||
public WhitelistPlugin(XmlPullParser xmlParser) {
|
||||
this(new Whitelist(), new Whitelist(), null);
|
||||
new CustomConfigXmlParser().parse(xmlParser);
|
||||
}
|
||||
public WhitelistPlugin(Whitelist allowedNavigations, Whitelist allowedIntents, Whitelist allowedRequests) {
|
||||
if (allowedRequests == null) {
|
||||
allowedRequests = new Whitelist();
|
||||
allowedRequests.addWhiteListEntry("file:///*", false);
|
||||
allowedRequests.addWhiteListEntry("data:*", false);
|
||||
}
|
||||
this.allowedNavigations = allowedNavigations;
|
||||
this.allowedIntents = allowedIntents;
|
||||
this.allowedRequests = allowedRequests;
|
||||
}
|
||||
@Override
|
||||
public void pluginInitialize() {
|
||||
if (allowedNavigations == null) {
|
||||
allowedNavigations = new Whitelist();
|
||||
allowedIntents = new Whitelist();
|
||||
allowedRequests = new Whitelist();
|
||||
new CustomConfigXmlParser().parse(webView.getContext());
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomConfigXmlParser extends ConfigXmlParser {
|
||||
@Override
|
||||
public void handleStartTag(XmlPullParser xml) {
|
||||
String strNode = xml.getName();
|
||||
if (strNode.equals("content")) {
|
||||
String startPage = xml.getAttributeValue(null, "src");
|
||||
allowedNavigations.addWhiteListEntry(startPage, false);
|
||||
} else if (strNode.equals("allow-navigation")) {
|
||||
String origin = xml.getAttributeValue(null, "href");
|
||||
if ("*".equals(origin)) {
|
||||
allowedNavigations.addWhiteListEntry("http://*/*", false);
|
||||
allowedNavigations.addWhiteListEntry("https://*/*", false);
|
||||
allowedNavigations.addWhiteListEntry("data:*", false);
|
||||
} else {
|
||||
allowedNavigations.addWhiteListEntry(origin, false);
|
||||
}
|
||||
} else if (strNode.equals("allow-intent")) {
|
||||
String origin = xml.getAttributeValue(null, "href");
|
||||
allowedIntents.addWhiteListEntry(origin, false);
|
||||
} else if (strNode.equals("access")) {
|
||||
String origin = xml.getAttributeValue(null, "origin");
|
||||
String subdomains = xml.getAttributeValue(null, "subdomains");
|
||||
boolean external = (xml.getAttributeValue(null, "launch-external") != null);
|
||||
if (origin != null) {
|
||||
if (external) {
|
||||
LOG.w(LOG_TAG, "Found <access launch-external> within config.xml. Please use <allow-intent> instead.");
|
||||
allowedIntents.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
|
||||
} else {
|
||||
if ("*".equals(origin)) {
|
||||
allowedRequests.addWhiteListEntry("http://*/*", false);
|
||||
allowedRequests.addWhiteListEntry("https://*/*", false);
|
||||
} else {
|
||||
allowedRequests.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void handleEndTag(XmlPullParser xml) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean shouldAllowNavigation(String url) {
|
||||
if (allowedNavigations.isUrlWhiteListed(url)) {
|
||||
return true;
|
||||
}
|
||||
return null; // Default policy
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean shouldAllowRequest(String url) {
|
||||
if (Boolean.TRUE == shouldAllowNavigation(url)) {
|
||||
return true;
|
||||
}
|
||||
if (allowedRequests.isUrlWhiteListed(url)) {
|
||||
return true;
|
||||
}
|
||||
return null; // Default policy
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean shouldOpenExternalUrl(String url) {
|
||||
if (allowedIntents.isUrlWhiteListed(url)) {
|
||||
return true;
|
||||
}
|
||||
return null; // Default policy
|
||||
}
|
||||
|
||||
public Whitelist getAllowedNavigations() {
|
||||
return allowedNavigations;
|
||||
}
|
||||
|
||||
public void setAllowedNavigations(Whitelist allowedNavigations) {
|
||||
this.allowedNavigations = allowedNavigations;
|
||||
}
|
||||
|
||||
public Whitelist getAllowedIntents() {
|
||||
return allowedIntents;
|
||||
}
|
||||
|
||||
public void setAllowedIntents(Whitelist allowedIntents) {
|
||||
this.allowedIntents = allowedIntents;
|
||||
}
|
||||
|
||||
public Whitelist getAllowedRequests() {
|
||||
return allowedRequests;
|
||||
}
|
||||
|
||||
public void setAllowedRequests(Whitelist allowedRequests) {
|
||||
this.allowedRequests = allowedRequests;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
Before Width: | Height: | Size: 593 B |
|
Before Width: | Height: | Size: 599 B |
|
Before Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 427 B |
|
Before Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 727 B |
|
Before Width: | Height: | Size: 744 B |
|
Before Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 1021 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 681 B |